Remove client-side isLoggedIn value
[ProtonMail-WebClient.git] / applications / drive / src / app / utils / telemetry.ts
blob8f7b2e6cd6d3a51cb1e23abbd5b661a02d7313e2
1 import { useEffect, useMemo } from 'react';
3 import { useApi } from '@proton/components';
4 import { CryptoProxy } from '@proton/crypto';
5 import createApi from '@proton/shared/lib/api/createApi';
6 import localStorageWithExpiry from '@proton/shared/lib/api/helpers/localStorageWithExpiry';
7 import { TelemetryDriveWebFeature, TelemetryMeasurementGroups } from '@proton/shared/lib/api/telemetry';
8 import { sendTelemetryReport } from '@proton/shared/lib/helpers/metrics';
9 import { randomHexString4 } from '@proton/shared/lib/helpers/uid';
10 import type { Api } from '@proton/shared/lib/interfaces';
11 import noop from '@proton/utils/noop';
13 import * as config from '../config';
14 import { sendErrorReport } from './errorHandling';
15 import { EnrichedError } from './errorHandling/EnrichedError';
16 import { getLastActivePersistedUserSession } from './lastActivePersistedUserSession';
18 export enum ExperimentGroup {
19     control = 'control',
20     treatment = 'treatment',
23 export enum Features {
24     mountToFirstItemRendered = 'mountToFirstItemRendered',
27 /**
28  * At the present time, these metrics will only work when user is authenticated
29  */
30 export enum Actions {
31     AddToBookmark = 'addToBookmark',
32     ViewSignUpFlowModal = 'viewSignUpFlowModal',
33     DismissSignUpFlowModal = 'dismissSignUpFlowModal',
34     SubmitSignUpFlowModal = 'submitSignUpFlowModal',
35     SignUpFlowModal = 'signUpFlowModal',
36     SignInFlowModal = 'signInFlowModal',
37     OpenPublicLinkFromSharedWithMe = 'openPublicLinkFromSharedWithMe',
38     PublicScanAndDownload = 'publicScanAndDownload',
39     PublicDownload = 'publicDownload',
40     PublicLinkVisit = 'publicLinkVisit',
41     DeleteBookmarkFromSharedWithMe = 'DeleteBookmarkFromSharedWithMe',
42     SignUpFlowAndRedirectCompleted = 'signUpFlowAndRedirectCompleted',
43     RedirectToCorrectContextShare = 'redirectToCorrectContextShare',
44     RedirectToCorrectAcceptInvitation = 'RedirectToCorrectAcceptInvitation',
45     AddToBookmarkTriggeredModal = 'addToBookmarkTriggeredModal',
46     DismissDocsSuggestionsOnboardingModal = 'dismissDocsSuggestionsOnboardingModal',
47     // onboarding actions
48     OnboardingV2Shown = 'onboardingV2Shown',
49     OnboardingV2InstallMacApp = 'onboardingV2InstallMacApp',
50     OnboardingV2InstallWindowsApp = 'onboardingV2InstallWindowsApp',
51     OnboardingV2InstallSkip = 'onboardingV2InstallSkip',
52     OnboardingV2B2BInvite = 'onboardingV2B2BInvite',
53     OnboardingV2B2BInviteSkip = 'onboardingV2B2BInviteSkip',
54     OnboardingV2UploadFile = 'onboardingV2UploadFile',
55     OnboardingV2UploadFolder = 'onboardingV2UploadFolder',
56     OnboardingV2UploadSkip = 'onboardingV2UploadSkip',
58     // Download info on what system will be used for file with size above MEMORY_DOWNLOAD_LIMIT
59     DownloadUsingSW = 'downloadUsingSW',
60     DownloadFallback = 'downloadFallback',
63 type PerformanceTelemetryAdditionalValues = {
64     quantity?: number;
67 const sendTelemetryFeaturePerformance = (
68     api: Api,
69     featureName: Features,
70     timeInMs: number,
71     treatment: ExperimentGroup,
72     additionalValues: PerformanceTelemetryAdditionalValues = {}
73 ) => {
74     void sendTelemetryReport({
75         api: api,
76         measurementGroup: TelemetryMeasurementGroups.driveWebFeaturePerformance,
77         event: TelemetryDriveWebFeature.performance,
78         values: {
79             milliseconds: timeInMs,
80             ...additionalValues,
81         },
82         dimensions: {
83             experimentGroup: treatment,
84             featureName,
85         },
86     });
89 const measureAndReport = (
90     api: Api,
91     feature: Features,
92     group: ExperimentGroup,
93     measureName: string,
94     startMark: string,
95     endMark: string,
96     additionalValues?: PerformanceTelemetryAdditionalValues
97 ) => {
98     try {
99         const measure = performance.measure(measureName, startMark, endMark);
100         // it can be undefined on browsers below Safari below 14.1 and Firefox 103
101         if (measure) {
102             sendTelemetryFeaturePerformance(api, feature, measure.duration, group, additionalValues);
103         }
104     } catch (e) {
105         sendErrorReport(
106             new EnrichedError('Telemetry Performance Error', {
107                 extra: {
108                     e,
109                 },
110             })
111         );
112     }
116  * Executes a feature group with either control or treatment functions.
118  * @param {Features} feature The type of feature to execute (e.g. 'A', 'B', etc.) defined in the `Features` enum
119  * @param {boolean} applyTreatment Whether to execute the treatment or control function
120  * @param {(()) => Promise<T>} controlFunction A function returning a Promise that should be fulfilled when `applyTreatment` is false
121  * @param {(()) => Promise<T>} treatmentFunction A function returning a Promise that should be fulfilled when `applyTreatment` is true
122  * @returns {Promise<T>} The Promise of the executed function, returns T when fulfilled, both control and treatment should have same return type
123  */
124 export const measureExperimentalPerformance = <T>(
125     api: Api,
126     feature: Features,
127     applyTreatment: boolean,
128     controlFunction: () => Promise<T>,
129     treatmentFunction: () => Promise<T>
130 ): Promise<T> => {
131     // Something somewhat unique, if we have collision it's not end of the world we drop the metric
132     const distinguisher = `${performance.now()}-${randomHexString4()}`;
133     const startMark = `start-${feature}-${distinguisher}`;
134     const endMark = `end-${feature}-${distinguisher}`;
135     const measureName = `measure-${feature}-${distinguisher}`;
137     performance.mark(startMark);
138     const result = applyTreatment ? treatmentFunction() : controlFunction();
140     result
141         .finally(() => {
142             performance.mark(endMark);
144             measureAndReport(
145                 api,
146                 feature,
147                 applyTreatment ? ExperimentGroup.treatment : ExperimentGroup.control,
148                 measureName,
149                 startMark,
150                 endMark
151             );
153             performance.clearMarks(endMark);
154             performance.clearMeasures(measureName);
155             performance.clearMarks(startMark);
156         })
157         .catch(noop);
159     return result;
162 export const measureFeaturePerformance = (api: Api, feature: Features, experimentGroup = ExperimentGroup.control) => {
163     const startMark = `start-${feature}-${randomHexString4()}`;
164     const endMark = `end-${feature}-${randomHexString4()}`;
165     const measureName = `measure-${feature}-${randomHexString4()}`;
167     let started = false;
168     let ended = false;
170     const clear = () => {
171         performance.clearMarks(startMark);
172         performance.clearMarks(endMark);
173         performance.clearMeasures(measureName);
174         started = false;
175         ended = false;
176     };
178     return {
179         start: () => {
180             if (!started) {
181                 started = true;
182                 performance.mark(startMark);
183             }
184         },
185         end: (additionalValues?: PerformanceTelemetryAdditionalValues) => {
186             if (!ended && started) {
187                 ended = true;
188                 performance.mark(endMark);
189                 measureAndReport(api, feature, experimentGroup, measureName, startMark, endMark, additionalValues);
190                 clear();
191             }
192         },
193         clear,
194     };
197 export const useMeasureFeaturePerformanceOnMount = (features: Features) => {
198     const api = useApi();
200     // It will be a new measure object each time the api changes or the features changes.
201     // If it changes in between the previous measure has started and not ended yet the values will be cleared and ignored.
202     const measure = useMemo(() => measureFeaturePerformance(api, features), [api, features]);
204     useEffect(() => {
205         measure.start();
206         return () => {
207             measure.clear();
208         };
209     }, [measure]);
211     return measure.end;
214 const apiInstance = createApi({ config, sendLocaleHeaders: true });
216 export const countActionWithTelemetry = (action: Actions, count: number = 1) => {
217     const persistedSession = getLastActivePersistedUserSession();
219     if (persistedSession?.UID) {
220         // API calls will now be Authenticated with x-pm-uid header
221         apiInstance.UID = persistedSession?.UID;
222     }
224     return sendTelemetryReport({
225         api: apiInstance,
226         measurementGroup: TelemetryMeasurementGroups.driveWebActions,
227         event: TelemetryDriveWebFeature.actions,
228         values: {
229             count,
230         },
231         dimensions: {
232             name: action,
233         },
234     });
237 const TEN_MINUTES = 10 * 60 * 1000;
239 export async function getTimeBasedHash(interval: number = TEN_MINUTES) {
240     const timeBase = Math.floor(Date.now() / interval) * interval;
242     const encoder = new TextEncoder();
243     const data = encoder.encode(timeBase.toString());
244     const hashBuffer = await CryptoProxy.computeHash({ algorithm: 'SHA256', data });
245     const hashArray = Array.from(new Uint8Array(hashBuffer));
246     const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
248     return hashHex;
252  * Tracks user actions within a specified time window, typically used for flows involving redirections.
254  * This function is useful when you want to log an action only if the user starts at point A
255  * and finishes at point B within a given time window. It's particularly helpful in scenarios
256  * where there are redirections in the middle of a flow and passing query parameters through
257  * all redirections is not feasible.
259  * @example
260  * // Sign-Up Modal flow:
261  * // 1. Sign-Up Modal appears (start A)
262  * // 2. User signs up, pays, redirects
263  * // 3. User goes back to shared-with-me page (end B)
264  * // If this happens within the time window, the flow is logged as completed.
266  * @param {Actions} action - The action to be tracked.
267  * @param {number} [duration=TEN_MINUTES] - The time window for tracking the action, default is 10 minutes.
269  * @returns {Object} An object with start and end methods to initiate and complete the tracking.
270  */
271 export const traceTelemetry = (action: Actions, duration: number = TEN_MINUTES) => {
272     return {
273         start: async () => {
274             localStorageWithExpiry.storeData(`telemetry-trace-${action}`, await getTimeBasedHash(duration), duration);
275         },
276         end: async () => {
277             const data = localStorageWithExpiry.getData(`telemetry-trace-${action}`);
278             if (data && data === (await getTimeBasedHash(duration))) {
279                 countActionWithTelemetry(action);
280             }
281             localStorageWithExpiry.deleteData(`telemetry-trace-${action}`);
282         },
283     };