Selaa lähdekoodia

Merge pull request #24979 from overleaf/mj-editor-event-hook

[web] Introduce React hook wrapper around sendMB and friends

GitOrigin-RevId: 3c693ae609c6d4e5ba280c45096692aca47975ca
Mathias Jakobsen 1 vuosi sitten
vanhempi
sitoutus
e98addf33a

+ 12 - 4
services/web/frontend/js/features/ide-redesign/components/rail.tsx

@@ -33,6 +33,7 @@ import DictionarySettingsModal from './settings/editor-settings/dictionary-setti
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
 import { useChatContext } from '@/features/chat/context/chat-context'
+import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
 
 type RailElement = {
   icon: AvailableUnfilledIcon
@@ -76,6 +77,7 @@ const RAIL_MODALS: {
 ]
 
 export const RailLayout = () => {
+  const { sendEvent } = useEditorAnalytics()
   const { t } = useTranslation()
   const {
     activeModal,
@@ -147,16 +149,20 @@ export const RailLayout = () => {
         key: 'settings',
         icon: 'settings',
         title: t('settings'),
-        action: () => setLeftMenuShown(true),
+        action: () => {
+          sendEvent('rail-click', { tab: 'settings' })
+          setLeftMenuShown(true)
+        },
       },
     ],
-    [setLeftMenuShown, t]
+    [setLeftMenuShown, t, sendEvent]
   )
 
   const onTabSelect = useCallback(
     (key: string | null) => {
       if (key === selectedTab) {
         togglePane()
+        sendEvent('rail-click', { tab: key, type: 'toggle' })
       } else {
         // HACK: Apparently the onSelect event is triggered with href attributes
         // from DropdownItems
@@ -164,15 +170,17 @@ export const RailLayout = () => {
           // Attempting to open a non-existent tab
           return
         }
+        const keyOrDefault = key ?? 'file-tree'
         // Change the selected tab and make sure it's open
-        openTab((key ?? 'file-tree') as RailTabKey)
+        openTab(keyOrDefault as RailTabKey)
+        sendEvent('rail-click', { tab: keyOrDefault })
 
         if (key === 'chat') {
           markMessagesAsRead()
         }
       }
     },
-    [openTab, togglePane, selectedTab, railTabs, markMessagesAsRead]
+    [openTab, togglePane, selectedTab, railTabs, sendEvent, markMessagesAsRead]
   )
 
   const isReviewPanelOpen = selectedTab === 'review-panel'

+ 13 - 2
services/web/frontend/js/features/ide-redesign/components/switcher-modal/modal.tsx

@@ -14,6 +14,7 @@ import {
 import Notification from '@/shared/components/notification'
 import { useSwitchEnableNewEditorState } from '../../hooks/use-switch-enable-new-editor-state'
 import { Trans, useTranslation } from 'react-i18next'
+import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
 
 export const IdeRedesignSwitcherModal = () => {
   const { t } = useTranslation()
@@ -66,13 +67,18 @@ const SwitcherModalContentEnabled: FC<ModalContentProps> = ({
   loading,
 }) => {
   const { t } = useTranslation()
+  const { sendEvent } = useEditorAnalytics()
   const disable = useCallback(() => {
+    sendEvent('editor-redesign-toggle', {
+      action: 'disable',
+      location: 'modal',
+    })
     setEditorRedesignStatus(false)
       .then(hide)
       .catch(() => {
         // do nothing, we're already showing the error
       })
-  }, [setEditorRedesignStatus, hide])
+  }, [setEditorRedesignStatus, hide, sendEvent])
   return (
     <>
       <OLModalBody>
@@ -116,13 +122,18 @@ const SwitcherModalContentDisabled: FC<ModalContentProps> = ({
   loading,
 }) => {
   const { t } = useTranslation()
+  const { sendEvent } = useEditorAnalytics()
   const enable = useCallback(() => {
+    sendEvent('editor-redesign-toggle', {
+      action: 'enable',
+      location: 'modal',
+    })
     setEditorRedesignStatus(true)
       .then(hide)
       .catch(() => {
         // do nothing, we're already showing the error
       })
-  }, [setEditorRedesignStatus, hide])
+  }, [setEditorRedesignStatus, hide, sendEvent])
   return (
     <>
       <OLModalBody>

+ 8 - 7
services/web/frontend/js/features/ide-redesign/components/toolbar/change-layout-options.tsx

@@ -9,10 +9,10 @@ import {
 } from '@/shared/context/layout-context'
 import React, { useCallback } from 'react'
 import { useTranslation } from 'react-i18next'
-import * as eventTracking from '../../../../infrastructure/event-tracking'
 import useEventListener from '@/shared/hooks/use-event-listener'
 import { DetachRole } from '@/shared/context/detach-context'
 import { Spinner } from 'react-bootstrap'
+import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
 
 type LayoutOption = 'sideBySide' | 'editorOnly' | 'pdfOnly' | 'detachedPdf'
 
@@ -87,6 +87,7 @@ const LayoutDropdownItem = ({
 }
 
 export default function ChangeLayoutOptions() {
+  const { sendEvent } = useEditorAnalytics()
   const {
     reattach,
     detach,
@@ -99,16 +100,16 @@ export default function ChangeLayoutOptions() {
 
   const handleDetach = useCallback(() => {
     detach()
-    eventTracking.sendMB('project-layout-detach')
-  }, [detach])
+    sendEvent('project-layout-detach')
+  }, [detach, sendEvent])
 
   const handleReattach = useCallback(() => {
     if (detachRole !== 'detacher') {
       return
     }
     reattach()
-    eventTracking.sendMB('project-layout-reattach')
-  }, [detachRole, reattach])
+    sendEvent('project-layout-reattach')
+  }, [detachRole, reattach, sendEvent])
 
   // reattach when the PDF pane opens
   useEventListener('ui:pdf-open', handleReattach)
@@ -117,12 +118,12 @@ export default function ChangeLayoutOptions() {
     (newLayout: IdeLayout, newView?: IdeView) => {
       handleReattach()
       changeLayout(newLayout, newView)
-      eventTracking.sendMB('project-layout-change', {
+      sendEvent('project-layout-change', {
         layout: newLayout,
         view: newView,
       })
     },
-    [changeLayout, handleReattach]
+    [changeLayout, handleReattach, sendEvent]
   )
 
   const { t } = useTranslation()

+ 1 - 0
services/web/frontend/js/features/ide-redesign/components/toolbar/command-dropdown.tsx

@@ -101,6 +101,7 @@ const CommandDropdownChild = ({ item }: { item: Entry<TaggedCommand> }) => {
   if (isTaggedCommand(item)) {
     return (
       <MenuBarOption
+        eventKey={item.id}
         key={item.id}
         title={item.label}
         // eslint-disable-next-line react/jsx-handler-names

+ 18 - 2
services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx

@@ -24,6 +24,7 @@ import { useRailContext } from '../../contexts/rail-context'
 import WordCountModal from '@/features/word-count-modal/components/word-count-modal'
 import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
+import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
 
 export const ToolbarMenuBar = () => {
   const { t } = useTranslation()
@@ -210,6 +211,7 @@ export const ToolbarMenuBar = () => {
           <ChangeLayoutOptions />
           <DropdownHeader>Editor settings</DropdownHeader>
           <MenuBarOption
+            eventKey="show_equation_preview"
             title={t('show_equation_preview')}
             trailingIcon={mathPreview ? 'check' : undefined}
             onClick={toggleMathPreview}
@@ -227,18 +229,25 @@ export const ToolbarMenuBar = () => {
           className="ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued"
         >
           <MenuBarOption
+            eventKey="keyboard_shortcuts"
             title={t('keyboard_shortcuts')}
             onClick={openKeyboardShortcutsModal}
           />
           <MenuBarOption
             title={t('documentation')}
+            eventKey="documentation"
             href="/learn"
             target="_blank"
             rel="noopener noreferrer"
           />
           <DropdownDivider />
-          <MenuBarOption title={t('contact_us')} onClick={openContactUsModal} />
           <MenuBarOption
+            eventKey="contact_us"
+            title={t('contact_us')}
+            onClick={openContactUsModal}
+          />
+          <MenuBarOption
+            eventKey="give_feedback"
             title={t('give_feedback')}
             href="https://forms.gle/soyVStc5qDx9na1Z6"
             target="_blank"
@@ -247,6 +256,7 @@ export const ToolbarMenuBar = () => {
           <DropdownDivider />
           <SwitchToOldEditorMenuBarOption />
           <MenuBarOption
+            eventKey="whats_new"
             title="What's new?"
             onClick={openEditorRedesignSwitcherModal}
           />
@@ -263,14 +273,19 @@ export const ToolbarMenuBar = () => {
 const SwitchToOldEditorMenuBarOption = () => {
   const { loading, error, setEditorRedesignStatus } =
     useSwitchEnableNewEditorState()
+  const { sendEvent } = useEditorAnalytics()
 
   const disable: MouseEventHandler = useCallback(
     event => {
       // Don't close the dropdown
       event.stopPropagation()
+      sendEvent('editor-redesign-toggle', {
+        action: 'disable',
+        location: 'menu-bar',
+      })
       setEditorRedesignStatus(false)
     },
-    [setEditorRedesignStatus]
+    [setEditorRedesignStatus, sendEvent]
   )
   let icon = null
   if (loading) {
@@ -280,6 +295,7 @@ const SwitchToOldEditorMenuBarOption = () => {
   }
   return (
     <MenuBarOption
+      eventKey="switch_to_old_editor"
       title="Switch to old editor"
       onClick={disable}
       disabled={loading}

+ 6 - 5
services/web/frontend/js/features/ide-redesign/components/toolbar/show-history-button.tsx

@@ -1,20 +1,21 @@
 import { useTranslation } from 'react-i18next'
-import * as eventTracking from '../../../../infrastructure/event-tracking'
+import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import { useLayoutContext } from '@/shared/context/layout-context'
 import { useCallback } from 'react'
-import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
-import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
+import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
 
 export default function ShowHistoryButton() {
   const { t } = useTranslation()
   const { view, setView } = useLayoutContext()
+  const { sendEvent } = useEditorAnalytics()
 
   const toggleHistoryOpen = useCallback(() => {
     const action = view === 'history' ? 'close' : 'open'
-    eventTracking.sendMB('navigation-clicked-history', { action })
+    sendEvent('navigation-clicked-history', { action })
 
     setView(view === 'history' ? 'editor' : 'history')
-  }, [view, setView])
+  }, [view, setView, sendEvent])
 
   return (
     <div className="ide-redesign-toolbar-button-container">

+ 1 - 1
services/web/frontend/js/infrastructure/event-tracking.ts

@@ -1,7 +1,7 @@
 import sessionStorage from './session-storage'
 import getMeta from '@/utils/meta'
 
-type Segmentation = Record<
+export type Segmentation = Record<
   string,
   string | number | boolean | undefined | unknown | any // TODO: RecurlyError
 >

+ 15 - 2
services/web/frontend/js/shared/components/menu-bar/menu-bar-option.tsx

@@ -1,7 +1,8 @@
 import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item'
 import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
 import { useNestableDropdown } from '@/shared/hooks/use-nestable-dropdown'
-import { MouseEventHandler, ReactNode } from 'react'
+import { MouseEventHandler, ReactNode, useCallback } from 'react'
 
 type MenuBarOptionProps = {
   title: string
@@ -11,18 +12,30 @@ type MenuBarOptionProps = {
   href?: string
   target?: string
   rel?: string
+  eventKey?: string
 }
 
 export const MenuBarOption = ({
   title,
-  onClick,
+  onClick: clickHandler,
   href,
   disabled,
   trailingIcon,
   target,
   rel,
+  eventKey,
 }: MenuBarOptionProps) => {
   const { setSelected } = useNestableDropdown()
+  const { sendEvent } = useEditorAnalytics()
+  const onClick: MouseEventHandler = useCallback(
+    e => {
+      if (eventKey) {
+        sendEvent('menu-bar-option-click', { key: eventKey })
+      }
+      return clickHandler?.(e)
+    },
+    [clickHandler, eventKey, sendEvent]
+  )
   return (
     <DropdownListItem>
       <DropdownItem

+ 44 - 0
services/web/frontend/js/shared/hooks/use-editor-analytics.ts

@@ -0,0 +1,44 @@
+import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
+import {
+  Segmentation,
+  sendMB,
+  sendMBOnce,
+  sendMBSampled,
+} from '@/infrastructure/event-tracking'
+import { useCallback } from 'react'
+
+export const useEditorAnalytics = () => {
+  const editorRedesign = useIsNewEditorEnabled()
+
+  const populateSegmentation = useCallback(
+    (segmentation: Segmentation | undefined = {}): Segmentation => {
+      return editorRedesign
+        ? { ...segmentation, 'editor-redesign': 'enabled' }
+        : segmentation
+    },
+    [editorRedesign]
+  )
+
+  const sendEvent: typeof sendMB = useCallback(
+    (key, segmentation) => {
+      sendMB(key, populateSegmentation(segmentation))
+    },
+    [populateSegmentation]
+  )
+
+  const sendEventOnce: typeof sendMBOnce = useCallback(
+    (key, segmentation) => {
+      sendMBOnce(key, populateSegmentation(segmentation))
+    },
+    [populateSegmentation]
+  )
+
+  const sendEventSampled: typeof sendMBSampled = useCallback(
+    (key, segmentation, rate) => {
+      sendMBSampled(key, populateSegmentation(segmentation), rate)
+    },
+    [populateSegmentation]
+  )
+
+  return { sendEvent, sendEventOnce, sendEventSampled }
+}