diff --git a/src/app/network/UploadFolderManager.test.ts b/src/app/network/UploadFolderManager.test.ts index 53579bd136..316ec73098 100644 --- a/src/app/network/UploadFolderManager.test.ts +++ b/src/app/network/UploadFolderManager.test.ts @@ -7,6 +7,7 @@ import { getUniqueFolderName } from 'app/store/slices/storage/folderUtils/getUni import tasksService from 'app/tasks/services/tasks.service'; import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { TaskFolder, UploadFoldersManager, uploadFoldersWithManager } from './UploadFolderManager'; +import * as networkInformation from './networkInformation'; vi.mock('app/drive/services/new-storage.service', () => ({ default: { @@ -68,6 +69,10 @@ vi.mock('services/referral.service', () => ({ }, })); +vi.mock('./networkInformation', () => ({ + logNetworkInfoForUpload: vi.fn(), +})); + vi.mock('services/error.service', () => ({ default: { castError: vi.fn().mockImplementation((e) => e), @@ -300,6 +305,64 @@ describe('checkUploadFolders', () => { expect(renameFolderSpy).not.toHaveBeenCalled(); }); + it('should log network information on successful folder upload', async () => { + const logNetworkInfoMock = networkInformation.logNetworkInfoForUpload as Mock; + const mockFolder: DriveFolderData = { + id: 0, + uuid: 'uuid', + name: 'MyFolder', + bucket: 'bucket', + parentId: 0, + parent_id: 0, + parentUuid: 'parentUuid', + userId: 0, + user_id: 0, + icon: null, + iconId: null, + icon_id: null, + isFolder: true, + color: null, + encrypt_version: null, + plain_name: 'MyFolder', + deleted: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + const taskId = 'task-id'; + + (createFolder as Mock).mockResolvedValueOnce(mockFolder); + (checkFolderDuplicated as Mock).mockResolvedValueOnce({ + duplicatedFoldersResponse: [], + foldersWithDuplicates: [], + foldersWithoutDuplicates: [mockFolder], + }); + vi.spyOn(tasksService, 'create').mockReturnValue(taskId); + vi.spyOn(tasksService, 'updateTask').mockReturnValue(); + vi.spyOn(tasksService, 'addListener').mockReturnValue(); + vi.spyOn(tasksService, 'removeListener').mockReturnValue(); + + await uploadFoldersWithManager({ + payload: [ + { + currentFolderId: 'currentFolderId', + root: { + folderId: mockFolder.uuid, + childrenFiles: [], + childrenFolders: [], + name: mockFolder.name, + fullPathEdited: 'path1', + }, + options: { taskId }, + }, + ], + selectedWorkspace: null, + dispatch: mockDispatch, + }); + + expect(logNetworkInfoMock).toHaveBeenCalledOnce(); + expect(logNetworkInfoMock).toHaveBeenCalledWith({ folderName: 'MyFolder' }); + }); + it('should abort the upload if abortController is called', async () => { const mockParentFolder: DriveFolderData = { id: 1, diff --git a/src/app/network/UploadFolderManager.ts b/src/app/network/UploadFolderManager.ts index 5d3b3e7aea..ecd71db340 100644 --- a/src/app/network/UploadFolderManager.ts +++ b/src/app/network/UploadFolderManager.ts @@ -19,6 +19,7 @@ import { QueueUtilsService } from 'utils/queueUtils'; import { wait } from 'utils/timeUtils'; import { ConnectionLostError } from './requests'; import referralService from 'services/referral.service'; +import { logNetworkInfoForUpload } from './networkInformation'; interface UploadFolderPayload { root: IRoot; @@ -403,6 +404,7 @@ export class UploadFoldersManager { options.onSuccess?.(); referralService.trackFolderUpload(); + logNetworkInfoForUpload({ folderName: root.name }); setTimeout(() => { this.dispatch(planThunks.fetchUsageThunk()); diff --git a/src/app/network/UploadManager.test.ts b/src/app/network/UploadManager.test.ts index 45edfd10eb..073eb84937 100644 --- a/src/app/network/UploadManager.test.ts +++ b/src/app/network/UploadManager.test.ts @@ -9,6 +9,7 @@ import { DriveFileData } from 'app/drive/types'; import RetryManager from './RetryManager'; import { TaskStatus } from 'app/tasks/types'; import { ErrorMessages } from 'app/core/constants'; +import * as networkInformation from './networkInformation'; vi.mock('app/drive/services/file.service/uploadFile', () => ({ default: vi.fn(() => Promise.resolve({} as DriveFileData)), @@ -51,6 +52,16 @@ vi.mock('app/repositories/DatabaseUploadRepository', () => { vi.mock('i18next', () => ({ default: { language: 'en' }, t: () => 'Translation message' })); +vi.mock('./networkInformation', () => ({ + logNetworkInfoForUpload: vi.fn(), +})); + +vi.mock('services/referral.service', () => ({ + default: { + trackFileUpload: vi.fn(), + }, +})); + const openMaxSpaceOccupiedDialogMock = vi.fn(); const mockFile1 = { @@ -622,6 +633,70 @@ describe('checkUploadFiles', () => { ); }); + it('should log network information on successful file upload', async () => { + const logNetworkInfoMock = networkInformation.logNetworkInfoForUpload as Mock; + (uploadFile as Mock).mockResolvedValueOnce(mockFile1); + vi.spyOn(tasksService, 'create').mockReturnValue('taskId'); + vi.spyOn(tasksService, 'updateTask').mockReturnValue(); + vi.spyOn(tasksService, 'addListener').mockReturnValue(); + vi.spyOn(tasksService, 'removeListener').mockReturnValue(); + + await uploadFileWithManager( + [ + { + taskId: 'taskId', + filecontent: { + content: 'file-content' as unknown as File, + type: 'text/plain', + name: 'file.txt', + size: 1024, + parentFolderId: 'folder-1', + }, + userEmail: '', + parentFolderId: '', + }, + ], + openMaxSpaceOccupiedDialogMock, + DatabaseUploadRepository.getInstance(), + ); + + expect(logNetworkInfoMock).toHaveBeenCalledOnce(); + expect(logNetworkInfoMock).toHaveBeenCalledWith({ fileName: 'file.txt', fileSize: 1024 }); + }); + + it('should not log network information when upload fails', async () => { + const logNetworkInfoMock = networkInformation.logNetworkInfoForUpload as Mock; + (uploadFile as Mock).mockRejectedValue(new AppError('Upload failed')); + vi.spyOn(tasksService, 'create').mockReturnValue('taskId'); + vi.spyOn(tasksService, 'updateTask').mockReturnValue(); + vi.spyOn(tasksService, 'addListener').mockReturnValue(); + vi.spyOn(tasksService, 'removeListener').mockReturnValue(); + vi.spyOn(errorService, 'reportError').mockReturnValue(); + + await expect( + uploadFileWithManager( + [ + { + taskId: 'taskId', + filecontent: { + content: 'file-content' as unknown as File, + type: 'text/plain', + name: 'file.txt', + size: 1024, + parentFolderId: 'folder-1', + }, + userEmail: '', + parentFolderId: '', + }, + ], + openMaxSpaceOccupiedDialogMock, + DatabaseUploadRepository.getInstance(), + ), + ).rejects.toThrow(); + + expect(logNetworkInfoMock).not.toHaveBeenCalled(); + }); + it('When uploading a personal file, then it uses personal credentials', async () => { const uploadFileSpy = (uploadFile as Mock).mockResolvedValueOnce(mockFile1); diff --git a/src/app/network/UploadManager.ts b/src/app/network/UploadManager.ts index d10f621c8f..ac19fd8263 100644 --- a/src/app/network/UploadManager.ts +++ b/src/app/network/UploadManager.ts @@ -15,6 +15,7 @@ import { ErrorMessages } from 'app/core/constants'; import { MAX_UPLOAD_ATTEMPTS, TWENTY_MEGABYTES, USE_MULTIPART_THRESHOLD_BYTES } from './networkConstants'; import { OwnerUserAuthenticationData } from './types'; import referralService from 'services/referral.service'; +import { logNetworkInfoForUpload } from './networkInformation'; enum FileSizeType { Big = 'big', @@ -229,6 +230,7 @@ class UploadManager { fileData.onFinishUploadFile?.(driveFileDataWithNameParsed, taskId); referralService.trackFileUpload(); + logNetworkInfoForUpload({ fileName: file.name, fileSize: file.size }); if (this.onFileUploadCallback) { this.onFileUploadCallback(driveFileDataWithNameParsed); diff --git a/src/app/network/networkInformation.test.ts b/src/app/network/networkInformation.test.ts new file mode 100644 index 0000000000..9fe1680c55 --- /dev/null +++ b/src/app/network/networkInformation.test.ts @@ -0,0 +1,128 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getNetworkInformation, logNetworkInfoForUpload } from './networkInformation'; + +const defineNavigatorConnection = (value: unknown) => { + Object.defineProperty(navigator, 'connection', { value, writable: true, configurable: true }); +}; + +describe('getNetworkInformation', () => { + beforeEach(() => { + defineNavigatorConnection(undefined); + }); + + it('returns null when the Network Information API is not supported', () => { + expect(getNetworkInformation()).toBeNull(); + }); + + it('returns all network info fields when navigator.connection is available', () => { + defineNavigatorConnection({ + type: 'wifi', + effectiveType: '4g', + downlink: 10, + downlinkMax: 100, + rtt: 50, + saveData: false, + }); + + expect(getNetworkInformation()).toEqual({ + type: 'wifi', + effectiveType: '4g', + downlink: 10, + downlinkMax: 100, + rtt: 50, + saveData: false, + }); + }); + + it('returns undefined fields when the connection object has no values', () => { + defineNavigatorConnection({}); + + expect(getNetworkInformation()).toEqual({ + type: undefined, + effectiveType: undefined, + downlink: undefined, + downlinkMax: undefined, + rtt: undefined, + saveData: undefined, + }); + }); + + it('captures saveData: true when the user has data saving enabled', () => { + defineNavigatorConnection({ + type: 'cellular', + effectiveType: '2g', + downlink: 0.5, + downlinkMax: 1, + rtt: 800, + saveData: true, + }); + + expect(getNetworkInformation()?.saveData).toBe(true); + }); +}); + +describe('logNetworkInfoForUpload', () => { + beforeEach(() => { + defineNavigatorConnection(undefined); + vi.restoreAllMocks(); + }); + + it('does not call console.log when the Network Information API is not supported', () => { + const consoleSpy = vi.spyOn(console, 'log'); + + logNetworkInfoForUpload({ fileName: 'file.txt', fileSize: 1024 }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('logs context and all network info fields together when the API is available', () => { + defineNavigatorConnection({ + type: 'wifi', + effectiveType: '4g', + downlink: 10, + downlinkMax: 100, + rtt: 50, + saveData: false, + }); + const consoleSpy = vi.spyOn(console, 'log'); + + logNetworkInfoForUpload({ fileName: 'file.txt', fileSize: 1024 }); + + expect(consoleSpy).toHaveBeenCalledOnce(); + expect(consoleSpy).toHaveBeenCalledWith('[Upload] Network information:', { + fileName: 'file.txt', + fileSize: 1024, + type: 'wifi', + effectiveType: '4g', + downlink: 10, + downlinkMax: 100, + rtt: 50, + saveData: false, + }); + }); + + it('logs context and network info for folder uploads', () => { + defineNavigatorConnection({ + type: 'cellular', + effectiveType: '3g', + downlink: 2, + downlinkMax: 10, + rtt: 200, + saveData: true, + }); + const consoleSpy = vi.spyOn(console, 'log'); + + logNetworkInfoForUpload({ folderName: 'MyFolder' }); + + expect(consoleSpy).toHaveBeenCalledOnce(); + expect(consoleSpy).toHaveBeenCalledWith('[Upload] Network information:', { + folderName: 'MyFolder', + type: 'cellular', + effectiveType: '3g', + downlink: 2, + downlinkMax: 10, + rtt: 200, + saveData: true, + }); + }); +}); diff --git a/src/app/network/networkInformation.ts b/src/app/network/networkInformation.ts new file mode 100644 index 0000000000..153567e71e --- /dev/null +++ b/src/app/network/networkInformation.ts @@ -0,0 +1,47 @@ +interface NetworkInformation { + type?: string; + effectiveType?: string; + downlink?: number; + downlinkMax?: number; + rtt?: number; + saveData?: boolean; +} + +interface NavigatorWithConnection extends Navigator { + connection?: NetworkInformation; + mozConnection?: NetworkInformation; + webkitConnection?: NetworkInformation; +} + +export type NetworkInfo = { + type: string | undefined; + effectiveType: string | undefined; + downlink: number | undefined; + downlinkMax: number | undefined; + rtt: number | undefined; + saveData: boolean | undefined; +}; + +export const getNetworkInformation = (): NetworkInfo | null => { + const nav = navigator as NavigatorWithConnection; + const connection = nav.connection ?? nav.mozConnection ?? nav.webkitConnection; + + if (!connection) return null; + + return { + type: connection.type, + effectiveType: connection.effectiveType, + downlink: connection.downlink, + downlinkMax: connection.downlinkMax, + rtt: connection.rtt, + saveData: connection.saveData, + }; +}; + +export const logNetworkInfoForUpload = (context: Record): void => { + const networkInfo = getNetworkInformation(); + + if (!networkInfo) return; + + console.log('[Upload] Network information:', { ...context, ...networkInfo }); +}; diff --git a/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts b/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts index cb7dba2671..da513af69b 100644 --- a/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts @@ -12,6 +12,7 @@ import { planThunks } from '../../plan'; import workspacesSelectors from '../../workspaces/workspaces.selectors'; import referralService from 'services/referral.service'; +import { logNetworkInfoForUpload } from 'app/network/networkInformation'; import { checkFolderDuplicated } from '../folderUtils/checkFolderDuplicated'; import { getUniqueFolderName } from '../folderUtils/getUniqueFolderName'; import { StorageState } from '../storage.model'; @@ -206,6 +207,7 @@ export const uploadFolderThunk = createAsyncThunk { dispatch(planThunks.fetchUsageThunk()); if (memberId) dispatch(planThunks.fetchBusinessLimitUsageThunk());