diff --git a/package-lock.json b/package-lock.json index 22ee2532..a5865aea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "earthdata-download", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "earthdata-download", - "version": "0.1.0", + "version": "0.1.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -6356,15 +6356,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "0.27.2", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, "node_modules/axobject-query": { "version": "3.1.1", "dev": true, @@ -18495,6 +18486,16 @@ "node": ">=12.0.0" } }, + "node_modules/wait-on/node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, "node_modules/walker": { "version": "1.0.8", "dev": true, @@ -23050,14 +23051,6 @@ "version": "4.7.0", "dev": true }, - "axios": { - "version": "0.27.2", - "dev": true, - "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, "axobject-query": { "version": "3.1.1", "dev": true, @@ -31113,6 +31106,18 @@ "lodash": "^4.17.21", "minimist": "^1.2.7", "rxjs": "^7.8.0" + }, + "dependencies": { + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + } } }, "walker": { diff --git a/package.json b/package.json index 033af3d6..cfc7cbfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "earthdata-download", - "version": "0.1.0", + "version": "0.1.1", "description": "Earthdata Download is a cross-platform download manager designed to improve how users download Earth Science data. It accepts lists of files from applications like Earthdata Search (https://search.earthdata.nasa.gov/) and enables clients to offer users a streamlined experience when downloading files from their browser.", "repository": "nasa/earthdata-download", "homepage": "https://github.com/nasa/earthdata-download#readme", diff --git a/src/main/config.json b/src/main/config.json new file mode 100644 index 00000000..dd8cbe4a --- /dev/null +++ b/src/main/config.json @@ -0,0 +1,4 @@ +{ + "note": "This endpoint is for EDSC-UAT CloudWatch", + "logging": "https://dycghwhsgr9el.cloudfront.net/edd_logger" +} diff --git a/src/main/eventHandlers/__tests__/cancelDownloadItem.test.ts b/src/main/eventHandlers/__tests__/cancelDownloadItem.test.ts index 4cfbaed3..e7793bf7 100644 --- a/src/main/eventHandlers/__tests__/cancelDownloadItem.test.ts +++ b/src/main/eventHandlers/__tests__/cancelDownloadItem.test.ts @@ -5,11 +5,17 @@ import MockDate from 'mockdate' import cancelDownloadItem from '../cancelDownloadItem' import downloadStates from '../../../app/constants/downloadStates' +import metricsLogger from '../../utils/metricsLogger.ts' beforeEach(() => { MockDate.set('2023-05-01') }) +jest.mock('../../utils/metricsLogger.ts', () => ({ + __esModule: true, + default: jest.fn(() => {}) +})) + describe('cancelDownloadItem', () => { describe('when downloadId and name are provided', () => { test('updates the file state', async () => { @@ -34,6 +40,14 @@ describe('cancelDownloadItem', () => { info }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadItemCancel', + data: { + downloadId: 'mock-download-id' + } + }) + expect(currentDownloadItems.cancelItem).toHaveBeenCalledTimes(1) expect(currentDownloadItems.cancelItem).toHaveBeenCalledWith('mock-download-id', 'mock-filename.png') @@ -76,6 +90,15 @@ describe('cancelDownloadItem', () => { info }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadCancel', + data: { + downloadIds: ['mock-download-id'], + cancelCount: 1 + } + }) + expect(currentDownloadItems.cancelItem).toHaveBeenCalledTimes(1) expect(currentDownloadItems.cancelItem).toHaveBeenCalledWith('mock-download-id', undefined) @@ -125,6 +148,15 @@ describe('cancelDownloadItem', () => { info }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadCancel', + data: { + downloadIds: [123, 456], + cancelCount: 2 + } + }) + expect(currentDownloadItems.cancelItem).toHaveBeenCalledTimes(1) expect(currentDownloadItems.cancelItem).toHaveBeenCalledWith(undefined, undefined) diff --git a/src/main/eventHandlers/__tests__/pauseDownloadItem.test.ts b/src/main/eventHandlers/__tests__/pauseDownloadItem.test.ts index d47d113f..5a930f4b 100644 --- a/src/main/eventHandlers/__tests__/pauseDownloadItem.test.ts +++ b/src/main/eventHandlers/__tests__/pauseDownloadItem.test.ts @@ -2,6 +2,12 @@ import pauseDownloadItem from '../pauseDownloadItem' import downloadStates from '../../../app/constants/downloadStates' +import metricsLogger from '../../utils/metricsLogger.ts' + +jest.mock('../../utils/metricsLogger.ts', () => ({ + __esModule: true, + default: jest.fn(() => {}) +})) describe('pauseDownloadItem', () => { describe('when downloadId and name are provided', () => { @@ -19,7 +25,9 @@ describe('pauseDownloadItem', () => { updateFilesWhere: jest.fn(), createPauseByDownloadIdAndFilename: jest.fn(), createPauseByDownloadId: jest.fn(), - createPauseForAllActiveDownloads: jest.fn() + createPauseForAllActiveDownloads: jest.fn().mockResolvedValue({ + pausedIds: ['mock-download-id-1', 'mock-download-id-2'] + }) } await pauseDownloadItem({ @@ -28,6 +36,8 @@ describe('pauseDownloadItem', () => { info }) + expect(metricsLogger).toHaveBeenCalledTimes(0) + expect(currentDownloadItems.pauseItem).toHaveBeenCalledTimes(1) expect(currentDownloadItems.pauseItem).toHaveBeenCalledWith('mock-download-id', 'mock-filename.png') @@ -63,7 +73,9 @@ describe('pauseDownloadItem', () => { updateFilesWhere: jest.fn(), createPauseByDownloadIdAndFilename: jest.fn(), createPauseByDownloadId: jest.fn(), - createPauseForAllActiveDownloads: jest.fn() + createPauseForAllActiveDownloads: jest.fn().mockResolvedValue({ + pausedIds: ['mock-download-id-1', 'mock-download-id-2'] + }) } await pauseDownloadItem({ @@ -72,6 +84,15 @@ describe('pauseDownloadItem', () => { info }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadPause', + data: { + downloadCount: 1, + downloadIds: ['mock-download-id'] + } + }) + expect(currentDownloadItems.pauseItem).toHaveBeenCalledTimes(1) expect(currentDownloadItems.pauseItem).toHaveBeenCalledWith('mock-download-id', undefined) @@ -101,7 +122,9 @@ describe('pauseDownloadItem', () => { updateFilesWhere: jest.fn(), createPauseByDownloadIdAndFilename: jest.fn(), createPauseByDownloadId: jest.fn(), - createPauseForAllActiveDownloads: jest.fn() + createPauseForAllActiveDownloads: jest.fn().mockResolvedValue({ + pausedIds: ['mock-download-id-1', 'mock-download-id-2'] + }) } await pauseDownloadItem({ @@ -110,6 +133,14 @@ describe('pauseDownloadItem', () => { info }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadPause', + data: { + downloadIds: ['mock-download-id-1', 'mock-download-id-2'] + } + }) + expect(currentDownloadItems.pauseItem).toHaveBeenCalledTimes(1) expect(currentDownloadItems.pauseItem).toHaveBeenCalledWith(undefined, undefined) diff --git a/src/main/eventHandlers/__tests__/restartDownload.test.ts b/src/main/eventHandlers/__tests__/restartDownload.test.ts index c08f9f0f..55dc2053 100644 --- a/src/main/eventHandlers/__tests__/restartDownload.test.ts +++ b/src/main/eventHandlers/__tests__/restartDownload.test.ts @@ -7,12 +7,18 @@ import downloadStates from '../../../app/constants/downloadStates' import restartDownload from '../restartDownload' import startNextDownload from '../../utils/startNextDownload' +import metricsLogger from '../../utils/metricsLogger' jest.mock('../../utils/startNextDownload', () => ({ __esModule: true, default: jest.fn(() => {}) })) +jest.mock('../../utils/metricsLogger.ts', () => ({ + __esModule: true, + default: jest.fn(() => {}) +})) + beforeEach(() => { MockDate.set('2023-05-01') @@ -32,7 +38,11 @@ describe('restartDownload', () => { updateDownloadById: jest.fn(), updateFilesWhere: jest.fn(), deletePausesByDownloadIdAndFilename: jest.fn(), - deleteAllPausesByDownloadId: jest.fn() + deleteAllPausesByDownloadId: jest.fn(), + getDownloadReport: jest.fn().mockResolvedValue({ + finishedFiles: 8, + totalFiles: 10 + }) } const downloadIdContext = {} const webContents = {} @@ -49,6 +59,16 @@ describe('restartDownload', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadRestart', + data: { + downloadId: 'mock-download-id', + filesCompleted: 8, + filesInProgress: 2 + } + }) + expect(currentDownloadItems.cancelItem).toHaveBeenCalledTimes(1) expect(currentDownloadItems.cancelItem).toHaveBeenCalledWith('mock-download-id', undefined) @@ -57,6 +77,8 @@ describe('restartDownload', () => { expect(database.deletePausesByDownloadIdAndFilename).toHaveBeenCalledTimes(0) + expect(database.getDownloadReport).toHaveBeenCalledTimes(1) + expect(database.deleteAllPausesByDownloadId).toHaveBeenCalledTimes(1) expect(database.deleteAllPausesByDownloadId).toHaveBeenCalledWith('mock-download-id') @@ -111,7 +133,11 @@ describe('restartDownload', () => { updateDownloadById: jest.fn(), updateFilesWhere: jest.fn(), deletePausesByDownloadIdAndFilename: jest.fn(), - deleteAllPausesByDownloadId: jest.fn() + deleteAllPausesByDownloadId: jest.fn(), + getDownloadReport: jest.fn().mockResolvedValue({ + finishedFiles: 8, + totalFiles: 10 + }) } const downloadIdContext = {} const webContents = {} @@ -129,6 +155,16 @@ describe('restartDownload', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadRestart', + data: { + downloadId: 'mock-download-id', + filesCompleted: 8, + filesInProgress: 2 + } + }) + expect(currentDownloadItems.cancelItem).toHaveBeenCalledTimes(1) expect(currentDownloadItems.cancelItem).toHaveBeenCalledWith('mock-download-id', 'mock-filename') @@ -142,6 +178,8 @@ describe('restartDownload', () => { expect(database.deleteAllPausesByDownloadId).toHaveBeenCalledTimes(0) + expect(database.getDownloadReport).toHaveBeenCalledTimes(1) + expect(database.updateFilesWhere).toHaveBeenCalledTimes(2) expect(database.updateFilesWhere).toHaveBeenCalledWith({ restartId: 'mock-restart-id' @@ -188,7 +226,11 @@ describe('restartDownload', () => { updateDownloadById: jest.fn(), updateFilesWhere: jest.fn(), deletePausesByDownloadIdAndFilename: jest.fn(), - deleteAllPausesByDownloadId: jest.fn() + deleteAllPausesByDownloadId: jest.fn(), + getDownloadReport: jest.fn().mockResolvedValue({ + finishedFiles: 8, + totalFiles: 10 + }) } const downloadIdContext = {} const webContents = {} @@ -206,12 +248,24 @@ describe('restartDownload', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadRestart', + data: { + downloadId: 'mock-download-id', + filesCompleted: 8, + filesInProgress: 2 + } + }) + expect(currentDownloadItems.cancelItem).toHaveBeenCalledTimes(1) expect(currentDownloadItems.cancelItem).toHaveBeenCalledWith('mock-download-id', 'mock-filename') expect(database.getDownloadById).toHaveBeenCalledTimes(1) expect(database.getDownloadById).toHaveBeenCalledWith('mock-download-id') + expect(database.getDownloadReport).toHaveBeenCalledTimes(1) + expect(database.createPauseWith).toHaveBeenCalledTimes(1) expect(database.createPauseWith).toHaveBeenCalledWith({ downloadId: 'mock-download-id', diff --git a/src/main/eventHandlers/__tests__/resumeDownloadItem.test.ts b/src/main/eventHandlers/__tests__/resumeDownloadItem.test.ts index decc6d82..835775ec 100644 --- a/src/main/eventHandlers/__tests__/resumeDownloadItem.test.ts +++ b/src/main/eventHandlers/__tests__/resumeDownloadItem.test.ts @@ -3,12 +3,18 @@ import resumeDownloadItem from '../resumeDownloadItem' import downloadStates from '../../../app/constants/downloadStates' import startNextDownload from '../../utils/startNextDownload' +import metricsLogger from '../../utils/metricsLogger.ts' jest.mock('../../utils/startNextDownload', () => ({ __esModule: true, default: jest.fn(() => {}) })) +jest.mock('../../utils/metricsLogger.ts', () => ({ + __esModule: true, + default: jest.fn(() => {}) +})) + describe('resumeDownloadItem', () => { describe('when downloadId and name are provided', () => { test('calls currentDownloadItems.resumeItem', async () => { @@ -20,7 +26,16 @@ describe('resumeDownloadItem', () => { filename: 'mock-filename.png' } const database = { - getAllDownloadsWhere: jest.fn(), + getAllDownloadsWhere: jest.fn().mockResolvedValue([ + { + id: '7072_Test_2019.0-20231109_032409', + state: 'PAUSED' + }, + { + id: 'AE_DySno_002-20231010_140411', + state: 'PAUSED' + } + ]), getFileCountWhere: jest.fn(), updateDownloadById: jest.fn(), updateFilesWhere: jest.fn(), @@ -35,6 +50,8 @@ describe('resumeDownloadItem', () => { webContents: {} }) + expect(metricsLogger).toHaveBeenCalledTimes(0) + expect(currentDownloadItems.resumeItem).toHaveBeenCalledTimes(1) expect(currentDownloadItems.resumeItem).toHaveBeenCalledWith('mock-download-id', 'mock-filename.png') @@ -72,7 +89,16 @@ describe('resumeDownloadItem', () => { downloadId: 'mock-download-id' } const database = { - getAllDownloadsWhere: jest.fn(), + getAllDownloadsWhere: jest.fn().mockResolvedValue([ + { + id: '7072_Test_2019.0-20231109_032409', + state: 'PAUSED' + }, + { + id: 'AE_DySno_002-20231010_140411', + state: 'PAUSED' + } + ]), getFileCountWhere: jest.fn().mockResolvedValue(0), updateDownloadById: jest.fn(), endPause: jest.fn() @@ -86,11 +112,20 @@ describe('resumeDownloadItem', () => { webContents: {} }) - expect(currentDownloadItems.resumeItem).toHaveBeenCalledTimes(1) - expect(currentDownloadItems.resumeItem).toHaveBeenCalledWith('mock-download-id', undefined) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadResume', + data: { + downloadIds: ['mock-download-id'], + downloadCount: 1 + } + }) expect(database.getAllDownloadsWhere).toHaveBeenCalledTimes(0) + expect(currentDownloadItems.resumeItem).toHaveBeenCalledTimes(1) + expect(currentDownloadItems.resumeItem).toHaveBeenCalledWith('mock-download-id', undefined) + expect(database.getFileCountWhere).toHaveBeenCalledTimes(1) expect(database.getFileCountWhere).toHaveBeenCalledWith({ downloadId: 'mock-download-id' }) @@ -120,7 +155,16 @@ describe('resumeDownloadItem', () => { downloadId: 'mock-download-id' } const database = { - getAllDownloadsWhere: jest.fn(), + getAllDownloadsWhere: jest.fn().mockResolvedValue([ + { + id: '7072_Test_2019.0-20231109_032409', + state: 'PAUSED' + }, + { + id: 'AE_DySno_002-20231010_140411', + state: 'PAUSED' + } + ]), getFileCountWhere: jest.fn().mockResolvedValue(1), updateDownloadById: jest.fn(), endPause: jest.fn() @@ -134,11 +178,20 @@ describe('resumeDownloadItem', () => { webContents: {} }) - expect(currentDownloadItems.resumeItem).toHaveBeenCalledTimes(1) - expect(currentDownloadItems.resumeItem).toHaveBeenCalledWith('mock-download-id', undefined) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadResume', + data: { + downloadIds: ['mock-download-id'], + downloadCount: 1 + } + }) expect(database.getAllDownloadsWhere).toHaveBeenCalledTimes(0) + expect(currentDownloadItems.resumeItem).toHaveBeenCalledTimes(1) + expect(currentDownloadItems.resumeItem).toHaveBeenCalledWith('mock-download-id', undefined) + expect(database.getFileCountWhere).toHaveBeenCalledTimes(1) expect(database.getFileCountWhere).toHaveBeenCalledWith({ downloadId: 'mock-download-id' }) @@ -193,6 +246,15 @@ describe('resumeDownloadItem', () => { webContents: {} }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadResume', + data: { + downloadIds: ['download1', 'download2', 'download3'], + downloadCount: 3 + } + }) + expect(currentDownloadItems.resumeItem).toHaveBeenCalledTimes(1) expect(currentDownloadItems.resumeItem).toHaveBeenCalledWith(undefined, undefined) diff --git a/src/main/eventHandlers/__tests__/sendToEula.test.ts b/src/main/eventHandlers/__tests__/sendToEula.test.ts index 649a1780..1021f7e9 100644 --- a/src/main/eventHandlers/__tests__/sendToEula.test.ts +++ b/src/main/eventHandlers/__tests__/sendToEula.test.ts @@ -5,6 +5,7 @@ import { shell } from 'electron' import sendToEula from '../sendToEula' import downloadStates from '../../../app/constants/downloadStates' +import metricsLogger from '../../utils/metricsLogger' jest.mock('electronShell', () => ({ shell: { @@ -12,6 +13,11 @@ jest.mock('electronShell', () => ({ } })) +jest.mock('../../utils/metricsLogger.ts', () => ({ + __esModule: true, + default: jest.fn(() => {}) +})) + describe('sendToEula', () => { test('opens the eulaRedirectUrl', async () => { const database = { @@ -38,6 +44,14 @@ describe('sendToEula', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'SentToEula', + data: { + downloadId: 'downloadID' + } + }) + expect(database.getFileWhere).toHaveBeenCalledTimes(0) expect(shell.openExternal).toHaveBeenCalledTimes(1) @@ -84,6 +98,14 @@ describe('sendToEula', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'SentToEula', + data: { + downloadId: 'downloadID' + } + }) + expect(database.getFileWhere).toHaveBeenCalledTimes(1) expect(database.getFileWhere).toHaveBeenCalledWith({ downloadId: 'downloadID', @@ -136,6 +158,8 @@ describe('sendToEula', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(0) + expect(database.getFileWhere).toHaveBeenCalledTimes(0) expect(shell.openExternal).toHaveBeenCalledTimes(0) expect(webContents.send).toHaveBeenCalledTimes(0) @@ -177,6 +201,14 @@ describe('sendToEula', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'SentToEula', + data: { + downloadId: 'downloadID' + } + }) + expect(database.getFileWhere).toHaveBeenCalledTimes(0) expect(shell.openExternal).toHaveBeenCalledTimes(1) diff --git a/src/main/eventHandlers/__tests__/sendToLogin.test.ts b/src/main/eventHandlers/__tests__/sendToLogin.test.ts index 89e08f09..f49ed604 100644 --- a/src/main/eventHandlers/__tests__/sendToLogin.test.ts +++ b/src/main/eventHandlers/__tests__/sendToLogin.test.ts @@ -5,6 +5,7 @@ import { shell } from 'electron' import sendToLogin from '../sendToLogin' import downloadStates from '../../../app/constants/downloadStates' +import metricsLogger from '../../utils/metricsLogger' jest.mock('electronShell', () => ({ shell: { @@ -12,6 +13,11 @@ jest.mock('electronShell', () => ({ } })) +jest.mock('../../utils/metricsLogger.ts', () => ({ + __esModule: true, + default: jest.fn(() => {}) +})) + describe('sendToLogin', () => { test('opens the authUrl', async () => { const database = { @@ -37,6 +43,14 @@ describe('sendToLogin', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'SentToEdl', + data: { + downloadId: 'downloadID' + } + }) + expect(database.getFileWhere).toHaveBeenCalledTimes(0) expect(shell.openExternal).toHaveBeenCalledTimes(1) @@ -82,6 +96,14 @@ describe('sendToLogin', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'SentToEdl', + data: { + downloadId: 'downloadID' + } + }) + expect(database.getFileWhere).toHaveBeenCalledTimes(1) expect(database.getFileWhere).toHaveBeenCalledWith({ downloadId: 'downloadID', @@ -133,6 +155,8 @@ describe('sendToLogin', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(0) + expect(database.getFileWhere).toHaveBeenCalledTimes(0) expect(shell.openExternal).toHaveBeenCalledTimes(0) expect(webContents.send).toHaveBeenCalledTimes(0) @@ -173,6 +197,14 @@ describe('sendToLogin', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'SentToEdl', + data: { + downloadId: 'downloadID' + } + }) + expect(database.getFileWhere).toHaveBeenCalledTimes(0) expect(shell.openExternal).toHaveBeenCalledTimes(1) diff --git a/src/main/eventHandlers/__tests__/setPreferenceFieldValue.test.ts b/src/main/eventHandlers/__tests__/setPreferenceFieldValue.test.ts index 7e12f646..1575690b 100644 --- a/src/main/eventHandlers/__tests__/setPreferenceFieldValue.test.ts +++ b/src/main/eventHandlers/__tests__/setPreferenceFieldValue.test.ts @@ -1,11 +1,18 @@ // @ts-nocheck +import metricsLogger from '../../utils/metricsLogger' import setPreferenceFieldValue from '../setPreferenceFieldValue' +jest.mock('../../utils/metricsLogger.ts', () => ({ + __esModule: true, + default: jest.fn(() => {}) +})) + describe('set a field in the preferences', () => { - test('Updates the value of an existing field in the preferences to specified user value', async () => { + test('Updates the value of an existing "concurrentDownloads" field in the preferences to the specified user value', async () => { const database = { - setPreferences: jest.fn() + setPreferences: jest.fn(), + getPreferences: jest.fn().mockResolvedValue({ concurrentDownloads: 5 }) } const info = { field: 'concurrentDownloads', @@ -16,7 +23,58 @@ describe('set a field in the preferences', () => { info }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'NewConcurrentDownloadsLimit', + data: { + newConcurrentDownloads: '2' + } + }) + expect(database.setPreferences).toHaveBeenCalledTimes(1) expect(database.setPreferences).toHaveBeenCalledWith({ concurrentDownloads: '2' }) }) + + test('Updates the "defaultDownloadLocation" field in the preferences', async () => { + const database = { + setPreferences: jest.fn(), + getPreferences: jest.fn().mockResolvedValue({}) + } + const info = { + field: 'defaultDownloadLocation', + value: 'new/location' + } + await setPreferenceFieldValue({ + database, + info + }) + + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'NewDefaultDownloadLocation' + }) + + expect(database.setPreferences).toHaveBeenCalledTimes(1) + expect(database.setPreferences).toHaveBeenCalledWith({ defaultDownloadLocation: 'new/location' }) + }) + + test('Updates preferences for an unknown field and logs the field name', async () => { + const database = { + setPreferences: jest.fn(), + getPreferences: jest.fn().mockResolvedValue({}) + } + const info = { + field: 'unknownField', + value: 'unknownValue' + } + await setPreferenceFieldValue({ + database, + info + }) + + expect(metricsLogger).toHaveBeenCalledTimes(0) + + expect(database.setPreferences).toHaveBeenCalledTimes(1) + expect(database.setPreferences).toHaveBeenCalledWith({ unknownField: 'unknownValue' }) + }) }) diff --git a/src/main/eventHandlers/cancelDownloadItem.ts b/src/main/eventHandlers/cancelDownloadItem.ts index 20638732..00bb1f5d 100644 --- a/src/main/eventHandlers/cancelDownloadItem.ts +++ b/src/main/eventHandlers/cancelDownloadItem.ts @@ -2,6 +2,8 @@ import downloadStates from '../../app/constants/downloadStates' import finishDownload from './willDownloadEvents/finishDownload' +import metricsLogger from '../utils/metricsLogger' +import downloadIdForMetrics from '../utils/downloadIdForMetrics' /** * Cancels a download and updates the database state @@ -37,6 +39,13 @@ const cancelDownloadItem = async ({ database, downloadId }) + + metricsLogger({ + eventType: 'DownloadItemCancel', + data: { + downloadId: downloadIdForMetrics(downloadId) + } + }) } if (downloadId && !filename) { @@ -56,6 +65,14 @@ const cancelDownloadItem = async ({ ) await database.endPause(downloadId) + + metricsLogger({ + eventType: 'DownloadCancel', + data: { + downloadIds: [downloadIdForMetrics(downloadId)], + cancelCount: 1 + } + }) } if (!downloadId) { @@ -71,10 +88,20 @@ const cancelDownloadItem = async ({ timeEnd: new Date().getTime() }) + const cancelledDownloadIds = [] await Promise.all(updatedDownloads.map(async (updatedDownload) => { const { id } = updatedDownload + cancelledDownloadIds.push(downloadIdForMetrics(id)) await database.endPause(id) })) + + metricsLogger({ + eventType: 'DownloadCancel', + data: { + downloadIds: cancelledDownloadIds, + cancelCount: cancelledDownloadIds.length + } + }) } } diff --git a/src/main/eventHandlers/pauseDownloadItem.ts b/src/main/eventHandlers/pauseDownloadItem.ts index dfc95d57..ab8b522d 100644 --- a/src/main/eventHandlers/pauseDownloadItem.ts +++ b/src/main/eventHandlers/pauseDownloadItem.ts @@ -1,6 +1,8 @@ // @ts-nocheck import downloadStates from '../../app/constants/downloadStates' +import metricsLogger from '../utils/metricsLogger' +import downloadIdForMetrics from '../utils/downloadIdForMetrics' /** * Pauses a download and updates the database @@ -35,10 +37,18 @@ const pauseDownloadItem = async ({ await database.updateDownloadById(downloadId, { state: downloadStates.paused }) + + metricsLogger({ + eventType: 'DownloadPause', + data: { + downloadIds: [downloadIdForMetrics(downloadId)], + downloadCount: 1 + } + }) } if (!downloadId) { - await database.createPauseForAllActiveDownloads() + const pauseResponse = await database.createPauseForAllActiveDownloads() await database.updateDownloadsWhereIn([ 'state', @@ -46,6 +56,14 @@ const pauseDownloadItem = async ({ ], { state: downloadStates.paused }) + + const metricIds = pauseResponse.pausedIds.map(downloadIdForMetrics) + metricsLogger({ + eventType: 'DownloadPause', + data: { + downloadIds: metricIds + } + }) } } diff --git a/src/main/eventHandlers/restartDownload.ts b/src/main/eventHandlers/restartDownload.ts index 9e003c55..61ed793c 100644 --- a/src/main/eventHandlers/restartDownload.ts +++ b/src/main/eventHandlers/restartDownload.ts @@ -1,6 +1,8 @@ // @ts-nocheck import downloadStates from '../../app/constants/downloadStates' +import downloadIdForMetrics from '../utils/downloadIdForMetrics' +import metricsLogger from '../utils/metricsLogger' import startNextDownload from '../utils/startNextDownload' /** @@ -25,6 +27,16 @@ const restartDownload = async ({ restartId } = info + const report = await database.getDownloadReport(downloadId) + metricsLogger({ + eventType: 'DownloadRestart', + data: { + downloadId: downloadIdForMetrics(downloadId), + filesCompleted: report.finishedFiles, + filesInProgress: report.totalFiles - report.finishedFiles + } + }) + await database.updateFilesWhere({ restartId }, { diff --git a/src/main/eventHandlers/resumeDownloadItem.ts b/src/main/eventHandlers/resumeDownloadItem.ts index ebcdbb71..ed1be687 100644 --- a/src/main/eventHandlers/resumeDownloadItem.ts +++ b/src/main/eventHandlers/resumeDownloadItem.ts @@ -4,6 +4,8 @@ import 'array-foreach-async' import downloadStates from '../../app/constants/downloadStates' import startNextDownload from '../utils/startNextDownload' +import metricsLogger from '../utils/metricsLogger' +import downloadIdForMetrics from '../utils/downloadIdForMetrics' /** * Resumes a download and updates the database @@ -35,6 +37,14 @@ const resumeDownloadItem = async ({ } if (downloadId && !filename) { + metricsLogger({ + eventType: 'DownloadResume', + data: { + downloadIds: [downloadIdForMetrics(downloadId)], + downloadCount: 1 + } + }) + await database.endPause(downloadId) const numberFiles = await database.getFileCountWhere({ downloadId }) @@ -52,6 +62,7 @@ const resumeDownloadItem = async ({ if (!downloadId) { const downloads = await database.getAllDownloadsWhere({ active: true }) + const downloadIds = [] await downloads.forEachAsync(async (download) => { const { id, state } = download @@ -69,6 +80,16 @@ const resumeDownloadItem = async ({ await database.updateDownloadById(id, { state: newState }) + + downloadIds.push(downloadIdForMetrics(id)) + }) + + metricsLogger({ + eventType: 'DownloadResume', + data: { + downloadIds, + downloadCount: downloadIds.length + } }) } diff --git a/src/main/eventHandlers/sendToEula.ts b/src/main/eventHandlers/sendToEula.ts index 675673b5..51a0d6e3 100644 --- a/src/main/eventHandlers/sendToEula.ts +++ b/src/main/eventHandlers/sendToEula.ts @@ -3,6 +3,8 @@ import { shell } from 'electron' import downloadStates from '../../app/constants/downloadStates' +import metricsLogger from '../utils/metricsLogger' +import downloadIdForMetrics from '../utils/downloadIdForMetrics' /** * Sends the user to the download's eulaUrl to get a new token @@ -73,6 +75,13 @@ const sendToEula = async ({ downloadId, showDialog: true }) + + metricsLogger({ + eventType: 'SentToEula', + data: { + downloadId: downloadIdForMetrics(downloadId) + } + }) } await database.updateFileById(fileId, { diff --git a/src/main/eventHandlers/sendToLogin.ts b/src/main/eventHandlers/sendToLogin.ts index 9ce7b024..d1c0a83c 100644 --- a/src/main/eventHandlers/sendToLogin.ts +++ b/src/main/eventHandlers/sendToLogin.ts @@ -3,6 +3,8 @@ import { shell } from 'electron' import downloadStates from '../../app/constants/downloadStates' +import metricsLogger from '../utils/metricsLogger' +import downloadIdForMetrics from '../utils/downloadIdForMetrics' /** * Sends the user to the download's authUrl to get a new token @@ -67,6 +69,13 @@ const sendToLogin = async ({ downloadId, showDialog: true }) + + metricsLogger({ + eventType: 'SentToEdl', + data: { + downloadId: downloadIdForMetrics(downloadId) + } + }) } await database.updateFileById(fileId, { diff --git a/src/main/eventHandlers/setPreferenceFieldValue.ts b/src/main/eventHandlers/setPreferenceFieldValue.ts index c3a49c54..d6ed5982 100644 --- a/src/main/eventHandlers/setPreferenceFieldValue.ts +++ b/src/main/eventHandlers/setPreferenceFieldValue.ts @@ -1,4 +1,5 @@ // @ts-nocheck +import metricsLogger from '../utils/metricsLogger' /** * Set the preference field to a specific value @@ -11,6 +12,29 @@ const setPreferenceFieldValue = async ({ info }) => { const { field, value } = info + + switch (field) { + case 'concurrentDownloads': + metricsLogger({ + eventType: 'NewConcurrentDownloadsLimit', + data: { + newConcurrentDownloads: value + } + }) + + break + + case 'defaultDownloadLocation': + metricsLogger({ + eventType: 'NewDefaultDownloadLocation' + }) + + break + + default: + break + } + database.setPreferences({ [field]: value }) diff --git a/src/main/eventHandlers/willDownloadEvents/__tests__/finishDownload.test.ts b/src/main/eventHandlers/willDownloadEvents/__tests__/finishDownload.test.ts index 4151b0da..3fcc767b 100644 --- a/src/main/eventHandlers/willDownloadEvents/__tests__/finishDownload.test.ts +++ b/src/main/eventHandlers/willDownloadEvents/__tests__/finishDownload.test.ts @@ -6,15 +6,30 @@ import finishDownload from '../finishDownload' import downloadStates from '../../../../app/constants/downloadStates' +import metricsLogger from '../../../utils/metricsLogger' + beforeEach(() => { MockDate.set('2023-05-13T22:00:00.000') }) +jest.mock('../../../utils/metricsLogger.ts', () => ({ + __esModule: true, + default: jest.fn(() => {}) +})) + describe('finishDownload', () => { test('marks the download as completed if all files are completed', async () => { const database = { getNotCompletedFilesCountByDownloadId: jest.fn().mockResolvedValue(0), - updateDownloadById: jest.fn() + updateDownloadById: jest.fn(), + getDownloadStatistics: jest.fn().mockResolvedValue({ + fileCount: 7, + receivedBytesSum: 461748278, + totalBytesSum: 461748278, + totalDownloadTime: 18938, + incompleteFileCount: 0, + pauseCount: 2 + }) } await finishDownload({ @@ -22,6 +37,20 @@ describe('finishDownload', () => { downloadId: 'mock-download-id' }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadComplete', + data: { + downloadId: 'mock-download-id', + receivedBytes: 461748278, + totalBytes: 461748278, + duration: '18.9', + filesFailed: 0, + filesDownloaded: 7, + pauseCount: 2 + } + }) + expect(database.updateDownloadById).toHaveBeenCalledTimes(1) expect(database.updateDownloadById).toHaveBeenCalledWith( 'mock-download-id', @@ -30,12 +59,22 @@ describe('finishDownload', () => { timeEnd: 1684029600000 } ) + + expect(database.getDownloadStatistics).toHaveBeenCalledTimes(1) }) test('does not mark the download as completed if files are still downloading', async () => { const database = { getNotCompletedFilesCountByDownloadId: jest.fn().mockResolvedValue(1), - updateDownloadById: jest.fn() + updateDownloadById: jest.fn(), + getDownloadStatistics: jest.fn().mockResolvedValue({ + fileCount: 7, + receivedBytesSum: 461748278, + totalBytesSum: 461748278, + totalDownloadTime: 18938, + incompleteFileCount: 0, + pauseCount: 2 + }) } await finishDownload({ @@ -43,6 +82,10 @@ describe('finishDownload', () => { downloadId: 'mock-download-id' }) + expect(metricsLogger).toHaveBeenCalledTimes(0) + expect(database.updateDownloadById).toHaveBeenCalledTimes(0) + + expect(database.getDownloadStatistics).toHaveBeenCalledTimes(0) }) }) diff --git a/src/main/eventHandlers/willDownloadEvents/finishDownload.ts b/src/main/eventHandlers/willDownloadEvents/finishDownload.ts index 8217ee6c..bd7e6240 100644 --- a/src/main/eventHandlers/willDownloadEvents/finishDownload.ts +++ b/src/main/eventHandlers/willDownloadEvents/finishDownload.ts @@ -1,6 +1,8 @@ // @ts-nocheck import downloadStates from '../../../app/constants/downloadStates' +import metricsLogger from '../../utils/metricsLogger' +import downloadIdForMetrics from '../../utils/downloadIdForMetrics' /** * If no more files are not completed, mark the download as completed @@ -13,12 +15,26 @@ const finishDownload = async ({ downloadId }) => { const notCompleteFilesCount = await database.getNotCompletedFilesCountByDownloadId(downloadId) - if (notCompleteFilesCount === 0) { await database.updateDownloadById(downloadId, { timeEnd: new Date().getTime(), state: downloadStates.completed }) + + const downloadStatistics = await database.getDownloadStatistics(downloadId) + + metricsLogger({ + eventType: 'DownloadComplete', + data: { + downloadId: downloadIdForMetrics(downloadId), + receivedBytes: downloadStatistics.receivedBytesSum, + totalBytes: downloadStatistics.totalBytesSum, + duration: (downloadStatistics.totalDownloadTime / 1000).toFixed(1), + filesDownloaded: downloadStatistics.fileCount, + filesFailed: downloadStatistics.incompleteFileCount, + pauseCount: downloadStatistics.pauseCount + } + }) } } diff --git a/src/main/utils/__tests__/downloadIdForMetrics.test.ts b/src/main/utils/__tests__/downloadIdForMetrics.test.ts new file mode 100644 index 00000000..e8ab461a --- /dev/null +++ b/src/main/utils/__tests__/downloadIdForMetrics.test.ts @@ -0,0 +1,21 @@ +import downloadIdForMetrics from '../downloadIdForMetrics' + +describe('downloadIdForMetrics', () => { + test('should extract download ID when it matches the pattern', () => { + const inputDownloadId = 'downloadId-20231116_164945' + const expectedExtractedId = 'downloadId-20231116_164945' + + const result = downloadIdForMetrics(inputDownloadId) + + expect(result).toEqual(expectedExtractedId) + }) + + test('should return the original download ID when it does not match the pattern', () => { + const inputDownloadId = 'invalidId123' + const expectedDownloadId = 'invalidId123' + + const result = downloadIdForMetrics(inputDownloadId) + + expect(result).toEqual(expectedDownloadId) + }) +}) diff --git a/src/main/utils/__tests__/initializeDownload.test.ts b/src/main/utils/__tests__/initializeDownload.test.ts index 6b7e74fd..7be48446 100644 --- a/src/main/utils/__tests__/initializeDownload.test.ts +++ b/src/main/utils/__tests__/initializeDownload.test.ts @@ -1,6 +1,7 @@ import { app } from 'electron' import initializeDownload from '../initializeDownload' +import metricsLogger from '../metricsLogger' jest.mock('electron', () => ({ app: { @@ -8,6 +9,11 @@ jest.mock('electron', () => ({ } })) +jest.mock('../../utils/metricsLogger.ts', () => ({ + __esModule: true, + default: jest.fn(() => {}) +})) + describe('initializeDownload', () => { test('sends the initializeDownload message with the system default downloadLocation', async () => { const webContents = { @@ -24,6 +30,14 @@ describe('initializeDownload', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadStarted', + data: { + downloadIds: ['mockDownloadId'] + } + }) + expect(app.getPath).toHaveBeenCalledTimes(1) expect(database.getPreferences).toHaveBeenCalledTimes(1) @@ -56,6 +70,14 @@ describe('initializeDownload', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadStarted', + data: { + downloadIds: ['mockDownloadId'] + } + }) + expect(app.getPath).toHaveBeenCalledTimes(1) expect(database.getPreferences).toHaveBeenCalledTimes(1) @@ -89,6 +111,14 @@ describe('initializeDownload', () => { webContents }) + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'DownloadStarted', + data: { + downloadIds: ['mockDownloadId'] + } + }) + expect(app.getPath).toHaveBeenCalledTimes(1) expect(database.getPreferences).toHaveBeenCalledTimes(1) diff --git a/src/main/utils/__tests__/metricsLogger.test.ts b/src/main/utils/__tests__/metricsLogger.test.ts new file mode 100644 index 00000000..c7ee3ea0 --- /dev/null +++ b/src/main/utils/__tests__/metricsLogger.test.ts @@ -0,0 +1,75 @@ +// @ts-nocheck + +import fetch from 'node-fetch' +import metricsLogger from '../metricsLogger' +import config from '../../config.json' + +jest.mock('node-fetch', () => ({ + __esModule: true, + default: jest.fn() +})) + +describe('metricsLogger', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + const event = { + eventType: 'DownloadComplete', + data: { + downloadId: '1010_Test', + fileCount: 10, + receivedBytes: 20480, + totalBytes: 25600, + filesDownloaded: 8, + filesFailed: 2, + duration: 14.1 + } + } + + test('should send a POST request to the specified logging endpoint', async () => { + const expectedBody = JSON.stringify({ params: event }) + + await metricsLogger(event) + + expect(fetch).toHaveBeenCalledTimes(1) + expect(fetch).toHaveBeenCalledWith( + config.logging, + { + method: 'POST', + body: expectedBody, + headers: { + 'Content-Type': 'application/json' + } + } + ) + }) + + test('should log an error when POST request fails', async () => { + const expectedError = new Error('Request failed') + + fetch + .mockImplementationOnce(() => { + throw new Error('Request failed') + }) + + console.error = jest.fn() + + await metricsLogger(event) + + expect(fetch).toHaveBeenCalledTimes(1) + + expect(fetch).toHaveBeenCalledWith( + config.logging, + { + method: 'POST', + body: JSON.stringify({ params: event }), + headers: { + 'Content-Type': 'application/json' + } + } + ) + + expect(console.error).toHaveBeenCalledWith(expectedError) + }) +}) diff --git a/src/main/utils/__tests__/windowStateKeeper.test.ts b/src/main/utils/__tests__/windowStateKeeper.test.ts index 113dbe32..892729c1 100644 --- a/src/main/utils/__tests__/windowStateKeeper.test.ts +++ b/src/main/utils/__tests__/windowStateKeeper.test.ts @@ -1,7 +1,13 @@ // @ts-nocheck +import metricsLogger from '../metricsLogger' import windowStateKeeper from '../windowStateKeeper' +jest.mock('../../utils/metricsLogger.ts', () => ({ + __esModule: true, + default: jest.fn(() => {}) +})) + describe('windowStateKeeper', () => { test('returns the default window state', async () => { const database = { @@ -17,6 +23,20 @@ describe('windowStateKeeper', () => { x: undefined, y: undefined })) + + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'WindowSizePreferences', + data: { + windowStateInfo: { + height: 600, + width: 800, + x: undefined, + y: undefined, + isMaximized: undefined + } + } + }) }) test('returns the saved window state', async () => { @@ -40,6 +60,20 @@ describe('windowStateKeeper', () => { x: 0, y: 0 })) + + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'WindowSizePreferences', + data: { + windowStateInfo: { + height: 700, + width: 900, + x: 0, + y: 0, + isMaximized: undefined + } + } + }) }) describe('when tracking the window state changes', () => { @@ -82,6 +116,20 @@ describe('windowStateKeeper', () => { window.send('resize') + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'WindowSizePreferences', + data: { + windowStateInfo: { + height: 700, + width: 900, + x: 0, + y: 0, + isMaximized: true + } + } + }) + expect(database.setPreferences).toHaveBeenCalledTimes(1) expect(database.setPreferences).toHaveBeenCalledWith({ windowState: '{"x":0,"y":0,"width":900,"height":700,"isMaximized":true}' @@ -126,6 +174,20 @@ describe('windowStateKeeper', () => { window.send('resize') + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'WindowSizePreferences', + data: { + windowStateInfo: { + height: 700, + width: 900, + x: 0, + y: 0, + isMaximized: false + } + } + }) + expect(database.setPreferences).toHaveBeenCalledTimes(1) expect(database.setPreferences).toHaveBeenCalledWith({ windowState: '{"x":0,"y":0,"width":800,"height":600,"isMaximized":false}' @@ -172,6 +234,20 @@ describe('windowStateKeeper', () => { window.send('move') + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'WindowSizePreferences', + data: { + windowStateInfo: { + height: 700, + width: 900, + x: 0, + y: 0, + isMaximized: true + } + } + }) + expect(database.setPreferences).toHaveBeenCalledTimes(1) expect(database.setPreferences).toHaveBeenCalledWith({ windowState: '{"x":0,"y":0,"width":900,"height":700,"isMaximized":true}' @@ -216,6 +292,20 @@ describe('windowStateKeeper', () => { window.send('move') + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'WindowSizePreferences', + data: { + windowStateInfo: { + height: 700, + width: 900, + x: 0, + y: 0, + isMaximized: false + } + } + }) + expect(database.setPreferences).toHaveBeenCalledTimes(1) expect(database.setPreferences).toHaveBeenCalledWith({ windowState: '{"x":0,"y":0,"width":800,"height":600,"isMaximized":false}' @@ -266,6 +356,20 @@ describe('windowStateKeeper', () => { expect(database.setPreferences).toHaveBeenCalledWith({ windowState: '{"x":0,"y":0,"width":900,"height":700,"isMaximized":true}' }) + + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'WindowSizePreferences', + data: { + windowStateInfo: { + height: 700, + width: 900, + x: 0, + y: 0, + isMaximized: true + } + } + }) }) test('saves the non-maximized window state ', async () => { @@ -306,6 +410,20 @@ describe('windowStateKeeper', () => { window.send('close') + expect(metricsLogger).toHaveBeenCalledTimes(1) + expect(metricsLogger).toHaveBeenCalledWith({ + eventType: 'WindowSizePreferences', + data: { + windowStateInfo: { + height: 700, + width: 900, + x: 0, + y: 0, + isMaximized: false + } + } + }) + expect(database.setPreferences).toHaveBeenCalledTimes(1) expect(database.setPreferences).toHaveBeenCalledWith({ windowState: '{"x":0,"y":0,"width":800,"height":600,"isMaximized":false}' diff --git a/src/main/utils/database/EddDatabase.ts b/src/main/utils/database/EddDatabase.ts index ad38c3e0..93d100f7 100644 --- a/src/main/utils/database/EddDatabase.ts +++ b/src/main/utils/database/EddDatabase.ts @@ -508,10 +508,17 @@ class EddDatabase { ...downloadsData ] - if (!data.length) return null + if (!data.length) { + return { + pausedIds: [] + } + } - return this.db('pauses') - .insert(data) + const pausedDownloadIds = data.map((item) => item.downloadId) + + return { + pausedIds: pausedDownloadIds + } } /** @@ -684,6 +691,26 @@ class EddDatabase { return query } + /** + * Returns statstics for a completed download. + * @param {String} downloadId downloadId for download. + */ + async getDownloadStatistics(downloadId) { + const result = await this.db('files') + .where({ downloadId }) + .first() + .count('id as fileCount') + .sum('receivedBytes as receivedBytesSum') + .sum('totalBytes as totalBytesSum') + .select( + this.db.raw('(IFNULL(MAX(timeEnd), UNIXEPOCH() * 1000) - MIN(timeStart)) as totalDownloadTime'), + this.db.raw('(SELECT COUNT(id) FROM files WHERE downloadId = ? AND state != "COMPLETED") as incompleteFileCount'), + this.db.raw('(SELECT COUNT(id) FROM pauses WHERE downloadId = ? AND fileId IS NULL) as pauseCount') + ) + + return result + } + /** * Returns the total number of files to be reported on in the Download * @param {Object} params diff --git a/src/main/utils/database/__tests__/EddDatabase.test.ts b/src/main/utils/database/__tests__/EddDatabase.test.ts index 80daf0c5..fd8e0d8a 100644 --- a/src/main/utils/database/__tests__/EddDatabase.test.ts +++ b/src/main/utils/database/__tests__/EddDatabase.test.ts @@ -941,7 +941,9 @@ describe('EddDatabase', () => { const result = await database.createPauseForAllActiveDownloads() - expect(result).toEqual(null) + expect(result).toEqual({ + pausedIds: [] + }) }) }) @@ -1793,4 +1795,35 @@ describe('EddDatabase', () => { await database.deleteByDeleteId('mock-delete-id') }) }) + + describe('getDownloadStatistics', () => { + test('returns statistics for a completed download', async () => { + dbTracker.on('query', (query) => { + expect(query.sql).toEqual(expect.stringContaining('select count(`id`) as `fileCount')) + expect(query.bindings).toEqual(['mock-download-id', 1]) + + query.response({ + fileCount: 5, + receivedBytesSum: 1000, + totalBytesSum: 2000, + totalDownloadTime: 5000, + incompleteFileCount: 0, + pauseCount: 2 + }) + }) + + const database = new EddDatabase('./') + + const result = await database.getDownloadStatistics('mock-download-id') + + expect(result).toEqual({ + fileCount: 5, + receivedBytesSum: 1000, + totalBytesSum: 2000, + totalDownloadTime: 5000, + incompleteFileCount: 0, + pauseCount: 2 + }) + }) + }) }) diff --git a/src/main/utils/downloadIdForMetrics.ts b/src/main/utils/downloadIdForMetrics.ts new file mode 100644 index 00000000..df5494a3 --- /dev/null +++ b/src/main/utils/downloadIdForMetrics.ts @@ -0,0 +1,20 @@ +// @ts-nocheck + +/** + * Trims timestamp suffix from downloadId + * @param {String} downloadId downloadId for a given download + */ +const downloadIdForMetrics = (downloadId) => { + const downloadIdString = downloadId.toString() + + const regexPattern = /^(.+)-\d{8}_\d{6}$/ + const match = downloadIdString.match(regexPattern) + + if (match) { + return match[0] + } + + return downloadId +} + +export default downloadIdForMetrics diff --git a/src/main/utils/initializeDownload.ts b/src/main/utils/initializeDownload.ts index 713a08f6..7d76e23f 100644 --- a/src/main/utils/initializeDownload.ts +++ b/src/main/utils/initializeDownload.ts @@ -2,6 +2,9 @@ import { app } from 'electron' +import metricsLogger from './metricsLogger' +import downloadIdForMetrics from '../utils/downloadIdForMetrics' + /** * Sends a message to the renderer process to start a download for the given downloadIds. * @param {Object} params @@ -40,6 +43,14 @@ const initializeDownload = async ({ downloadLocation: location, shouldUseDefaultLocation: !!defaultDownloadLocation }) + + const metricIds = downloadIds.map(downloadIdForMetrics) + metricsLogger({ + eventType: 'DownloadStarted', + data: { + downloadIds: metricIds + } + }) } } diff --git a/src/main/utils/metricsLogger.ts b/src/main/utils/metricsLogger.ts new file mode 100644 index 00000000..97390301 --- /dev/null +++ b/src/main/utils/metricsLogger.ts @@ -0,0 +1,25 @@ +// @ts-nocheck + +import fetch from 'node-fetch' + +import config from '../config.json' + +/** + * Dispatches specified events to edd_logger lambda to be logged on CloudWatch + * @param {Object} event json object to be sent to edd_logger + */ +const metricsLogger = async (event) => { + try { + await fetch(config.logging, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ params: event }) + }) + } catch (error) { + console.error(error) + } +} + +export default metricsLogger diff --git a/src/main/utils/windowStateKeeper.ts b/src/main/utils/windowStateKeeper.ts index 85f87e82..c97e523b 100644 --- a/src/main/utils/windowStateKeeper.ts +++ b/src/main/utils/windowStateKeeper.ts @@ -1,5 +1,7 @@ // @ts-nocheck +import metricsLogger from './metricsLogger' + /** * Keeps the current window state (size/location) update in the database. So the window * will open in the same place as the user last had it open. @@ -50,12 +52,23 @@ const windowStateKeeper = async (database) => { await setBounds() - return ({ + const windowStateInfo = { x: windowState.x, y: windowState.y, width: windowState.width, height: windowState.height, - isMaximized: windowState.isMaximized, + isMaximized: windowState.isMaximized + } + + metricsLogger({ + eventType: 'WindowSizePreferences', + data: { + windowStateInfo + } + }) + + return ({ + ...windowStateInfo, track }) }