Просмотр исходного кода

Merge pull request #18071 from overleaf/jdt-bib-events

Bibliography events for 3rd party integrations

GitOrigin-RevId: d8d7f4378d75166481d5265d2e8bef72d75968c3
Jimmy Domagala-Tang 2 лет назад
Родитель
Сommit
2c11ad84e0

+ 7 - 0
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 })
       }
     )

+ 4 - 1
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]
   )

+ 4 - 1
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({

+ 4 - 1
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',

+ 7 - 1
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)

+ 32 - 5
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() {
         <MenuItem onClick={startRenaming}>{t('rename')}</MenuItem>
       ) : null}
       {downloadPath ? (
-        <MenuItem href={downloadPath} download>
+        <MenuItem href={downloadPath} onClick={downloadWithAnalytics} download>
           {t('download')}
         </MenuItem>
       ) : null}

+ 19 - 3
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(

+ 22 - 3
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({
       </div>
       <div>
         <ActionButton
+          integration={title}
           hasFeature={hasFeature}
           linked={linked}
           handleUnlinkClick={handleUnlinkClick}
@@ -81,6 +86,7 @@ export function IntegrationLinkingWidget({
         />
       </div>
       <UnlinkConfirmationModal
+        integration={title}
         show={showModal}
         title={unlinkConfirmationTitle}
         content={unlinkConfirmationText}
@@ -92,6 +98,7 @@ export function IntegrationLinkingWidget({
 }
 
 type ActionButtonProps = {
+  integration: string
   hasFeature?: boolean
   linked?: boolean
   handleUnlinkClick: () => void
@@ -105,6 +112,7 @@ function ActionButton({
   handleUnlinkClick,
   linkPath,
   disabled,
+  integration,
 }: ActionButtonProps) {
   const { t } = useTranslation()
   if (!hasFeature) {
@@ -112,7 +120,7 @@ function ActionButton({
       <ButtonWrapper
         variant="primary"
         href="/user/subscription/plans"
-        onClick={trackUpgradeClick}
+        onClick={() => trackUpgradeClick(integration)}
         bs3Props={{ bsStyle: null, className: 'btn-primary' }}
       >
         <span className="text-capitalize">{t('upgrade')}</span>
@@ -152,6 +160,7 @@ function ActionButton({
               bs5: 'text-capitalize',
             })}
             bs3Props={{ bsStyle: null }}
+            onClick={() => trackLinkingClick(integration)}
           >
             {t('link')}
           </ButtonWrapper>
@@ -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 (
     <AccessibleModal show={show} onHide={handleHide}>
       <Modal.Header closeButton>
@@ -209,6 +227,7 @@ function UnlinkConfirmationModal({
             type="submit"
             variant="danger-ghost"
             bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
+            onClick={handleConfirm}
           >
             {t('unlink')}
           </ButtonWrapper>