1 import { useRef } from 'react';
3 import metrics from '@proton/metrics';
10 } from '@proton/shared/lib/api/helpers/apiErrorHelper';
11 import { API_CUSTOM_ERROR_CODES } from '@proton/shared/lib/errors';
13 import { is4xx, is5xx } from '../../../utils/errorHandling/apiErrors';
14 import type { Share } from '../../_shares/interface';
15 import { ShareType } from '../../_shares/interface';
16 import useShareState from '../../_shares/useSharesState';
17 import { isVerificationError } from '../worker/verifier';
18 import type { FileUploadReady } from './interface';
20 // TODO: DRVWEB-4123 Unify Share Types
21 export enum UploadShareType {
28 export enum UploadErrorCategory {
29 FreeSpaceExceeded = 'free_space_exceeded',
30 TooManyChildren = 'too_many_children',
31 NetworkError = 'network_error',
32 ServerError = 'server_error',
33 IntegrityError = 'integrity_error',
35 RateLimited = 'rate_limited',
36 HTTPClientError = '4xx',
37 HTTPServerError = '5xx',
40 export interface FailedUploadMetadata {
42 numberOfErrors: number;
43 encryptedTotalTransferSize: number;
44 roundedUnencryptedFileSize: number;
47 const IGNORED_ERROR_CATEGORIES_FROM_SUCCESS_RATE = [
48 UploadErrorCategory.FreeSpaceExceeded,
49 UploadErrorCategory.TooManyChildren,
50 UploadErrorCategory.NetworkError,
53 const REPORT_ERROR_USERS_EVERY = 5 * 60 * 1000; // 5 minutes,
55 const ROUND_BYTES = 10000; // For privacy we round file.size metrics to 10k bytes
57 export const getFailedUploadMetadata = (
58 nextFileUpload: FileUploadReady,
62 ): FailedUploadMetadata => ({
63 shareId: nextFileUpload.shareId,
64 numberOfErrors: nextFileUpload.numberOfErrors,
65 encryptedTotalTransferSize: Object.values(progresses).reduce((sum, value) => sum + value, 0),
66 roundedUnencryptedFileSize: Math.max(Math.round(nextFileUpload.file.size / ROUND_BYTES) * ROUND_BYTES, ROUND_BYTES),
69 export default function useUploadMetrics(isPaid: boolean, metricsModule = metrics) {
70 const lastErroringUserReport = useRef(0);
72 // Hack: ideally we should use useShare. But that adds complexity with
73 // promises and need to handle exceptions etc. that are not essential
74 // for metrics, and when file is uploading, we know the share must be
75 // already in cache. (to be continued...)
76 const { getShare } = useShareState();
78 const getShareIdType = (shareId: string): UploadShareType => {
79 const share = getShare(shareId);
80 return getShareType(share);
83 const uploadSucceeded = (shareId: string, numberOfErrors = 0) => {
84 const shareType = getShareIdType(shareId);
85 const retry = numberOfErrors > 1;
87 metricsModule.drive_upload_success_rate_total.increment({
90 retry: retry ? 'true' : 'false',
91 initiator: 'explicit',
95 const uploadFailed = (failedUploadMetadata: FailedUploadMetadata, error: any) => {
96 const shareType = getShareIdType(failedUploadMetadata.shareId);
97 const errorCategory = getErrorCategory(error);
98 const retry = failedUploadMetadata.numberOfErrors > 1;
100 if (!IGNORED_ERROR_CATEGORIES_FROM_SUCCESS_RATE.includes(errorCategory)) {
101 metricsModule.drive_upload_success_rate_total.increment({
104 retry: retry ? 'true' : 'false',
105 initiator: 'explicit',
109 metricsModule.drive_upload_errors_total.increment({
111 shareType: shareType === UploadShareType.Own ? 'main' : shareType,
112 initiator: 'explicit',
115 // How many encrypted bytes were sent before it failed
116 metricsModule.drive_upload_errors_transfer_size_histogram.observe({
117 Value: failedUploadMetadata.encryptedTotalTransferSize,
121 // Rounded unencrypted file size of the file that failed the upload
122 metricsModule.drive_upload_errors_file_size_histogram.observe({
123 Value: failedUploadMetadata.roundedUnencryptedFileSize,
127 if (Date.now() - lastErroringUserReport.current > REPORT_ERROR_USERS_EVERY) {
128 metricsModule.drive_upload_erroring_users_total.increment({
129 plan: isPaid ? 'paid' : 'free',
131 initiator: 'explicit',
134 lastErroringUserReport.current = Date.now();
144 export function getShareType(share?: Share): UploadShareType {
145 // (see above...) But if the share is not there anyway, we need to
146 // still decide about share type. Own shares are always loaded by
147 // default, so we can bet that its not own/device/photo and thus
148 // we can set its shared one.
150 return UploadShareType.Shared;
153 if (share.type === ShareType.default) {
154 return UploadShareType.Own;
155 } else if (share.type === ShareType.photos) {
156 return UploadShareType.Photo;
157 } else if (share.type === ShareType.device) {
158 return UploadShareType.Device;
160 return UploadShareType.Shared;
163 export function getErrorCategory(error: any): UploadErrorCategory {
164 const apiError = getApiError(error);
165 const statusCode = apiError?.code || error?.statusCode;
167 if (statusCode === API_CUSTOM_ERROR_CODES.TOO_MANY_CHILDREN) {
168 return UploadErrorCategory.TooManyChildren;
169 } else if (statusCode === API_CUSTOM_ERROR_CODES.FREE_SPACE_EXCEEDED) {
170 return UploadErrorCategory.FreeSpaceExceeded;
171 } else if (getIsUnreachableError(error) || getIsTimeoutError(error)) {
172 return UploadErrorCategory.ServerError;
173 } else if (getIsOfflineError(error) || getIsNetworkError(error)) {
174 return UploadErrorCategory.NetworkError;
175 } else if (isVerificationError(error)) {
176 return UploadErrorCategory.IntegrityError;
177 } else if (error?.statusCode && error?.statusCode === 429) {
178 return UploadErrorCategory.RateLimited;
179 } else if (error?.statusCode && is4xx(error?.statusCode)) {
180 return UploadErrorCategory.HTTPClientError;
181 } else if (error?.statusCode && is5xx(error?.statusCode)) {
182 return UploadErrorCategory.HTTPServerError;
184 return UploadErrorCategory.Unknown;