diff --git a/services/web/frontend/js/features/editor-left-menu/components/download-pdf.tsx b/services/web/frontend/js/features/editor-left-menu/components/download-pdf.tsx index b0b3bc106e..0d756d135f 100644 --- a/services/web/frontend/js/features/editor-left-menu/components/download-pdf.tsx +++ b/services/web/frontend/js/features/editor-left-menu/components/download-pdf.tsx @@ -4,6 +4,7 @@ import { useProjectContext } from '../../../shared/context/project-context' import Icon from '../../../shared/components/icon' import Tooltip from '../../../shared/components/tooltip' import * as eventTracking from '../../../infrastructure/event-tracking' +import { isMobileDevice } from '../../../infrastructure/event-tracking' export default function DownloadPDF() { const { t } = useTranslation() @@ -14,6 +15,7 @@ export default function DownloadPDF() { eventTracking.sendMB('download-pdf-button-click', { projectId, location: 'left-menu', + isMobileDevice, }) } diff --git a/services/web/frontend/js/features/editor-left-menu/components/download-source.tsx b/services/web/frontend/js/features/editor-left-menu/components/download-source.tsx index e3a8e715d4..b99eb178de 100644 --- a/services/web/frontend/js/features/editor-left-menu/components/download-source.tsx +++ b/services/web/frontend/js/features/editor-left-menu/components/download-source.tsx @@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next' import { useProjectContext } from '../../../shared/context/project-context' import Icon from '../../../shared/components/icon' import * as eventTracking from '../../../infrastructure/event-tracking' +import { isMobileDevice } from '../../../infrastructure/event-tracking' export default function DownloadSource() { const { t } = useTranslation() @@ -11,6 +12,7 @@ export default function DownloadSource() { eventTracking.sendMB('download-zip-button-click', { projectId, location: 'left-menu', + isMobileDevice, }) } diff --git a/services/web/frontend/js/features/event-tracking/document-first-change-event.js b/services/web/frontend/js/features/event-tracking/document-first-change-event.js new file mode 100644 index 0000000000..e89e892a9e --- /dev/null +++ b/services/web/frontend/js/features/event-tracking/document-first-change-event.js @@ -0,0 +1,12 @@ +import { isMobileDevice, sendMB } from '@/infrastructure/event-tracking' +import getMeta from '@/utils/meta' + +// record once per page load +let recorded = false + +export function recordDocumentFirstChangeEvent() { + if (recorded) return + recorded = true + const projectId = getMeta('ol-project_id') + sendMB('document-first-change', { projectId, isMobileDevice }) +} diff --git a/services/web/frontend/js/features/ide-react/editor/share-js-doc.ts b/services/web/frontend/js/features/ide-react/editor/share-js-doc.ts index 506dcd7205..92aea65653 100644 --- a/services/web/frontend/js/features/ide-react/editor/share-js-doc.ts +++ b/services/web/frontend/js/features/ide-react/editor/share-js-doc.ts @@ -16,6 +16,7 @@ import { TrackChangesIdSeeds, } from '@/features/ide-react/editor/types/document' import { EditorFacade } from '@/features/source-editor/extensions/realtime' +import { recordDocumentFirstChangeEvent } from '@/features/event-tracking/document-first-change-event' // All times below are in milliseconds const SINGLE_USER_FLUSH_DELAY = 2000 @@ -421,6 +422,7 @@ export class ShareJsDoc extends EventEmitter { private bindToDocChanges(doc: Doc) { const { submitOp } = doc doc.submitOp = (op: ShareJsOperation, callback?: () => void) => { + recordDocumentFirstChangeEvent() this.trigger('op:sent', op) doc.pendingCallbacks.push(() => { return this.trigger('op:acknowledged', op) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx index c56792656f..fcc0b992b2 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx @@ -5,6 +5,7 @@ import Icon from '../../../shared/components/icon' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' import { useProjectContext } from '../../../shared/context/project-context' import * as eventTracking from '../../../infrastructure/event-tracking' +import { isMobileDevice } from '../../../infrastructure/event-tracking' function PdfHybridDownloadButton() { const { pdfDownloadUrl } = useCompileContext() @@ -20,6 +21,7 @@ function PdfHybridDownloadButton() { eventTracking.sendMB('download-pdf-button-click', { projectId, location: 'pdf-preview', + isMobileDevice, }) } diff --git a/services/web/frontend/js/features/project-list/components/modals/projects-action-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/projects-action-modal.tsx index f0b36c9e10..0f443a1c4d 100644 --- a/services/web/frontend/js/features/project-list/components/modals/projects-action-modal.tsx +++ b/services/web/frontend/js/features/project-list/components/modals/projects-action-modal.tsx @@ -6,6 +6,7 @@ import AccessibleModal from '../../../../shared/components/accessible-modal' import { getUserFacingMessage } from '../../../../infrastructure/fetch-json' import useIsMounted from '../../../../shared/hooks/use-is-mounted' import * as eventTracking from '../../../../infrastructure/event-tracking' +import { isMobileDevice } from '../../../../infrastructure/event-tracking' type ProjectsActionModalProps = { title?: string @@ -57,7 +58,10 @@ function ProjectsActionModal({ useEffect(() => { if (showModal) { - eventTracking.sendMB('project-list-page-interaction', { action }) + eventTracking.sendMB('project-list-page-interaction', { + action, + isMobileDevice, + }) } }, [action, showModal]) diff --git a/services/web/frontend/js/features/project-list/components/modals/rename-project-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/rename-project-modal.tsx index 07cac6cbf7..944e1c1cc0 100644 --- a/services/web/frontend/js/features/project-list/components/modals/rename-project-modal.tsx +++ b/services/web/frontend/js/features/project-list/components/modals/rename-project-modal.tsx @@ -16,6 +16,7 @@ import useAsync from '../../../../shared/hooks/use-async' import { useProjectListContext } from '../../context/project-list-context' import { getUserFacingMessage } from '../../../../infrastructure/fetch-json' import { debugConsole } from '@/utils/debugging' +import { isMobileDevice } from '../../../../infrastructure/event-tracking' type RenameProjectModalProps = { handleCloseModal: () => void @@ -37,9 +38,11 @@ function RenameProjectModal({ if (showModal) { eventTracking.sendMB('project-list-page-interaction', { action: 'rename', + projectId: project.id, + isMobileDevice, }) } - }, [showModal]) + }, [showModal, project.id]) const isValid = useMemo( () => newProjectName !== project.name && newProjectName.trim().length > 0, diff --git a/services/web/frontend/js/features/project-list/components/search-form.tsx b/services/web/frontend/js/features/project-list/components/search-form.tsx index 9e9f1cd249..c922d5a57f 100644 --- a/services/web/frontend/js/features/project-list/components/search-form.tsx +++ b/services/web/frontend/js/features/project-list/components/search-form.tsx @@ -11,6 +11,7 @@ import * as eventTracking from '../../../infrastructure/event-tracking' import classnames from 'classnames' import { Tag } from '../../../../../app/src/Features/Tags/types' import { Filter } from '../context/project-list-context' +import { isMobileDevice } from '../../../infrastructure/event-tracking' type SearchFormOwnProps = { inputValue: string @@ -64,7 +65,10 @@ function SearchForm({ HTMLInputElement & Omit > ) => { - eventTracking.sendMB('project-list-page-interaction', { action: 'search' }) + eventTracking.sendMB('project-list-page-interaction', { + action: 'search', + isMobileDevice, + }) setInputValue(e.target.value) } diff --git a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/copy-project-button.tsx b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/copy-project-button.tsx index 55f1fb2453..3077e551aa 100644 --- a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/copy-project-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/copy-project-button.tsx @@ -11,6 +11,7 @@ import { Project, } from '../../../../../../../../types/project/dashboard/api' import { useProjectTags } from '@/features/project-list/hooks/use-project-tags' +import { isMobileDevice } from '../../../../../../infrastructure/event-tracking' type CopyButtonProps = { project: Project @@ -41,7 +42,11 @@ function CopyProjectButton({ project, children }: CopyButtonProps) { const handleAfterCloned = useCallback( (clonedProject: ClonedProject, tags: { _id: string }[]) => { - eventTracking.sendMB('project-list-page-interaction', { action: 'clone' }) + eventTracking.sendMB('project-list-page-interaction', { + action: 'clone', + projectId: project.id, + isMobileDevice, + }) addClonedProjectToViewData(clonedProject) for (const tag of tags) { addProjectToTagInView(tag._id, clonedProject.project_id) diff --git a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-project-button.tsx b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-project-button.tsx index 76f176e93b..358a8ae1ee 100644 --- a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-project-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-project-button.tsx @@ -5,6 +5,7 @@ import Icon from '../../../../../../shared/components/icon' import Tooltip from '../../../../../../shared/components/tooltip' import * as eventTracking from '../../../../../../infrastructure/event-tracking' import { useLocation } from '../../../../../../shared/hooks/use-location' +import { isMobileDevice } from '../../../../../../infrastructure/event-tracking' type DownloadProjectButtonProps = { project: Project @@ -22,6 +23,8 @@ function DownloadProjectButton({ const downloadProject = useCallback(() => { eventTracking.sendMB('project-list-page-interaction', { action: 'downloadZip', + projectId: project.id, + isMobileDevice, }) location.assign(`/project/${project.id}/download/zip`) }, [project, location]) diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/download-projects-button.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/download-projects-button.tsx index 34d49e3518..3d1704dfd0 100644 --- a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/download-projects-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/download-projects-button.tsx @@ -5,6 +5,7 @@ import Tooltip from '../../../../../../shared/components/tooltip' import * as eventTracking from '../../../../../../infrastructure/event-tracking' import { useProjectListContext } from '../../../../context/project-list-context' import { useLocation } from '../../../../../../shared/hooks/use-location' +import { isMobileDevice } from '../../../../../../infrastructure/event-tracking' function DownloadProjectsButton() { const { selectedProjects, selectOrUnselectAllProjects } = @@ -18,6 +19,7 @@ function DownloadProjectsButton() { const handleDownloadProjects = useCallback(() => { eventTracking.sendMB('project-list-page-interaction', { action: 'downloadZips', + isMobileDevice, }) location.assign(`/project/download/zip?project_ids=${projectIds.join(',')}`) diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx index 5196cbd667..22b2935907 100644 --- a/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx +++ b/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx @@ -7,6 +7,7 @@ import { useProjectListContext } from '../../../../context/project-list-context' import * as eventTracking from '../../../../../../infrastructure/event-tracking' import { ClonedProject } from '../../../../../../../../types/project/dashboard/api' import { useProjectTags } from '@/features/project-list/hooks/use-project-tags' +import { isMobileDevice } from '../../../../../../infrastructure/event-tracking' function CopyProjectMenuItem() { const { @@ -33,7 +34,11 @@ function CopyProjectMenuItem() { const handleAfterCloned = useCallback( (clonedProject: ClonedProject, tags: { _id: string }[]) => { const project = selectedProjects[0] - eventTracking.sendMB('project-list-page-interaction', { action: 'clone' }) + eventTracking.sendMB('project-list-page-interaction', { + action: 'clone', + projectId: project.id, + isMobileDevice, + }) addClonedProjectToViewData(clonedProject) for (const tag of tags) { addProjectToTagInView(tag._id, clonedProject.project_id) diff --git a/services/web/frontend/js/ide/editor/ShareJsDoc.js b/services/web/frontend/js/ide/editor/ShareJsDoc.js index 28836bc4a6..5f44448ee7 100644 --- a/services/web/frontend/js/ide/editor/ShareJsDoc.js +++ b/services/web/frontend/js/ide/editor/ShareJsDoc.js @@ -20,6 +20,7 @@ import EventEmitter from '../../utils/EventEmitter' import ShareJs from '../../vendor/libs/sharejs' import EditorWatchdogManager from '../connection/EditorWatchdogManager' import { debugConsole } from '@/utils/debugging' +import { recordDocumentFirstChangeEvent } from '@/features/event-tracking/document-first-change-event' let ShareJsDoc const SINGLE_USER_FLUSH_DELAY = 2000 // ms @@ -457,6 +458,7 @@ export default ShareJsDoc = (function () { _bindToDocChanges(doc) { const { submitOp } = doc doc.submitOp = (...args) => { + recordDocumentFirstChangeEvent() this.trigger('op:sent', ...Array.from(args)) doc.pendingCallbacks.push(() => { return this.trigger('op:acknowledged', ...Array.from(args)) diff --git a/services/web/frontend/js/infrastructure/event-tracking.js b/services/web/frontend/js/infrastructure/event-tracking.js index 19c2b3dd76..3e2cecb0ad 100644 --- a/services/web/frontend/js/infrastructure/event-tracking.js +++ b/services/web/frontend/js/infrastructure/event-tracking.js @@ -51,6 +51,12 @@ export function sendMBSampled(key, body = {}, rate = 0.01) { } } +// Use breakpoint @screen-xs-max from less: +// @screen-xs-max: (@screen-sm-min - 1); +// @screen-sm-min: @screen-sm; +// @screen-sm: 768px; +export const isMobileDevice = window.matchMedia('(max-width: 767px)').matches + function sendBeacon(key, data) { if (!navigator || !navigator.sendBeacon) return diff --git a/services/web/test/frontend/bootstrap.js b/services/web/test/frontend/bootstrap.js index be859893cb..577e0102a1 100644 --- a/services/web/test/frontend/bootstrap.js +++ b/services/web/test/frontend/bootstrap.js @@ -120,6 +120,9 @@ globalThis.WebSocket = class WebSocket { static CLOSED = 3 } +// add stub for window.matchMedia +window.matchMedia = () => ({ matches: false }) + // node-fetch doesn't accept relative URL's: https://github.com/node-fetch/node-fetch/blob/master/docs/v2-LIMITS.md#known-differences const fetch = require('node-fetch') globalThis.fetch = diff --git a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx index e1b52aa19a..60c0ea9d3f 100644 --- a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx +++ b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx @@ -10,6 +10,8 @@ import { owner, archivedProjects, makeLongProjectList, + archiveableProject, + copyableProject, } from '../fixtures/projects-data' import * as useLocationModule from '../../../../../frontend/js/shared/hooks/use-location' @@ -818,6 +820,8 @@ describe('', function () { { action: 'rename', page: '/', + projectId: copyableProject.id, + isMobileDevice: false, } ) @@ -949,6 +953,8 @@ describe('', function () { { action: 'clone', page: '/', + projectId: archiveableProject.id, + isMobileDevice: false, } ) @@ -1106,6 +1112,8 @@ describe('', function () { { action: 'clone', page: '/', + projectId: archiveableProject.id, + isMobileDevice: false, } ) diff --git a/services/web/test/frontend/features/project-list/components/project-search.test.tsx b/services/web/test/frontend/features/project-list/components/project-search.test.tsx index c09d8431ed..d93ebb3190 100644 --- a/services/web/test/frontend/features/project-list/components/project-search.test.tsx +++ b/services/web/test/frontend/features/project-list/components/project-search.test.tsx @@ -85,6 +85,7 @@ describe('Project list search form', function () { expect(sendMBSpy).to.have.been.calledWith('project-list-page-interaction', { action: 'search', page: '/', + isMobileDevice: false, }) expect(setInputValueMock).to.be.calledWith(value) }) diff --git a/services/web/test/frontend/features/project-list/components/table/projects-action-modal.test.tsx b/services/web/test/frontend/features/project-list/components/table/projects-action-modal.test.tsx index 2a0a6fa9df..1375c68b80 100644 --- a/services/web/test/frontend/features/project-list/components/table/projects-action-modal.test.tsx +++ b/services/web/test/frontend/features/project-list/components/table/projects-action-modal.test.tsx @@ -91,6 +91,7 @@ describe('', function () { expect(sendMBSpy).to.have.been.calledWith('project-list-page-interaction', { action: 'archive', page: '/', + isMobileDevice: false, }) }) })