diff --git a/src/db/migrations/004_sentry_sdk_init.sql b/src/db/migrations/004_sentry_sdk_init.sql new file mode 100644 index 0000000..785904c --- /dev/null +++ b/src/db/migrations/004_sentry_sdk_init.sql @@ -0,0 +1,11 @@ +-- Sentry SDK init duration, sourced from the test app's User Timing API. +-- +-- The instrumented apps emit a `performance.measure('sentry-sdk-init-duration')` +-- spanning SDK initialisation. Lighthouse surfaces it in the `user-timings` +-- audit, and the worker denormalises the measure's duration here (like the +-- other scalar metrics) so the publisher can ship it to Sentry. +-- +-- NULL means the measure was absent from the run (e.g. no-sentry cells, or the +-- mark fired after Lighthouse's trace window closed). + +ALTER TABLE runs ADD COLUMN sentry_sdk_init_ms INTEGER; diff --git a/src/publisher/runner.js b/src/publisher/runner.js index b11f9c7..e306dca 100644 --- a/src/publisher/runner.js +++ b/src/publisher/runner.js @@ -88,7 +88,7 @@ function pickUnpublishedCells() { function loadRunsForCell(cellId) { return getDb().prepare(` SELECT run_id, run_index, performance_score, lcp_ms, fcp_ms, tbt_ms, - cls, total_bytes, collected_at + cls, total_bytes, sentry_sdk_init_ms, collected_at FROM runs WHERE cell_id = ? ORDER BY run_index `).all(cellId); } @@ -147,6 +147,11 @@ function emitRunMetrics(run, baseAttrs) { // to a string-typed field and refuse to compute p50/p90 over it. Score // and CLS therefore both go through the numeric path below. + // collect number of runs as a metric (only as attributes doesn't allow for visualization in Sentry) + Sentry.metrics.count('lighthouse.run.completed', 1, { + attributes: attrs, + }); + if (run.performance_score != null) { // LHR scores are 0..1 floats; Lighthouse's own UI shows them as 0..100. // We multiply here so Sentry dashboards render '78%' instead of '0.78' @@ -188,4 +193,13 @@ function emitRunMetrics(run, baseAttrs) { attributes: attrs, }); } + if (run.sentry_sdk_init_ms != null) { + // `performance.measure('sentry-sdk-init-duration')` from the instrumented + // test app, surfaced by Lighthouse's user-timings audit. Null for + // no-sentry cells, so the dashboard only sees it where it's meaningful. + Sentry.metrics.distribution('lighthouse.sentry_sdk_init', run.sentry_sdk_init_ms, { + unit: 'millisecond', + attributes: attrs, + }); + } } diff --git a/src/worker/lighthouse.js b/src/worker/lighthouse.js index f3c34cf..aea2908 100644 --- a/src/worker/lighthouse.js +++ b/src/worker/lighthouse.js @@ -109,5 +109,19 @@ export function extractMetrics(lhr) { tbtMs: round(audit('total-blocking-time')), cls: audit('cumulative-layout-shift'), bytes: round(audit('total-byte-weight')), + sentrySdkInitMs: round(userTimingMeasure(lhr, 'sentry-sdk-init-duration')), }; } + +/** + * Pull a single `performance.measure()` duration (ms) out of Lighthouse's + * `user-timings` audit by name. Returns null if the measure isn't present — + * which happens for no-sentry cells, or if the measure fired after the trace + * window closed. Only `timingType: 'Measure'` entries carry a duration; marks + * are ignored. + */ +function userTimingMeasure(lhr, name) { + const items = lhr?.audits?.['user-timings']?.details?.items ?? []; + const measure = items.find(i => i.name === name && i.timingType === 'Measure'); + return measure?.duration ?? null; +} diff --git a/src/worker/runner.js b/src/worker/runner.js index 76d2eab..351607b 100644 --- a/src/worker/runner.js +++ b/src/worker/runner.js @@ -226,8 +226,9 @@ function writeRunsToDb({ cell, persisted }) { INSERT INTO runs ( run_id, cell_id, run_index, is_representative, performance_score, lcp_ms, fcp_ms, tbt_ms, cls, total_bytes, + sentry_sdk_init_ms, lhr_json_path, report_html_path, collected_at - ) VALUES (?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const completedIso = new Date().toISOString(); @@ -237,6 +238,7 @@ function writeRunsToDb({ cell, persisted }) { r.runId, cell.cell_id, r.runIndex, r.metrics.score, r.metrics.lcpMs, r.metrics.fcpMs, r.metrics.tbtMs, r.metrics.cls, r.metrics.bytes, + r.metrics.sentrySdkInitMs, r.jsonPath, r.htmlPath, completedIso, ); }