File size: 2,879 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
export const roundMs = (n: number | undefined): number | undefined =>
  typeof n === 'number' && Number.isFinite(n) ? Math.round(n) : undefined;

/**
 * Fraction of (already good-trimmed) field Web-Vital events forwarded to Sentry.
 *
 * The good-trim (#4565) drops the `good` bucket, but the surviving
 * needs-improvement/poor tail still runs ~12k events/day — ~92% of ALL Sentry
 * volume — which is pure telemetry, not errors. This uniformly samples that tail
 * to cut that volume ~80%. Uniform (not rating-aware) sampling is deliberate: it
 * leaves the rating split, formFactor split, attribution-target distribution, and
 * p75 unbiased — only the sample size shrinks. Captured events carry a
 * `sampleRate` tag so absolute field volume is reconstructable (× 1/sampleRate).
 * At current traffic 20% still yields ~2.4k events/day — ample for weekly
 * page-level CrUX cross-checks and per-target histograms.
 */
export const WEB_VITAL_SAMPLE_RATE = 0.2;

/**
 * Uniform sampling gate for field Web-Vital reporting; returns true when the
 * event should be forwarded. `rate` in [0,1]; `rng` is injectable for tests.
 * `rate >= 1` (or NaN) always keeps — a misconfigured rate over-reports rather
 * than silently losing data; `rate <= 0` always drops.
 */
export function shouldSampleWebVital(
  rate: number = WEB_VITAL_SAMPLE_RATE,
  rng: () => number = Math.random,
): boolean {
  if (!(rate < 1)) return true;
  if (rate <= 0) return false;
  return rng() < rate;
}

export type WebVitalsFormFactor = 'mobile' | 'desktop';

function mediaQueryMatches(query: string): boolean {
  return typeof window.matchMedia === 'function' && window.matchMedia(query).matches;
}

/**
 * Low-cardinality surface tag for field Web Vitals.
 *
 * This intentionally folds tablet/touch and <=1024px responsive layouts into
 * `mobile`, leaving only `mobile|desktop` as Sentry facets.
 */
export function getWebVitalsFormFactor(): WebVitalsFormFactor {
  if (typeof window === 'undefined') return 'desktop';
  const navigatorWithUaData = window.navigator as Navigator & {
    userAgentData?: { mobile?: boolean };
  };
  if (navigatorWithUaData.userAgentData?.mobile === true) return 'mobile';
  if (
    mediaQueryMatches('(pointer: coarse)')
    || mediaQueryMatches('(hover: none)')
    || mediaQueryMatches('(max-width: 1024px)')
  ) {
    return 'mobile';
  }
  return window.innerWidth > 0 && window.innerWidth <= 1024 ? 'mobile' : 'desktop';
}

export function sanitizeWebVitalUrl(raw: string | undefined): string {
  if (!raw) return '';
  try {
    const url = new URL(raw, typeof window !== 'undefined' ? window.location.href : 'https://worldmonitor.app/');
    const query = url.search ? '?[redacted]' : '';
    return `${url.origin}${url.pathname}${query}`;
  } catch {
    const [withoutQuery = raw] = raw.split('?');
    return withoutQuery.slice(0, 200);
  }
}