From f242721f3b77a137d8cd04cef1d749463ee71742 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Mon, 28 Mar 2022 14:46:40 +0200 Subject: [PATCH] chore: add ga for most actions (#4829) --- src/actions/actionAddToLibrary.ts | 1 + src/actions/actionAlign.tsx | 8 ++ src/actions/actionBoundText.tsx | 2 + src/actions/actionCanvas.tsx | 9 +++ src/actions/actionClipboard.tsx | 4 + src/actions/actionDeleteSelected.tsx | 1 + src/actions/actionDistribute.tsx | 2 + src/actions/actionDuplicateSelection.tsx | 1 + src/actions/actionExport.tsx | 10 ++- src/actions/actionFinalize.tsx | 1 + src/actions/actionFlip.ts | 2 + src/actions/actionGroup.tsx | 2 + src/actions/actionHistory.tsx | 2 + src/actions/actionMenu.tsx | 4 + src/actions/actionNavigate.tsx | 1 + src/actions/actionProperties.tsx | 15 ++++ src/actions/actionSelectAll.ts | 1 + src/actions/actionStyles.ts | 2 + src/actions/actionToggleGridMode.tsx | 6 +- src/actions/actionToggleStats.tsx | 1 + src/actions/actionToggleViewMode.tsx | 6 +- src/actions/actionToggleZenMode.tsx | 7 +- src/actions/actionZindex.tsx | 5 ++ src/actions/manager.tsx | 73 ++++++++++--------- src/actions/types.ts | 32 +++++--- src/analytics.ts | 16 ++-- src/components/Actions.tsx | 4 + src/components/App.tsx | 29 +++++--- src/components/ContextMenu.tsx | 4 +- src/components/ImageExportDialog.tsx | 6 +- src/components/JSONExportDialog.tsx | 17 +++-- src/components/LayerUI.tsx | 6 +- src/components/LibraryMenu.tsx | 2 + src/element/Hyperlink.tsx | 4 +- src/element/newElement.test.ts | 6 +- src/excalidraw-app/collab/CollabWrapper.tsx | 3 +- .../components/ExportToExcalidrawPlus.tsx | 3 + src/excalidraw-app/index.tsx | 2 + src/types.ts | 1 + src/utils.ts | 13 ++++ 40 files changed, 221 insertions(+), 93 deletions(-) diff --git a/src/actions/actionAddToLibrary.ts b/src/actions/actionAddToLibrary.ts index f9b93625..7d2f9877 100644 --- a/src/actions/actionAddToLibrary.ts +++ b/src/actions/actionAddToLibrary.ts @@ -7,6 +7,7 @@ import { t } from "../i18n"; export const actionAddToLibrary = register({ name: "addToLibrary", + trackEvent: { category: "element" }, perform: (elements, appState, _, app) => { const selectedElements = getSelectedElements( getNonDeletedElements(elements), diff --git a/src/actions/actionAlign.tsx b/src/actions/actionAlign.tsx index f00ae298..06e721fd 100644 --- a/src/actions/actionAlign.tsx +++ b/src/actions/actionAlign.tsx @@ -43,6 +43,7 @@ const alignSelectedElements = ( export const actionAlignTop = register({ name: "alignTop", + trackEvent: { category: "element" }, perform: (elements, appState) => { return { appState, @@ -72,6 +73,7 @@ export const actionAlignTop = register({ export const actionAlignBottom = register({ name: "alignBottom", + trackEvent: { category: "element" }, perform: (elements, appState) => { return { appState, @@ -101,6 +103,7 @@ export const actionAlignBottom = register({ export const actionAlignLeft = register({ name: "alignLeft", + trackEvent: { category: "element" }, perform: (elements, appState) => { return { appState, @@ -130,6 +133,8 @@ export const actionAlignLeft = register({ export const actionAlignRight = register({ name: "alignRight", + trackEvent: { category: "element" }, + perform: (elements, appState) => { return { appState, @@ -159,6 +164,8 @@ export const actionAlignRight = register({ export const actionAlignVerticallyCentered = register({ name: "alignVerticallyCentered", + trackEvent: { category: "element" }, + perform: (elements, appState) => { return { appState, @@ -184,6 +191,7 @@ export const actionAlignVerticallyCentered = register({ export const actionAlignHorizontallyCentered = register({ name: "alignHorizontallyCentered", + trackEvent: { category: "element" }, perform: (elements, appState) => { return { appState, diff --git a/src/actions/actionBoundText.tsx b/src/actions/actionBoundText.tsx index 6334e076..07a56d87 100644 --- a/src/actions/actionBoundText.tsx +++ b/src/actions/actionBoundText.tsx @@ -21,6 +21,7 @@ import { register } from "./register"; export const actionUnbindText = register({ name: "unbindText", contextItemLabel: "labels.unbindText", + trackEvent: { category: "element" }, contextItemPredicate: (elements, appState) => { const selectedElements = getSelectedElements(elements, appState); return selectedElements.some((element) => hasBoundTextElement(element)); @@ -62,6 +63,7 @@ export const actionUnbindText = register({ export const actionBindText = register({ name: "bindText", contextItemLabel: "labels.bindText", + trackEvent: { category: "element" }, contextItemPredicate: (elements, appState) => { const selectedElements = getSelectedElements(elements, appState); diff --git a/src/actions/actionCanvas.tsx b/src/actions/actionCanvas.tsx index da3bf42e..5ad42574 100644 --- a/src/actions/actionCanvas.tsx +++ b/src/actions/actionCanvas.tsx @@ -21,6 +21,7 @@ import clsx from "clsx"; export const actionChangeViewBackgroundColor = register({ name: "changeViewBackgroundColor", + trackEvent: false, perform: (_, appState, value) => { return { appState: { ...appState, ...value }, @@ -50,6 +51,7 @@ export const actionChangeViewBackgroundColor = register({ export const actionClearCanvas = register({ name: "clearCanvas", + trackEvent: { category: "canvas" }, perform: (elements, appState, _, app) => { app.imageCache.clear(); return { @@ -82,6 +84,7 @@ export const actionClearCanvas = register({ export const actionZoomIn = register({ name: "zoomIn", + trackEvent: { category: "canvas" }, perform: (_elements, appState, _, app) => { return { appState: { @@ -117,6 +120,7 @@ export const actionZoomIn = register({ export const actionZoomOut = register({ name: "zoomOut", + trackEvent: { category: "canvas" }, perform: (_elements, appState, _, app) => { return { appState: { @@ -152,6 +156,7 @@ export const actionZoomOut = register({ export const actionResetZoom = register({ name: "resetZoom", + trackEvent: { category: "canvas" }, perform: (_elements, appState, _, app) => { return { appState: { @@ -250,6 +255,7 @@ const zoomToFitElements = ( export const actionZoomToSelected = register({ name: "zoomToSelection", + trackEvent: { category: "canvas" }, perform: (elements, appState) => zoomToFitElements(elements, appState, true), keyTest: (event) => event.code === CODES.TWO && @@ -260,6 +266,7 @@ export const actionZoomToSelected = register({ export const actionZoomToFit = register({ name: "zoomToFit", + trackEvent: { category: "canvas" }, perform: (elements, appState) => zoomToFitElements(elements, appState, false), keyTest: (event) => event.code === CODES.ONE && @@ -270,6 +277,7 @@ export const actionZoomToFit = register({ export const actionToggleTheme = register({ name: "toggleTheme", + trackEvent: { category: "canvas" }, perform: (_, appState, value) => { return { appState: { @@ -295,6 +303,7 @@ export const actionToggleTheme = register({ export const actionErase = register({ name: "eraser", + trackEvent: { category: "toolbar" }, perform: (elements, appState) => { return { appState: { diff --git a/src/actions/actionClipboard.tsx b/src/actions/actionClipboard.tsx index 836dedf6..64c70532 100644 --- a/src/actions/actionClipboard.tsx +++ b/src/actions/actionClipboard.tsx @@ -9,6 +9,7 @@ import { t } from "../i18n"; export const actionCopy = register({ name: "copy", + trackEvent: { category: "element" }, perform: (elements, appState, _, app) => { copyToClipboard(getNonDeletedElements(elements), appState, app.files); @@ -23,6 +24,7 @@ export const actionCopy = register({ export const actionCut = register({ name: "cut", + trackEvent: { category: "element" }, perform: (elements, appState, data, app) => { actionCopy.perform(elements, appState, data, app); return actionDeleteSelected.perform(elements, appState); @@ -33,6 +35,7 @@ export const actionCut = register({ export const actionCopyAsSvg = register({ name: "copyAsSvg", + trackEvent: { category: "element" }, perform: async (elements, appState, _data, app) => { if (!app.canvas) { return { @@ -73,6 +76,7 @@ export const actionCopyAsSvg = register({ export const actionCopyAsPng = register({ name: "copyAsPng", + trackEvent: { category: "element" }, perform: async (elements, appState, _data, app) => { if (!app.canvas) { return { diff --git a/src/actions/actionDeleteSelected.tsx b/src/actions/actionDeleteSelected.tsx index 003cd880..7079936e 100644 --- a/src/actions/actionDeleteSelected.tsx +++ b/src/actions/actionDeleteSelected.tsx @@ -58,6 +58,7 @@ const handleGroupEditingState = ( export const actionDeleteSelected = register({ name: "deleteSelectedElements", + trackEvent: { category: "element", action: "delete" }, perform: (elements, appState) => { if (appState.editingLinearElement) { const { diff --git a/src/actions/actionDistribute.tsx b/src/actions/actionDistribute.tsx index 5b61e44d..8bfeb90c 100644 --- a/src/actions/actionDistribute.tsx +++ b/src/actions/actionDistribute.tsx @@ -39,6 +39,7 @@ const distributeSelectedElements = ( export const distributeHorizontally = register({ name: "distributeHorizontally", + trackEvent: { category: "element" }, perform: (elements, appState) => { return { appState, @@ -68,6 +69,7 @@ export const distributeHorizontally = register({ export const distributeVertically = register({ name: "distributeVertically", + trackEvent: { category: "element" }, perform: (elements, appState) => { return { appState, diff --git a/src/actions/actionDuplicateSelection.tsx b/src/actions/actionDuplicateSelection.tsx index 73ac16de..2ef0d25c 100644 --- a/src/actions/actionDuplicateSelection.tsx +++ b/src/actions/actionDuplicateSelection.tsx @@ -22,6 +22,7 @@ import { isBoundToContainer } from "../element/typeChecks"; export const actionDuplicateSelection = register({ name: "duplicateSelection", + trackEvent: { category: "element" }, perform: (elements, appState) => { // duplicate selected point(s) if editing a line if (appState.editingLinearElement) { diff --git a/src/actions/actionExport.tsx b/src/actions/actionExport.tsx index e8bd520d..dda8569c 100644 --- a/src/actions/actionExport.tsx +++ b/src/actions/actionExport.tsx @@ -1,4 +1,3 @@ -import { trackEvent } from "../analytics"; import { load, questionCircle, saveAs } from "../components/icons"; import { ProjectName } from "../components/ProjectName"; import { ToolButton } from "../components/ToolButton"; @@ -23,8 +22,8 @@ import { Theme } from "../element/types"; export const actionChangeProjectName = register({ name: "changeProjectName", + trackEvent: false, perform: (_elements, appState, value) => { - trackEvent("change", "title"); return { appState: { ...appState, name: value }, commitToHistory: false }; }, PanelComponent: ({ appState, updateData, appProps }) => ( @@ -41,6 +40,7 @@ export const actionChangeProjectName = register({ export const actionChangeExportScale = register({ name: "changeExportScale", + trackEvent: { category: "export", action: "scale" }, perform: (_elements, appState, value) => { return { appState: { ...appState, exportScale: value }, @@ -89,6 +89,7 @@ export const actionChangeExportScale = register({ export const actionChangeExportBackground = register({ name: "changeExportBackground", + trackEvent: { category: "export", action: "toggleBackground" }, perform: (_elements, appState, value) => { return { appState: { ...appState, exportBackground: value }, @@ -107,6 +108,7 @@ export const actionChangeExportBackground = register({ export const actionChangeExportEmbedScene = register({ name: "changeExportEmbedScene", + trackEvent: { category: "export", action: "embedScene" }, perform: (_elements, appState, value) => { return { appState: { ...appState, exportEmbedScene: value }, @@ -128,6 +130,7 @@ export const actionChangeExportEmbedScene = register({ export const actionSaveToActiveFile = register({ name: "saveToActiveFile", + trackEvent: { category: "export" }, perform: async (elements, appState, value, app) => { const fileHandleExists = !!appState.fileHandle; @@ -172,6 +175,7 @@ export const actionSaveToActiveFile = register({ export const actionSaveFileToDisk = register({ name: "saveFileToDisk", + trackEvent: { category: "export" }, perform: async (elements, appState, value, app) => { try { const { fileHandle } = await saveAsJSON( @@ -210,6 +214,7 @@ export const actionSaveFileToDisk = register({ export const actionLoadScene = register({ name: "loadScene", + trackEvent: { category: "export" }, perform: async (elements, appState, _, app) => { try { const { @@ -252,6 +257,7 @@ export const actionLoadScene = register({ export const actionExportWithDarkMode = register({ name: "exportWithDarkMode", + trackEvent: { category: "export", action: "toggleTheme" }, perform: (_elements, appState, value) => { return { appState: { ...appState, exportWithDarkMode: value }, diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index 8e07add6..da7b3f4c 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -17,6 +17,7 @@ import { isBindingElement } from "../element/typeChecks"; export const actionFinalize = register({ name: "finalize", + trackEvent: false, perform: (elements, appState, _, { canvas, focusContainer }) => { if (appState.editingLinearElement) { const { elementId, startBindingElement, endBindingElement } = diff --git a/src/actions/actionFlip.ts b/src/actions/actionFlip.ts index bdca75e2..671c0dce 100644 --- a/src/actions/actionFlip.ts +++ b/src/actions/actionFlip.ts @@ -35,6 +35,7 @@ const enableActionFlipVertical = ( export const actionFlipHorizontal = register({ name: "flipHorizontal", + trackEvent: { category: "element" }, perform: (elements, appState) => { return { elements: flipSelectedElements(elements, appState, "horizontal"), @@ -50,6 +51,7 @@ export const actionFlipHorizontal = register({ export const actionFlipVertical = register({ name: "flipVertical", + trackEvent: { category: "element" }, perform: (elements, appState) => { return { elements: flipSelectedElements(elements, appState, "vertical"), diff --git a/src/actions/actionGroup.tsx b/src/actions/actionGroup.tsx index af680ac9..1d4b4518 100644 --- a/src/actions/actionGroup.tsx +++ b/src/actions/actionGroup.tsx @@ -54,6 +54,7 @@ const enableActionGroup = ( export const actionGroup = register({ name: "group", + trackEvent: { category: "element" }, perform: (elements, appState) => { const selectedElements = getSelectedElements( getNonDeletedElements(elements), @@ -147,6 +148,7 @@ export const actionGroup = register({ export const actionUngroup = register({ name: "ungroup", + trackEvent: { category: "element" }, perform: (elements, appState) => { const groupIds = getSelectedGroupIds(appState); if (groupIds.length === 0) { diff --git a/src/actions/actionHistory.tsx b/src/actions/actionHistory.tsx index 3aa4ca8a..f8b1293b 100644 --- a/src/actions/actionHistory.tsx +++ b/src/actions/actionHistory.tsx @@ -62,6 +62,7 @@ type ActionCreator = (history: History) => Action; export const createUndoAction: ActionCreator = (history) => ({ name: "undo", + trackEvent: { category: "history" }, perform: (elements, appState) => writeData(elements, appState, () => history.undoOnce()), keyTest: (event) => @@ -82,6 +83,7 @@ export const createUndoAction: ActionCreator = (history) => ({ export const createRedoAction: ActionCreator = (history) => ({ name: "redo", + trackEvent: { category: "history" }, perform: (elements, appState) => writeData(elements, appState, () => history.redoOnce()), keyTest: (event) => diff --git a/src/actions/actionMenu.tsx b/src/actions/actionMenu.tsx index b63e75a1..7a6368d4 100644 --- a/src/actions/actionMenu.tsx +++ b/src/actions/actionMenu.tsx @@ -9,6 +9,7 @@ import { HelpIcon } from "../components/HelpIcon"; export const actionToggleCanvasMenu = register({ name: "toggleCanvasMenu", + trackEvent: { category: "menu" }, perform: (_, appState) => ({ appState: { ...appState, @@ -29,6 +30,7 @@ export const actionToggleCanvasMenu = register({ export const actionToggleEditMenu = register({ name: "toggleEditMenu", + trackEvent: { category: "menu" }, perform: (_elements, appState) => ({ appState: { ...appState, @@ -53,6 +55,7 @@ export const actionToggleEditMenu = register({ export const actionFullScreen = register({ name: "toggleFullScreen", + trackEvent: { category: "canvas", predicate: (appState) => !isFullScreen() }, perform: () => { if (!isFullScreen()) { allowFullScreen(); @@ -69,6 +72,7 @@ export const actionFullScreen = register({ export const actionShortcuts = register({ name: "toggleShortcuts", + trackEvent: { category: "menu", action: "toggleHelpDialog" }, perform: (_elements, appState, _, { focusContainer }) => { if (appState.showHelpDialog) { focusContainer(); diff --git a/src/actions/actionNavigate.tsx b/src/actions/actionNavigate.tsx index 420a4712..453fd527 100644 --- a/src/actions/actionNavigate.tsx +++ b/src/actions/actionNavigate.tsx @@ -6,6 +6,7 @@ import { register } from "./register"; export const actionGoToCollaborator = register({ name: "goToCollaborator", + trackEvent: { category: "collab" }, perform: (_elements, appState, value) => { const point = value as Collaborator["pointer"]; if (!point) { diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index 3ccb2f02..53801a22 100644 --- a/src/actions/actionProperties.tsx +++ b/src/actions/actionProperties.tsx @@ -194,6 +194,7 @@ const changeFontSize = ( export const actionChangeStrokeColor = register({ name: "changeStrokeColor", + trackEvent: false, perform: (elements, appState, value) => { return { ...(value.currentItemStrokeColor && { @@ -243,6 +244,7 @@ export const actionChangeStrokeColor = register({ export const actionChangeBackgroundColor = register({ name: "changeBackgroundColor", + trackEvent: false, perform: (elements, appState, value) => { return { ...(value.currentItemBackgroundColor && { @@ -285,6 +287,7 @@ export const actionChangeBackgroundColor = register({ export const actionChangeFillStyle = register({ name: "changeFillStyle", + trackEvent: false, perform: (elements, appState, value) => { return { elements: changeProperty(elements, appState, (el) => @@ -334,6 +337,7 @@ export const actionChangeFillStyle = register({ export const actionChangeStrokeWidth = register({ name: "changeStrokeWidth", + trackEvent: false, perform: (elements, appState, value) => { return { elements: changeProperty(elements, appState, (el) => @@ -381,6 +385,7 @@ export const actionChangeStrokeWidth = register({ export const actionChangeSloppiness = register({ name: "changeSloppiness", + trackEvent: false, perform: (elements, appState, value) => { return { elements: changeProperty(elements, appState, (el) => @@ -429,6 +434,7 @@ export const actionChangeSloppiness = register({ export const actionChangeStrokeStyle = register({ name: "changeStrokeStyle", + trackEvent: false, perform: (elements, appState, value) => { return { elements: changeProperty(elements, appState, (el) => @@ -476,6 +482,7 @@ export const actionChangeStrokeStyle = register({ export const actionChangeOpacity = register({ name: "changeOpacity", + trackEvent: false, perform: (elements, appState, value) => { return { elements: changeProperty(elements, appState, (el) => @@ -525,6 +532,7 @@ export const actionChangeOpacity = register({ export const actionChangeFontSize = register({ name: "changeFontSize", + trackEvent: false, perform: (elements, appState, value) => { return changeFontSize(elements, appState, () => value, value); }, @@ -582,6 +590,7 @@ export const actionChangeFontSize = register({ export const actionDecreaseFontSize = register({ name: "decreaseFontSize", + trackEvent: false, perform: (elements, appState, value) => { return changeFontSize(elements, appState, (element) => Math.round( @@ -603,6 +612,7 @@ export const actionDecreaseFontSize = register({ export const actionIncreaseFontSize = register({ name: "increaseFontSize", + trackEvent: false, perform: (elements, appState, value) => { return changeFontSize(elements, appState, (element) => Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)), @@ -620,6 +630,7 @@ export const actionIncreaseFontSize = register({ export const actionChangeFontFamily = register({ name: "changeFontFamily", + trackEvent: false, perform: (elements, appState, value) => { return { elements: changeProperty( @@ -701,6 +712,7 @@ export const actionChangeFontFamily = register({ export const actionChangeTextAlign = register({ name: "changeTextAlign", + trackEvent: false, perform: (elements, appState, value) => { return { elements: changeProperty( @@ -773,6 +785,7 @@ export const actionChangeTextAlign = register({ }); export const actionChangeVerticalAlign = register({ name: "changeVerticalAlign", + trackEvent: { category: "element" }, perform: (elements, appState, value) => { return { elements: changeProperty( @@ -840,6 +853,7 @@ export const actionChangeVerticalAlign = register({ export const actionChangeSharpness = register({ name: "changeSharpness", + trackEvent: false, perform: (elements, appState, value) => { const targetElements = getTargetElements( getNonDeletedElements(elements), @@ -904,6 +918,7 @@ export const actionChangeSharpness = register({ export const actionChangeArrowhead = register({ name: "changeArrowhead", + trackEvent: false, perform: ( elements, appState, diff --git a/src/actions/actionSelectAll.ts b/src/actions/actionSelectAll.ts index eaff8321..6b793b93 100644 --- a/src/actions/actionSelectAll.ts +++ b/src/actions/actionSelectAll.ts @@ -5,6 +5,7 @@ import { getNonDeletedElements, isTextElement } from "../element"; export const actionSelectAll = register({ name: "selectAll", + trackEvent: { category: "canvas" }, perform: (elements, appState) => { if (appState.editingLinearElement) { return false; diff --git a/src/actions/actionStyles.ts b/src/actions/actionStyles.ts index f80c9175..0f4aff99 100644 --- a/src/actions/actionStyles.ts +++ b/src/actions/actionStyles.ts @@ -19,6 +19,7 @@ export let copiedStyles: string = "{}"; export const actionCopyStyles = register({ name: "copyStyles", + trackEvent: { category: "element" }, perform: (elements, appState) => { const element = elements.find((el) => appState.selectedElementIds[el.id]); if (element) { @@ -39,6 +40,7 @@ export const actionCopyStyles = register({ export const actionPasteStyles = register({ name: "pasteStyles", + trackEvent: { category: "element" }, perform: (elements, appState) => { const pastedElement = JSON.parse(copiedStyles); if (!isExcalidrawElement(pastedElement)) { diff --git a/src/actions/actionToggleGridMode.tsx b/src/actions/actionToggleGridMode.tsx index 983d19a0..a86cdb96 100644 --- a/src/actions/actionToggleGridMode.tsx +++ b/src/actions/actionToggleGridMode.tsx @@ -2,12 +2,14 @@ import { CODES, KEYS } from "../keys"; import { register } from "./register"; import { GRID_SIZE } from "../constants"; import { AppState } from "../types"; -import { trackEvent } from "../analytics"; export const actionToggleGridMode = register({ name: "gridMode", + trackEvent: { + category: "canvas", + predicate: (appState) => !appState.gridSize, + }, perform(elements, appState) { - trackEvent("view", "mode", "grid"); return { appState: { ...appState, diff --git a/src/actions/actionToggleStats.tsx b/src/actions/actionToggleStats.tsx index 68236093..08b74dd8 100644 --- a/src/actions/actionToggleStats.tsx +++ b/src/actions/actionToggleStats.tsx @@ -3,6 +3,7 @@ import { CODES, KEYS } from "../keys"; export const actionToggleStats = register({ name: "stats", + trackEvent: { category: "menu" }, perform(elements, appState) { return { appState: { diff --git a/src/actions/actionToggleViewMode.tsx b/src/actions/actionToggleViewMode.tsx index d18b5eef..be88f563 100644 --- a/src/actions/actionToggleViewMode.tsx +++ b/src/actions/actionToggleViewMode.tsx @@ -1,11 +1,13 @@ import { CODES, KEYS } from "../keys"; import { register } from "./register"; -import { trackEvent } from "../analytics"; export const actionToggleViewMode = register({ name: "viewMode", + trackEvent: { + category: "canvas", + predicate: (appState) => !appState.viewModeEnabled, + }, perform(elements, appState) { - trackEvent("view", "mode", "view"); return { appState: { ...appState, diff --git a/src/actions/actionToggleZenMode.tsx b/src/actions/actionToggleZenMode.tsx index 5ff494ea..22266b03 100644 --- a/src/actions/actionToggleZenMode.tsx +++ b/src/actions/actionToggleZenMode.tsx @@ -1,12 +1,13 @@ import { CODES, KEYS } from "../keys"; import { register } from "./register"; -import { trackEvent } from "../analytics"; export const actionToggleZenMode = register({ name: "zenMode", + trackEvent: { + category: "canvas", + predicate: (appState) => !appState.zenModeEnabled, + }, perform(elements, appState) { - trackEvent("view", "mode", "zen"); - return { appState: { ...appState, diff --git a/src/actions/actionZindex.tsx b/src/actions/actionZindex.tsx index f1cdcfea..629b3864 100644 --- a/src/actions/actionZindex.tsx +++ b/src/actions/actionZindex.tsx @@ -18,6 +18,7 @@ import { export const actionSendBackward = register({ name: "sendBackward", + trackEvent: { category: "element" }, perform: (elements, appState) => { return { elements: moveOneLeft(elements, appState), @@ -45,6 +46,7 @@ export const actionSendBackward = register({ export const actionBringForward = register({ name: "bringForward", + trackEvent: { category: "element" }, perform: (elements, appState) => { return { elements: moveOneRight(elements, appState), @@ -72,6 +74,7 @@ export const actionBringForward = register({ export const actionSendToBack = register({ name: "sendToBack", + trackEvent: { category: "element" }, perform: (elements, appState) => { return { elements: moveAllLeft(elements, appState), @@ -106,6 +109,8 @@ export const actionSendToBack = register({ export const actionBringToFront = register({ name: "bringToFront", + trackEvent: { category: "element" }, + perform: (elements, appState) => { return { elements: moveAllRight(elements, appState), diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index 5385d928..434a3849 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -1,11 +1,11 @@ import React from "react"; import { Action, - ActionsManagerInterface, UpdaterFn, ActionName, ActionResult, PanelComponentProps, + ActionSource, } from "./types"; import { ExcalidrawElement } from "../element/types"; import { AppClassProperties, AppState } from "../types"; @@ -14,21 +14,25 @@ import { trackEvent } from "../analytics"; const trackAction = ( action: Action, - source: "ui" | "keyboard" | "api", + source: ActionSource, + appState: Readonly, + elements: readonly ExcalidrawElement[], + app: AppClassProperties, value: any, ) => { - if (action.trackEvent !== false) { + if (action.trackEvent) { try { - if (action.trackEvent === true) { - trackEvent( - action.name, - source, - typeof value === "number" || typeof value === "string" - ? String(value) - : undefined, - ); - } else { - action.trackEvent?.(action, source, value); + if (typeof action.trackEvent === "object") { + const shouldTrack = action.trackEvent.predicate + ? action.trackEvent.predicate(appState, elements, value) + : true; + if (shouldTrack) { + trackEvent( + action.trackEvent.category, + action.trackEvent.action || action.name, + `${source} (${app.deviceType.isMobile ? "mobile" : "desktop"})`, + ); + } } } catch (error) { console.error("error while logging action:", error); @@ -36,8 +40,8 @@ const trackAction = ( } }; -export class ActionManager implements ActionsManagerInterface { - actions = {} as ActionsManagerInterface["actions"]; +export class ActionManager { + actions = {} as Record; updater: (actionResult: ActionResult | Promise) => void; @@ -106,30 +110,25 @@ export class ActionManager implements ActionsManagerInterface { } } - trackAction(action, "keyboard", null); + const elements = this.getElementsIncludingDeleted(); + const appState = this.getAppState(); + const value = null; + + trackAction(action, "keyboard", appState, elements, this.app, null); event.preventDefault(); - this.updater( - data[0].perform( - this.getElementsIncludingDeleted(), - this.getAppState(), - null, - this.app, - ), - ); + this.updater(data[0].perform(elements, appState, value, this.app)); return true; } - executeAction(action: Action) { - this.updater( - action.perform( - this.getElementsIncludingDeleted(), - this.getAppState(), - null, - this.app, - ), - ); - trackAction(action, "api", null); + executeAction(action: Action, source: ActionSource = "api") { + const elements = this.getElementsIncludingDeleted(); + const appState = this.getAppState(); + const value = null; + + trackAction(action, source, appState, elements, this.app, value); + + this.updater(action.perform(elements, appState, value, this.app)); } /** @@ -147,7 +146,11 @@ export class ActionManager implements ActionsManagerInterface { ) { const action = this.actions[name]; const PanelComponent = action.PanelComponent!; + const elements = this.getElementsIncludingDeleted(); + const appState = this.getAppState(); const updateData = (formState?: any) => { + trackAction(action, "ui", appState, elements, this.app, formState); + this.updater( action.perform( this.getElementsIncludingDeleted(), @@ -156,8 +159,6 @@ export class ActionManager implements ActionsManagerInterface { this.app, ), ); - - trackAction(action, "ui", formState); }; return ( diff --git a/src/actions/types.ts b/src/actions/types.ts index 9c6bc2c9..029d1d25 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -8,6 +8,8 @@ import { } from "../types"; import { ToolButtonSize } from "../components/ToolButton"; +export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api"; + /** if false, the action should be prevented */ export type ActionResult = | { @@ -139,15 +141,23 @@ export interface Action { appState: AppState, ) => boolean; checked?: (appState: Readonly) => boolean; - trackEvent?: - | boolean - | ((action: Action, type: "ui" | "keyboard" | "api", value: any) => void); -} - -export interface ActionsManagerInterface { - actions: Record; - registerAction: (action: Action) => void; - handleKeyDown: (event: React.KeyboardEvent | KeyboardEvent) => boolean; - renderAction: (name: ActionName) => React.ReactElement | null; - executeAction: (action: Action) => void; + trackEvent: + | false + | { + category: + | "toolbar" + | "element" + | "canvas" + | "export" + | "history" + | "menu" + | "collab" + | "hyperlink"; + action?: string; + predicate?: ( + appState: Readonly, + elements: readonly ExcalidrawElement[], + value: any, + ) => boolean; + }; } diff --git a/src/analytics.ts b/src/analytics.ts index c4a2c4e3..668d49c2 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -4,15 +4,19 @@ export const trackEvent = typeof window !== "undefined" && window.gtag ? (category: string, action: string, label?: string, value?: number) => { - window.gtag("event", action, { - event_category: category, - event_label: label, - value, - }); + try { + window.gtag("event", action, { + event_category: category, + event_label: label, + value, + }); + } catch (error) { + console.error("error logging to ga", error); + } } : typeof process !== "undefined" && process.env?.JEST_WORKER_ID ? (category: string, action: string, label?: string, value?: number) => {} : (category: string, action: string, label?: string, value?: number) => { // Uncomment the next line to track locally - // console.info("Track Event", category, action, label, value); + // console.log("Track Event", { category, action, label, value }); }; diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index 50812f72..a7cc92bf 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -24,6 +24,7 @@ import { import Stack from "./Stack"; import { ToolButton } from "./ToolButton"; import { hasStrokeColor } from "../scene/comparisons"; +import { trackEvent } from "../analytics"; import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks"; export const SelectedShapeActions = ({ @@ -209,6 +210,9 @@ export const ShapesSwitcher = ({ activeToolType: typeof SHAPES[number]["value"]; pointerType: PointerType | null; }) => { + if (appState.activeTool.type !== activeToolType) { + trackEvent("toolbar", activeToolType, "ui"); + } if (!appState.penDetected && pointerType === "pen") { setAppState({ penDetected: true, diff --git a/src/components/App.tsx b/src/components/App.tsx index 7b9365cf..1d9bc12e 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -38,7 +38,6 @@ import { ActionResult } from "../actions/types"; import { trackEvent } from "../analytics"; import { getDefaultAppState, isEraserActive } from "../appState"; import { - copyToClipboard, parseClipboard, probablySupportsClipboardBlob, probablySupportsClipboardWriteText, @@ -1291,12 +1290,11 @@ class App extends React.Component { }); private cutAll = () => { - this.copyAll(); - this.actionManager.executeAction(actionDeleteSelected); + this.actionManager.executeAction(actionCut, "keyboard"); }; private copyAll = () => { - copyToClipboard(this.scene.getElements(), this.state, this.files); + this.actionManager.executeAction(actionCopy, "keyboard"); }; private static resetTapTwice() { @@ -1570,7 +1568,14 @@ class App extends React.Component { gesture.pointers.delete(event.pointerId); }; - toggleLock = () => { + toggleLock = (source: "keyboard" | "ui" = "ui") => { + if (!this.state.elementLocked) { + trackEvent( + "toolbar", + "toggleLock", + `${source} (${this.deviceType.isMobile ? "mobile" : "desktop"})`, + ); + } this.setState((prevState) => { return { elementLocked: !prevState.elementLocked, @@ -1594,9 +1599,6 @@ class App extends React.Component { }; toggleStats = () => { - if (!this.state.showStats) { - trackEvent("dialog", "stats"); - } this.actionManager.executeAction(actionToggleStats); }; @@ -1851,9 +1853,16 @@ class App extends React.Component { ) { const shape = findShapeByKey(event.key); if (shape) { + if (this.state.activeTool.type !== shape) { + trackEvent( + "toolbar", + shape, + `keyboard (${this.deviceType.isMobile ? "mobile" : "desktop"})`, + ); + } this.setActiveTool({ type: shape }); } else if (event.key === KEYS.Q) { - this.toggleLock(); + this.toggleLock("keyboard"); } } if (event.key === KEYS.SPACE && gesture.pointers.size === 0) { @@ -5493,6 +5502,7 @@ class App extends React.Component { options: [ this.deviceType.isMobile && navigator.clipboard && { + trackEvent: false, name: "paste", perform: (elements, appStates) => { this.pasteFromClipboard(null); @@ -5549,6 +5559,7 @@ class App extends React.Component { this.deviceType.isMobile && navigator.clipboard && { name: "paste", + trackEvent: false, perform: (elements, appStates) => { this.pasteFromClipboard(null); return { diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index c10e1356..d172334f 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -70,7 +70,9 @@ const ContextMenu = ({ dangerous: actionName === "deleteSelectedElements", checkmark: option.checked?.(appState), })} - onClick={() => actionManager.executeAction(option)} + onClick={() => + actionManager.executeAction(option, "contextMenu") + } >
{label}
diff --git a/src/components/ImageExportDialog.tsx b/src/components/ImageExportDialog.tsx index ca2e9077..ca21362d 100644 --- a/src/components/ImageExportDialog.tsx +++ b/src/components/ImageExportDialog.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; import { render, unmountComponentAtNode } from "react-dom"; -import { ActionsManagerInterface } from "../actions/types"; import { probablySupportsClipboardBlob } from "../clipboard"; import { canvasToBlob } from "../data/blob"; import { NonDeletedExcalidrawElement } from "../element/types"; @@ -19,6 +18,7 @@ import OpenColor from "open-color"; import { CheckboxItem } from "./CheckboxItem"; import { DEFAULT_EXPORT_PADDING } from "../constants"; import { nativeFileSystemSupported } from "../data/filesystem"; +import { ActionManager } from "../actions/manager"; const supportsContextFilters = "filter" in document.createElement("canvas").getContext("2d")!; @@ -90,7 +90,7 @@ const ImageExportModal = ({ elements: readonly NonDeletedExcalidrawElement[]; files: BinaryFiles; exportPadding?: number; - actionManager: ActionsManagerInterface; + actionManager: ActionManager; onExportToPng: ExportCB; onExportToSvg: ExportCB; onExportToClipboard: ExportCB; @@ -229,7 +229,7 @@ export const ImageExportDialog = ({ elements: readonly NonDeletedExcalidrawElement[]; files: BinaryFiles; exportPadding?: number; - actionManager: ActionsManagerInterface; + actionManager: ActionManager; onExportToPng: ExportCB; onExportToSvg: ExportCB; onExportToClipboard: ExportCB; diff --git a/src/components/JSONExportDialog.tsx b/src/components/JSONExportDialog.tsx index 0b367230..5f29360d 100644 --- a/src/components/JSONExportDialog.tsx +++ b/src/components/JSONExportDialog.tsx @@ -1,5 +1,4 @@ import React, { useState } from "react"; -import { ActionsManagerInterface } from "../actions/types"; import { NonDeletedExcalidrawElement } from "../element/types"; import { t } from "../i18n"; import { useDeviceType } from "./App"; @@ -12,6 +11,9 @@ import { Card } from "./Card"; import "./ExportDialog.scss"; import { nativeFileSystemSupported } from "../data/filesystem"; +import { trackEvent } from "../analytics"; +import { ActionManager } from "../actions/manager"; +import { getFrame } from "../utils"; export type ExportCB = ( elements: readonly NonDeletedExcalidrawElement[], @@ -29,7 +31,7 @@ const JSONExportModal = ({ appState: AppState; files: BinaryFiles; elements: readonly NonDeletedExcalidrawElement[]; - actionManager: ActionsManagerInterface; + actionManager: ActionManager; onCloseRequest: () => void; exportOpts: ExportOpts; canvas: HTMLCanvasElement | null; @@ -54,7 +56,7 @@ const JSONExportModal = ({ aria-label={t("exportDialog.disk_button")} showAriaLabel={true} onClick={() => { - actionManager.executeAction(actionSaveFileToDisk); + actionManager.executeAction(actionSaveFileToDisk, "ui"); }} /> @@ -70,9 +72,10 @@ const JSONExportModal = ({ title={t("exportDialog.link_button")} aria-label={t("exportDialog.link_button")} showAriaLabel={true} - onClick={() => - onExportToBackend(elements, appState, files, canvas) - } + onClick={() => { + onExportToBackend(elements, appState, files, canvas); + trackEvent("export", "link", `ui (${getFrame()})`); + }} /> )} @@ -94,7 +97,7 @@ export const JSONExportDialog = ({ elements: readonly NonDeletedExcalidrawElement[]; appState: AppState; files: BinaryFiles; - actionManager: ActionsManagerInterface; + actionManager: ActionManager; exportOpts: ExportOpts; canvas: HTMLCanvasElement | null; }) => { diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 91f02166..9792f38e 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -36,6 +36,7 @@ import { LibraryMenu } from "./LibraryMenu"; import "./LayerUI.scss"; import "./Toolbar.scss"; import { PenModeButton } from "./PenModeButton"; +import { trackEvent } from "../analytics"; import { useDeviceType } from "../components/App"; interface LayerUIProps { @@ -122,6 +123,7 @@ const LayerUI = ({ const createExporter = (type: ExportType): ExportCB => async (exportedElements) => { + trackEvent("export", type, "ui"); const fileHandle = await exportCanvas( type, exportedElements, @@ -326,7 +328,7 @@ const LayerUI = ({ onLockToggle()} title={t("toolBar.lock")} /> onLockToggle()} onPenModeToggle={onPenModeToggle} canvas={canvas} isCollaborating={isCollaborating} diff --git a/src/components/LibraryMenu.tsx b/src/components/LibraryMenu.tsx index 763dfb88..508683d5 100644 --- a/src/components/LibraryMenu.tsx +++ b/src/components/LibraryMenu.tsx @@ -19,6 +19,7 @@ import LibraryMenuItems from "./LibraryMenuItems"; import { EVENT } from "../constants"; import { KEYS } from "../keys"; import { arrayToMap } from "../utils"; +import { trackEvent } from "../analytics"; const useOnClickOutside = ( ref: RefObject, @@ -157,6 +158,7 @@ export const LibraryMenu = ({ const addToLibrary = useCallback( async (elements: LibraryItem["elements"]) => { + trackEvent("element", "addToLibrary", "ui"); if (elements.some((element) => element.type === "image")) { return setAppState({ errorMessage: "Support for adding images to the library coming soon!", diff --git a/src/element/Hyperlink.tsx b/src/element/Hyperlink.tsx index 42a7a8f0..509af764 100644 --- a/src/element/Hyperlink.tsx +++ b/src/element/Hyperlink.tsx @@ -262,9 +262,7 @@ export const actionLink = register({ commitToHistory: true, }; }, - trackEvent: (action, source) => { - trackEvent("hyperlink", "edit", source); - }, + trackEvent: { category: "hyperlink", action: "click" }, keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K, contextItemLabel: (elements, appState) => getContextMenuLabel(elements, appState), diff --git a/src/element/newElement.test.ts b/src/element/newElement.test.ts index 2945de84..2fc13ffd 100644 --- a/src/element/newElement.test.ts +++ b/src/element/newElement.test.ts @@ -2,11 +2,7 @@ import { duplicateElement } from "./newElement"; import { mutateElement } from "./mutateElement"; import { API } from "../tests/helpers/api"; import { FONT_FAMILY } from "../constants"; - -const isPrimitive = (val: any) => { - const type = typeof val; - return val == null || (type !== "object" && type !== "function"); -}; +import { isPrimitive } from "../utils"; const assertCloneObjects = (source: any, clone: any) => { for (const key in clone) { diff --git a/src/excalidraw-app/collab/CollabWrapper.tsx b/src/excalidraw-app/collab/CollabWrapper.tsx index a2bff452..2697ed8d 100644 --- a/src/excalidraw-app/collab/CollabWrapper.tsx +++ b/src/excalidraw-app/collab/CollabWrapper.tsx @@ -11,6 +11,7 @@ import { import { getSceneVersion } from "../../packages/excalidraw/index"; import { Collaborator, Gesture } from "../../types"; import { + getFrame, preventUnload, resolvablePromise, withBatchedUpdates, @@ -239,7 +240,7 @@ class CollabWrapper extends PureComponent { }; openPortal = async () => { - trackEvent("share", "room creation"); + trackEvent("share", "room creation", `ui (${getFrame()})`); return this.initializeSocketClient(null); }; diff --git a/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx b/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx index 8328f1bd..049a4ddf 100644 --- a/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx +++ b/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx @@ -13,6 +13,8 @@ import { isInitializedImageElement } from "../../element/typeChecks"; import { FILE_UPLOAD_MAX_BYTES } from "../app_constants"; import { encodeFilesForUpload } from "../data/FileManager"; import { MIME_TYPES } from "../../constants"; +import { trackEvent } from "../../analytics"; +import { getFrame } from "../../utils"; const exportToExcalidrawPlus = async ( elements: readonly NonDeletedExcalidrawElement[], @@ -92,6 +94,7 @@ export const ExportToExcalidrawPlus: React.FC<{ showAriaLabel={true} onClick={async () => { try { + trackEvent("export", "eplus", `ui (${getFrame()})`); await exportToExcalidrawPlus(elements, appState, files); } catch (error: any) { console.error(error); diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index b0601c63..7396e06e 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -34,6 +34,7 @@ import { import { debounce, getVersion, + getFrame, isTestEnv, preventUnload, ResolvablePromise, @@ -302,6 +303,7 @@ const ExcalidrawWrapper = () => { } useEffect(() => { + trackEvent("load", "frame", getFrame()); // Delayed so that the app has a time to load the latest SW setTimeout(() => { trackEvent("load", "version", getVersion()); diff --git a/src/types.ts b/src/types.ts index 5d2269c4..7917c6ad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -323,6 +323,7 @@ export type AppClassProperties = { } >; files: BinaryFiles; + deviceType: App["deviceType"]; }; export type PointerDownState = Readonly<{ diff --git a/src/utils.ts b/src/utils.ts index 0ab1ccae..7cc08d07 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -612,3 +612,16 @@ export const updateObject = >( ...updates, }; }; + +export const isPrimitive = (val: any) => { + const type = typeof val; + return val == null || (type !== "object" && type !== "function"); +}; + +export const getFrame = () => { + try { + return window.self === window.top ? "top" : "iframe"; + } catch (error) { + return "iframe"; + } +};