From 2c11ad84e0cb954a29608ff2b9ca025819aff6a5 Mon Sep 17 00:00:00 2001 From: Jimmy Domagala-Tang Date: Tue, 23 Apr 2024 09:49:11 -0700 Subject: [PATCH] Merge pull request #18071 from overleaf/jdt-bib-events Bibliography events for 3rd party integrations GitOrigin-RevId: d8d7f4378d75166481d5265d2e8bef72d75968c3 --- .../LinkedFiles/LinkedFilesController.js | 7 ++++ .../modes/file-tree-create-new-doc.jsx | 5 ++- .../modes/file-tree-import-from-project.tsx | 5 ++- .../modes/file-tree-import-from-url.jsx | 5 ++- .../modes/file-tree-upload-doc.tsx | 8 +++- .../file-tree-item-menu-items.jsx | 37 ++++++++++++++++--- .../context/file-tree-open-context.tsx | 22 +++++++++-- .../components/linking/integration-widget.tsx | 25 +++++++++++-- 8 files changed, 99 insertions(+), 15 deletions(-) diff --git a/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js b/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js index eff0e9513b..e809c02bec 100644 --- a/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js +++ b/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js @@ -18,7 +18,9 @@ const ProjectLocator = require('../Project/ProjectLocator') const Settings = require('@overleaf/settings') const logger = require('@overleaf/logger') const _ = require('lodash') +const AnalyticsManager = require('../../../../app/src/Features/Analytics/AnalyticsManager') const LinkedFilesHandler = require('./LinkedFilesHandler') + const { CompileFailedError, UrlFetchFailedError, @@ -91,6 +93,11 @@ module.exports = LinkedFilesController = { if (err != null) { return LinkedFilesController.handleError(err, req, res, next) } + if (name.endsWith('.bib')) { + AnalyticsManager.recordEventForUser(userId, 'linked-bib-file', { + integration: provider, + }) + } return res.json({ new_file_id: newFileId }) } ) diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-create-new-doc.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-create-new-doc.jsx index 9e60cb467e..2f72234c0a 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-create-new-doc.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-create-new-doc.jsx @@ -22,7 +22,10 @@ export default function FileTreeCreateNewDoc() { event.preventDefault() finishCreatingDoc({ name }) - eventTracking.sendMB('new-file-created', { method: 'doc' }) + eventTracking.sendMB('new-file-created', { + method: 'doc', + extension: name.split('.').length > 1 ? name.split('.').pop() : '', + }) }, [finishCreatingDoc, name] ) diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-project.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-project.tsx index 9380084a4a..c8f181ab9c 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-project.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-project.tsx @@ -95,7 +95,10 @@ export default function FileTreeImportFromProject() { // form submission: create a linked file with this name, from this entity or output file const handleSubmit: FormEventHandler = event => { event.preventDefault() - eventTracking.sendMB('new-file-created', { method: 'project' }) + eventTracking.sendMB('new-file-created', { + method: 'project', + extension: name.split('.').length > 1 ? name.split('.').pop() : '', + }) if (isOutputFilesMode) { finishCreatingLinkedFile({ diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-url.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-url.jsx index 13d006aa00..23aac7d7e1 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-url.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-url.jsx @@ -36,7 +36,10 @@ export default function FileTreeImportFromUrl() { // form submission: create a linked file with this name, from this URL const handleSubmit = event => { event.preventDefault() - eventTracking.sendMB('new-file-created', { method: 'url' }) + eventTracking.sendMB('new-file-created', { + method: 'url', + extension: name.split('.').length > 1 ? name.split('.').pop() : '', + }) finishCreatingLinkedFile({ name, provider: 'url', diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.tsx index ec04578ebe..ccf7428101 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.tsx @@ -145,7 +145,13 @@ export default function FileTreeUploadDoc() { }) // broadcast doc metadata after each successful upload .on('upload-success', (file, response) => { - eventTracking.sendMB('new-file-created', { method: 'upload' }) + eventTracking.sendMB('new-file-created', { + method: 'upload', + extension: + file?.name && file?.name.split('.').length > 1 + ? file?.name.split('.').pop() + : '', + }) if (response.body.entity_type === 'doc') { window.setTimeout(() => { refreshProjectMetadata(projectId, response.body.entity_id) diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx index 05608fe488..85431aa5ef 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx @@ -1,12 +1,30 @@ +import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import * as eventTracking from '../../../../infrastructure/event-tracking' +import { useProjectContext } from '@/shared/context/project-context' import { MenuItem } from 'react-bootstrap' import { useFileTreeActionable } from '../../contexts/file-tree-actionable' +import { useFileTreeData } from '@/shared/context/file-tree-data-context' +import { useFileTreeSelectable } from '../../contexts/file-tree-selectable' +import { findInTree } from '../../util/find-in-tree' function FileTreeItemMenuItems() { const { t } = useTranslation() + const { fileTreeData } = useFileTreeData() + const { selectedEntityIds } = useFileTreeSelectable() + + // return the name of the selected file or doc if there is only one selected + const selectedFileName = useMemo(() => { + if (selectedEntityIds.size === 1) { + const [selectedEntityId] = selectedEntityIds + const selectedEntity = findInTree(fileTreeData, selectedEntityId) + return selectedEntity?.entity?.name + } + return null + }, [fileTreeData, selectedEntityIds]) + const { canRename, canDelete, @@ -19,15 +37,24 @@ function FileTreeItemMenuItems() { downloadPath, } = useFileTreeActionable() - const createWithAnalytics = () => { + const { owner } = useProjectContext() + + const downloadWithAnalytics = useCallback(() => { + // we are only interested in downloads of bib files WRT analytics, for the purposes of promoting the tpr integrations + if (selectedFileName?.endsWith('.bib')) { + eventTracking.sendMB('download-bib-file', { projectOwner: owner._id }) + } + }, [selectedFileName, owner]) + + const createWithAnalytics = useCallback(() => { eventTracking.sendMB('new-file-click', { location: 'file-menu' }) startCreatingDocOrFile() - } + }, [startCreatingDocOrFile]) - const uploadWithAnalytics = () => { + const uploadWithAnalytics = useCallback(() => { eventTracking.sendMB('upload-click', { location: 'file-menu' }) startUploadingDocOrFile() - } + }, [startUploadingDocOrFile]) return ( <> @@ -35,7 +62,7 @@ function FileTreeItemMenuItems() { {t('rename')} ) : null} {downloadPath ? ( - + {t('download')} ) : null} diff --git a/services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx b/services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx index 0f2f9efac0..a7441d6c64 100644 --- a/services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx @@ -21,6 +21,8 @@ import { } from '@/features/ide-react/types/file-tree' import { debugConsole } from '@/utils/debugging' import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view' +import { sendMB } from '@/infrastructure/event-tracking' +import { FileRef } from '../../../../../types/file-ref' const FileTreeOpenContext = createContext< | { @@ -34,7 +36,7 @@ const FileTreeOpenContext = createContext< >(undefined) export const FileTreeOpenProvider: FC = ({ children }) => { - const { rootDocId } = useProjectContext() + const { rootDocId, owner } = useProjectContext() const { eventEmitter, projectJoined } = useIdeReactContext() const { openDocId: openDocWithId, @@ -71,8 +73,14 @@ export const FileTreeOpenProvider: FC = ({ children }) => { setOpenEntity(selected) if (selected.type === 'doc' && fileTreeReady) { openDocWithId(selected.entity._id) + if (selected.entity.name.endsWith('.bib')) { + sendMB('open-bib-file', { + projectOwner: owner._id, + isSampleFile: selected.entity.name === 'sample.bib', + linkedFileProvider: null, + }) + } } - // Keep openFile scope value in sync with the file tree const openFile = selected.type === 'fileRef' @@ -80,10 +88,18 @@ export const FileTreeOpenProvider: FC = ({ children }) => { : null setOpenFile(openFile) if (openFile) { + if (selected?.entity?.name?.endsWith('.bib')) { + sendMB('open-bib-file', { + projectOwner: owner._id, + isSampleFile: false, + linkedFileProvider: (selected.entity as FileRef).linkedFileData + ?.provider, + }) + } window.dispatchEvent(new CustomEvent('file-view:file-opened')) } }, - [fileTreeReady, setOpenFile, openDocWithId] + [fileTreeReady, setOpenFile, openDocWithId, owner] ) const handleFileTreeDelete = useCallback( diff --git a/services/web/frontend/js/features/settings/components/linking/integration-widget.tsx b/services/web/frontend/js/features/settings/components/linking/integration-widget.tsx index aae910dcde..6ff979a417 100644 --- a/services/web/frontend/js/features/settings/components/linking/integration-widget.tsx +++ b/services/web/frontend/js/features/settings/components/linking/integration-widget.tsx @@ -8,8 +8,12 @@ import { sendMB } from '../../../../infrastructure/event-tracking' import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper' import { bsVersion } from '@/features/utils/bootstrap-5' -function trackUpgradeClick() { - sendMB('settings-upgrade-click') +function trackUpgradeClick(integration: string) { + sendMB('settings-upgrade-click', { integration }) +} + +function trackLinkingClick(integration: string) { + sendMB('link-integration-click', { integration, location: 'Settings' }) } type IntegrationLinkingWidgetProps = { @@ -73,6 +77,7 @@ export function IntegrationLinkingWidget({
void @@ -105,6 +112,7 @@ function ActionButton({ handleUnlinkClick, linkPath, disabled, + integration, }: ActionButtonProps) { const { t } = useTranslation() if (!hasFeature) { @@ -112,7 +120,7 @@ function ActionButton({ trackUpgradeClick(integration)} bs3Props={{ bsStyle: null, className: 'btn-primary' }} > {t('upgrade')} @@ -152,6 +160,7 @@ function ActionButton({ bs5: 'text-capitalize', })} bs3Props={{ bsStyle: null }} + onClick={() => trackLinkingClick(integration)} > {t('link')} @@ -164,6 +173,7 @@ function ActionButton({ type UnlinkConfirmModalProps = { show: boolean title: string + integration: string content: string unlinkPath: string handleHide: () => void @@ -172,6 +182,7 @@ type UnlinkConfirmModalProps = { function UnlinkConfirmationModal({ show, title, + integration, content, unlinkPath, handleHide, @@ -182,6 +193,13 @@ function UnlinkConfirmationModal({ event.preventDefault() handleHide() } + + const handleConfirm = () => { + sendMB('unlink-integration-click', { + integration, + }) + } + return ( @@ -209,6 +227,7 @@ function UnlinkConfirmationModal({ type="submit" variant="danger-ghost" bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }} + onClick={handleConfirm} > {t('unlink')}