Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / shared / lib / metrics / webvitals.ts
blob8b61090834ff3567edfa8c18d87d092edfa5a9f7
1 import type {
2     CLSAttribution,
3     CLSMetric,
4     CLSMetricWithAttribution,
5     INPAttribution,
6     INPMetric,
7     INPMetricWithAttribution,
8     LCPAttribution,
9     LCPMetric,
10     LCPMetricWithAttribution,
11 } from 'web-vitals';
12 import { onCLS, onINP, onLCP } from 'web-vitals';
13 import {
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()
26  */
28 const canLogAttribution = (percentage: number) => {
29     if (percentage < 0 || percentage > 100) {
30         throw new Error('Probability must be between 0 and 100');
31     }
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 || '';
42     }
43     return '';
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
56  */
57 export const reportWebVitals = (context: 'public' | 'private' = 'private') => {
58     const reportMetric = (metric: CLSMetric | INPMetric | LCPMetric) => {
59         metrics.core_webvitals_total.increment({
60             type: metric.name,
61             rating: metric.rating,
62             context,
63         });
64     };
66     const reportMetricWithAttribution = (
67         metric: CLSMetricWithAttribution | INPMetricWithAttribution | LCPMetricWithAttribution
68     ) => {
69         metrics.core_webvitals_total.increment({
70             type: metric.name,
71             rating: metric.rating,
72             context,
73         });
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 =
79             // @ts-ignore
80             hasURLFragment(metric.attribution?.url) || hasURLFragment(metric.attribution?.navigationEntry?.name);
82         if (!reportIncludesPIIInfo && metric.rating === 'poor') {
83             captureMessage(`${metric.name} has poor rating`, {
84                 level: 'info',
85                 tags: {
86                     element: getImpactedElement(metric.attribution),
87                 },
88                 extra: {
89                     ...metric.attribution,
90                 },
91             });
92         }
93     };
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);
104     } else {
105         onCLS(reportMetric);
106         onINP(reportMetric);
107         onLCP(reportMetric);
108     }