1 import { useApi } from '@proton/components';
3 TelemetryDriveWebFeature,
4 TelemetryMeasurementGroups,
6 } from '@proton/shared/lib/api/telemetry';
7 import { setMetricsEnabled } from '@proton/shared/lib/helpers/metrics';
13 countActionWithTelemetry,
15 measureExperimentalPerformance,
16 measureFeaturePerformance,
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', () => {
25 setMetricsEnabled(true);
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();
37 it('measureExperimentalPerformance: executes the control function when flag is false', async () => {
38 const feature = 'testFeature' as Features;
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,
58 experimentGroup: ExperimentGroup.control,
64 it('measureExperimentalPerformance: executes the treatment function when flag is true', async () => {
65 const feature = 'testFeature' as Features;
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,
85 experimentGroup: ExperimentGroup.treatment,
91 it('measureFeaturePerformance: measure duration between a start and end', () => {
92 const measure = measureFeaturePerformance(useApi(), Features.mountToFirstItemRendered);
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,
107 experimentGroup: ExperimentGroup.control,
108 featureName: Features.mountToFirstItemRendered,
114 describe('countActionWithTelemetry', () => {
116 setMetricsEnabled(true);
120 jest.resetAllMocks();
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,
133 name: Actions.PublicDownload,
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,
148 name: Actions.PublicDownload,
154 describe('getTimeBasedHash', () => {
155 let originalDateNow: () => number;
156 const now = 1625097600000;
159 setupCryptoProxyForTesting();
160 originalDateNow = Date.now;
164 releaseCryptoProxy();
165 Date.now = originalDateNow;
169 Date.now = jest.fn(() => now); // 2021-07-01T00:00:00.000Z
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);
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);
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);
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);
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);
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