Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / drive-store / utils / telemetry.test.ts
blob196a72c8673665df86d4802c8acbcc9c405aef37
1 import { useApi } from '@proton/components';
2 import {
3     TelemetryDriveWebFeature,
4     TelemetryMeasurementGroups,
5     sendTelemetryData,
6 } from '@proton/shared/lib/api/telemetry';
7 import { setMetricsEnabled } from '@proton/shared/lib/helpers/metrics';
9 import {
10     Actions,
11     ExperimentGroup,
12     Features,
13     countActionWithTelemetry,
14     getTimeBasedHash,
15     measureExperimentalPerformance,
16     measureFeaturePerformance,
17 } from './telemetry';
18 import { releaseCryptoProxy, setupCryptoProxyForTesting } from './test/crypto';
20 jest.mock('@proton/shared/lib/api/telemetry');
21 jest.mock('@proton/components/hooks/useApi');
23 describe('Performance Telemetry', () => {
24     beforeEach(() => {
25         setMetricsEnabled(true);
26         jest.useFakeTimers();
27         window.performance.mark = jest.fn();
28         window.performance.measure = jest.fn().mockImplementation(() => ({ duration: 100 }));
29         window.performance.clearMarks = jest.fn();
30         window.performance.clearMeasures = jest.fn();
31     });
33     afterEach(() => {
34         jest.resetAllMocks();
35     });
37     it('measureExperimentalPerformance: executes the control function when flag is false', async () => {
38         const feature = 'testFeature' as Features;
39         const flag = false;
40         const controlFunction = jest.fn(() => Promise.resolve('control result'));
41         const treatmentFunction = jest.fn(() => Promise.resolve('treatment result'));
43         await measureExperimentalPerformance(useApi(), feature, flag, controlFunction, treatmentFunction);
45         expect(controlFunction).toHaveBeenCalledTimes(1);
46         expect(performance.mark).toHaveBeenCalledTimes(2);
47         expect(performance.measure).toHaveBeenCalledTimes(1);
48         expect(performance.clearMarks).toHaveBeenCalledTimes(2);
49         expect(performance.clearMeasures).toHaveBeenCalledTimes(1);
50         expect(sendTelemetryData).toHaveBeenCalledTimes(1);
51         expect(sendTelemetryData).toHaveBeenCalledWith({
52             MeasurementGroup: TelemetryMeasurementGroups.driveWebFeaturePerformance,
53             Event: TelemetryDriveWebFeature.performance,
54             Values: {
55                 milliseconds: 100,
56             },
57             Dimensions: {
58                 experimentGroup: ExperimentGroup.control,
59                 featureName: feature,
60             },
61         });
62     });
64     it('measureExperimentalPerformance: executes the treatment function when flag is true', async () => {
65         const feature = 'testFeature' as Features;
66         const flag = true;
67         const controlFunction = jest.fn(() => Promise.resolve('control result'));
68         const treatmentFunction = jest.fn(() => Promise.resolve('treatment result'));
70         await measureExperimentalPerformance(useApi(), feature, flag, controlFunction, treatmentFunction);
72         expect(treatmentFunction).toHaveBeenCalledTimes(1);
73         expect(performance.mark).toHaveBeenCalledTimes(2);
74         expect(performance.measure).toHaveBeenCalledTimes(1);
75         expect(performance.clearMarks).toHaveBeenCalledTimes(2);
76         expect(performance.clearMeasures).toHaveBeenCalledTimes(1);
77         expect(sendTelemetryData).toHaveBeenCalledTimes(1);
78         expect(sendTelemetryData).toHaveBeenCalledWith({
79             MeasurementGroup: TelemetryMeasurementGroups.driveWebFeaturePerformance,
80             Event: TelemetryDriveWebFeature.performance,
81             Values: {
82                 milliseconds: 100,
83             },
84             Dimensions: {
85                 experimentGroup: ExperimentGroup.treatment,
86                 featureName: feature,
87             },
88         });
89     });
91     it('measureFeaturePerformance: measure duration between a start and end', () => {
92         const measure = measureFeaturePerformance(useApi(), Features.mountToFirstItemRendered);
93         measure.start();
94         measure.end();
95         expect(performance.mark).toHaveBeenCalledTimes(2);
96         expect(performance.measure).toHaveBeenCalledTimes(1);
97         expect(performance.clearMarks).toHaveBeenCalledTimes(2);
98         expect(performance.clearMeasures).toHaveBeenCalledTimes(1);
99         expect(sendTelemetryData).toHaveBeenCalledTimes(1);
100         expect(sendTelemetryData).toHaveBeenCalledWith({
101             MeasurementGroup: TelemetryMeasurementGroups.driveWebFeaturePerformance,
102             Event: TelemetryDriveWebFeature.performance,
103             Values: {
104                 milliseconds: 100,
105             },
106             Dimensions: {
107                 experimentGroup: ExperimentGroup.control,
108                 featureName: Features.mountToFirstItemRendered,
109             },
110         });
111     });
114 describe('countActionWithTelemetry', () => {
115     beforeEach(() => {
116         setMetricsEnabled(true);
117     });
119     afterEach(() => {
120         jest.resetAllMocks();
121     });
123     it('countActionWithTelemetry: should send telemetry report with a count', () => {
124         countActionWithTelemetry(Actions.PublicDownload);
125         expect(sendTelemetryData).toHaveBeenCalledTimes(1);
126         expect(sendTelemetryData).toHaveBeenCalledWith({
127             MeasurementGroup: TelemetryMeasurementGroups.driveWebActions,
128             Event: TelemetryDriveWebFeature.actions,
129             Values: {
130                 count: 1,
131             },
132             Dimensions: {
133                 name: Actions.PublicDownload,
134             },
135         });
136     });
138     it('countActionWithTelemetry: should send telemetry report with custom count', () => {
139         countActionWithTelemetry(Actions.PublicDownload, 15);
140         expect(sendTelemetryData).toHaveBeenCalledTimes(1);
141         expect(sendTelemetryData).toHaveBeenCalledWith({
142             MeasurementGroup: TelemetryMeasurementGroups.driveWebActions,
143             Event: TelemetryDriveWebFeature.actions,
144             Values: {
145                 count: 15,
146             },
147             Dimensions: {
148                 name: Actions.PublicDownload,
149             },
150         });
151     });
154 describe('getTimeBasedHash', () => {
155     let originalDateNow: () => number;
156     const now = 1625097600000;
158     beforeAll(() => {
159         setupCryptoProxyForTesting();
160         originalDateNow = Date.now;
161     });
163     afterAll(() => {
164         releaseCryptoProxy();
165         Date.now = originalDateNow;
166     });
168     beforeEach(() => {
169         Date.now = jest.fn(() => now); // 2021-07-01T00:00:00.000Z
170     });
172     it('should return the same hash for inputs within the same interval', async () => {
173         const hash1 = await getTimeBasedHash();
174         Date.now = jest.fn(() => now + 5 * 60 * 1000); // 5 minutes later
175         const hash2 = await getTimeBasedHash();
177         expect(hash1).toBe(hash2);
178     });
180     it('should return different hashes for inputs in different intervals', async () => {
181         const hash1 = await getTimeBasedHash();
182         Date.now = jest.fn(() => now + 15 * 60 * 1000); // 15 minutes later
183         const hash2 = await getTimeBasedHash();
185         expect(hash1).not.toBe(hash2);
186     });
188     it('should use the default interval of 10 minutes when not specified', async () => {
189         const hash1 = await getTimeBasedHash();
190         Date.now = jest.fn(() => now + 9 * 60 * 1000); // 9 minutes later
191         const hash2 = await getTimeBasedHash();
192         Date.now = jest.fn(() => now + 11 * 60 * 1000); // 11 minutes later
193         const hash3 = await getTimeBasedHash();
195         expect(hash1).toBe(hash2);
196         expect(hash1).not.toBe(hash3);
197     });
199     it('should use a custom interval when specified', async () => {
200         const customInterval = 5 * 60 * 1000; // 5 minutes
202         const hash1 = await getTimeBasedHash(customInterval);
203         Date.now = jest.fn(() => now + 4 * 60 * 1000); // 4 minutes later
204         const hash2 = await getTimeBasedHash(customInterval);
205         Date.now = jest.fn(() => now + 6 * 60 * 1000); // 6 minutes later
206         const hash3 = await getTimeBasedHash(customInterval);
208         expect(hash1).toBe(hash2);
209         expect(hash1).not.toBe(hash3);
210     });
212     it('should handle edge cases around interval boundaries', async () => {
213         const hash1 = await getTimeBasedHash();
214         Date.now = jest.fn(() => now + 10 * 60 * 1000 - 1); // 1ms before next interval
215         const hash2 = await getTimeBasedHash();
216         Date.now = jest.fn(() => now + 10 * 60 * 1000); // Exactly at next interval
217         const hash3 = await getTimeBasedHash();
219         expect(hash1).toBe(hash2);
220         expect(hash1).not.toBe(hash3);
221     });
223     it('should produce a hex string of correct length', async () => {
224         const hash = await getTimeBasedHash();
226         expect(hash).toMatch(/^[0-9a-f]{64}$/); // 32 bytes = 64 hex characters
227     });