diff --git a/.github/CI-TELEMETRY.md b/.github/CI-TELEMETRY.md index 96e105a3f11..f8665912626 100644 --- a/.github/CI-TELEMETRY.md +++ b/.github/CI-TELEMETRY.md @@ -101,9 +101,10 @@ GROUP BY 1, 2 ORDER BY 1; ```javascript import { sendMetrics, metric } from './send-metrics.mjs'; -await sendMetrics([ +// Fire-and-forget — best-effort telemetry, must never block CI. +sendMetrics([ metric('my-metric', 42.0, 'ms', { context: 'value' }), -]); +]).catch((err) => console.warn(`[metrics] send failed: ${err.message}`)); ``` **From a Playwright test:** diff --git a/.github/scripts/send-build-stats.mjs b/.github/scripts/send-build-stats.mjs index 23dde7472d2..7639b1fb650 100644 --- a/.github/scripts/send-build-stats.mjs +++ b/.github/scripts/send-build-stats.mjs @@ -70,4 +70,10 @@ metrics.push( }), ); -await sendMetrics(metrics, 'build-stats'); +// 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}`), +); diff --git a/.github/scripts/send-docker-stats.mjs b/.github/scripts/send-docker-stats.mjs index 3d555c80ab6..edf2b091c5c 100644 --- a/.github/scripts/send-docker-stats.mjs +++ b/.github/scripts/send-docker-stats.mjs @@ -85,4 +85,7 @@ if (metrics.length === 0) { process.exit(0); } -await sendMetrics(metrics, 'docker-stats'); +// Fire-and-forget: see send-build-stats.mjs for rationale. +sendMetrics(metrics, 'docker-stats').catch((err) => + console.warn(`[metrics] send failed: ${err.message}`), +); diff --git a/.github/scripts/send-metrics.mjs b/.github/scripts/send-metrics.mjs index 8c2551aa7fb..c1df54ed8c4 100644 --- a/.github/scripts/send-metrics.mjs +++ b/.github/scripts/send-metrics.mjs @@ -3,9 +3,10 @@ * Shared metrics sender for CI scripts. * See .github/CI-TELEMETRY.md for payload shape and BigQuery schema. * - * Usage: + * Usage (fire-and-forget — best-effort, never blocks CI): * import { sendMetrics, metric } from './send-metrics.mjs'; - * await sendMetrics([metric('build-duration', 45.2, 's', { package: '@n8n/cli' })]); + * sendMetrics([metric('build-duration', 45.2, 's', { package: '@n8n/cli' })]) + * .catch((err) => console.warn(`[metrics] send failed: ${err.message}`)); * * Env: QA_METRICS_WEBHOOK_URL, QA_METRICS_WEBHOOK_USER, QA_METRICS_WEBHOOK_PASSWORD */ @@ -78,22 +79,31 @@ export async function sendMetrics(metrics, benchmarkName = null) { const payload = { ...buildContext(benchmarkName), metrics }; const basicAuth = Buffer.from(`${webhookUser}:${webhookPassword}`).toString('base64'); - const response = await fetch(webhookUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Basic ${basicAuth}`, - }, - body: JSON.stringify(payload), - signal: AbortSignal.timeout(30_000), - }); + // Best-effort telemetry: never throw into the caller. Slow/down webhook + // becomes a one-line warning instead of an uncaught rejection. 5s caps + // wall-time in callers that do await this; fire-and-forget callers exit + // before then anyway. + try { + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${basicAuth}`, + }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(5_000), + }); - if (!response.ok) { - const body = await response.text().catch(() => ''); - throw new Error( - `Webhook failed: ${response.status} ${response.statusText}${body ? `\n${body}` : ''}`, - ); + if (!response.ok) { + const body = await response.text().catch(() => ''); + console.warn( + `[metrics] webhook ${response.status} ${response.statusText}${body ? `\n${body}` : ''}`, + ); + return; + } + + console.log(`Sent ${metrics.length} metric(s): ${response.status}`); + } catch (err) { + console.warn(`[metrics] send failed (non-fatal): ${err.message}`); } - - console.log(`Sent ${metrics.length} metric(s): ${response.status}`); }