diff --git a/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs b/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs index 7f240e8b5e..0b7e7508be 100644 --- a/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs +++ b/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs @@ -7,6 +7,7 @@ import { prepareZipAttachment } from '../../infrastructure/Response.mjs' import SessionManager from '../Authentication/SessionManager.mjs' import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.mjs' import DocumentConversionManager from '../Uploads/DocumentConversionManager.mjs' +import AnalyticsManager from '../Analytics/AnalyticsManager.mjs' import Validation from '../../infrastructure/Validation.mjs' import { expressify } from '@overleaf/promise-utils' import { pipeline } from 'node:stream/promises' @@ -78,12 +79,30 @@ async function exportProjectConversion(req, res) { const userId = SessionManager.getLoggedInUserId(req.session) Metrics.inc('document-exports', 1, { type }) - const { conversionId, buildId, clsiServerId, file } = - await DocumentConversionManager.promises.convertProjectToDocument( - projectId, - userId, - type - ) + let conversionResult + try { + conversionResult = + await DocumentConversionManager.promises.convertProjectToDocument( + projectId, + userId, + type + ) + AnalyticsManager.recordEventForUserInBackground(userId, 'convert-format', { + sourceFormat: 'latex', + targetFormat: type, + status: 'success', + operation: 'export', + }) + } catch (error) { + AnalyticsManager.recordEventForUserInBackground(userId, 'convert-format', { + sourceFormat: 'latex', + targetFormat: type, + status: 'failure', + operation: 'export', + }) + throw error + } + const { conversionId, buildId, clsiServerId, file } = conversionResult ProjectAuditLogHandler.addEntryInBackground( projectId, `project-exported-${type}`, diff --git a/services/web/app/src/Features/Uploads/ProjectUploadController.mjs b/services/web/app/src/Features/Uploads/ProjectUploadController.mjs index 22a88a477a..bb0970c3b7 100644 --- a/services/web/app/src/Features/Uploads/ProjectUploadController.mjs +++ b/services/web/app/src/Features/Uploads/ProjectUploadController.mjs @@ -16,6 +16,7 @@ import { expressify } from '@overleaf/promise-utils' import { DuplicateNameError, FileTooLargeError } from '../Errors/Errors.js' import DocumentConversionManager from './DocumentConversionManager.mjs' import ProjectOptionsHandler from '../Project/ProjectOptionsHandler.mjs' +import AnalyticsManager from '../Analytics/AnalyticsManager.mjs' const defaultsDeep = lodash.defaultsDeep @@ -202,6 +203,16 @@ async function importDocument(req, res, next) { archivePath ) await ProjectOptionsHandler.promises.setCompiler(project._id, 'lualatex') + AnalyticsManager.recordEventForUserInBackground( + userId, + 'convert-format', + { + sourceFormat: conversionType, + targetFormat: 'latex', + status: 'success', + operation: 'import', + } + ) res.json({ success: true, project_id: project._id }) } finally { await fsPromises.unlink(archivePath).catch(unlinkErr => { @@ -213,6 +224,12 @@ async function importDocument(req, res, next) { } } catch (error) { logger.error({ error }, 'error importing document file') + AnalyticsManager.recordEventForUserInBackground(userId, 'convert-format', { + sourceFormat: conversionType, + targetFormat: 'latex', + status: 'failure', + operation: 'import', + }) if ( error instanceof FileTooLargeError || error?.name === 'FileTooLargeError' diff --git a/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs index 6dd2359539..aa74b26e22 100644 --- a/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs +++ b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs @@ -92,6 +92,15 @@ describe('ProjectDownloadsController', function () { pipeline: (ctx.pipeline = sinon.stub().resolves()), })) + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager.mjs', + () => ({ + default: (ctx.AnalyticsManager = { + recordEventForUserInBackground: sinon.stub(), + }), + }) + ) + ctx.ProjectDownloadsController = (await import(modulePath)).default }) @@ -349,6 +358,20 @@ describe('ProjectDownloadsController', function () { it('should stream the document to the response', function (ctx) { sinon.assert.calledWith(ctx.pipeline, ctx.exportStream, ctx.res) }) + + it('should record a successful convert-format analytics event', function (ctx) { + sinon.assert.calledWith( + ctx.AnalyticsManager.recordEventForUserInBackground, + ctx.userId, + 'convert-format', + { + sourceFormat: 'latex', + targetFormat: 'docx', + status: 'success', + operation: 'export', + } + ) + }) }) describe('with responseFormat=json', function () { diff --git a/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs b/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs index 1c2fc5476b..ebe9895814 100644 --- a/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs +++ b/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs @@ -112,6 +112,16 @@ describe('ProjectUploadController', function () { }) ) + ctx.AnalyticsManager = { + recordEventForUserInBackground: sinon.stub(), + } + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager.mjs', + () => ({ + default: ctx.AnalyticsManager, + }) + ) + vi.doMock('node:fs', () => ({ default: (ctx.fs = {}), })) @@ -525,6 +535,20 @@ describe('ProjectUploadController', function () { ctx.req.file.path ) }) + + it('should record a successful convert-format analytics event', function (ctx) { + sinon.assert.calledWith( + ctx.AnalyticsManager.recordEventForUserInBackground, + ctx.user_id, + 'convert-format', + { + sourceFormat: 'docx', + targetFormat: 'latex', + status: 'success', + operation: 'import', + } + ) + }) }) }) @@ -581,6 +605,20 @@ describe('ProjectUploadController', function () { it('should unlink the uploaded file', function (ctx) { expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.req.file.path) }) + + it('should record a successful convert-format analytics event', function (ctx) { + sinon.assert.calledWith( + ctx.AnalyticsManager.recordEventForUserInBackground, + ctx.user_id, + 'convert-format', + { + sourceFormat: 'markdown', + targetFormat: 'latex', + status: 'success', + operation: 'import', + } + ) + }) }) describe('with an invalid conversionType', async function () { @@ -639,6 +677,20 @@ describe('ProjectUploadController', function () { it('should return http 500', function (ctx) { expect(ctx.res.statusCode).to.equal(500) }) + + it('should record a failed convert-format analytics event', function (ctx) { + sinon.assert.calledWith( + ctx.AnalyticsManager.recordEventForUserInBackground, + ctx.user_id, + 'convert-format', + { + sourceFormat: 'docx', + targetFormat: 'latex', + status: 'failure', + operation: 'import', + } + ) + }) }) describe('when the converted archive is too large', async function () {