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 {
20 treatment = 'treatment',
23 export enum Features {
24 mountToFirstItemRendered = 'mountToFirstItemRendered',
28 * At the present time, these metrics will only work when user is authenticated
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',
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 = {
67 const sendTelemetryFeaturePerformance = (
69 featureName: Features,
71 treatment: ExperimentGroup,
72 additionalValues: PerformanceTelemetryAdditionalValues = {}
74 void sendTelemetryReport({
76 measurementGroup: TelemetryMeasurementGroups.driveWebFeaturePerformance,
77 event: TelemetryDriveWebFeature.performance,
79 milliseconds: timeInMs,
83 experimentGroup: treatment,
89 const measureAndReport = (
92 group: ExperimentGroup,
96 additionalValues?: PerformanceTelemetryAdditionalValues
99 const measure = performance.measure(measureName, startMark, endMark);
100 // it can be undefined on browsers below Safari below 14.1 and Firefox 103
102 sendTelemetryFeaturePerformance(api, feature, measure.duration, group, additionalValues);
106 new EnrichedError('Telemetry Performance Error', {
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
124 export const measureExperimentalPerformance = <T>(
127 applyTreatment: boolean,
128 controlFunction: () => Promise<T>,
129 treatmentFunction: () => 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();
142 performance.mark(endMark);
147 applyTreatment ? ExperimentGroup.treatment : ExperimentGroup.control,
153 performance.clearMarks(endMark);
154 performance.clearMeasures(measureName);
155 performance.clearMarks(startMark);
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()}`;
170 const clear = () => {
171 performance.clearMarks(startMark);
172 performance.clearMarks(endMark);
173 performance.clearMeasures(measureName);
182 performance.mark(startMark);
185 end: (additionalValues?: PerformanceTelemetryAdditionalValues) => {
186 if (!ended && started) {
188 performance.mark(endMark);
189 measureAndReport(api, feature, experimentGroup, measureName, startMark, endMark, additionalValues);
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]);
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;
224 return sendTelemetryReport({
226 measurementGroup: TelemetryMeasurementGroups.driveWebActions,
227 event: TelemetryDriveWebFeature.actions,
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('');
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.
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.
271 export const traceTelemetry = (action: Actions, duration: number = TEN_MINUTES) => {
274 localStorageWithExpiry.storeData(`telemetry-trace-${action}`, await getTimeBasedHash(duration), duration);
277 const data = localStorageWithExpiry.getData(`telemetry-trace-${action}`);
278 if (data && data === (await getTimeBasedHash(duration))) {
279 countActionWithTelemetry(action);
281 localStorageWithExpiry.deleteData(`telemetry-trace-${action}`);