diff --git a/app/actions/definitions/collections.tsx b/app/actions/definitions/collections.tsx index 8efabd190..accc602c4 100644 --- a/app/actions/definitions/collections.tsx +++ b/app/actions/definitions/collections.tsx @@ -23,6 +23,7 @@ const ColorCollectionIcon = ({ collection }: { collection: Collection }) => { export const openCollection = createAction({ name: ({ t }) => t("Open collection"), + analyticsName: "Open collection", section: CollectionSection, shortcut: ["o", "c"], icon: , @@ -42,6 +43,7 @@ export const openCollection = createAction({ export const createCollection = createAction({ name: ({ t }) => t("New collection"), + analyticsName: "New collection", section: CollectionSection, icon: , keywords: "create", @@ -60,6 +62,7 @@ export const createCollection = createAction({ export const editCollection = createAction({ name: ({ t, isContextMenu }) => isContextMenu ? `${t("Edit")}…` : t("Edit collection"), + analyticsName: "Edit collection", section: CollectionSection, icon: , visible: ({ stores, activeCollectionId }) => @@ -85,6 +88,7 @@ export const editCollection = createAction({ export const editCollectionPermissions = createAction({ name: ({ t, isContextMenu }) => isContextMenu ? `${t("Permissions")}…` : t("Collection permissions"), + analyticsName: "Collection permissions", section: CollectionSection, icon: , visible: ({ stores, activeCollectionId }) => @@ -104,6 +108,7 @@ export const editCollectionPermissions = createAction({ export const starCollection = createAction({ name: ({ t }) => t("Star"), + analyticsName: "Star collection", section: CollectionSection, icon: , keywords: "favorite bookmark", @@ -129,6 +134,7 @@ export const starCollection = createAction({ export const unstarCollection = createAction({ name: ({ t }) => t("Unstar"), + analyticsName: "Unstar collection", section: CollectionSection, icon: , keywords: "unfavorite unbookmark", diff --git a/app/actions/definitions/documents.tsx b/app/actions/definitions/documents.tsx index ba9b0a3dd..78dd82bbf 100644 --- a/app/actions/definitions/documents.tsx +++ b/app/actions/definitions/documents.tsx @@ -45,6 +45,7 @@ import { export const openDocument = createAction({ name: ({ t }) => t("Open document"), + analyticsName: "Open document", section: DocumentSection, shortcut: ["o", "d"], keywords: "go to", @@ -69,6 +70,7 @@ export const openDocument = createAction({ export const createDocument = createAction({ name: ({ t }) => t("New document"), + analyticsName: "New document", section: DocumentSection, icon: , keywords: "create", @@ -82,6 +84,7 @@ export const createDocument = createAction({ export const starDocument = createAction({ name: ({ t }) => t("Star"), + analyticsName: "Star document", section: DocumentSection, icon: , keywords: "favorite bookmark", @@ -106,6 +109,7 @@ export const starDocument = createAction({ export const unstarDocument = createAction({ name: ({ t }) => t("Unstar"), + analyticsName: "Unstar document", section: DocumentSection, icon: , keywords: "unfavorite unbookmark", @@ -131,6 +135,7 @@ export const unstarDocument = createAction({ export const publishDocument = createAction({ name: ({ t }) => t("Publish"), + analyticsName: "Publish document", section: DocumentSection, icon: , visible: ({ activeDocumentId, stores }) => { @@ -171,6 +176,7 @@ export const publishDocument = createAction({ export const unpublishDocument = createAction({ name: ({ t }) => t("Unpublish"), + analyticsName: "Unpublish document", section: DocumentSection, icon: , visible: ({ activeDocumentId, stores }) => { @@ -196,6 +202,7 @@ export const unpublishDocument = createAction({ export const subscribeDocument = createAction({ name: ({ t }) => t("Subscribe"), + analyticsName: "Subscribe to document", section: DocumentSection, icon: , visible: ({ activeDocumentId, stores }) => { @@ -227,6 +234,7 @@ export const subscribeDocument = createAction({ export const unsubscribeDocument = createAction({ name: ({ t }) => t("Unsubscribe"), + analyticsName: "Unsubscribe from document", section: DocumentSection, icon: , visible: ({ activeDocumentId, stores }) => { @@ -258,6 +266,7 @@ export const unsubscribeDocument = createAction({ export const downloadDocumentAsHTML = createAction({ name: ({ t }) => t("HTML"), + analyticsName: "Download document as HTML", section: DocumentSection, keywords: "html export", icon: , @@ -276,6 +285,7 @@ export const downloadDocumentAsHTML = createAction({ export const downloadDocumentAsPDF = createAction({ name: ({ t }) => t("PDF"), + analyticsName: "Download document as PDF", section: DocumentSection, keywords: "export", icon: , @@ -303,6 +313,7 @@ export const downloadDocumentAsPDF = createAction({ export const downloadDocumentAsMarkdown = createAction({ name: ({ t }) => t("Markdown"), + analyticsName: "Download document as Markdown", section: DocumentSection, keywords: "md markdown export", icon: , @@ -322,6 +333,7 @@ export const downloadDocumentAsMarkdown = createAction({ export const downloadDocument = createAction({ name: ({ t, isContextMenu }) => isContextMenu ? t("Download") : t("Download document"), + analyticsName: "Download document", section: DocumentSection, icon: , keywords: "export", @@ -335,6 +347,7 @@ export const downloadDocument = createAction({ export const duplicateDocument = createAction({ name: ({ t, isContextMenu }) => isContextMenu ? t("Duplicate") : t("Duplicate document"), + analyticsName: "Duplicate document", section: DocumentSection, icon: , keywords: "copy", @@ -371,7 +384,7 @@ export const pinDocumentToCollection = createAction({ collectionName, }); }, - + analyticsName: "Pin document to collection", section: DocumentSection, icon: , iconInContextMenu: false, @@ -407,6 +420,7 @@ export const pinDocumentToCollection = createAction({ */ export const pinDocumentToHome = createAction({ name: ({ t }) => t("Pin to home"), + analyticsName: "Pin document to home", section: DocumentSection, icon: , iconInContextMenu: false, @@ -438,6 +452,7 @@ export const pinDocumentToHome = createAction({ export const pinDocument = createAction({ name: ({ t }) => t("Pin"), + analyticsName: "Pin document", section: DocumentSection, icon: , children: [pinDocumentToCollection, pinDocumentToHome], @@ -446,6 +461,7 @@ export const pinDocument = createAction({ export const printDocument = createAction({ name: ({ t, isContextMenu }) => isContextMenu ? t("Print") : t("Print document"), + analyticsName: "Print document", section: DocumentSection, icon: , visible: ({ activeDocumentId }) => !!(activeDocumentId && window.print), @@ -456,6 +472,7 @@ export const printDocument = createAction({ export const importDocument = createAction({ name: ({ t }) => t("Import document"), + analyticsName: "Import document", section: DocumentSection, icon: , keywords: "upload", @@ -504,6 +521,7 @@ export const importDocument = createAction({ export const createTemplate = createAction({ name: ({ t }) => t("Templatize"), + analyticsName: "Templatize document", section: DocumentSection, icon: , keywords: "new create template", @@ -536,8 +554,9 @@ export const createTemplate = createAction({ export const openRandomDocument = createAction({ id: "random", - section: DocumentSection, name: ({ t }) => t(`Open random document`), + analyticsName: "Open random document", + section: DocumentSection, icon: , perform: ({ stores, activeDocumentId }) => { const documentPaths = stores.collections.pathsToDocuments.filter( @@ -555,9 +574,10 @@ export const openRandomDocument = createAction({ export const searchDocumentsForQuery = (searchQuery: string) => createAction({ id: "search", - section: DocumentSection, name: ({ t }) => t(`Search documents for "{{searchQuery}}"`, { searchQuery }), + analyticsName: "Search documents", + section: DocumentSection, icon: , perform: () => history.push(searchPath(searchQuery)), visible: ({ location }) => location.pathname !== searchPath(), @@ -565,6 +585,7 @@ export const searchDocumentsForQuery = (searchQuery: string) => export const moveDocument = createAction({ name: ({ t }) => t("Move"), + analyticsName: "Move document", section: DocumentSection, icon: , visible: ({ activeDocumentId, stores }) => { @@ -593,6 +614,7 @@ export const moveDocument = createAction({ export const archiveDocument = createAction({ name: ({ t }) => t("Archive"), + analyticsName: "Archive document", section: DocumentSection, icon: , visible: ({ activeDocumentId, stores }) => { @@ -618,6 +640,7 @@ export const archiveDocument = createAction({ export const deleteDocument = createAction({ name: ({ t }) => t("Delete"), + analyticsName: "Delete document", section: DocumentSection, icon: , dangerous: true, @@ -652,6 +675,7 @@ export const deleteDocument = createAction({ export const permanentlyDeleteDocument = createAction({ name: ({ t }) => t("Permanently delete"), + analyticsName: "Permanently delete document", section: DocumentSection, icon: , dangerous: true, @@ -686,6 +710,7 @@ export const permanentlyDeleteDocument = createAction({ export const openDocumentHistory = createAction({ name: ({ t }) => t("History"), + analyticsName: "Open document history", section: DocumentSection, icon: , visible: ({ activeDocumentId, stores }) => { @@ -706,6 +731,7 @@ export const openDocumentHistory = createAction({ export const openDocumentInsights = createAction({ name: ({ t }) => t("Insights"), + analyticsName: "Open document insights", section: DocumentSection, icon: , visible: ({ activeDocumentId, stores }) => { diff --git a/app/actions/definitions/navigation.tsx b/app/actions/definitions/navigation.tsx index a14ad3240..4c7d85bdf 100644 --- a/app/actions/definitions/navigation.tsx +++ b/app/actions/definitions/navigation.tsx @@ -43,6 +43,7 @@ import { export const navigateToHome = createAction({ name: ({ t }) => t("Home"), + analyticsName: "Navigate to home", section: NavigationSection, shortcut: ["d"], icon: , @@ -54,12 +55,14 @@ export const navigateToRecentSearchQuery = (searchQuery: SearchQuery) => createAction({ section: RecentSearchesSection, name: searchQuery.query, + analyticsName: "Navigate to recent search query", icon: , perform: () => history.push(searchPath(searchQuery.query)), }); export const navigateToDrafts = createAction({ name: ({ t }) => t("Drafts"), + analyticsName: "Navigate to drafts", section: NavigationSection, icon: , perform: () => history.push(draftsPath()), @@ -68,6 +71,7 @@ export const navigateToDrafts = createAction({ export const navigateToTemplates = createAction({ name: ({ t }) => t("Templates"), + analyticsName: "Navigate to templates", section: NavigationSection, icon: , perform: () => history.push(templatesPath()), @@ -76,6 +80,7 @@ export const navigateToTemplates = createAction({ export const navigateToArchive = createAction({ name: ({ t }) => t("Archive"), + analyticsName: "Navigate to archive", section: NavigationSection, shortcut: ["g", "a"], icon: , @@ -85,6 +90,7 @@ export const navigateToArchive = createAction({ export const navigateToTrash = createAction({ name: ({ t }) => t("Trash"), + analyticsName: "Navigate to trash", section: NavigationSection, icon: , perform: () => history.push(trashPath()), @@ -93,6 +99,7 @@ export const navigateToTrash = createAction({ export const navigateToSettings = createAction({ name: ({ t }) => t("Settings"), + analyticsName: "Navigate to settings", section: NavigationSection, shortcut: ["g", "s"], icon: , @@ -103,6 +110,7 @@ export const navigateToSettings = createAction({ export const navigateToProfileSettings = createAction({ name: ({ t }) => t("Profile"), + analyticsName: "Navigate to profile settings", section: NavigationSection, iconInContextMenu: false, icon: , @@ -111,6 +119,7 @@ export const navigateToProfileSettings = createAction({ export const navigateToAccountPreferences = createAction({ name: ({ t }) => t("Preferences"), + analyticsName: "Navigate to account preferences", section: NavigationSection, iconInContextMenu: false, icon: , @@ -119,6 +128,7 @@ export const navigateToAccountPreferences = createAction({ export const openAPIDocumentation = createAction({ name: ({ t }) => t("API documentation"), + analyticsName: "Open API documentation", section: NavigationSection, iconInContextMenu: false, icon: , @@ -127,6 +137,7 @@ export const openAPIDocumentation = createAction({ export const openFeedbackUrl = createAction({ name: ({ t }) => t("Send us feedback"), + analyticsName: "Open feedback", section: NavigationSection, iconInContextMenu: false, icon: , @@ -135,12 +146,14 @@ export const openFeedbackUrl = createAction({ export const openBugReportUrl = createAction({ name: ({ t }) => t("Report a bug"), + analyticsName: "Open bug report", section: NavigationSection, perform: () => window.open(githubIssuesUrl()), }); export const openChangelog = createAction({ name: ({ t }) => t("Changelog"), + analyticsName: "Open changelog", section: NavigationSection, iconInContextMenu: false, icon: , @@ -149,6 +162,7 @@ export const openChangelog = createAction({ export const openKeyboardShortcuts = createAction({ name: ({ t }) => t("Keyboard shortcuts"), + analyticsName: "Open keyboard shortcuts", section: NavigationSection, shortcut: ["?"], iconInContextMenu: false, @@ -166,6 +180,7 @@ export const downloadApp = createAction({ t("Download {{ platform }} app", { platform: isMac() ? "macOS" : "Windows", }), + analyticsName: "Download app", section: NavigationSection, iconInContextMenu: false, icon: , @@ -177,6 +192,7 @@ export const downloadApp = createAction({ export const logout = createAction({ name: ({ t }) => t("Log out"), + analyticsName: "Log out", section: NavigationSection, icon: , perform: () => stores.auth.logout(), diff --git a/app/actions/definitions/revisions.tsx b/app/actions/definitions/revisions.tsx index ed41ffea7..93f3be098 100644 --- a/app/actions/definitions/revisions.tsx +++ b/app/actions/definitions/revisions.tsx @@ -10,6 +10,7 @@ import { documentHistoryUrl, matchDocumentHistory } from "~/utils/routeHelpers"; export const restoreRevision = createAction({ name: ({ t }) => t("Restore revision"), + analyticsName: "Restore revision", icon: , section: RevisionSection, visible: ({ activeDocumentId, stores }) => @@ -50,6 +51,7 @@ export const restoreRevision = createAction({ export const copyLinkToRevision = createAction({ name: ({ t }) => t("Copy link"), + analyticsName: "Copy link to revision", icon: , section: RevisionSection, perform: async ({ activeDocumentId, stores, t }) => { diff --git a/app/actions/definitions/settings.tsx b/app/actions/definitions/settings.tsx index 9372b1b0e..5405c606f 100644 --- a/app/actions/definitions/settings.tsx +++ b/app/actions/definitions/settings.tsx @@ -7,6 +7,7 @@ import { SettingsSection } from "~/actions/sections"; export const changeToDarkTheme = createAction({ name: ({ t }) => t("Dark"), + analyticsName: "Change to dark theme", icon: , iconInContextMenu: false, keywords: "theme dark night", @@ -17,6 +18,7 @@ export const changeToDarkTheme = createAction({ export const changeToLightTheme = createAction({ name: ({ t }) => t("Light"), + analyticsName: "Change to light theme", icon: , iconInContextMenu: false, keywords: "theme light day", @@ -27,6 +29,7 @@ export const changeToLightTheme = createAction({ export const changeToSystemTheme = createAction({ name: ({ t }) => t("System"), + analyticsName: "Change to system theme", icon: , iconInContextMenu: false, keywords: "theme system default", @@ -38,6 +41,7 @@ export const changeToSystemTheme = createAction({ export const changeTheme = createAction({ name: ({ t, isContextMenu }) => isContextMenu ? t("Appearance") : t("Change theme"), + analyticsName: "Change theme", placeholder: ({ t }) => t("Change theme to"), icon: () => stores.ui.resolvedTheme === "light" ? : , diff --git a/app/actions/definitions/teams.tsx b/app/actions/definitions/teams.tsx index b9334e421..f5bb08a91 100644 --- a/app/actions/definitions/teams.tsx +++ b/app/actions/definitions/teams.tsx @@ -14,6 +14,7 @@ export const createTeamsList = ({ stores }: { stores: RootStore }) => { stores.auth.availableTeams?.map((session) => ({ id: `switch-${session.id}`, name: session.name, + analyticsName: "Switch workspace", section: TeamSection, keywords: "change switch workspace organization team", icon: () => ( @@ -38,6 +39,7 @@ export const createTeamsList = ({ stores }: { stores: RootStore }) => { export const switchTeam = createAction({ name: ({ t }) => t("Switch workspace"), placeholder: ({ t }) => t("Select a workspace"), + analyticsName: "Switch workspace", keywords: "change switch workspace organization team", section: TeamSection, visible: ({ stores }) => @@ -47,6 +49,7 @@ export const switchTeam = createAction({ export const createTeam = createAction({ name: ({ t }) => `${t("New workspace")}…`, + analyticsName: "New workspace", keywords: "create change switch workspace organization team", section: TeamSection, icon: , diff --git a/app/actions/definitions/users.tsx b/app/actions/definitions/users.tsx index dbfd08b77..07ddcc276 100644 --- a/app/actions/definitions/users.tsx +++ b/app/actions/definitions/users.tsx @@ -7,6 +7,7 @@ import { UserSection } from "~/actions/sections"; export const inviteUser = createAction({ name: ({ t }) => `${t("Invite people")}…`, + analyticsName: "Invite people", icon: , keywords: "team member workspace user", section: UserSection, diff --git a/app/actions/index.ts b/app/actions/index.ts index 738022f71..a88a20a08 100644 --- a/app/actions/index.ts +++ b/app/actions/index.ts @@ -9,6 +9,7 @@ import { MenuItemButton, MenuItemWithChildren, } from "~/types"; +import Analytics from "~/utils/Analytics"; function resolve(value: any, context: ActionContext): T { return typeof value === "function" ? value(context) : value; @@ -17,6 +18,21 @@ function resolve(value: any, context: ActionContext): T { export function createAction(definition: Optional): Action { return { ...definition, + perform: (context) => { + // We muse use the specific analytics name here as the action name is + // translated and potentially contains user strings. + if (definition.analyticsName) { + Analytics.track("perform_action", definition.analyticsName, { + context: context.isButton + ? "button" + : context.isCommandBar + ? "commandbar" + : "contextmenu", + }); + } + + return definition.perform?.(context); + }, id: uuidv4(), }; } @@ -93,12 +109,13 @@ export function actionToKBar( { id: action.id, name: resolvedName, + analyticsName: action.analyticsName, section: resolvedSection, placeholder: resolvedPlaceholder, keywords: action.keywords ?? "", shortcut: action.shortcut || [], icon: resolvedIcon, - perform: action.perform ? () => action?.perform?.(context) : undefined, + perform: () => action.perform?.(context), }, // @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. ].concat(children.map((child) => ({ ...child, parent: action.id }))); diff --git a/app/types.ts b/app/types.ts index 9f12c805b..01261a47b 100644 --- a/app/types.ts +++ b/app/types.ts @@ -89,6 +89,7 @@ export type ActionContext = { export type Action = { type?: undefined; id: string; + analyticsName?: string; name: ((context: ActionContext) => string) | string; section: ((context: ActionContext) => string) | string; shortcut?: string[]; diff --git a/app/utils/Analytics.ts b/app/utils/Analytics.ts new file mode 100644 index 000000000..8b07b5032 --- /dev/null +++ b/app/utils/Analytics.ts @@ -0,0 +1,26 @@ +/** + * Helper class to track events across various analytics integrations + */ +export default class Analytics { + /** + * Send an event to Analytics + * + * @param event The event name + * @param action The action name + */ + public static track = ( + event: string, + action: string, + metadata?: Record + ) => { + // GA3 + ga?.("send", "event", event, action); + + // GA4 + window.dataLayer?.push({ + event, + action, + ...metadata, + }); + }; +}