Răsfoiți Sursa

Merge pull request #31035 from overleaf/mg-context-menu-analytics

Add analytics event for context menu, comment, track changes, and jump to location

GitOrigin-RevId: 8412cc3c8039cd1582ccee20b162b4bef4467dea
Malik Glossop 5 luni în urmă
părinte
comite
a293474c0c

+ 14 - 1
services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.tsx

@@ -10,6 +10,7 @@ import { Placement } from 'react-bootstrap/types'
 import useSynctex from '../hooks/use-synctex'
 import { useFeatureFlag } from '@/shared/context/split-test-context'
 import OLSpinner from '@/shared/components/ol/ol-spinner'
+import { sendMB } from '@/infrastructure/event-tracking'
 
 const GoToCodeButton = memo(function GoToCodeButton({
   syncToCode,
@@ -35,6 +36,10 @@ const GoToCodeButton = memo(function GoToCodeButton({
   }
 
   const syncToCodeWithButton = useCallback(() => {
+    sendMB('jump-to-location', {
+      method: 'arrow',
+      direction: 'pdf-location-in-code',
+    })
     syncToCode({ visualOffset: 72 })
   }, [syncToCode])
 
@@ -85,6 +90,14 @@ const GoToPdfButton = memo(function GoToPdfButton({
     'detach-synctex-control': !!isDetachLayout,
   })
 
+  const handleSyncToPdf = useCallback(() => {
+    sendMB('jump-to-location', {
+      method: 'arrow',
+      direction: 'code-location-in-pdf',
+    })
+    syncToPdf()
+  }, [syncToPdf])
+
   let buttonIcon = null
   if (syncToPdfInFlight) {
     buttonIcon = <OLSpinner size="sm" />
@@ -104,7 +117,7 @@ const GoToPdfButton = memo(function GoToPdfButton({
         <OLButton
           variant="secondary"
           size="sm"
-          onClick={syncToPdf}
+          onClick={handleSyncToPdf}
           disabled={syncToPdfInFlight || !canSyncToPdf}
           className={buttonClasses}
           aria-label={t('go_to_code_location_in_pdf')}

+ 5 - 2
services/web/frontend/js/features/review-panel/components/review-mode-switcher.tsx

@@ -28,7 +28,7 @@ function ReviewModeSwitcher() {
   const { permissionsLevel } = useIdeReactContext()
   const { write, trackedWrite } = usePermissionsContext()
   const { features } = useProjectContext()
-  const { setShowUpgradeModal } = useEditorContext()
+  const { setUpgradeTrackChangesModal } = useEditorContext()
   const showViewOption = permissionsLevel === 'readOnly'
   const view = useCodeMirrorViewContext()
 
@@ -73,7 +73,10 @@ function ReviewModeSwitcher() {
                 return
               }
               if (!features.trackChanges) {
-                setShowUpgradeModal(true)
+                setUpgradeTrackChangesModal({
+                  show: true,
+                  location: 'review-switcher',
+                })
               } else {
                 sendMB('editing-mode-change', {
                   role: permissionsLevel,

+ 9 - 1
services/web/frontend/js/features/review-panel/components/review-tooltip-menu.tsx

@@ -36,6 +36,7 @@ import classNames from 'classnames'
 import useEventListener from '@/shared/hooks/use-event-listener'
 import useReviewPanelLayout from '../hooks/use-review-panel-layout'
 import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
+import { sendMB } from '@/infrastructure/event-tracking'
 
 const EDIT_MODE_SWITCH_WIDGET_HEIGHT = 40
 const CM_LINE_RIGHT_PADDING = 8
@@ -110,6 +111,13 @@ const ReviewTooltipMenuContent = memo<{ onAddComment: () => void }>(
     >()
     const [visible, setVisible] = useState(false)
 
+    const handleAddCommentClick = useCallback(() => {
+      sendMB('add-comment', {
+        location: 'tooltip',
+      })
+      onAddComment()
+    }, [onAddComment])
+
     const changesInSelection = useMemo(() => {
       return (ranges?.changes ?? []).filter(({ op }) => {
         const opFrom = op.p
@@ -219,7 +227,7 @@ const ReviewTooltipMenuContent = memo<{ onAddComment: () => void }>(
       >
         <button
           className="review-tooltip-menu-button review-tooltip-add-comment-button"
-          onClick={onAddComment}
+          onClick={handleAddCommentClick}
         >
           <MaterialIcon type="chat" />
           {t('add_comment')}

+ 38 - 10
services/web/frontend/js/features/review-panel/components/upgrade-track-changes-modal.tsx

@@ -4,7 +4,8 @@ import { useUserContext } from '@/shared/context/user-context'
 import teaserVideo from '../images/teaser-track-changes.mp4'
 import teaserImage from '../images/teaser-track-changes.gif'
 import { startFreeTrial, upgradePlan } from '@/main/account-upgrade'
-import { memo } from 'react'
+import { sendMB } from '@/infrastructure/event-tracking'
+import { memo, useEffect } from 'react'
 import {
   OLModal,
   OLModalBody,
@@ -22,14 +23,34 @@ function UpgradeTrackChangesModal() {
   const { t } = useTranslation()
   const { project } = useProjectContext()
   const user = useUserContext()
-  const { showUpgradeModal, setShowUpgradeModal } = useEditorContext()
+  const {
+    upgradeTrackChangesModal: { show, location = 'unknown' },
+    setUpgradeTrackChangesModal,
+  } = useEditorContext()
 
-  if (!showUpgradeModal) {
+  const handleClose = () => {
+    sendMB('paywall-dismiss', {
+      'paywall-type': 'track-changes',
+      location,
+    })
+    setUpgradeTrackChangesModal({ show: false })
+  }
+
+  useEffect(() => {
+    if (show) {
+      sendMB('paywall-prompt', {
+        'paywall-type': 'track-changes',
+        location,
+      })
+    }
+  }, [show, location])
+
+  if (!show) {
     return null
   }
 
   return (
-    <OLModal show={showUpgradeModal} onHide={() => setShowUpgradeModal(false)}>
+    <OLModal show={show} onHide={handleClose}>
       <OLModalHeader>
         <OLModalTitle>{t('upgrade_to_review')}</OLModalTitle>
       </OLModalHeader>
@@ -67,14 +88,24 @@ function UpgradeTrackChangesModal() {
               user.allowedFreeTrial ? (
                 <OLButton
                   variant="premium"
-                  onClick={() => startFreeTrial('track-changes')}
+                  onClick={() =>
+                    startFreeTrial('track-changes', undefined, {
+                      location,
+                    })
+                  }
                 >
                   {t('try_it_for_free')}
                 </OLButton>
               ) : (
                 <OLButton
                   variant="premium"
-                  onClick={() => upgradePlan('project-sharing')}
+                  onClick={() => {
+                    sendMB('paywall-click', {
+                      'paywall-type': 'track-changes',
+                      location,
+                    })
+                    upgradePlan('project-sharing')
+                  }}
                 >
                   {t('upgrade')}
                 </OLButton>
@@ -92,10 +123,7 @@ function UpgradeTrackChangesModal() {
         )}
       </OLModalBody>
       <OLModalFooter>
-        <OLButton
-          variant="secondary"
-          onClick={() => setShowUpgradeModal(false)}
-        >
+        <OLButton variant="secondary" onClick={handleClose}>
           {t('close')}
         </OLButton>
       </OLModalFooter>

+ 7 - 1
services/web/frontend/js/features/source-editor/components/editor-context-menu-feedback.tsx

@@ -6,9 +6,15 @@ import {
 } from '@/shared/components/dropdown/dropdown-menu'
 import DropdownListItem from '@/shared/components/dropdown/dropdown-list-item'
 import SplitTestBadge from '@/shared/components/split-test-badge'
+import { sendContextMenuEvent } from '../utils/context-menu-analytics'
 
 const FEEDBACK_FORM_URL = 'https://forms.gle/BsbNQeSwGKEwXpxTA'
-const handleClick = () => {
+
+function handleClick() {
+  sendContextMenuEvent('menu-click', {
+    location: 'editor-context-menu',
+    item: 'give-feedback',
+  })
   window.open(FEEDBACK_FORM_URL, '_blank', 'noopener,noreferrer')
 }
 

+ 4 - 0
services/web/frontend/js/features/source-editor/components/editor-context-menu.tsx

@@ -16,6 +16,7 @@ import { useFeatureFlag } from '@/shared/context/split-test-context'
 import { useContextMenuItems } from '../hooks/use-context-menu-items'
 import DropdownListItem from '@/shared/components/dropdown/dropdown-list-item'
 import { EditorContextMenuFeedback } from './editor-context-menu-feedback'
+import { sendContextMenuEvent } from '../utils/context-menu-analytics'
 
 const EditorContextMenu: FC = () => {
   const state = useCodeMirrorStateContext()
@@ -40,6 +41,9 @@ const EditorContextMenuContent: FC = memo(function EditorContextMenuContent() {
   const menuRef = useRef<any>(null)
 
   useEffect(() => {
+    sendContextMenuEvent('menu-expand', {
+      location: 'editor-context-menu',
+    })
     menuRef.current?.focus()
   }, [])
 

+ 3 - 1
services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx

@@ -17,6 +17,8 @@ import { useProjectContext } from '@/shared/context/project-context'
 import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
 import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
 
+const addCommentFromToolbar = () => commands.addComment('toolbar')
+
 export const ToolbarItems: FC<{
   state: EditorState
   overflowed?: Set<string>
@@ -135,7 +137,7 @@ export const ToolbarItems: FC<{
                   id="toolbar-add-comment"
                   label={t('add_comment')}
                   disabled={state.selection.main.empty}
-                  command={commands.addComment}
+                  command={addCommentFromToolbar}
                   icon="add_comment"
                 />
               )}

+ 4 - 0
services/web/frontend/js/features/source-editor/extensions/shortcuts.ts

@@ -23,6 +23,7 @@ import {
   deleteToVisualLineStart,
 } from './visual-line-selection'
 import { emitShortcutEvent } from '@/features/source-editor/extensions/toolbar/utils/analytics'
+import { sendMB } from '@/infrastructure/event-tracking'
 
 const toggleReviewPanel = () => {
   window.dispatchEvent(new Event('ui.toggle-review-panel'))
@@ -30,6 +31,9 @@ const toggleReviewPanel = () => {
 }
 
 const addNewCommentFromKbdShortcut = () => {
+  sendMB('add-comment', {
+    location: 'shortcut',
+  })
   window.dispatchEvent(new Event('add-new-review-comment'))
   return true
 }

+ 5 - 1
services/web/frontend/js/features/source-editor/extensions/toolbar/commands.ts

@@ -5,6 +5,7 @@ import {
   openSearchPanel,
   searchPanelOpen,
 } from '@codemirror/search'
+import { sendMB } from '@/infrastructure/event-tracking'
 import { toggleRanges, wrapRanges } from '../../commands/ranges'
 import {
   ancestorListType,
@@ -163,7 +164,10 @@ export const toggleSearch: Command = view => {
   return true
 }
 
-export const addComment = () => {
+export const addComment = (location: string) => {
+  sendMB('add-comment', {
+    location,
+  })
   window.dispatchEvent(new Event('add-new-review-comment'))
 }
 

+ 54 - 23
services/web/frontend/js/features/source-editor/hooks/use-context-menu-items.tsx

@@ -26,6 +26,10 @@ import {
 import { isVisual } from '../extensions/visual/visual'
 import { useEditorContext } from '@/shared/context/editor-context'
 import { useTrackingChangesMode } from '@/shared/hooks/use-tracking-changes-mode'
+import {
+  sendContextMenuEvent,
+  ContextMenuItemSegmentation,
+} from '../utils/context-menu-analytics'
 
 export const useContextMenuItems = () => {
   const view = useCodeMirrorViewContext()
@@ -45,7 +49,7 @@ export const useContextMenuItems = () => {
   const { shortcuts } = useCommandRegistry()
   const { features } = useProjectContext()
   const requestedPdfSyncRef = useRef(false)
-  const { setShowUpgradeModal } = useEditorContext()
+  const { setUpgradeTrackChangesModal } = useEditorContext()
   const trackingChangesMode = useTrackingChangesMode()
   const isReview = trackingChangesMode === 'review'
 
@@ -87,40 +91,58 @@ export const useContextMenuItems = () => {
     pdfUrl && pdfViewer !== 'native' && !visualPreviewEnabled && canSyncToPdf
 
   const wrapForContextMenu = useCallback(
-    (command: () => Promise<boolean> | boolean) => async () => {
-      const result = await command()
-      if (result !== false) {
-        view.focus()
-        closeMenu()
-      }
-    },
+    (
+      item: ContextMenuItemSegmentation,
+      command: () => Promise<boolean> | boolean
+    ) =>
+      async () => {
+        sendContextMenuEvent('menu-click', {
+          location: 'editor-context-menu',
+          item,
+        })
+        const result = await command()
+        if (result !== false) {
+          view.focus()
+          closeMenu()
+        }
+      },
     [view, closeMenu]
   )
 
   const inVisualMode = isVisual(view)
 
-  const handleCut = wrapForContextMenu(() => cutSelection(view))
-  const handleCopy = wrapForContextMenu(() => copySelection(view))
-  const handlePaste = wrapForContextMenu(() =>
+  const handleCut = wrapForContextMenu('cut', () => cutSelection(view))
+  const handleCopy = wrapForContextMenu('copy', () => copySelection(view))
+  const handlePaste = wrapForContextMenu('paste', () =>
     inVisualMode ? pasteWithFormatting(view) : pasteWithoutFormatting(view)
   )
-  const handlePasteSpecial = wrapForContextMenu(() =>
-    inVisualMode ? pasteWithoutFormatting(view) : pasteWithFormatting(view)
+  const handlePasteSpecial = wrapForContextMenu(
+    inVisualMode ? 'paste-without-formatting' : 'paste-with-formatting',
+    () =>
+      inVisualMode ? pasteWithoutFormatting(view) : pasteWithFormatting(view)
+  )
+  const handleDelete = wrapForContextMenu('delete', () =>
+    commands.deleteSelection(view)
   )
-  const handleDelete = wrapForContextMenu(() => commands.deleteSelection(view))
 
-  const handleToggleTrackChanges = wrapForContextMenu(() => {
-    // Matching the logic in review toggle to ensure consistency for server pro
-    if (!features.trackChanges && !isReview) {
-      setShowUpgradeModal(true)
+  const handleToggleTrackChanges = wrapForContextMenu(
+    wantTrackChanges ? 'back-to-editing' : 'suggest-edits',
+    () => {
+      // Matching the logic in review toggle to ensure consistency for server pro
+      if (!features.trackChanges && !isReview) {
+        setUpgradeTrackChangesModal({
+          show: true,
+          location: 'editor-context-menu',
+        })
+        return true
+      }
+      window.dispatchEvent(new Event('toggle-track-changes'))
       return true
     }
-    window.dispatchEvent(new Event('toggle-track-changes'))
-    return true
-  })
+  )
 
-  const handleComment = wrapForContextMenu(() => {
-    commands.addComment()
+  const handleComment = wrapForContextMenu('comment', () => {
+    commands.addComment('editor-context-menu')
     return true
   })
 
@@ -130,6 +152,15 @@ export const useContextMenuItems = () => {
     if (isEditorOnly) {
       changeLayout('sideBySide')
     }
+
+    sendContextMenuEvent('menu-click', {
+      location: 'editor-context-menu',
+      item: 'jump-to-location-in-pdf',
+    })
+    sendContextMenuEvent('jump-to-location', {
+      method: 'editor-context-menu',
+      direction: 'code-location-in-pdf',
+    })
     requestedPdfSyncRef.current = true
     syncToPdf()
     view.focus()

+ 1 - 1
services/web/frontend/js/features/source-editor/hooks/use-toolbar-menu-editor-commands.tsx

@@ -175,7 +175,7 @@ export const useToolbarMenuBarEditorCommands = () => {
         id: 'comment',
         label: t('comment'),
         handler: () => {
-          commands.addComment()
+          commands.addComment('toolbar')
         },
         disabled: !comment || state.selection.main.empty,
       },

+ 44 - 0
services/web/frontend/js/features/source-editor/utils/context-menu-analytics.ts

@@ -0,0 +1,44 @@
+import { sendMB } from '@/infrastructure/event-tracking'
+
+export type ContextMenuItemSegmentation =
+  | 'cut'
+  | 'copy'
+  | 'paste'
+  | 'paste-without-formatting'
+  | 'paste-with-formatting'
+  | 'give-feedback'
+  | 'delete'
+  | 'jump-to-location-in-pdf'
+  | 'suggest-edits'
+  | 'back-to-editing'
+  | 'comment'
+
+export type ContextMenuAnalyticsEvents = {
+  'menu-expand': {
+    location: 'editor-context-menu'
+  }
+  'menu-click': {
+    location: 'editor-context-menu'
+    item: ContextMenuItemSegmentation
+  }
+  'jump-to-location': {
+    method: 'editor-context-menu'
+    direction: 'code-location-in-pdf'
+  }
+  'add-comment': {
+    location: 'editor-context-menu'
+  }
+  'paywall-prompt': {
+    'paywall-type': 'track-changes'
+    location: 'editor-context-menu'
+  }
+}
+
+export const sendContextMenuEvent = <
+  T extends keyof ContextMenuAnalyticsEvents,
+>(
+  eventName: T,
+  segmentation: ContextMenuAnalyticsEvents[T]
+) => {
+  sendMB(eventName, segmentation)
+}

+ 13 - 5
services/web/frontend/js/shared/context/editor-context.tsx

@@ -20,6 +20,11 @@ import { WritefullAPI } from './types/writefull-instance'
 import { Cobranding } from '../../../../types/cobranding'
 import { SymbolWithCharacter } from '../../../../modules/symbol-palette/frontend/js/data/symbols'
 
+type UpgradeTrackChangesModal = {
+  show: boolean
+  location?: string
+}
+
 export const EditorContext = createContext<
   | {
       cobranding?: Cobranding
@@ -35,8 +40,10 @@ export const EditorContext = createContext<
       premiumSuggestionResetDate: Date
       writefullInstance: WritefullAPI | null
       setWritefullInstance: (instance: WritefullAPI) => void
-      showUpgradeModal: boolean
-      setShowUpgradeModal: Dispatch<SetStateAction<boolean>>
+      upgradeTrackChangesModal: UpgradeTrackChangesModal
+      setUpgradeTrackChangesModal: Dispatch<
+        SetStateAction<UpgradeTrackChangesModal>
+      >
     }
   | undefined
 >(undefined)
@@ -88,7 +95,8 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
         : new Date()
     })
 
-  const [showUpgradeModal, setShowUpgradeModal] = useState(false)
+  const [showUpgradeModal, setShowUpgradeModal] =
+    useState<UpgradeTrackChangesModal>({ show: false })
 
   const isPendingEditor = useMemo(
     () =>
@@ -170,8 +178,8 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
       setPremiumSuggestionResetDate,
       writefullInstance,
       setWritefullInstance,
-      showUpgradeModal,
-      setShowUpgradeModal,
+      upgradeTrackChangesModal: showUpgradeModal,
+      setUpgradeTrackChangesModal: setShowUpgradeModal,
     }),
     [
       cobranding,

+ 2 - 2
services/web/test/frontend/helpers/editor-providers.tsx

@@ -292,10 +292,10 @@ export function makeEditorProvider({
       setPremiumSuggestionResetDate: () => {},
       writefullInstance: null,
       setWritefullInstance: () => {},
-      showUpgradeModal: false,
-      setShowUpgradeModal: () => {},
       cobranding,
       isRestrictedTokenMember,
+      upgradeTrackChangesModal: { show: false },
+      setUpgradeTrackChangesModal: () => {},
     }
 
     return (