diff --git a/analytics.md b/analytics.md index 9ab22248..3d8fb52d 100644 --- a/analytics.md +++ b/analytics.md @@ -3,21 +3,19 @@ | Shape / Selection | shape | selection, rectangle, diamond, etc | `toolbar` or `shortcut` | | Text on double click | shape | text | `double-click` | | Lock selection | shape | lock | `on` or `off` | -| Load file | action | load | `MIME type` | -| Import from URL | action | import | -| Save | action | save | -| Save as | action | save as | | Clear canvas | action | clear canvas | | Zoom in | action | zoom | in | `zoom` | | Zoom out | action | zoom | out | `zoom` | | Zoom fit | action | zoom | fit | `zoom` | | Zoom reset | action | zoom | reset | `zoom` | -| Export dialog | action | export | dialog | -| Export to backend | action | export | backend | -| Export as SVG | action | export | `svg` or `clipboard-svg` | -| Export to PNG | action | export | `png` or `clipboard-png` | | Scroll back to content | action | scroll to content | -| Open shortcut menu | action | keyboard shortcuts | +| Load file | io | load | `MIME type` | +| Import from URL | io | import | +| Save | io | save | +| Save as | io | save as | +| Export to backend | io | export | backend | +| Export as SVG | io | export | `svg` or `clipboard-svg` | +| Export to PNG | io | export | `png` or `clipboard-png` | | Canvas color | change | canvas color | `color` | | Background color | change | background color | `color` | | Stroke color | change | stroke color | `color` | @@ -42,6 +40,15 @@ | Center vertically | align | vertically | `center` | | Distribute horizontally | align | distribute | `horizontally` | | Distribute vertically | align | distribute | `vertically` | +| Start session | share | session start | +| Join session | share | session join | +| Start end | share | session end | +| Copy room link | share | copy link | +| Go to collaborator | share | go to collaborator | +| Change name | share | name | +| Shortcuts dialog | dialog | shortcuts | +| Collaboration dialog | dialog | collaboration | +| Export dialog | dialog | export | | E2EE shield | exit | e2ee shield | | GitHub corner | exit | github | | Excalidraw blog | exit | blog | diff --git a/src/actions/actionExport.tsx b/src/actions/actionExport.tsx index bb92ee62..fae8e4ca 100644 --- a/src/actions/actionExport.tsx +++ b/src/actions/actionExport.tsx @@ -1,14 +1,14 @@ import React from "react"; -import { ProjectName } from "../components/ProjectName"; -import { saveAsJSON, loadFromJSON } from "../data"; +import { EVENT_CHANGE, EVENT_IO, trackEvent } from "../analytics"; import { load, save, saveAs } from "../components/icons"; +import { ProjectName } from "../components/ProjectName"; import { ToolButton } from "../components/ToolButton"; +import { loadFromJSON, saveAsJSON } from "../data"; import { t } from "../i18n"; import useIsMobile from "../is-mobile"; -import { register } from "./register"; import { KEYS } from "../keys"; import { muteFSAbortError } from "../utils"; -import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics"; +import { register } from "./register"; export const actionChangeProjectName = register({ name: "changeProjectName", @@ -90,7 +90,7 @@ export const actionSaveScene = register({ perform: async (elements, appState, value) => { try { const { fileHandle } = await saveAsJSON(elements, appState); - trackEvent(EVENT_ACTION, "save"); + trackEvent(EVENT_IO, "save"); return { commitToHistory: false, appState: { ...appState, fileHandle } }; } catch (error) { if (error?.name !== "AbortError") { @@ -121,7 +121,7 @@ export const actionSaveAsScene = register({ ...appState, fileHandle: null, }); - trackEvent(EVENT_ACTION, "save as"); + trackEvent(EVENT_IO, "save as"); return { commitToHistory: false, appState: { ...appState, fileHandle } }; } catch (error) { if (error?.name !== "AbortError") { diff --git a/src/actions/actionMenu.tsx b/src/actions/actionMenu.tsx index 3545ad7b..aa98bd30 100644 --- a/src/actions/actionMenu.tsx +++ b/src/actions/actionMenu.tsx @@ -7,7 +7,7 @@ import { register } from "./register"; import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils"; import { CODES, KEYS } from "../keys"; import { HelpIcon } from "../components/HelpIcon"; -import { EVENT_ACTION, trackEvent } from "../analytics"; +import { EVENT_DIALOG, trackEvent } from "../analytics"; export const actionToggleCanvasMenu = register({ name: "toggleCanvasMenu", @@ -72,7 +72,7 @@ export const actionFullScreen = register({ export const actionShortcuts = register({ name: "toggleShortcuts", perform: (_elements, appState) => { - trackEvent(EVENT_ACTION, "keyboard shortcuts"); + trackEvent(EVENT_DIALOG, "shortcuts"); return { appState: { ...appState, diff --git a/src/actions/actionNavigate.tsx b/src/actions/actionNavigate.tsx index c54af14e..e41b2eb0 100644 --- a/src/actions/actionNavigate.tsx +++ b/src/actions/actionNavigate.tsx @@ -4,11 +4,13 @@ import { register } from "./register"; import { getClientColors, getClientInitials } from "../clients"; import { Collaborator } from "../types"; import { centerScrollOn } from "../scene/scroll"; +import { EVENT_SHARE, trackEvent } from "../analytics"; export const actionGoToCollaborator = register({ name: "goToCollaborator", perform: (_elements, appState, value) => { const point = value as Collaborator["pointer"]; + trackEvent(EVENT_SHARE, "go to collaborator"); if (!point) { return { appState, commitToHistory: false }; } diff --git a/src/analytics.ts b/src/analytics.ts index fb4c3b7d..1730fdec 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -4,6 +4,9 @@ export const EVENT_CHANGE = "change"; export const EVENT_SHAPE = "shape"; export const EVENT_LAYER = "layer"; export const EVENT_ALIGN = "align"; +export const EVENT_SHARE = "share"; +export const EVENT_IO = "io"; +export const EVENT_DIALOG = "dialog"; export const trackEvent = window.gtag ? (category: string, name: string, label?: string, value?: number) => { diff --git a/src/components/App.tsx b/src/components/App.tsx index 08222883..69fb29d3 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -181,7 +181,7 @@ import { isSavedToFirebase, } from "../data/firebase"; import { getNewZoom } from "../scene/zoom"; -import { EVENT_SHAPE, trackEvent } from "../analytics"; +import { EVENT_SHAPE, EVENT_SHARE, trackEvent } from "../analytics"; /** * @param func handler taking at most single parameter (event). @@ -657,8 +657,8 @@ class App extends React.Component { // when joining a room we don't want user's local scene data to be merged // into the remote scene this.resetScene(); - this.initializeSocketClient({ showLoadingState: true }); + trackEvent(EVENT_SHARE, "session join"); } else if (scene) { if (scene.appState) { scene.appState = { @@ -1262,12 +1262,14 @@ class App extends React.Component { this.scene.replaceAllElements(this.scene.getElements()); await this.initializeSocketClient({ showLoadingState: false }); + trackEvent(EVENT_SHARE, "session start"); }; closePortal = () => { this.saveCollabRoomToFirebase(); window.history.pushState({}, "Excalidraw", window.location.origin); this.destroySocketClient(); + trackEvent(EVENT_SHARE, "session end"); }; toggleLock = () => { diff --git a/src/components/ExportDialog.tsx b/src/components/ExportDialog.tsx index 617d0f7c..f073bf90 100644 --- a/src/components/ExportDialog.tsx +++ b/src/components/ExportDialog.tsx @@ -1,24 +1,21 @@ -import "./ExportDialog.scss"; - -import React, { useState, useEffect, useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { render, unmountComponentAtNode } from "react-dom"; - -import { ToolButton } from "./ToolButton"; -import { clipboard, exportFile, link } from "./icons"; -import { NonDeletedExcalidrawElement } from "../element/types"; -import { AppState } from "../types"; -import { exportToCanvas, getExportSize } from "../scene/export"; import { ActionsManagerInterface } from "../actions/types"; -import Stack from "./Stack"; -import { t } from "../i18n"; - +import { EVENT_DIALOG, trackEvent } from "../analytics"; import { probablySupportsClipboardBlob } from "../clipboard"; -import { getSelectedElements, isSomeElementSelected } from "../scene"; -import useIsMobile from "../is-mobile"; -import { Dialog } from "./Dialog"; import { canvasToBlob } from "../data/blob"; +import { NonDeletedExcalidrawElement } from "../element/types"; import { CanvasError } from "../errors"; -import { EVENT_ACTION, trackEvent } from "../analytics"; +import { t } from "../i18n"; +import useIsMobile from "../is-mobile"; +import { getSelectedElements, isSomeElementSelected } from "../scene"; +import { exportToCanvas, getExportSize } from "../scene/export"; +import { AppState } from "../types"; +import { Dialog } from "./Dialog"; +import "./ExportDialog.scss"; +import { clipboard, exportFile, link } from "./icons"; +import Stack from "./Stack"; +import { ToolButton } from "./ToolButton"; const scales = [1, 2, 3]; const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1; @@ -252,7 +249,7 @@ export const ExportDialog = ({ <> { - trackEvent(EVENT_ACTION, "export", "dialog"); + trackEvent(EVENT_DIALOG, "export"); setModalIsShown(true); }} icon={exportFile} diff --git a/src/components/RoomDialog.tsx b/src/components/RoomDialog.tsx index 1a391acd..bc58fae6 100644 --- a/src/components/RoomDialog.tsx +++ b/src/components/RoomDialog.tsx @@ -1,15 +1,15 @@ -import React, { useState, useEffect, useRef } from "react"; import clsx from "clsx"; -import { ToolButton } from "./ToolButton"; +import React, { useEffect, useRef, useState } from "react"; +import { EVENT_DIALOG, EVENT_SHARE, trackEvent } from "../analytics"; +import { copyTextToSystemClipboard } from "../clipboard"; import { t } from "../i18n"; import useIsMobile from "../is-mobile"; -import { users, clipboard, start, stop } from "./icons"; - -import "./RoomDialog.scss"; -import { copyTextToSystemClipboard } from "../clipboard"; -import { Dialog } from "./Dialog"; -import { AppState } from "../types"; import { KEYS } from "../keys"; +import { AppState } from "../types"; +import { Dialog } from "./Dialog"; +import { clipboard, start, stop, users } from "./icons"; +import "./RoomDialog.scss"; +import { ToolButton } from "./ToolButton"; const RoomModal = ({ activeRoomLink, @@ -33,6 +33,7 @@ const RoomModal = ({ const copyRoomLink = async () => { try { await copyTextToSystemClipboard(activeRoomLink); + trackEvent(EVENT_SHARE, "copy link"); } catch (error) { setErrorMessage(error.message); } @@ -95,6 +96,7 @@ const RoomModal = ({ value={username || ""} className="RoomDialog-username TextInput" onChange={(event) => onUsernameChange(event.target.value)} + onBlur={() => trackEvent(EVENT_SHARE, "name")} onKeyPress={(event) => event.key === KEYS.ENTER && onPressingEnter() } @@ -161,7 +163,10 @@ export const RoomDialog = ({ className={clsx("RoomDialog-modalButton", { "is-collaborating": isCollaborating, })} - onClick={() => setModalIsShown(true)} + onClick={() => { + trackEvent(EVENT_DIALOG, "collaboration"); + setModalIsShown(true); + }} icon={users} type="button" title={t("buttons.roomDialog")} diff --git a/src/data/blob.ts b/src/data/blob.ts index 224dc216..73692879 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -1,13 +1,13 @@ +import { EVENT_IO, trackEvent } from "../analytics"; import { cleanAppStateForExport } from "../appState"; -import { restore } from "./restore"; -import { t } from "../i18n"; -import { AppState } from "../types"; -import { LibraryData, ImportedDataState } from "./types"; -import { calculateScrollCenter } from "../scene"; import { MIME_TYPES } from "../constants"; -import { CanvasError } from "../errors"; import { clearElementsForExport } from "../element"; -import { EVENT_ACTION, trackEvent } from "../analytics"; +import { CanvasError } from "../errors"; +import { t } from "../i18n"; +import { calculateScrollCenter } from "../scene"; +import { AppState } from "../types"; +import { restore } from "./restore"; +import { ImportedDataState, LibraryData } from "./types"; export const parseFileContents = async (blob: Blob | File) => { let contents: string; @@ -111,7 +111,7 @@ export const loadFromBlob = async ( localAppState, ); - trackEvent(EVENT_ACTION, "load", getMimeType(blob)); + trackEvent(EVENT_IO, "load", getMimeType(blob)); return result; } catch (error) { console.error(error.message); diff --git a/src/data/index.ts b/src/data/index.ts index 257533fd..b9b3f365 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -1,29 +1,25 @@ -import { - ExcalidrawElement, - NonDeletedExcalidrawElement, -} from "../element/types"; - -import { getDefaultAppState } from "../appState"; - -import { AppState } from "../types"; -import { exportToCanvas, exportToSvg } from "../scene/export"; import { fileSave } from "browser-nativefs"; - -import { t } from "../i18n"; +import { EVENT_IO, trackEvent } from "../analytics"; +import { getDefaultAppState } from "../appState"; import { copyCanvasToClipboardAsPng, copyTextToSystemClipboard, } from "../clipboard"; -import { serializeAsJSON } from "./json"; - +import { + ExcalidrawElement, + NonDeletedExcalidrawElement, +} from "../element/types"; +import { t } from "../i18n"; +import { exportToCanvas, exportToSvg } from "../scene/export"; import { ExportType } from "../scene/types"; +import { AppState } from "../types"; +import { canvasToBlob } from "./blob"; +import { serializeAsJSON } from "./json"; import { restore } from "./restore"; import { ImportedDataState } from "./types"; -import { canvasToBlob } from "./blob"; -import { EVENT_ACTION, trackEvent } from "../analytics"; export { loadFromBlob } from "./blob"; -export { saveAsJSON, loadFromJSON } from "./json"; +export { loadFromJSON, saveAsJSON } from "./json"; const BACKEND_GET = process.env.REACT_APP_BACKEND_V1_GET_URL; @@ -218,7 +214,7 @@ export const exportToBackend = async ( url.hash = `json=${json.id},${exportedKey.k!}`; const urlString = url.toString(); window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString); - trackEvent(EVENT_ACTION, "export", "backend"); + trackEvent(EVENT_IO, "export", "backend"); } else if (json.error_class === "RequestTooLargeError") { window.alert(t("alerts.couldNotCreateShareableLinkTooBig")); } else { @@ -265,7 +261,7 @@ const importFromBackend = async ( data = await response.json(); } - trackEvent(EVENT_ACTION, "import"); + trackEvent(EVENT_IO, "import"); return { elements: data.elements || null, appState: data.appState || null, @@ -322,10 +318,10 @@ export const exportCanvas = async ( fileName: `${name}.svg`, extensions: [".svg"], }); - trackEvent(EVENT_ACTION, "export", "svg"); + trackEvent(EVENT_IO, "export", "svg"); return; } else if (type === "clipboard-svg") { - trackEvent(EVENT_ACTION, "export", "clipboard-svg"); + trackEvent(EVENT_IO, "export", "clipboard-svg"); copyTextToSystemClipboard(tempSvg.outerHTML); return; } @@ -357,11 +353,11 @@ export const exportCanvas = async ( fileName, extensions: [".png"], }); - trackEvent(EVENT_ACTION, "export", "png"); + trackEvent(EVENT_IO, "export", "png"); } else if (type === "clipboard") { try { await copyCanvasToClipboardAsPng(tempCanvas); - trackEvent(EVENT_ACTION, "export", "clipboard-png"); + trackEvent(EVENT_IO, "export", "clipboard-png"); } catch (error) { if (error.name === "CANVAS_POSSIBLY_TOO_BIG") { throw error;