chore: Add tracking for hyperlinks (#4703)
* chore: Add tracking for hyperlinks * update * fix * remove * tweak * disable ga logging in dev again * add logging for hyperlink `edit` & support for tracking in manager * event label tweaks * fix tests & make more typesafe Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
e203203993
commit
21e9fcb2f5
@ -10,6 +10,31 @@ import {
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { MODES } from "../constants";
|
import { MODES } from "../constants";
|
||||||
|
import { trackEvent } from "../analytics";
|
||||||
|
|
||||||
|
const trackAction = (
|
||||||
|
action: Action,
|
||||||
|
source: "ui" | "keyboard" | "api",
|
||||||
|
value: any,
|
||||||
|
) => {
|
||||||
|
if (action.trackEvent !== false) {
|
||||||
|
try {
|
||||||
|
if (action.trackEvent === true) {
|
||||||
|
trackEvent(
|
||||||
|
action.name,
|
||||||
|
source,
|
||||||
|
typeof value === "number" || typeof value === "string"
|
||||||
|
? String(value)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
action.trackEvent?.(action, source, value);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("error while logging action:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export class ActionManager implements ActionsManagerInterface {
|
export class ActionManager implements ActionsManagerInterface {
|
||||||
actions = {} as ActionsManagerInterface["actions"];
|
actions = {} as ActionsManagerInterface["actions"];
|
||||||
@ -65,9 +90,12 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length !== 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const action = data[0];
|
||||||
|
|
||||||
const { viewModeEnabled } = this.getAppState();
|
const { viewModeEnabled } = this.getAppState();
|
||||||
if (viewModeEnabled) {
|
if (viewModeEnabled) {
|
||||||
if (!Object.values(MODES).includes(data[0].name)) {
|
if (!Object.values(MODES).includes(data[0].name)) {
|
||||||
@ -75,6 +103,8 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trackAction(action, "keyboard", null);
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.updater(
|
this.updater(
|
||||||
data[0].perform(
|
data[0].perform(
|
||||||
@ -96,6 +126,7 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
this.app,
|
this.app,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
trackAction(action, "api", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,6 +153,8 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
this.app,
|
this.app,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
trackAction(action, "ui", formState);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isDarwin } from "../keys";
|
import { isDarwin } from "../keys";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
|
import { ActionName } from "./types";
|
||||||
|
|
||||||
export type ShortcutName =
|
export type ShortcutName = SubtypeOf<
|
||||||
|
ActionName,
|
||||||
| "cut"
|
| "cut"
|
||||||
| "copy"
|
| "copy"
|
||||||
| "paste"
|
| "paste"
|
||||||
@ -26,7 +28,8 @@ export type ShortcutName =
|
|||||||
| "viewMode"
|
| "viewMode"
|
||||||
| "flipHorizontal"
|
| "flipHorizontal"
|
||||||
| "flipVertical"
|
| "flipVertical"
|
||||||
| "link";
|
| "hyperlink"
|
||||||
|
>;
|
||||||
|
|
||||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||||
@ -63,7 +66,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
flipHorizontal: [getShortcutKey("Shift+H")],
|
flipHorizontal: [getShortcutKey("Shift+H")],
|
||||||
flipVertical: [getShortcutKey("Shift+V")],
|
flipVertical: [getShortcutKey("Shift+V")],
|
||||||
viewMode: [getShortcutKey("Alt+R")],
|
viewMode: [getShortcutKey("Alt+R")],
|
||||||
link: [getShortcutKey("CtrlOrCmd+K")],
|
hyperlink: [getShortcutKey("CtrlOrCmd+K")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||||
|
@ -105,7 +105,7 @@ export type ActionName =
|
|||||||
| "increaseFontSize"
|
| "increaseFontSize"
|
||||||
| "decreaseFontSize"
|
| "decreaseFontSize"
|
||||||
| "unbindText"
|
| "unbindText"
|
||||||
| "link";
|
| "hyperlink";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
@ -136,6 +136,9 @@ export interface Action {
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
checked?: (appState: Readonly<AppState>) => boolean;
|
checked?: (appState: Readonly<AppState>) => boolean;
|
||||||
|
trackEvent?:
|
||||||
|
| boolean
|
||||||
|
| ((action: Action, type: "ui" | "keyboard" | "api", value: any) => void);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionsManagerInterface {
|
export interface ActionsManagerInterface {
|
||||||
|
@ -3,16 +3,16 @@ export const trackEvent =
|
|||||||
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
|
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
|
||||||
typeof window !== "undefined" &&
|
typeof window !== "undefined" &&
|
||||||
window.gtag
|
window.gtag
|
||||||
? (category: string, name: string, label?: string, value?: number) => {
|
? (category: string, action: string, label?: string, value?: number) => {
|
||||||
window.gtag("event", name, {
|
window.gtag("event", action, {
|
||||||
event_category: category,
|
event_category: category,
|
||||||
event_label: label,
|
event_label: label,
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID
|
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID
|
||||||
? (category: string, name: string, label?: string, value?: number) => {}
|
? (category: string, action: string, label?: string, value?: number) => {}
|
||||||
: (category: string, name: string, label?: string, value?: number) => {
|
: (category: string, action: string, label?: string, value?: number) => {
|
||||||
// Uncomment the next line to track locally
|
// Uncomment the next line to track locally
|
||||||
// console.info("Track Event", category, name, label, value);
|
// console.info("Track Event", category, action, label, value);
|
||||||
};
|
};
|
||||||
|
@ -158,7 +158,7 @@ export const SelectedShapeActions = ({
|
|||||||
{!isMobile && renderAction("deleteSelectedElements")}
|
{!isMobile && renderAction("deleteSelectedElements")}
|
||||||
{renderAction("group")}
|
{renderAction("group")}
|
||||||
{renderAction("ungroup")}
|
{renderAction("ungroup")}
|
||||||
{targetElements.length === 1 && renderAction("link")}
|
{targetElements.length === 1 && renderAction("hyperlink")}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
)}
|
)}
|
||||||
|
@ -31,6 +31,7 @@ import { isPointHittingElementBoundingBox } from "./collision";
|
|||||||
import { getElementAbsoluteCoords } from "./";
|
import { getElementAbsoluteCoords } from "./";
|
||||||
|
|
||||||
import "./Hyperlink.scss";
|
import "./Hyperlink.scss";
|
||||||
|
import { trackEvent } from "../analytics";
|
||||||
|
|
||||||
const CONTAINER_WIDTH = 320;
|
const CONTAINER_WIDTH = 320;
|
||||||
const SPACE_BOTTOM = 85;
|
const SPACE_BOTTOM = 85;
|
||||||
@ -69,6 +70,10 @@ export const Hyperlink = ({
|
|||||||
|
|
||||||
const link = normalizeLink(inputRef.current.value);
|
const link = normalizeLink(inputRef.current.value);
|
||||||
|
|
||||||
|
if (!element.link && link) {
|
||||||
|
trackEvent("hyperlink", "create");
|
||||||
|
}
|
||||||
|
|
||||||
mutateElement(element, { link });
|
mutateElement(element, { link });
|
||||||
setAppState({ showHyperlinkPopup: "info" });
|
setAppState({ showHyperlinkPopup: "info" });
|
||||||
}, [element, setAppState]);
|
}, [element, setAppState]);
|
||||||
@ -108,6 +113,7 @@ export const Hyperlink = ({
|
|||||||
}, [appState, element, isEditing, setAppState]);
|
}, [appState, element, isEditing, setAppState]);
|
||||||
|
|
||||||
const handleRemove = useCallback(() => {
|
const handleRemove = useCallback(() => {
|
||||||
|
trackEvent("hyperlink", "delete");
|
||||||
mutateElement(element, { link: null });
|
mutateElement(element, { link: null });
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
inputRef.current!.value = "";
|
inputRef.current!.value = "";
|
||||||
@ -116,6 +122,7 @@ export const Hyperlink = ({
|
|||||||
}, [setAppState, element, isEditing]);
|
}, [setAppState, element, isEditing]);
|
||||||
|
|
||||||
const onEdit = () => {
|
const onEdit = () => {
|
||||||
|
trackEvent("hyperlink", "edit", "popup-ui");
|
||||||
setAppState({ showHyperlinkPopup: "editor" });
|
setAppState({ showHyperlinkPopup: "editor" });
|
||||||
};
|
};
|
||||||
const { x, y } = getCoordsForPopover(element, appState);
|
const { x, y } = getCoordsForPopover(element, appState);
|
||||||
@ -239,11 +246,12 @@ export const isLocalLink = (link: string | null) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actionLink = register({
|
export const actionLink = register({
|
||||||
name: "link",
|
name: "hyperlink",
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
if (appState.showHyperlinkPopup === "editor") {
|
if (appState.showHyperlinkPopup === "editor") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements,
|
elements,
|
||||||
appState: {
|
appState: {
|
||||||
@ -254,6 +262,9 @@ export const actionLink = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
trackEvent: (action, source) => {
|
||||||
|
trackEvent("hyperlink", "edit", source);
|
||||||
|
},
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
|
||||||
contextItemLabel: (elements, appState) =>
|
contextItemLabel: (elements, appState) =>
|
||||||
getContextMenuLabel(elements, appState),
|
getContextMenuLabel(elements, appState),
|
||||||
@ -400,6 +411,7 @@ const renderTooltip = (
|
|||||||
},
|
},
|
||||||
"top",
|
"top",
|
||||||
);
|
);
|
||||||
|
trackEvent("hyperlink", "tooltip", "link-icon");
|
||||||
|
|
||||||
IS_HYPERLINK_TOOLTIP_VISIBLE = true;
|
IS_HYPERLINK_TOOLTIP_VISIBLE = true;
|
||||||
};
|
};
|
||||||
|
4
src/global.d.ts
vendored
4
src/global.d.ts
vendored
@ -34,6 +34,10 @@ type Mutable<T> = {
|
|||||||
-readonly [P in keyof T]: T[P];
|
-readonly [P in keyof T]: T[P];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** utility type to assert that the second type is a subtype of the first type.
|
||||||
|
* Returns the subtype. */
|
||||||
|
type SubtypeOf<Supertype, Subtype extends Supertype> = Subtype;
|
||||||
|
|
||||||
type ResolutionType<T extends (...args: any) => any> = T extends (
|
type ResolutionType<T extends (...args: any) => any> = T extends (
|
||||||
...args: any
|
...args: any
|
||||||
) => Promise<infer R>
|
) => Promise<infer R>
|
||||||
|
@ -136,7 +136,7 @@ describe("contextMenu element", () => {
|
|||||||
"sendToBack",
|
"sendToBack",
|
||||||
"bringToFront",
|
"bringToFront",
|
||||||
"duplicateSelection",
|
"duplicateSelection",
|
||||||
"link",
|
"hyperlink",
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(contextMenu).not.toBeNull();
|
expect(contextMenu).not.toBeNull();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user