1 import { HTTP_STATUS_CODE, SECOND } from '@proton/shared/lib/constants';
2 import { getAppVersionHeaders, getAuthHeaders, getUIDHeaders } from '@proton/shared/lib/fetch/headers';
3 import { wait } from '@proton/shared/lib/helpers/promise';
5 import { METRICS_DEFAULT_RETRY_SECONDS, METRICS_MAX_ATTEMPTS, METRICS_REQUEST_TIMEOUT_SECONDS } from './../constants';
6 import type IMetricsApi from './types/IMetricsApi';
8 class TooManyRequestsError extends Error {
10 super('Too many requests');
11 this.name = 'TooManyRequestsError';
15 class MetricsApi implements IMetricsApi {
16 private _authHeaders: { [key: string]: string };
18 private _versionHeaders: { [key: string]: string };
20 constructor({ uid, clientID, appVersion }: { uid?: string; clientID?: string; appVersion?: string } = {}) {
21 this._authHeaders = this.getAuthHeaders(uid);
22 this._versionHeaders = this.getVersionHeaders(clientID, appVersion);
25 public setAuthHeaders(uid: string, accessToken?: string) {
26 this._authHeaders = this.getAuthHeaders(uid, accessToken);
29 public setVersionHeaders(clientID: string, appVersion: string) {
30 this._versionHeaders = this.getVersionHeaders(clientID, appVersion);
33 private async _fetchWithTimeout(requestInfo: RequestInfo | URL, requestInit: RequestInit) {
34 const abortController = new AbortController();
36 const timeoutId = setTimeout(() => {
37 abortController.abort();
38 }, METRICS_REQUEST_TIMEOUT_SECONDS * SECOND);
40 return fetch(requestInfo, {
41 signal: abortController.signal,
44 clearTimeout(timeoutId);
49 requestInfo: RequestInfo | URL,
50 requestInit: RequestInit = {},
52 ): Promise<Response | undefined> {
54 const response = await this._fetchWithTimeout(requestInfo, {
57 ...requestInit?.headers,
58 'content-type': 'application/json',
61 ...this._versionHeaders,
65 if (attempt >= METRICS_MAX_ATTEMPTS) {
66 throw new TooManyRequestsError();
69 if (response.status === HTTP_STATUS_CODE.TOO_MANY_REQUESTS) {
70 const retryAfterSeconds = (() => {
71 const retryHeader = response?.headers?.get('retry-after') || '';
73 const parsedInt = parseInt(retryHeader, 10);
75 if (parsedInt <= 0 || isNaN(parsedInt)) {
76 return METRICS_DEFAULT_RETRY_SECONDS;
82 await wait(retryAfterSeconds * SECOND);
83 return await this.fetch(requestInfo, requestInit, attempt + 1);
87 } catch (error: any) {
88 if (error.name !== 'AbortError' || error.name === 'TooManyRequestsError') {
92 if (attempt >= METRICS_MAX_ATTEMPTS) {
93 throw new TooManyRequestsError();
96 await wait(METRICS_DEFAULT_RETRY_SECONDS * SECOND);
97 return this.fetch(requestInfo, requestInit, attempt + 1);
101 private getAuthHeaders(uid?: string, accessToken?: string) {
106 if (uid && accessToken) {
107 return getAuthHeaders(uid, accessToken);
110 return getUIDHeaders(uid);
113 private getVersionHeaders(clientID?: string, appVersion?: string) {
114 if (!clientID || !appVersion) {
118 return getAppVersionHeaders(clientID, appVersion);
122 export default MetricsApi;