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 { AppClassProperties, AppState } from "../types";
|
||||
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 {
|
||||
actions = {} as ActionsManagerInterface["actions"];
|
||||
@ -65,9 +90,12 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
),
|
||||
);
|
||||
|
||||
if (data.length === 0) {
|
||||
if (data.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const action = data[0];
|
||||
|
||||
const { viewModeEnabled } = this.getAppState();
|
||||
if (viewModeEnabled) {
|
||||
if (!Object.values(MODES).includes(data[0].name)) {
|
||||
@ -75,6 +103,8 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
trackAction(action, "keyboard", null);
|
||||
|
||||
event.preventDefault();
|
||||
this.updater(
|
||||
data[0].perform(
|
||||
@ -96,6 +126,7 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
this.app,
|
||||
),
|
||||
);
|
||||
trackAction(action, "api", null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,6 +153,8 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
this.app,
|
||||
),
|
||||
);
|
||||
|
||||
trackAction(action, "ui", formState);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { t } from "../i18n";
|
||||
import { isDarwin } from "../keys";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import { ActionName } from "./types";
|
||||
|
||||
export type ShortcutName =
|
||||
export type ShortcutName = SubtypeOf<
|
||||
ActionName,
|
||||
| "cut"
|
||||
| "copy"
|
||||
| "paste"
|
||||
@ -26,7 +28,8 @@ export type ShortcutName =
|
||||
| "viewMode"
|
||||
| "flipHorizontal"
|
||||
| "flipVertical"
|
||||
| "link";
|
||||
| "hyperlink"
|
||||
>;
|
||||
|
||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||
@ -63,7 +66,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
flipHorizontal: [getShortcutKey("Shift+H")],
|
||||
flipVertical: [getShortcutKey("Shift+V")],
|
||||
viewMode: [getShortcutKey("Alt+R")],
|
||||
link: [getShortcutKey("CtrlOrCmd+K")],
|
||||
hyperlink: [getShortcutKey("CtrlOrCmd+K")],
|
||||
};
|
||||
|
||||
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||
|
@ -105,7 +105,7 @@ export type ActionName =
|
||||
| "increaseFontSize"
|
||||
| "decreaseFontSize"
|
||||
| "unbindText"
|
||||
| "link";
|
||||
| "hyperlink";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
@ -136,6 +136,9 @@ export interface Action {
|
||||
appState: AppState,
|
||||
) => boolean;
|
||||
checked?: (appState: Readonly<AppState>) => boolean;
|
||||
trackEvent?:
|
||||
| boolean
|
||||
| ((action: Action, type: "ui" | "keyboard" | "api", value: any) => void);
|
||||
}
|
||||
|
||||
export interface ActionsManagerInterface {
|
||||
|
@ -3,16 +3,16 @@ export const trackEvent =
|
||||
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
|
||||
typeof window !== "undefined" &&
|
||||
window.gtag
|
||||
? (category: string, name: string, label?: string, value?: number) => {
|
||||
window.gtag("event", name, {
|
||||
? (category: string, action: string, label?: string, value?: number) => {
|
||||
window.gtag("event", action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
value,
|
||||
});
|
||||
}
|
||||
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID
|
||||
? (category: string, name: string, label?: string, value?: number) => {}
|
||||
: (category: string, name: string, label?: string, value?: number) => {
|
||||
? (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, name, label, value);
|
||||
// console.info("Track Event", category, action, label, value);
|
||||
};
|
||||
|
@ -158,7 +158,7 @@ export const SelectedShapeActions = ({
|
||||
{!isMobile && renderAction("deleteSelectedElements")}
|
||||
{renderAction("group")}
|
||||
{renderAction("ungroup")}
|
||||
{targetElements.length === 1 && renderAction("link")}
|
||||
{targetElements.length === 1 && renderAction("hyperlink")}
|
||||
</div>
|
||||
</fieldset>
|
||||
)}
|
||||
|
@ -31,6 +31,7 @@ import { isPointHittingElementBoundingBox } from "./collision";
|
||||
import { getElementAbsoluteCoords } from "./";
|
||||
|
||||
import "./Hyperlink.scss";
|
||||
import { trackEvent } from "../analytics";
|
||||
|
||||
const CONTAINER_WIDTH = 320;
|
||||
const SPACE_BOTTOM = 85;
|
||||
@ -69,6 +70,10 @@ export const Hyperlink = ({
|
||||
|
||||
const link = normalizeLink(inputRef.current.value);
|
||||
|
||||
if (!element.link && link) {
|
||||
trackEvent("hyperlink", "create");
|
||||
}
|
||||
|
||||
mutateElement(element, { link });
|
||||
setAppState({ showHyperlinkPopup: "info" });
|
||||
}, [element, setAppState]);
|
||||
@ -108,6 +113,7 @@ export const Hyperlink = ({
|
||||
}, [appState, element, isEditing, setAppState]);
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
trackEvent("hyperlink", "delete");
|
||||
mutateElement(element, { link: null });
|
||||
if (isEditing) {
|
||||
inputRef.current!.value = "";
|
||||
@ -116,6 +122,7 @@ export const Hyperlink = ({
|
||||
}, [setAppState, element, isEditing]);
|
||||
|
||||
const onEdit = () => {
|
||||
trackEvent("hyperlink", "edit", "popup-ui");
|
||||
setAppState({ showHyperlinkPopup: "editor" });
|
||||
};
|
||||
const { x, y } = getCoordsForPopover(element, appState);
|
||||
@ -239,11 +246,12 @@ export const isLocalLink = (link: string | null) => {
|
||||
};
|
||||
|
||||
export const actionLink = register({
|
||||
name: "link",
|
||||
name: "hyperlink",
|
||||
perform: (elements, appState) => {
|
||||
if (appState.showHyperlinkPopup === "editor") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
elements,
|
||||
appState: {
|
||||
@ -254,6 +262,9 @@ export const actionLink = register({
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
trackEvent: (action, source) => {
|
||||
trackEvent("hyperlink", "edit", source);
|
||||
},
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
|
||||
contextItemLabel: (elements, appState) =>
|
||||
getContextMenuLabel(elements, appState),
|
||||
@ -400,6 +411,7 @@ const renderTooltip = (
|
||||
},
|
||||
"top",
|
||||
);
|
||||
trackEvent("hyperlink", "tooltip", "link-icon");
|
||||
|
||||
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];
|
||||
};
|
||||
|
||||
/** 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 (
|
||||
...args: any
|
||||
) => Promise<infer R>
|
||||
|
@ -136,7 +136,7 @@ describe("contextMenu element", () => {
|
||||
"sendToBack",
|
||||
"bringToFront",
|
||||
"duplicateSelection",
|
||||
"link",
|
||||
"hyperlink",
|
||||
];
|
||||
|
||||
expect(contextMenu).not.toBeNull();
|
||||
|
Loading…
x
Reference in New Issue
Block a user