Merge branch 'MAILWEB-6067-improve-circular-dependencies-prevention' into 'main'
[ProtonMail-WebClient.git] / packages / drive-store / store / _events / driveEventsMetrics.ts
blob8e385d9922b0c4943e10525e4f33acb695a1b2ed
1 import 'core-js/actual/set/difference';
3 import metrics from '@proton/metrics';
4 import type { HttpsProtonMeDriveSyncErrorsTotalV1SchemaJson } from '@proton/metrics/types/drive_sync_errors_total_v1.schema';
5 import { getIsNetworkError, getIsOfflineError } from '@proton/shared/lib/api/helpers/apiErrorHelper';
6 import { EVENT_TYPES } from '@proton/shared/lib/drive/constants';
7 import type { DriveEventsResult } from '@proton/shared/lib/interfaces/drive/events';
9 import { is4xx, is5xx } from '../../utils/errorHandling/apiErrors';
10 import type { VolumeType } from '../_volumes';
11 import type { DriveEvent } from './interface';
13 const EVENT_METRICS = ['delete', 'create', 'update', 'update_metadata'] as const;
15 export function countEventsPerType(type: VolumeType, driveEvents: DriveEventsResult) {
16     if (!driveEvents.Events) {
17         return;
18     }
20     const deleteCount = driveEvents.Events.filter((event) => event.EventType === EVENT_TYPES.DELETE).length;
21     if (deleteCount) {
22         metrics.drive_sync_event_total.increment(
23             {
24                 volumeType: type,
25                 eventType: EVENT_METRICS[EVENT_TYPES.DELETE],
26             },
27             deleteCount
28         );
29     }
31     const createCount = driveEvents.Events.filter((event) => event.EventType === EVENT_TYPES.CREATE).length;
32     if (createCount) {
33         metrics.drive_sync_event_total.increment(
34             {
35                 volumeType: type,
36                 eventType: EVENT_METRICS[EVENT_TYPES.CREATE],
37             },
38             createCount
39         );
40     }
42     const updateCount = driveEvents.Events.filter((event) => event.EventType === EVENT_TYPES.UPDATE).length;
43     if (updateCount) {
44         metrics.drive_sync_event_total.increment(
45             {
46                 volumeType: type,
47                 eventType: EVENT_METRICS[EVENT_TYPES.UPDATE],
48             },
49             updateCount
50         );
51     }
53     const updateMetadata = driveEvents.Events.filter((event) => event.EventType === EVENT_TYPES.UPDATE_METADATA).length;
54     if (updateMetadata) {
55         metrics.drive_sync_event_total.increment(
56             {
57                 volumeType: type,
58                 eventType: EVENT_METRICS[EVENT_TYPES.UPDATE_METADATA],
59             },
60             updateMetadata
61         );
62     }
65 type EVENT_METRICS_TYPE = (typeof EVENT_METRICS)[number];
66 type StateEvents = {
67     [K in EVENT_METRICS_TYPE]?: Set<string>;
69 type ProcessMap = Map<string, StateEvents>;
71 export class EventsMetrics {
72     private state: ProcessMap = new Map();
74     private events: ProcessMap = new Map();
76     processed(eventId: string, event: DriveEvent) {
77         const { eventType, encryptedLink } = event;
78         const { volumeId, linkId } = encryptedLink;
79         const key = `${volumeId}-${eventId}`;
80         const metricKey = EVENT_METRICS[eventType];
81         const processedEvents = this.state.get(key) || {};
82         if (!processedEvents[metricKey]) {
83             processedEvents[metricKey] = new Set();
84         }
85         processedEvents[metricKey].add(linkId);
86         this.state.set(key, processedEvents);
87     }
89     batchStart(volumeId: string, driveEvents: DriveEventsResult) {
90         const key = `${volumeId}-${driveEvents.EventID}`;
91         this.events.set(key, {
92             delete: new Set(
93                 driveEvents.Events.filter((event) => event.EventType === EVENT_TYPES.DELETE).map(
94                     (event) => event.Link.LinkID
95                 )
96             ),
97             create: new Set(
98                 driveEvents.Events.filter((event) => event.EventType === EVENT_TYPES.CREATE).map(
99                     (event) => event.Link.LinkID
100                 )
101             ),
102             update: new Set(
103                 driveEvents.Events.filter((event) => event.EventType === EVENT_TYPES.UPDATE).map(
104                     (event) => event.Link.LinkID
105                 )
106             ),
107             update_metadata: new Set(
108                 driveEvents.Events.filter((event) => event.EventType === EVENT_TYPES.UPDATE_METADATA).map(
109                     (event) => event.Link.LinkID
110                 )
111             ),
112         });
113     }
115     batchCompleted(volumeId: string, eventId: string, type: VolumeType) {
116         const key = `${volumeId}-${eventId}`;
117         const batch = this.state.get(key) || {};
118         const events = this.events.get(key) || {};
119         this.processBatch(batch, events, type);
120         this.state.delete(key);
121         this.events.delete(key);
122     }
124     private processBatch(batch: StateEvents, events: StateEvents, type: VolumeType) {
125         EVENT_METRICS.forEach((eventType) => {
126             const processed = batch[eventType];
127             const total = events[eventType];
128             if (total !== undefined && total.size > 0) {
129                 const difference = total.difference(processed || new Set());
130                 if (difference.size) {
131                     metrics.drive_sync_event_unecessary_total.increment(
132                         {
133                             volumeType: type,
134                             eventType: eventType,
135                         },
136                         difference.size
137                     );
138                 }
139             }
140         });
141     }
143 // TODO: DRVWEB-4319 Implement sync errors & sync erroring users and revamp this function with new metric
144 export function getErrorCategory(error: any): HttpsProtonMeDriveSyncErrorsTotalV1SchemaJson['Labels']['type'] {
145     if (getIsOfflineError(error) || getIsNetworkError(error)) {
146         return 'network_error';
147     } else if (error?.statusCode && is4xx(error?.statusCode)) {
148         return '4xx';
149     } else if (error?.statusCode && is5xx(error?.statusCode)) {
150         return '5xx';
151     }
152     return 'unknown';