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