4 CLSMetricWithAttribution,
7 INPMetricWithAttribution,
10 LCPMetricWithAttribution,
12 import { onCLS, onINP, onLCP } from 'web-vitals';
14 onCLS as onCLSWithAttribution,
15 onINP as onINPWithAttribution,
16 onLCP as onLCPWithAttribution,
17 } from 'web-vitals/attribution';
19 import metrics from '@proton/metrics';
21 import { captureMessage } from '../helpers/sentry';
24 * Make sure you run this only once per react application
25 * Ideally you can run it before doing ReactDOM.render()
28 const canLogAttribution = (percentage: number) => {
29 if (percentage < 0 || percentage > 100) {
30 throw new Error('Probability must be between 0 and 100');
32 return Math.random() * 100 < percentage;
35 const getImpactedElement = (attribution: CLSAttribution | INPAttribution | LCPAttribution): string => {
36 if ('largestShiftTarget' in attribution) {
37 return attribution.largestShiftTarget || '';
38 } else if ('interactionTarget' in attribution) {
39 return attribution.interactionTarget;
40 } else if ('element' in attribution) {
41 return attribution.element || '';
46 const hasURLFragment = (url?: string): boolean => {
47 return url ? url.includes('#') : false;
51 * Observability: you can access the main dashboard on Graphana /d/dd4d3306-9ce3-4c23-a447-a64017c1cc9a/web-vitals?orgId=1&from=now-7d&to=now
52 * Attribution: our goal is to first reduce amount of "poor" ratings for all 3 main Web Vitals, so we log the attribution details in Sentry
53 * On Sentry you can see in the tags which `element` is top % affected for the `${metric.name} has poor rating` issue, based on this `element` you can investigate which React component is the culprit.
54 * The raw details are also available and can be used for further investigate. A fully fledge documentation is written here in Confluence at /wiki/spaces/DRV/pages/97944663/Web+Vitals
55 * Interesting Read: https://web.dev/articles/debug-performance-in-the-field
57 export const reportWebVitals = (context: 'public' | 'private' = 'private') => {
58 const reportMetric = (metric: CLSMetric | INPMetric | LCPMetric) => {
59 metrics.core_webvitals_total.increment({
61 rating: metric.rating,
66 const reportMetricWithAttribution = (
67 metric: CLSMetricWithAttribution | INPMetricWithAttribution | LCPMetricWithAttribution
69 metrics.core_webvitals_total.increment({
71 rating: metric.rating,
75 // Fragment is considered private as that is part of URL not sending to the server.
76 // For public sharing, we depend on not sharing this part with the server.
77 // We rather ignore such reports as navigation entry cannot be modified.
78 const reportIncludesPIIInfo =
80 hasURLFragment(metric.attribution?.url) || hasURLFragment(metric.attribution?.navigationEntry?.name);
82 if (!reportIncludesPIIInfo && metric.rating === 'poor') {
83 captureMessage(`${metric.name} has poor rating`, {
86 element: getImpactedElement(metric.attribution),
89 ...metric.attribution,
95 // We do NOT need to log attributions for every single users
96 // Just a few percentage suffice to help us debug and improve our web vitals
97 // We currently report attribution only if rating is 'poor' in 3% of the users
98 // Logging attribution is more "heavy" so that's why we don't want to do it all the time (it creates a bunch of IntersectionObserver instances)
99 // Currently the attribution is logged in Sentry for ease of use and because all teams are already onboarded.
100 if (canLogAttribution(3)) {
101 onCLSWithAttribution(reportMetricWithAttribution);
102 onINPWithAttribution(reportMetricWithAttribution);
103 onLCPWithAttribution(reportMetricWithAttribution);