Add basic event actions to analytics (#2375)

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Lipis 2020-12-02 23:57:51 +02:00 committed by GitHub
parent 014097a97e
commit abde1daba4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 183 additions and 61 deletions

View file

@ -14,10 +14,14 @@ import { AppState, NormalizedZoomValue } from "../types";
import { getCommonBounds } from "../element";
import { getNewZoom } from "../scene/zoom";
import { centerScrollOn } from "../scene/scroll";
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
perform: (_, appState, value) => {
if (value !== appState.viewBackgroundColor) {
trackEvent(EVENT_CHANGE, "canvas color", value);
}
return {
appState: { ...appState, viewBackgroundColor: value },
commitToHistory: true,
@ -40,6 +44,7 @@ export const actionChangeViewBackgroundColor = register({
export const actionClearCanvas = register({
name: "clearCanvas",
perform: (elements, appState: AppState) => {
trackEvent(EVENT_ACTION, "clear canvas");
return {
elements: elements.map((element) =>
newElementWith(element, { isDeleted: true }),
@ -78,14 +83,16 @@ const ZOOM_STEP = 0.1;
export const actionZoomIn = register({
name: "zoomIn",
perform: (_elements, appState) => {
const zoom = getNewZoom(
getNormalizedZoom(appState.zoom.value + ZOOM_STEP),
appState.zoom,
{ x: appState.width / 2, y: appState.height / 2 },
);
trackEvent(EVENT_ACTION, "zoom", "in", zoom.value * 100);
return {
appState: {
...appState,
zoom: getNewZoom(
getNormalizedZoom(appState.zoom.value + ZOOM_STEP),
appState.zoom,
{ x: appState.width / 2, y: appState.height / 2 },
),
zoom,
},
commitToHistory: false,
};
@ -109,14 +116,17 @@ export const actionZoomIn = register({
export const actionZoomOut = register({
name: "zoomOut",
perform: (_elements, appState) => {
const zoom = getNewZoom(
getNormalizedZoom(appState.zoom.value - ZOOM_STEP),
appState.zoom,
{ x: appState.width / 2, y: appState.height / 2 },
);
trackEvent(EVENT_ACTION, "zoom", "out", zoom.value * 100);
return {
appState: {
...appState,
zoom: getNewZoom(
getNormalizedZoom(appState.zoom.value - ZOOM_STEP),
appState.zoom,
{ x: appState.width / 2, y: appState.height / 2 },
),
zoom,
},
commitToHistory: false,
};
@ -140,6 +150,7 @@ export const actionZoomOut = register({
export const actionResetZoom = register({
name: "resetZoom",
perform: (_elements, appState) => {
trackEvent(EVENT_ACTION, "zoom", "reset", 100);
return {
appState: {
...appState,
@ -201,7 +212,7 @@ export const actionZoomToFit = register({
const [x1, y1, x2, y2] = commonBounds;
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
trackEvent(EVENT_ACTION, "zoom", "fit", newZoom.value * 100);
return {
appState: {
...appState,

View file

@ -8,10 +8,12 @@ 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";
export const actionChangeProjectName = register({
name: "changeProjectName",
perform: (_elements, appState, value) => {
trackEvent(EVENT_CHANGE, "title");
return { appState: { ...appState, name: value }, commitToHistory: false };
},
PanelComponent: ({ appState, updateData }) => (
@ -88,6 +90,7 @@ export const actionSaveScene = register({
perform: async (elements, appState, value) => {
try {
const { fileHandle } = await saveAsJSON(elements, appState);
trackEvent(EVENT_ACTION, "save");
return { commitToHistory: false, appState: { ...appState, fileHandle } };
} catch (error) {
if (error?.name !== "AbortError") {
@ -118,6 +121,7 @@ export const actionSaveAsScene = register({
...appState,
fileHandle: null,
});
trackEvent(EVENT_ACTION, "save as");
return { commitToHistory: false, appState: { ...appState, fileHandle } };
} catch (error) {
if (error?.name !== "AbortError") {
@ -149,16 +153,14 @@ export const actionLoadScene = register({
elements,
appState,
{ elements: loadedElements, appState: loadedAppState, error },
) => {
return {
elements: loadedElements,
appState: {
...loadedAppState,
errorMessage: error,
},
commitToHistory: true,
};
},
) => ({
elements: loadedElements,
appState: {
...loadedAppState,
errorMessage: error,
},
commitToHistory: true,
}),
PanelComponent: ({ updateData, appState }) => (
<ToolButton
type="button"

View file

@ -7,6 +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";
export const actionToggleCanvasMenu = register({
name: "toggleCanvasMenu",
@ -71,6 +72,7 @@ export const actionFullScreen = register({
export const actionShortcuts = register({
name: "toggleShortcuts",
perform: (_elements, appState) => {
trackEvent(EVENT_ACTION, "keyboard shortcuts");
return {
appState: {
...appState,

View file

@ -40,6 +40,7 @@ import {
SloppinessArtistIcon,
SloppinessCartoonistIcon,
} from "../components/icons";
import { EVENT_CHANGE, trackEvent } from "../analytics";
const changeProperty = (
elements: readonly ExcalidrawElement[],
@ -81,6 +82,9 @@ const getFormValue = function <T>(
export const actionChangeStrokeColor = register({
name: "changeStrokeColor",
perform: (elements, appState, value) => {
if (value !== appState.currentItemStrokeColor) {
trackEvent(EVENT_CHANGE, "stroke color", value);
}
return {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
@ -112,6 +116,10 @@ export const actionChangeStrokeColor = register({
export const actionChangeBackgroundColor = register({
name: "changeBackgroundColor",
perform: (elements, appState, value) => {
if (value !== appState.currentItemBackgroundColor) {
trackEvent(EVENT_CHANGE, "background color", value);
}
return {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
@ -143,6 +151,7 @@ export const actionChangeBackgroundColor = register({
export const actionChangeFillStyle = register({
name: "changeFillStyle",
perform: (elements, appState, value) => {
trackEvent(EVENT_CHANGE, "fill", value);
return {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
@ -192,6 +201,7 @@ export const actionChangeFillStyle = register({
export const actionChangeStrokeWidth = register({
name: "changeStrokeWidth",
perform: (elements, appState, value) => {
trackEvent(EVENT_CHANGE, "stroke", "width", value);
return {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
@ -254,6 +264,7 @@ export const actionChangeStrokeWidth = register({
export const actionChangeSloppiness = register({
name: "changeSloppiness",
perform: (elements, appState, value) => {
trackEvent(EVENT_CHANGE, "stroke", "sloppiness", value);
return {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
@ -349,6 +360,7 @@ export const actionChangeStrokeStyle = register({
export const actionChangeOpacity = register({
name: "changeOpacity",
perform: (elements, appState, value) => {
trackEvent(EVENT_CHANGE, "opacity", "value", value);
return {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
@ -545,6 +557,7 @@ export const actionChangeSharpness = register({
const shouldUpdateForLinearElements = targetElements.length
? targetElements.every(isLinearElement)
: isLinearElementType(appState.elementType);
trackEvent(EVENT_CHANGE, "edge", value);
return {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {

16
src/analytics.ts Normal file
View file

@ -0,0 +1,16 @@
export const EVENT_ACTION = "action";
export const EVENT_EXIT = "exit";
export const EVENT_CHANGE = "change";
export const EVENT_SHAPE = "shape";
export const trackEvent = window.gtag
? (name: string, category: string, label?: string, value?: number) => {
window.gtag("event", name, {
event_category: category,
event_label: label,
value,
});
}
: (name: string, category: string, label?: string, value?: number) => {
console.info("Track Event", name, category, label, value);
};

View file

@ -16,6 +16,7 @@ import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
import Stack from "./Stack";
import useIsMobile from "../is-mobile";
import { getNonDeletedElements } from "../element";
import { trackEvent, EVENT_SHAPE } from "../analytics";
export const SelectedShapeActions = ({
appState,
@ -173,6 +174,7 @@ export const ShapesSwitcher = ({
aria-keyshortcuts={shortcut}
data-testid={value}
onChange={() => {
trackEvent(EVENT_SHAPE, value, "toolbar");
setAppState({
elementType: value,
multiElement: null,

View file

@ -181,6 +181,7 @@ import {
isSavedToFirebase,
} from "../data/firebase";
import { getNewZoom } from "../scene/zoom";
import { EVENT_SHAPE, trackEvent } from "../analytics";
/**
* @param func handler taking at most single parameter (event).
@ -1270,12 +1271,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
};
toggleLock = () => {
this.setState((prevState) => ({
elementLocked: !prevState.elementLocked,
elementType: prevState.elementLocked
? "selection"
: prevState.elementType,
}));
this.setState((prevState) => {
trackEvent(EVENT_SHAPE, "lock", !prevState.elementLocked ? "on" : "off");
return {
elementLocked: !prevState.elementLocked,
elementType: prevState.elementLocked
? "selection"
: prevState.elementType,
};
});
};
toggleZenMode = () => {
@ -1655,6 +1659,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
) {
const shape = findShapeByKey(event.key);
if (shape) {
trackEvent(EVENT_SHAPE, shape, "shortcut");
this.selectShapeTool(shape);
} else if (event.key === KEYS.Q) {
this.toggleLock();

View file

@ -1,5 +1,6 @@
import React from "react";
import { ActionManager } from "../actions/manager";
import { EVENT_CHANGE, trackEvent } from "../analytics";
import { AppState } from "../types";
import { DarkModeToggle } from "./DarkModeToggle";
@ -18,6 +19,8 @@ export const BackgroundPickerAndDarkModeToggle = ({
<DarkModeToggle
value={appState.appearance}
onChange={(appearance) => {
// TODO: track the theme on the first load too
trackEvent(EVENT_CHANGE, "theme", appearance);
setAppState({ appearance });
}}
/>

View file

@ -1,5 +1,6 @@
import React from "react";
import oc from "open-color";
import { EVENT_EXIT, trackEvent } from "../analytics";
// https://github.com/tholman/github-corners
export const GitHubCorner = React.memo(
@ -16,6 +17,9 @@ export const GitHubCorner = React.memo(
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub repository"
onClick={() => {
trackEvent(EVENT_EXIT, "github");
}}
>
<path
d="M0 0l115 115h15l12 27 108 108V0z"

View file

@ -45,6 +45,7 @@ import { muteFSAbortError } from "../utils";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
import clsx from "clsx";
import { Library } from "../data/library";
import { EVENT_EXIT, trackEvent } from "../analytics";
interface LayerUIProps {
actionManager: ActionManager;
@ -310,6 +311,9 @@ const LayerUI = ({
href="https://blog.excalidraw.com/end-to-end-encryption/"
target="_blank"
rel="noopener noreferrer"
onClick={() => {
trackEvent(EVENT_EXIT, "e2ee shield");
}}
>
<span className="tooltip-text" dir="auto">
{t("encrypted.tooltip")}

View file

@ -4,6 +4,7 @@ import { isDarwin } from "../keys";
import { Dialog } from "./Dialog";
import { getShortcutKey } from "../utils";
import "./ShortcutsDialog.scss";
import { EVENT_EXIT, trackEvent } from "../analytics";
const Columns = (props: { children: React.ReactNode }) => (
<div
@ -91,6 +92,9 @@ const Footer = () => (
href="https://blog.excalidraw.com"
target="_blank"
rel="noopener noreferrer"
onClick={() => {
trackEvent(EVENT_EXIT, "blog");
}}
>
{t("shortcutsDialog.blog")}
</a>
@ -98,6 +102,9 @@ const Footer = () => (
href="https://howto.excalidraw.com"
target="_blank"
rel="noopener noreferrer"
onClick={() => {
trackEvent(EVENT_EXIT, "guides");
}}
>
{t("shortcutsDialog.howto")}
</a>
@ -105,6 +112,9 @@ const Footer = () => (
href="https://github.com/excalidraw/excalidraw/issues"
target="_blank"
rel="noopener noreferrer"
onClick={() => {
trackEvent(EVENT_EXIT, "issues");
}}
>
{t("shortcutsDialog.github")}
</a>

View file

@ -7,6 +7,7 @@ import { calculateScrollCenter } from "../scene";
import { MIME_TYPES } from "../constants";
import { CanvasError } from "../errors";
import { clearElementsForExport } from "../element";
import { EVENT_ACTION, trackEvent } from "../analytics";
export const parseFileContents = async (blob: Blob | File) => {
let contents: string;
@ -89,7 +90,7 @@ export const loadFromBlob = async (
if (data.type !== "excalidraw") {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
return restore(
const result = restore(
{
elements: clearElementsForExport(data.elements || []),
appState: {
@ -109,6 +110,9 @@ export const loadFromBlob = async (
},
localAppState,
);
trackEvent(EVENT_ACTION, "load", getMimeType(blob));
return result;
} catch (error) {
console.error(error.message);
throw new Error(t("alerts.couldNotLoadInvalidFile"));

View file

@ -20,6 +20,7 @@ import { ExportType } from "../scene/types";
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";
@ -263,6 +264,7 @@ const importFromBackend = async (
data = await response.json();
}
trackEvent(EVENT_ACTION, "import");
return {
elements: data.elements || null,
appState: data.appState || null,

View file

@ -42,7 +42,6 @@ export const saveAsJSON = async (
},
appState.fileHandle,
);
return { fileHandle };
};

1
src/global.d.ts vendored
View file

@ -12,6 +12,7 @@ interface Document {
interface Window {
ClipboardItem: any;
__EXCALIDRAW_SHA__: string | undefined;
gtag: Function;
}
// https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts

View file

@ -1,4 +1,5 @@
import LanguageDetector from "i18next-browser-languagedetector";
import { EVENT_CHANGE, trackEvent } from "./analytics";
import fallbackLanguageData from "./locales/en.json";
import percentages from "./locales/percentages.json";
@ -67,8 +68,8 @@ export const setLanguage = async (newLng: string | undefined) => {
currentLanguageData = await import(
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLanguage.lng}.json`
);
languageDetector.cacheUserLanguage(currentLanguage.lng);
trackEvent(EVENT_CHANGE, "language", currentLanguage.lng);
};
export const setLanguageFirstTime = async () => {
@ -84,6 +85,7 @@ export const setLanguageFirstTime = async () => {
);
languageDetector.cacheUserLanguage(currentLanguage.lng);
trackEvent(EVENT_CHANGE, "language on load", currentLanguage.lng);
};
export const getLanguage = () => currentLanguage;