File size: 4,474 Bytes
fa9c65f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | /**
* Field INP attribution reporting (#4537).
*
* `reportInpMetric` shapes one web-vitals INP measurement (attribution build)
* into a Sentry event and routes it through `enqueueSentryCall` so it survives
* Sentry's deferred (~10s idle) init: the call buffers and drains on init
* rather than being dropped because the SDK hasn't loaded when the interaction
* occurs. Reporting interaction target + the three INP sub-parts lets us see
* which real interaction is slow and whether the cost is input delay,
* processing, or presentation — the data that drives fix prioritization.
* Good-rated events are trimmed (#4565), so captured-event p75 is conditioned
* on the bad tail. Verify fixes with bad-event rate per formFactor plus
* weekly page-level CrUX queryHistoryRecord, not p75 of captured Sentry events.
*
* The `onINP` registration that calls this lives behind the `web-vitals`
* dependency (see `registerInpReporting` doc at the bottom). This module keeps
* the reportable logic free of that import so it builds and is unit-tested
* without the package present.
*/
import { getGlobeMarkerExtra } from '@/bootstrap/globe-marker-probe';
import { enqueueSentryCall } from '@/bootstrap/sentry-defer';
import {
getWebVitalsFormFactor,
roundMs,
shouldSampleWebVital,
WEB_VITAL_SAMPLE_RATE,
} from '@/bootstrap/web-vitals-utils';
/** Structural subset of web-vitals' INP attribution (kept local to avoid the dep). */
export interface InpAttributionLike {
interactionTarget?: string;
interactionType?: string;
inputDelay?: number;
processingDuration?: number;
presentationDelay?: number;
loadState?: string;
}
/** Structural subset of web-vitals' INPMetricWithAttribution. */
export interface InpMetricLike {
value: number;
rating?: 'good' | 'needs-improvement' | 'poor';
attribution?: InpAttributionLike;
}
/**
* Report one field INP measurement to Sentry (R1, R2). `enqueue` is injectable
* for tests; in production it defaults to the deferred-Sentry queue.
*/
export function reportInpMetric(
metric: InpMetricLike,
enqueue: typeof enqueueSentryCall = enqueueSentryCall,
keep: () => boolean = shouldSampleWebVital,
globeExtra: typeof getGlobeMarkerExtra = getGlobeMarkerExtra,
): void {
// Volume trim (#4565): skip 'good' (<200ms) INP — ~70% of field events, low
// diagnostic value. Report needs-improvement / poor / unknown only, so the
// actionable worst-case signal still lands while Sentry event volume drops ~70%.
if (metric.rating === 'good') return;
// Uniform sample of the surviving bad tail (~50% poor here) to cut Sentry
// volume ~80% without biasing the rating/formFactor/target distributions.
if (!keep()) return;
const a = metric.attribution ?? {};
const formFactor = getWebVitalsFormFactor();
// #5368: when the 3D globe is mounted, how many DOM markers it is carrying is
// the dominant term in its frame cost — and therefore in presentationDelay.
// Null (and absent from the event) whenever the globe is not mounted.
const globe = globeExtra();
enqueue((s) => {
s.captureMessage('web-vital: INP', {
level: 'info',
tags: {
webvital: 'inp',
formFactor,
sampleRate: String(WEB_VITAL_SAMPLE_RATE),
'inp.rating': metric.rating ?? 'unknown',
'inp.interactionType': a.interactionType ?? 'unknown',
},
extra: {
value: Math.round(metric.value),
interactionTarget: a.interactionTarget ?? 'unknown',
inputDelay: roundMs(a.inputDelay),
processingDuration: roundMs(a.processingDuration),
presentationDelay: roundMs(a.presentationDelay),
loadState: a.loadState,
...(globe ?? {}),
},
});
});
}
/**
* Register the field INP listener (R1–R3). Browser-only. Uses a dynamic import
* so `web-vitals` code-splits into its own chunk (loaded post-paint when this
* runs) and so this module stays node-loadable for unit tests. web-vitals'
* `onINP` default reports once per page lifecycle (on visibility-hide) — the
* quota-safe production behavior (R3); `reportAllChanges` is dev-only debugging.
*/
export function registerInpReporting(): void {
if (typeof window === 'undefined') return;
void import('web-vitals/attribution')
.then(({ onINP }) => {
onINP((metric) => reportInpMetric(metric as unknown as InpMetricLike));
})
.catch(() => { /* web-vitals chunk failed to load (adblock/CDN) — non-fatal */ });
}
|