Add basic event actions to analytics (#2375)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
014097a97e
commit
abde1daba4
1
.env.production
Normal file
1
.env.production
Normal file
@ -0,0 +1 @@
|
|||||||
|
REACT_APP_INCLUDE_GTAG=true
|
31
analytics.md
Normal file
31
analytics.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
| Excalidraw | Name | Category | Label | Value |
|
||||||
|
| ------------------ | ------ | ---------------------------------- | ----------------------- | --------- |
|
||||||
|
| Shape / Selection | shape | selection, rectangle, diamond, etc | `toolbar` or `shortcut` |
|
||||||
|
| 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` |
|
||||||
|
| Open shortcut menu | action | keyboard shortcuts |
|
||||||
|
| Canvas color | change | canvas color | `color` |
|
||||||
|
| Background color | change | background color | `color` |
|
||||||
|
| Stroke color | change | stroke color | `color` |
|
||||||
|
| Stroke width | change | stroke | width | `width` |
|
||||||
|
| Stroke sloppiness | change | stroke | sloppiness | `value` |
|
||||||
|
| Fill | change | fill | `value` |
|
||||||
|
| Edge | change | edge | `value` |
|
||||||
|
| Opacity | change | opacity | value | `opacity` |
|
||||||
|
| Project name | change | title |
|
||||||
|
| Theme | change | theme | `light` or `dark` |
|
||||||
|
| Change language | change | language | `language` |
|
||||||
|
| Language on load | change | language on load | `language` |
|
||||||
|
| E2EE shield | exit | e2ee shield |
|
||||||
|
| GitHub corner | exit | github |
|
||||||
|
| Excalidraw blog | exit | blog |
|
||||||
|
| Excalidraw guides | exit | guides |
|
||||||
|
| File issues | exit | issues |
|
69
package-lock.json
generated
69
package-lock.json
generated
@ -15933,9 +15933,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
"version": "6.14.8",
|
"version": "6.14.9",
|
||||||
"resolved": "https://registry.npmjs.org/npm/-/npm-6.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/npm/-/npm-6.14.9.tgz",
|
||||||
"integrity": "sha512-HBZVBMYs5blsj94GTeQZel7s9odVuuSUHy1+AlZh7rPVux1os2ashvEGLy/STNK7vUjbrCg5Kq9/GXisJgdf6A==",
|
"integrity": "sha512-yHi1+i9LyAZF1gAmgyYtVk+HdABlLy94PMIDoK1TRKWvmFQAt5z3bodqVwKvzY0s6dLqQPVsRLiwhJfNtiHeCg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"JSONStream": "^1.3.5",
|
"JSONStream": "^1.3.5",
|
||||||
"abbrev": "~1.1.1",
|
"abbrev": "~1.1.1",
|
||||||
@ -16017,7 +16017,7 @@
|
|||||||
"npm-pick-manifest": "^3.0.2",
|
"npm-pick-manifest": "^3.0.2",
|
||||||
"npm-profile": "^4.0.4",
|
"npm-profile": "^4.0.4",
|
||||||
"npm-registry-fetch": "^4.0.7",
|
"npm-registry-fetch": "^4.0.7",
|
||||||
"npm-user-validate": "~1.0.0",
|
"npm-user-validate": "^1.0.1",
|
||||||
"npmlog": "~4.1.2",
|
"npmlog": "~4.1.2",
|
||||||
"once": "~1.4.0",
|
"once": "~1.4.0",
|
||||||
"opener": "^1.5.1",
|
"opener": "^1.5.1",
|
||||||
@ -16088,16 +16088,6 @@
|
|||||||
"humanize-ms": "^1.2.1"
|
"humanize-ms": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ajv": {
|
|
||||||
"version": "5.5.2",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"co": "^4.6.0",
|
|
||||||
"fast-deep-equal": "^1.0.0",
|
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
|
||||||
"json-schema-traverse": "^0.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ansi-align": {
|
"ansi-align": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
@ -16383,10 +16373,6 @@
|
|||||||
"mkdirp": "~0.5.0"
|
"mkdirp": "~0.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"co": {
|
|
||||||
"version": "4.6.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true
|
"bundled": true
|
||||||
@ -16775,10 +16761,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"bundled": true
|
"bundled": true
|
||||||
},
|
},
|
||||||
"fast-deep-equal": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"fast-json-stable-stringify": {
|
"fast-json-stable-stringify": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"bundled": true
|
"bundled": true
|
||||||
@ -17063,11 +17045,31 @@
|
|||||||
"bundled": true
|
"bundled": true
|
||||||
},
|
},
|
||||||
"har-validator": {
|
"har-validator": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.5",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ajv": "^5.3.0",
|
"ajv": "^6.12.3",
|
||||||
"har-schema": "^2.0.0"
|
"har-schema": "^2.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": {
|
||||||
|
"version": "6.12.6",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.4.1",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"json-schema-traverse": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"bundled": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"has": {
|
"has": {
|
||||||
@ -17310,10 +17312,6 @@
|
|||||||
"version": "0.2.3",
|
"version": "0.2.3",
|
||||||
"bundled": true
|
"bundled": true
|
||||||
},
|
},
|
||||||
"json-schema-traverse": {
|
|
||||||
"version": "0.3.1",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"json-stringify-safe": {
|
"json-stringify-safe": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"bundled": true
|
"bundled": true
|
||||||
@ -17884,7 +17882,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm-user-validate": {
|
"npm-user-validate": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"bundled": true
|
"bundled": true
|
||||||
},
|
},
|
||||||
"npmlog": {
|
"npmlog": {
|
||||||
@ -18759,6 +18757,19 @@
|
|||||||
"xdg-basedir": "^3.0.0"
|
"xdg-basedir": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"uri-js": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"punycode": "^2.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"bundled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"url-parse-lax": {
|
"url-parse-lax": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
|
@ -87,7 +87,7 @@
|
|||||||
<% if (process.env.REACT_APP_INCLUDE_GTAG === 'true') { %>
|
<% if (process.env.REACT_APP_INCLUDE_GTAG === 'true') { %>
|
||||||
<script
|
<script
|
||||||
async
|
async
|
||||||
src="https://www.googletagmanager.com/gtag/js?id=G-H3S0KQSBGX"
|
src="https://www.googletagmanager.com/gtag/js?id=UA-387204-13"
|
||||||
></script>
|
></script>
|
||||||
<script>
|
<script>
|
||||||
window.dataLayer = window.dataLayer || [];
|
window.dataLayer = window.dataLayer || [];
|
||||||
@ -96,7 +96,6 @@
|
|||||||
}
|
}
|
||||||
gtag("js", new Date());
|
gtag("js", new Date());
|
||||||
gtag("config", "UA-387204-13");
|
gtag("config", "UA-387204-13");
|
||||||
gtag("config", "G-H3S0KQSBGX");
|
|
||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
@ -14,10 +14,14 @@ import { AppState, NormalizedZoomValue } from "../types";
|
|||||||
import { getCommonBounds } from "../element";
|
import { getCommonBounds } from "../element";
|
||||||
import { getNewZoom } from "../scene/zoom";
|
import { getNewZoom } from "../scene/zoom";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
|
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
perform: (_, appState, value) => {
|
perform: (_, appState, value) => {
|
||||||
|
if (value !== appState.viewBackgroundColor) {
|
||||||
|
trackEvent(EVENT_CHANGE, "canvas color", value);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
appState: { ...appState, viewBackgroundColor: value },
|
appState: { ...appState, viewBackgroundColor: value },
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
@ -40,6 +44,7 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
export const actionClearCanvas = register({
|
export const actionClearCanvas = register({
|
||||||
name: "clearCanvas",
|
name: "clearCanvas",
|
||||||
perform: (elements, appState: AppState) => {
|
perform: (elements, appState: AppState) => {
|
||||||
|
trackEvent(EVENT_ACTION, "clear canvas");
|
||||||
return {
|
return {
|
||||||
elements: elements.map((element) =>
|
elements: elements.map((element) =>
|
||||||
newElementWith(element, { isDeleted: true }),
|
newElementWith(element, { isDeleted: true }),
|
||||||
@ -78,14 +83,16 @@ const ZOOM_STEP = 0.1;
|
|||||||
export const actionZoomIn = register({
|
export const actionZoomIn = register({
|
||||||
name: "zoomIn",
|
name: "zoomIn",
|
||||||
perform: (_elements, appState) => {
|
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 {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
zoom: getNewZoom(
|
zoom,
|
||||||
getNormalizedZoom(appState.zoom.value + ZOOM_STEP),
|
|
||||||
appState.zoom,
|
|
||||||
{ x: appState.width / 2, y: appState.height / 2 },
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
@ -109,14 +116,17 @@ export const actionZoomIn = register({
|
|||||||
export const actionZoomOut = register({
|
export const actionZoomOut = register({
|
||||||
name: "zoomOut",
|
name: "zoomOut",
|
||||||
perform: (_elements, appState) => {
|
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 {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
zoom: getNewZoom(
|
zoom,
|
||||||
getNormalizedZoom(appState.zoom.value - ZOOM_STEP),
|
|
||||||
appState.zoom,
|
|
||||||
{ x: appState.width / 2, y: appState.height / 2 },
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
@ -140,6 +150,7 @@ export const actionZoomOut = register({
|
|||||||
export const actionResetZoom = register({
|
export const actionResetZoom = register({
|
||||||
name: "resetZoom",
|
name: "resetZoom",
|
||||||
perform: (_elements, appState) => {
|
perform: (_elements, appState) => {
|
||||||
|
trackEvent(EVENT_ACTION, "zoom", "reset", 100);
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
@ -201,7 +212,7 @@ export const actionZoomToFit = register({
|
|||||||
const [x1, y1, x2, y2] = commonBounds;
|
const [x1, y1, x2, y2] = commonBounds;
|
||||||
const centerX = (x1 + x2) / 2;
|
const centerX = (x1 + x2) / 2;
|
||||||
const centerY = (y1 + y2) / 2;
|
const centerY = (y1 + y2) / 2;
|
||||||
|
trackEvent(EVENT_ACTION, "zoom", "fit", newZoom.value * 100);
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
|
@ -8,10 +8,12 @@ import useIsMobile from "../is-mobile";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { muteFSAbortError } from "../utils";
|
import { muteFSAbortError } from "../utils";
|
||||||
|
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
|
||||||
|
|
||||||
export const actionChangeProjectName = register({
|
export const actionChangeProjectName = register({
|
||||||
name: "changeProjectName",
|
name: "changeProjectName",
|
||||||
perform: (_elements, appState, value) => {
|
perform: (_elements, appState, value) => {
|
||||||
|
trackEvent(EVENT_CHANGE, "title");
|
||||||
return { appState: { ...appState, name: value }, commitToHistory: false };
|
return { appState: { ...appState, name: value }, commitToHistory: false };
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
@ -88,6 +90,7 @@ export const actionSaveScene = register({
|
|||||||
perform: async (elements, appState, value) => {
|
perform: async (elements, appState, value) => {
|
||||||
try {
|
try {
|
||||||
const { fileHandle } = await saveAsJSON(elements, appState);
|
const { fileHandle } = await saveAsJSON(elements, appState);
|
||||||
|
trackEvent(EVENT_ACTION, "save");
|
||||||
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.name !== "AbortError") {
|
if (error?.name !== "AbortError") {
|
||||||
@ -118,6 +121,7 @@ export const actionSaveAsScene = register({
|
|||||||
...appState,
|
...appState,
|
||||||
fileHandle: null,
|
fileHandle: null,
|
||||||
});
|
});
|
||||||
|
trackEvent(EVENT_ACTION, "save as");
|
||||||
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.name !== "AbortError") {
|
if (error?.name !== "AbortError") {
|
||||||
@ -149,16 +153,14 @@ export const actionLoadScene = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
{ elements: loadedElements, appState: loadedAppState, error },
|
{ elements: loadedElements, appState: loadedAppState, error },
|
||||||
) => {
|
) => ({
|
||||||
return {
|
elements: loadedElements,
|
||||||
elements: loadedElements,
|
appState: {
|
||||||
appState: {
|
...loadedAppState,
|
||||||
...loadedAppState,
|
errorMessage: error,
|
||||||
errorMessage: error,
|
},
|
||||||
},
|
commitToHistory: true,
|
||||||
commitToHistory: true,
|
}),
|
||||||
};
|
|
||||||
},
|
|
||||||
PanelComponent: ({ updateData, appState }) => (
|
PanelComponent: ({ updateData, appState }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -7,6 +7,7 @@ import { register } from "./register";
|
|||||||
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { HelpIcon } from "../components/HelpIcon";
|
import { HelpIcon } from "../components/HelpIcon";
|
||||||
|
import { EVENT_ACTION, trackEvent } from "../analytics";
|
||||||
|
|
||||||
export const actionToggleCanvasMenu = register({
|
export const actionToggleCanvasMenu = register({
|
||||||
name: "toggleCanvasMenu",
|
name: "toggleCanvasMenu",
|
||||||
@ -71,6 +72,7 @@ export const actionFullScreen = register({
|
|||||||
export const actionShortcuts = register({
|
export const actionShortcuts = register({
|
||||||
name: "toggleShortcuts",
|
name: "toggleShortcuts",
|
||||||
perform: (_elements, appState) => {
|
perform: (_elements, appState) => {
|
||||||
|
trackEvent(EVENT_ACTION, "keyboard shortcuts");
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
|
@ -40,6 +40,7 @@ import {
|
|||||||
SloppinessArtistIcon,
|
SloppinessArtistIcon,
|
||||||
SloppinessCartoonistIcon,
|
SloppinessCartoonistIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
|
import { EVENT_CHANGE, trackEvent } from "../analytics";
|
||||||
|
|
||||||
const changeProperty = (
|
const changeProperty = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
@ -81,6 +82,9 @@ const getFormValue = function <T>(
|
|||||||
export const actionChangeStrokeColor = register({
|
export const actionChangeStrokeColor = register({
|
||||||
name: "changeStrokeColor",
|
name: "changeStrokeColor",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
|
if (value !== appState.currentItemStrokeColor) {
|
||||||
|
trackEvent(EVENT_CHANGE, "stroke color", value);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
newElementWith(el, {
|
newElementWith(el, {
|
||||||
@ -112,6 +116,10 @@ export const actionChangeStrokeColor = register({
|
|||||||
export const actionChangeBackgroundColor = register({
|
export const actionChangeBackgroundColor = register({
|
||||||
name: "changeBackgroundColor",
|
name: "changeBackgroundColor",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
|
if (value !== appState.currentItemBackgroundColor) {
|
||||||
|
trackEvent(EVENT_CHANGE, "background color", value);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
newElementWith(el, {
|
newElementWith(el, {
|
||||||
@ -143,6 +151,7 @@ export const actionChangeBackgroundColor = register({
|
|||||||
export const actionChangeFillStyle = register({
|
export const actionChangeFillStyle = register({
|
||||||
name: "changeFillStyle",
|
name: "changeFillStyle",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
|
trackEvent(EVENT_CHANGE, "fill", value);
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
newElementWith(el, {
|
newElementWith(el, {
|
||||||
@ -192,6 +201,7 @@ export const actionChangeFillStyle = register({
|
|||||||
export const actionChangeStrokeWidth = register({
|
export const actionChangeStrokeWidth = register({
|
||||||
name: "changeStrokeWidth",
|
name: "changeStrokeWidth",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
|
trackEvent(EVENT_CHANGE, "stroke", "width", value);
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
newElementWith(el, {
|
newElementWith(el, {
|
||||||
@ -254,6 +264,7 @@ export const actionChangeStrokeWidth = register({
|
|||||||
export const actionChangeSloppiness = register({
|
export const actionChangeSloppiness = register({
|
||||||
name: "changeSloppiness",
|
name: "changeSloppiness",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
|
trackEvent(EVENT_CHANGE, "stroke", "sloppiness", value);
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
newElementWith(el, {
|
newElementWith(el, {
|
||||||
@ -349,6 +360,7 @@ export const actionChangeStrokeStyle = register({
|
|||||||
export const actionChangeOpacity = register({
|
export const actionChangeOpacity = register({
|
||||||
name: "changeOpacity",
|
name: "changeOpacity",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
|
trackEvent(EVENT_CHANGE, "opacity", "value", value);
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
newElementWith(el, {
|
newElementWith(el, {
|
||||||
@ -545,6 +557,7 @@ export const actionChangeSharpness = register({
|
|||||||
const shouldUpdateForLinearElements = targetElements.length
|
const shouldUpdateForLinearElements = targetElements.length
|
||||||
? targetElements.every(isLinearElement)
|
? targetElements.every(isLinearElement)
|
||||||
: isLinearElementType(appState.elementType);
|
: isLinearElementType(appState.elementType);
|
||||||
|
trackEvent(EVENT_CHANGE, "edge", value);
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
newElementWith(el, {
|
newElementWith(el, {
|
||||||
|
16
src/analytics.ts
Normal file
16
src/analytics.ts
Normal 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);
|
||||||
|
};
|
@ -16,6 +16,7 @@ import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
|
|||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
|
import { trackEvent, EVENT_SHAPE } from "../analytics";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
appState,
|
appState,
|
||||||
@ -173,6 +174,7 @@ export const ShapesSwitcher = ({
|
|||||||
aria-keyshortcuts={shortcut}
|
aria-keyshortcuts={shortcut}
|
||||||
data-testid={value}
|
data-testid={value}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
|
trackEvent(EVENT_SHAPE, value, "toolbar");
|
||||||
setAppState({
|
setAppState({
|
||||||
elementType: value,
|
elementType: value,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
|
@ -181,6 +181,7 @@ import {
|
|||||||
isSavedToFirebase,
|
isSavedToFirebase,
|
||||||
} from "../data/firebase";
|
} from "../data/firebase";
|
||||||
import { getNewZoom } from "../scene/zoom";
|
import { getNewZoom } from "../scene/zoom";
|
||||||
|
import { EVENT_SHAPE, trackEvent } from "../analytics";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param func handler taking at most single parameter (event).
|
* @param func handler taking at most single parameter (event).
|
||||||
@ -1270,12 +1271,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
toggleLock = () => {
|
toggleLock = () => {
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => {
|
||||||
elementLocked: !prevState.elementLocked,
|
trackEvent(EVENT_SHAPE, "lock", !prevState.elementLocked ? "on" : "off");
|
||||||
elementType: prevState.elementLocked
|
return {
|
||||||
? "selection"
|
elementLocked: !prevState.elementLocked,
|
||||||
: prevState.elementType,
|
elementType: prevState.elementLocked
|
||||||
}));
|
? "selection"
|
||||||
|
: prevState.elementType,
|
||||||
|
};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleZenMode = () => {
|
toggleZenMode = () => {
|
||||||
@ -1655,6 +1659,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
) {
|
) {
|
||||||
const shape = findShapeByKey(event.key);
|
const shape = findShapeByKey(event.key);
|
||||||
if (shape) {
|
if (shape) {
|
||||||
|
trackEvent(EVENT_SHAPE, shape, "shortcut");
|
||||||
this.selectShapeTool(shape);
|
this.selectShapeTool(shape);
|
||||||
} else if (event.key === KEYS.Q) {
|
} else if (event.key === KEYS.Q) {
|
||||||
this.toggleLock();
|
this.toggleLock();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
|
import { EVENT_CHANGE, trackEvent } from "../analytics";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { DarkModeToggle } from "./DarkModeToggle";
|
import { DarkModeToggle } from "./DarkModeToggle";
|
||||||
|
|
||||||
@ -18,6 +19,8 @@ export const BackgroundPickerAndDarkModeToggle = ({
|
|||||||
<DarkModeToggle
|
<DarkModeToggle
|
||||||
value={appState.appearance}
|
value={appState.appearance}
|
||||||
onChange={(appearance) => {
|
onChange={(appearance) => {
|
||||||
|
// TODO: track the theme on the first load too
|
||||||
|
trackEvent(EVENT_CHANGE, "theme", appearance);
|
||||||
setAppState({ appearance });
|
setAppState({ appearance });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
|
import { EVENT_EXIT, trackEvent } from "../analytics";
|
||||||
|
|
||||||
// https://github.com/tholman/github-corners
|
// https://github.com/tholman/github-corners
|
||||||
export const GitHubCorner = React.memo(
|
export const GitHubCorner = React.memo(
|
||||||
@ -16,6 +17,9 @@ export const GitHubCorner = React.memo(
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
aria-label="GitHub repository"
|
aria-label="GitHub repository"
|
||||||
|
onClick={() => {
|
||||||
|
trackEvent(EVENT_EXIT, "github");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0l115 115h15l12 27 108 108V0z"
|
d="M0 0l115 115h15l12 27 108 108V0z"
|
||||||
|
@ -45,6 +45,7 @@ import { muteFSAbortError } from "../utils";
|
|||||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Library } from "../data/library";
|
import { Library } from "../data/library";
|
||||||
|
import { EVENT_EXIT, trackEvent } from "../analytics";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@ -310,6 +311,9 @@ const LayerUI = ({
|
|||||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
onClick={() => {
|
||||||
|
trackEvent(EVENT_EXIT, "e2ee shield");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className="tooltip-text" dir="auto">
|
<span className="tooltip-text" dir="auto">
|
||||||
{t("encrypted.tooltip")}
|
{t("encrypted.tooltip")}
|
||||||
|
@ -4,6 +4,7 @@ import { isDarwin } from "../keys";
|
|||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import "./ShortcutsDialog.scss";
|
import "./ShortcutsDialog.scss";
|
||||||
|
import { EVENT_EXIT, trackEvent } from "../analytics";
|
||||||
|
|
||||||
const Columns = (props: { children: React.ReactNode }) => (
|
const Columns = (props: { children: React.ReactNode }) => (
|
||||||
<div
|
<div
|
||||||
@ -91,6 +92,9 @@ const Footer = () => (
|
|||||||
href="https://blog.excalidraw.com"
|
href="https://blog.excalidraw.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
onClick={() => {
|
||||||
|
trackEvent(EVENT_EXIT, "blog");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("shortcutsDialog.blog")}
|
{t("shortcutsDialog.blog")}
|
||||||
</a>
|
</a>
|
||||||
@ -98,6 +102,9 @@ const Footer = () => (
|
|||||||
href="https://howto.excalidraw.com"
|
href="https://howto.excalidraw.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
onClick={() => {
|
||||||
|
trackEvent(EVENT_EXIT, "guides");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("shortcutsDialog.howto")}
|
{t("shortcutsDialog.howto")}
|
||||||
</a>
|
</a>
|
||||||
@ -105,6 +112,9 @@ const Footer = () => (
|
|||||||
href="https://github.com/excalidraw/excalidraw/issues"
|
href="https://github.com/excalidraw/excalidraw/issues"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
onClick={() => {
|
||||||
|
trackEvent(EVENT_EXIT, "issues");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("shortcutsDialog.github")}
|
{t("shortcutsDialog.github")}
|
||||||
</a>
|
</a>
|
||||||
|
@ -7,6 +7,7 @@ import { calculateScrollCenter } from "../scene";
|
|||||||
import { MIME_TYPES } from "../constants";
|
import { MIME_TYPES } from "../constants";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError } from "../errors";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
|
import { EVENT_ACTION, trackEvent } from "../analytics";
|
||||||
|
|
||||||
export const parseFileContents = async (blob: Blob | File) => {
|
export const parseFileContents = async (blob: Blob | File) => {
|
||||||
let contents: string;
|
let contents: string;
|
||||||
@ -89,7 +90,7 @@ export const loadFromBlob = async (
|
|||||||
if (data.type !== "excalidraw") {
|
if (data.type !== "excalidraw") {
|
||||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||||
}
|
}
|
||||||
return restore(
|
const result = restore(
|
||||||
{
|
{
|
||||||
elements: clearElementsForExport(data.elements || []),
|
elements: clearElementsForExport(data.elements || []),
|
||||||
appState: {
|
appState: {
|
||||||
@ -109,6 +110,9 @@ export const loadFromBlob = async (
|
|||||||
},
|
},
|
||||||
localAppState,
|
localAppState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
trackEvent(EVENT_ACTION, "load", getMimeType(blob));
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error.message);
|
console.error(error.message);
|
||||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||||
|
@ -20,6 +20,7 @@ import { ExportType } from "../scene/types";
|
|||||||
import { restore } from "./restore";
|
import { restore } from "./restore";
|
||||||
import { ImportedDataState } from "./types";
|
import { ImportedDataState } from "./types";
|
||||||
import { canvasToBlob } from "./blob";
|
import { canvasToBlob } from "./blob";
|
||||||
|
import { EVENT_ACTION, trackEvent } from "../analytics";
|
||||||
|
|
||||||
export { loadFromBlob } from "./blob";
|
export { loadFromBlob } from "./blob";
|
||||||
export { saveAsJSON, loadFromJSON } from "./json";
|
export { saveAsJSON, loadFromJSON } from "./json";
|
||||||
@ -263,6 +264,7 @@ const importFromBackend = async (
|
|||||||
data = await response.json();
|
data = await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trackEvent(EVENT_ACTION, "import");
|
||||||
return {
|
return {
|
||||||
elements: data.elements || null,
|
elements: data.elements || null,
|
||||||
appState: data.appState || null,
|
appState: data.appState || null,
|
||||||
|
@ -42,7 +42,6 @@ export const saveAsJSON = async (
|
|||||||
},
|
},
|
||||||
appState.fileHandle,
|
appState.fileHandle,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { fileHandle };
|
return { fileHandle };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@ -12,6 +12,7 @@ interface Document {
|
|||||||
interface Window {
|
interface Window {
|
||||||
ClipboardItem: any;
|
ClipboardItem: any;
|
||||||
__EXCALIDRAW_SHA__: string | undefined;
|
__EXCALIDRAW_SHA__: string | undefined;
|
||||||
|
gtag: Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts
|
// https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
import { EVENT_CHANGE, trackEvent } from "./analytics";
|
||||||
|
|
||||||
import fallbackLanguageData from "./locales/en.json";
|
import fallbackLanguageData from "./locales/en.json";
|
||||||
import percentages from "./locales/percentages.json";
|
import percentages from "./locales/percentages.json";
|
||||||
@ -67,8 +68,8 @@ export const setLanguage = async (newLng: string | undefined) => {
|
|||||||
currentLanguageData = await import(
|
currentLanguageData = await import(
|
||||||
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLanguage.lng}.json`
|
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLanguage.lng}.json`
|
||||||
);
|
);
|
||||||
|
|
||||||
languageDetector.cacheUserLanguage(currentLanguage.lng);
|
languageDetector.cacheUserLanguage(currentLanguage.lng);
|
||||||
|
trackEvent(EVENT_CHANGE, "language", currentLanguage.lng);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setLanguageFirstTime = async () => {
|
export const setLanguageFirstTime = async () => {
|
||||||
@ -84,6 +85,7 @@ export const setLanguageFirstTime = async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
languageDetector.cacheUserLanguage(currentLanguage.lng);
|
languageDetector.cacheUserLanguage(currentLanguage.lng);
|
||||||
|
trackEvent(EVENT_CHANGE, "language on load", currentLanguage.lng);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLanguage = () => currentLanguage;
|
export const getLanguage = () => currentLanguage;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user