From 48e758a5fec296aaca57a1518aab8910322362ce Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Tue, 11 Jul 2023 14:31:36 +0100 Subject: [PATCH] Record each completion selection for analytics (#13665) GitOrigin-RevId: bc8e92ceca51f6365c4311204a35fc85914969b0 --- .../components/toolbar/button-menu.tsx | 4 +-- .../toolbar/insert-figure-dropdown.tsx | 4 +-- .../components/toolbar/math-dropdown.tsx | 6 ++-- .../toolbar/section-heading-dropdown.tsx | 4 +-- .../components/toolbar/toolbar-button.tsx | 4 +-- .../extensions/completion-logger.ts | 28 +++++++++++++++++++ .../source-editor/extensions/index.ts | 2 ++ .../extensions/toolbar/utils/analytics.ts | 16 +++++++++-- .../visual-widgets/editable-graphics.ts | 4 +-- 9 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 services/web/frontend/js/features/source-editor/extensions/completion-logger.ts diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx index fcdde3b067..d3adeae023 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx @@ -4,7 +4,7 @@ import Icon from '../../../../shared/components/icon' import useDropdown from '../../../../shared/hooks/use-dropdown' import Tooltip from '../../../../shared/components/tooltip' import { EditorView } from '@codemirror/view' -import { emitCommandEvent } from '../../extensions/toolbar/utils/analytics' +import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics' import { useCodeMirrorViewContext } from '../codemirror-editor' import MaterialIcon from '../../../../shared/components/material-icon' @@ -38,7 +38,7 @@ export const ToolbarButtonMenu: FC<{ }} onClick={event => { if (event.altKey && altCommand && open === false) { - emitCommandEvent(view, id) + emitToolbarEvent(view, id) event.preventDefault() altCommand(view) view.focus() diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/insert-figure-dropdown.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/insert-figure-dropdown.tsx index c8d51f4bb0..bfd724a0fc 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/insert-figure-dropdown.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/insert-figure-dropdown.tsx @@ -4,7 +4,7 @@ import Icon from '../../../../shared/components/icon' import { useCallback } from 'react' import { FigureModalSource } from '../figure-modal/figure-modal-context' import { useTranslation } from 'react-i18next' -import { emitCommandEvent } from '../../extensions/toolbar/utils/analytics' +import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics' import { useCodeMirrorViewContext } from '../codemirror-editor' import { insertFigure } from '../../extensions/toolbar/commands' @@ -13,7 +13,7 @@ export const InsertFigureDropdown = () => { const view = useCodeMirrorViewContext() const openFigureModal = useCallback( (source: FigureModalSource, sourceName: string) => { - emitCommandEvent(view, `toolbar-figure-modal-${sourceName}`) + emitToolbarEvent(view, `toolbar-figure-modal-${sourceName}`) window.dispatchEvent( new CustomEvent('figure-modal:open', { detail: source, diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/math-dropdown.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/math-dropdown.tsx index 78f214ec35..ea0c35d5b0 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/math-dropdown.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/math-dropdown.tsx @@ -1,6 +1,6 @@ import { ListGroupItem } from 'react-bootstrap' import { ToolbarButtonMenu } from './button-menu' -import { emitCommandEvent } from '../../extensions/toolbar/utils/analytics' +import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics' import MaterialIcon from '../../../../shared/components/material-icon' import { useTranslation } from 'react-i18next' import { useCodeMirrorViewContext } from '../codemirror-editor' @@ -23,7 +23,7 @@ export function MathDropdown() { { - emitCommandEvent(view, 'toolbar-inline-math') + emitToolbarEvent(view, 'toolbar-inline-math') event.preventDefault() wrapInInlineMath(view) view.focus() @@ -35,7 +35,7 @@ export function MathDropdown() { { - emitCommandEvent(view, 'toolbar-display-math') + emitToolbarEvent(view, 'toolbar-display-math') event.preventDefault() wrapInDisplayMath(view) view.focus() diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/section-heading-dropdown.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/section-heading-dropdown.tsx index 39a265bf5a..8bb07a79c6 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/section-heading-dropdown.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/section-heading-dropdown.tsx @@ -11,7 +11,7 @@ import { useCallback, useRef } from 'react' import { Overlay, Popover } from 'react-bootstrap' import useEventListener from '../../../../shared/hooks/use-event-listener' import useDropdown from '../../../../shared/hooks/use-dropdown' -import { emitCommandEvent } from '../../extensions/toolbar/utils/analytics' +import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics' import Icon from '../../../../shared/components/icon' import { useTranslation } from 'react-i18next' @@ -90,7 +90,7 @@ export const SectionHeadingDropdown = () => { role="menuitem" key={level} onClick={() => { - emitCommandEvent(view, 'section-level-change') + emitToolbarEvent(view, 'section-level-change') setSectionHeadingLevel(view, level) view.focus() setOverflowOpen(false) diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-button.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-button.tsx index d352d68648..b6fab0c409 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-button.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-button.tsx @@ -4,7 +4,7 @@ import { useCodeMirrorViewContext } from '../codemirror-editor' import { Button } from 'react-bootstrap' import classnames from 'classnames' import Tooltip from '../../../../shared/components/tooltip' -import { emitCommandEvent } from '../../extensions/toolbar/utils/analytics' +import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics' import Icon from '../../../../shared/components/icon' export const ToolbarButton = memo<{ @@ -38,7 +38,7 @@ export const ToolbarButton = memo<{ const handleClick = useCallback( event => { - emitCommandEvent(view, id) + emitToolbarEvent(view, id) if (command) { event.preventDefault() command(view) diff --git a/services/web/frontend/js/features/source-editor/extensions/completion-logger.ts b/services/web/frontend/js/features/source-editor/extensions/completion-logger.ts new file mode 100644 index 0000000000..7ded941b05 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/extensions/completion-logger.ts @@ -0,0 +1,28 @@ +import { ViewPlugin } from '@codemirror/view' +import { pickedCompletion } from '@codemirror/autocomplete' +import { emitCompletionEvent } from './toolbar/utils/analytics' + +/** + * A custom view plugin that watches for transactions with the `pickedCompletion` annotation. + * If the completion label starts with a command, log that command for analytics. + */ +export const completionLogger = ViewPlugin.define(view => { + return { + update(update) { + for (const tr of update.transactions) { + const completion = tr.annotation(pickedCompletion) + if (completion) { + const command = completionCommand(completion.label) + if (command) { + emitCompletionEvent(view, command) + } + } + } + }, + } +}) + +const completionCommand = (label: string): string | null => { + const matches = label.match(/^(\\\w+)/) + return matches ? matches[1] : null +} diff --git a/services/web/frontend/js/features/source-editor/extensions/index.ts b/services/web/frontend/js/features/source-editor/extensions/index.ts index 2f9a017039..7cf7257165 100644 --- a/services/web/frontend/js/features/source-editor/extensions/index.ts +++ b/services/web/frontend/js/features/source-editor/extensions/index.ts @@ -48,6 +48,7 @@ import { highlightSpecialChars } from './highlight-special-chars' import { toolbarPanel } from './toolbar/toolbar-panel' import { geometryChangeEvent } from './geometry-change-event' import { isSplitTestEnabled } from '../../../utils/splitTestUtils' +import { completionLogger } from './completion-logger' const moduleExtensions: Array<() => Extension> = importOverleafModules( 'sourceEditorExtensions' @@ -128,6 +129,7 @@ export const createExtensions = (options: Record): Extension[] => [ // The built-in extension that highlights the active line in the gutter. highlightActiveLineGutter(), inlineBackground(options.visual.visual), + completionLogger, codemirrorDevTools(), exceptionLogger(), // CodeMirror extensions provided by modules diff --git a/services/web/frontend/js/features/source-editor/extensions/toolbar/utils/analytics.ts b/services/web/frontend/js/features/source-editor/extensions/toolbar/utils/analytics.ts index 38ad29d495..c1519c7791 100644 --- a/services/web/frontend/js/features/source-editor/extensions/toolbar/utils/analytics.ts +++ b/services/web/frontend/js/features/source-editor/extensions/toolbar/utils/analytics.ts @@ -2,7 +2,19 @@ import { EditorView } from '@codemirror/view' import { sendMB } from '../../../../../infrastructure/event-tracking' import { isVisual } from '../../visual/visual' -export function emitCommandEvent(view: EditorView, command: string) { +export function emitCommandEvent( + view: EditorView, + key: string, + command: string +) { const mode = isVisual(view) ? 'visual' : 'source' - sendMB('codemirror-toolbar-event', { command, mode }) + sendMB(key, { command, mode }) +} + +export function emitToolbarEvent(view: EditorView, command: string) { + emitCommandEvent(view, 'codemirror-toolbar-event', command) +} + +export function emitCompletionEvent(view: EditorView, command: string) { + emitCommandEvent(view, 'codemirror-completion-event', command) } diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/editable-graphics.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/editable-graphics.ts index b49d7fbe08..a4ef1dd6c0 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/editable-graphics.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/editable-graphics.ts @@ -1,7 +1,7 @@ import { EditorView } from '@codemirror/view' import { GraphicsWidget } from './graphics' import { editFigureDataEffect } from '../../figure-modal' -import { emitCommandEvent } from '../../toolbar/utils/analytics' +import { emitToolbarEvent } from '../../toolbar/utils/analytics' export class EditableGraphicsWidget extends GraphicsWidget { setEditDispatcher(button: HTMLButtonElement, view: EditorView) { @@ -12,7 +12,7 @@ export class EditableGraphicsWidget extends GraphicsWidget { event.stopImmediatePropagation() view.dispatch({ effects: editFigureDataEffect.of(this.figureData) }) window.dispatchEvent(new CustomEvent('figure-modal:open-modal')) - emitCommandEvent(view, 'toolbar-figure-modal-edit') + emitToolbarEvent(view, 'toolbar-figure-modal-edit') return false } } else {