n8n/.github/scripts/send-build-stats.mjs
n8n-cat-bot[bot] 6e8a7fcd2d
fix: Make QA metrics telemetry truly fire-and-forget (#32597)
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 16:36:30 +00:00

80 lines
2.6 KiB
JavaScript

#!/usr/bin/env node
/**
* Sends Turbo build stats to the unified QA metrics webhook.
*
* Reads the Turbo run summary from .turbo/runs/ and emits per-package
* build-duration metrics with {package, cache, task} dimensions, plus
* a run-level build-total-duration summary.
*
* Usage: node send-build-stats.mjs
*
* Environment variables:
* QA_METRICS_WEBHOOK_URL - Webhook URL (required to send)
* QA_METRICS_WEBHOOK_USER - Basic auth username
* QA_METRICS_WEBHOOK_PASSWORD - Basic auth password
*/
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { sendMetrics, metric } from './send-metrics.mjs';
const runsDir = '.turbo/runs';
if (!existsSync(runsDir)) {
console.log('No .turbo/runs directory found (turbo --summarize not used), skipping.');
process.exit(0);
}
const files = readdirSync(runsDir)
.filter((f) => f.endsWith('.json'))
.sort();
if (files.length === 0) {
console.error('No summary file found in .turbo/runs/');
process.exit(1);
}
const summary = JSON.parse(readFileSync(join(runsDir, files.at(-1)), 'utf-8'));
const metrics = [];
// turbo 2.x emits start/end timestamps rather than a `durationMs` field.
// For cache hits, the execution window measures only restore overhead (~ms),
// so we use `cache.timeSaved` — the duration of the run we avoided — instead.
const elapsedMs = ({ startTime, endTime } = {}) =>
startTime && endTime ? endTime - startTime : 0;
for (const task of summary.tasks ?? []) {
if (task.execution?.exitCode !== 0) continue;
const cacheHit = task.cache?.status === 'HIT';
const durationMs = cacheHit ? (task.cache.timeSaved ?? 0) : elapsedMs(task.execution);
// taskId format: "package-name#task-name"
const [pkg, taskName] = task.taskId?.split('#') ?? [task.package, task.task];
metrics.push(
metric('build-duration', durationMs / 1000, 's', {
package: pkg ?? 'unknown',
task: taskName ?? 'build',
cache: cacheHit ? 'hit' : 'miss',
}),
);
}
const totalMs = elapsedMs(summary.execution);
const totalTasks = summary.tasks?.length ?? 0;
const cachedTasks = summary.tasks?.filter((t) => t.cache?.status === 'HIT').length ?? 0;
metrics.push(
metric('build-total-duration', totalMs / 1000, 's', {
total_tasks: totalTasks,
cached_tasks: cachedTasks,
}),
);
// Fire-and-forget: don't await. The in-flight fetch is cancelled on process
// exit, which is the right trade-off — we drop a data point rather than block
// CI on a slow webhook. sendMetrics swallows its own errors, but attach a
// .catch defensively in case that ever changes.
sendMetrics(metrics, 'build-stats').catch((err) =>
console.warn(`[metrics] send failed: ${err.message}`),
);