| /** | |
| * 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 */ }); | |
| } | |