From 3e23878e0ff96ef79b6c6cf251cfb39a583928ce Mon Sep 17 00:00:00 2001 From: pengx17 Date: Mon, 13 May 2024 03:36:32 +0000 Subject: [PATCH] feat: add more tracking events (#6866) Added most tracking events what is missing: - still need a way to track events in blocksuite - some events may not 100% accurate of the one defined in the PRD --- .../common/infra/src/framework/core/event.ts | 4 ++ .../core/src/commands/affine-navigation.tsx | 24 +++++++ .../core/src/commands/affine-settings.tsx | 4 ++ .../affine/ai-onboarding/edgeless.dialog.tsx | 6 ++ .../affine/ai-onboarding/general.dialog.tsx | 6 ++ .../affine/auth/user-plan-button.tsx | 5 ++ .../page-history-modal/history-modal.tsx | 5 +- .../quota-reached-modal/cloud-quota-modal.tsx | 6 ++ .../account-setting/ai-usage-panel.tsx | 7 +++ .../setting-modal/account-setting/index.tsx | 7 ++- .../general-setting/billing/index.tsx | 13 ++-- .../plans/ai/actions/subscribe.tsx | 6 +- .../setting-modal/setting-sidebar/index.tsx | 33 +++++++--- .../new-workspace-setting-detail/members.tsx | 7 +++ .../share-menu/share-page.tsx | 7 +++ .../share-menu/use-share-url.ts | 7 ++- .../block-suite-editor/ai/provider.tsx | 6 ++ .../block-suite-header/menu/index.tsx | 30 ++++++++- .../block-suite-page-list/utils.tsx | 63 ++++++++++++------- .../page-list/components/new-page-button.tsx | 22 +++++-- .../page-list/docs/page-list-header.tsx | 25 +++++++- .../components/page-list/operation-cell.tsx | 8 +++ .../src/components/pure/cmdk/data-hooks.tsx | 12 ++++ .../favorite/add-favourite-button.tsx | 17 +++++ .../root-app-sidebar/import-page.tsx | 27 +++++++- .../src/components/root-app-sidebar/index.tsx | 19 ++++-- .../components/root-app-sidebar/user-info.tsx | 7 +++ .../core/src/components/workspace/index.tsx | 2 + .../core/src/hooks/affine/use-export-page.ts | 6 ++ ...se-register-blocksuite-editor-commands.tsx | 6 ++ .../core/src/layouts/workspace-layout.tsx | 22 ++++++- .../modules/telemetry/services/telemetry.ts | 7 +++ .../modules/workbench/services/workbench.ts | 24 ++++++- .../frontend/core/src/pages/subscribe.tsx | 11 ++++ .../workspace/all-page/all-page-header.tsx | 26 +++++++- packages/frontend/core/src/router.tsx | 14 ----- packages/frontend/i18n/src/resources/en.json | 1 + 37 files changed, 433 insertions(+), 69 deletions(-) diff --git a/packages/common/infra/src/framework/core/event.ts b/packages/common/infra/src/framework/core/event.ts index 19c0731682..cf1b824272 100644 --- a/packages/common/infra/src/framework/core/event.ts +++ b/packages/common/infra/src/framework/core/event.ts @@ -37,6 +37,10 @@ export class EventBus { } } + get root(): EventBus { + return this.parent?.root ?? this; + } + on(id: string, listener: (event: FrameworkEvent) => void) { if (!this.listeners[id]) { this.listeners[id] = []; diff --git a/packages/frontend/core/src/commands/affine-navigation.tsx b/packages/frontend/core/src/commands/affine-navigation.tsx index 677697821b..5e6454175e 100644 --- a/packages/frontend/core/src/commands/affine-navigation.tsx +++ b/packages/frontend/core/src/commands/affine-navigation.tsx @@ -7,6 +7,7 @@ import type { createStore } from 'jotai'; import { openSettingModalAtom, openWorkspaceListModalAtom } from '../atoms'; import type { useNavigateHelper } from '../hooks/use-navigate-helper'; +import { mixpanel } from '../utils/mixpanel'; export function registerAffineNavigationCommands({ t, @@ -76,6 +77,10 @@ export function registerAffineNavigationCommands({ label: t['com.affine.cmdk.affine.navigation.open-settings'](), keyBinding: '$mod+,', run() { + mixpanel.track('SettingsViewed', { + // page: + segment: 'cmdk', + }); store.set(openSettingModalAtom, s => ({ activeTab: 'appearance', open: !s.open, @@ -84,6 +89,25 @@ export function registerAffineNavigationCommands({ }) ); + unsubs.push( + registerAffineCommand({ + id: 'affine:open-account', + category: 'affine:navigation', + icon: , + label: t['com.affine.cmdk.affine.navigation.open-account-settings'](), + run() { + mixpanel.track('AccountSettingsViewed', { + // page: + segment: 'cmdk', + }); + store.set(openSettingModalAtom, s => ({ + activeTab: 'account', + open: !s.open, + })); + }, + }) + ); + unsubs.push( registerAffineCommand({ id: 'affine:goto-trash', diff --git a/packages/frontend/core/src/commands/affine-settings.tsx b/packages/frontend/core/src/commands/affine-settings.tsx index c3762709da..2429931945 100644 --- a/packages/frontend/core/src/commands/affine-settings.tsx +++ b/packages/frontend/core/src/commands/affine-settings.tsx @@ -11,6 +11,7 @@ import type { useTheme } from 'next-themes'; import { openQuickSearchModalAtom } from '../atoms'; import type { useLanguageHelper } from '../hooks/affine/use-language-helper'; +import { mixpanel } from '../utils'; export function registerAffineSettingsCommands({ t, @@ -38,6 +39,9 @@ export function registerAffineSettingsCommands({ label: '', icon: , run() { + mixpanel.track('QuickSearchOpened', { + control: 'shortcut', + }); const quickSearchModalState = store.get(openQuickSearchModalAtom); if (!editor) { diff --git a/packages/frontend/core/src/components/affine/ai-onboarding/edgeless.dialog.tsx b/packages/frontend/core/src/components/affine/ai-onboarding/edgeless.dialog.tsx index 67ef274aac..e4c6096f32 100644 --- a/packages/frontend/core/src/components/affine/ai-onboarding/edgeless.dialog.tsx +++ b/packages/frontend/core/src/components/affine/ai-onboarding/edgeless.dialog.tsx @@ -1,6 +1,7 @@ import { Button, FlexWrapper, notify } from '@affine/component'; import { openSettingModalAtom } from '@affine/core/atoms'; import { SubscriptionService } from '@affine/core/modules/cloud'; +import { mixpanel } from '@affine/core/utils'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { AiIcon } from '@blocksuite/icons'; @@ -69,6 +70,11 @@ export const AIOnboardingEdgeless = ({ const mode = useLiveData(doc.mode$); const goToPricingPlans = useCallback(() => { + mixpanel.track('PlansViewed', { + page: 'whiteboard editor', + segment: 'ai onboarding', + module: 'whiteboard dialog', + }); setSettingModal({ open: true, activeTab: 'plans', diff --git a/packages/frontend/core/src/components/affine/ai-onboarding/general.dialog.tsx b/packages/frontend/core/src/components/affine/ai-onboarding/general.dialog.tsx index 5191df7a28..f265b886b2 100644 --- a/packages/frontend/core/src/components/affine/ai-onboarding/general.dialog.tsx +++ b/packages/frontend/core/src/components/affine/ai-onboarding/general.dialog.tsx @@ -2,6 +2,7 @@ import { Button, IconButton, Modal } from '@affine/component'; import { openSettingModalAtom } from '@affine/core/atoms'; import { useBlurRoot } from '@affine/core/hooks/use-blur-root'; import { SubscriptionService } from '@affine/core/modules/cloud'; +import { mixpanel } from '@affine/core/utils'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { Trans } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; @@ -122,6 +123,11 @@ export const AIOnboardingGeneral = ({ activeTab: 'plans', scrollAnchor: 'aiPricingPlan', }); + mixpanel.track('PlansViewed', { + page: 'whiteboard-editor', + segment: 'ai onboarding', + module: 'general', + }); closeAndDismiss(); }, [closeAndDismiss, setSettingModal]); const onPrev = useCallback(() => { diff --git a/packages/frontend/core/src/components/affine/auth/user-plan-button.tsx b/packages/frontend/core/src/components/affine/auth/user-plan-button.tsx index 49cfb58d1a..0cc8fbb6ef 100644 --- a/packages/frontend/core/src/components/affine/auth/user-plan-button.tsx +++ b/packages/frontend/core/src/components/affine/auth/user-plan-button.tsx @@ -1,4 +1,5 @@ import { Tooltip } from '@affine/component/ui/tooltip'; +import { mixpanel } from '@affine/core/utils'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useLiveData, useServices } from '@toeverything/infra'; import { useSetAtom } from 'jotai'; @@ -40,6 +41,10 @@ export const UserPlanButton = () => { open: true, activeTab: 'plans', }); + mixpanel.track('PlansViewed', { + segment: 'settings panel', + module: 'profile and badge', + }); }, [setSettingModalAtom] ); diff --git a/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx b/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx index 10254c24cd..64d8ce3a5a 100644 --- a/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx +++ b/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx @@ -225,6 +225,9 @@ const PlanPrompt = () => { open: true, activeTab: 'plans', }); + mixpanel.track('PlansViewed', { + segment: 'doc history', + }); }, [setSettingModalAtom]); const t = useAFFiNEI18N(); @@ -233,7 +236,7 @@ const PlanPrompt = () => { return (
{ - isProWorkspace === null + isProWorkspace !== null ? !isProWorkspace ? t[ 'com.affine.history.confirm-restore-modal.plan-prompt.limited-title' diff --git a/packages/frontend/core/src/components/affine/quota-reached-modal/cloud-quota-modal.tsx b/packages/frontend/core/src/components/affine/quota-reached-modal/cloud-quota-modal.tsx index fbb6363536..44dba1216f 100644 --- a/packages/frontend/core/src/components/affine/quota-reached-modal/cloud-quota-modal.tsx +++ b/packages/frontend/core/src/components/affine/quota-reached-modal/cloud-quota-modal.tsx @@ -3,6 +3,7 @@ import { openQuotaModalAtom, openSettingModalAtom } from '@affine/core/atoms'; import { UserQuotaService } from '@affine/core/modules/cloud'; import { WorkspacePermissionService } from '@affine/core/modules/permissions'; import { WorkspaceQuotaService } from '@affine/core/modules/quota'; +import { mixpanel } from '@affine/core/utils'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useLiveData, useService, WorkspaceService } from '@toeverything/infra'; import bytes from 'bytes'; @@ -48,6 +49,11 @@ export const CloudQuotaModal = () => { activeTab: 'plans', }); + mixpanel.track('PlansViewed', { + segment: 'payment wall', + category: 'payment wall storage', + }); + setOpen(false); }, [setOpen, setSettingModalAtom]); diff --git a/packages/frontend/core/src/components/affine/setting-modal/account-setting/ai-usage-panel.tsx b/packages/frontend/core/src/components/affine/setting-modal/account-setting/ai-usage-panel.tsx index 3b1e121615..daa5c50ae7 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/account-setting/ai-usage-panel.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/account-setting/ai-usage-panel.tsx @@ -6,6 +6,7 @@ import { SubscriptionService, UserCopilotQuotaService, } from '@affine/core/modules/cloud'; +import { mixpanel } from '@affine/core/utils'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useLiveData, useService } from '@toeverything/infra'; import { cssVar } from '@toeverything/theme'; @@ -46,6 +47,12 @@ export const AIUsagePanel = () => { open: true, activeTab: 'billing', }); + mixpanel.track('BillingViewed', { + segment: 'settings panel', + module: 'account usage list', + control: 'change plan button', + type: 'ai subscription', + }); }, [setOpenSettingModal]); if (loading) { diff --git a/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx index 1800b407b1..b2fe64c866 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx @@ -162,8 +162,11 @@ const StoragePanel = () => { const setSettingModalAtom = useSetAtom(openSettingModalAtom); const onUpgrade = useCallback(() => { - mixpanel.track('Button', { - resolve: 'UpgradeStorage', + mixpanel.track('PlansViewed', { + segment: 'settings panel', + module: 'account usage list', + control: 'cloud storage upgrade button', + type: 'cloud subscription', }); setSettingModalAtom({ open: true, diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx index 091e92a579..e85cc10d30 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx @@ -108,17 +108,22 @@ const SubscriptionSettings = () => { const openPlans = useCallback( (scrollAnchor?: string) => { - mixpanel.track('Button', { - resolve: 'ChangePlan', - currentPlan: proSubscription?.plan, + mixpanel.track('PlansViewed', { + type: proSubscription?.plan, + category: proSubscription?.recurring, + // page: + segment: 'settings panel', + module: 'billing subscription list', + control: 'change plan button', }); + setOpenSettingModalAtom({ open: true, activeTab: 'plans', scrollAnchor: scrollAnchor, }); }, - [proSubscription?.plan, setOpenSettingModalAtom] + [proSubscription?.plan, proSubscription?.recurring, setOpenSettingModalAtom] ); const gotoCloudPlansSetting = useCallback(() => openPlans(), [openPlans]); const gotoAiPlanSetting = useCallback( diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/subscribe.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/subscribe.tsx index da4c68f160..0d9a98a9fb 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/subscribe.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/subscribe.tsx @@ -1,7 +1,7 @@ import { Button, type ButtonProps } from '@affine/component'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { SubscriptionService } from '@affine/core/modules/cloud'; -import { popupWindow } from '@affine/core/utils'; +import { mixpanel, popupWindow } from '@affine/core/utils'; import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useLiveData, useService } from '@toeverything/infra'; @@ -42,6 +42,10 @@ export const AISubscribe = ({ ...btnProps }: AISubscribeProps) => { const subscribe = useAsyncCallback(async () => { setMutating(true); + mixpanel.track('plan upgrade started', { + category: SubscriptionRecurring.Yearly, + type: SubscriptionPlan.AI, + }); try { const session = await subscriptionService.createCheckoutSession({ recurring: SubscriptionRecurring.Yearly, diff --git a/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx index 3821d538b4..db77e97711 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx @@ -115,15 +115,22 @@ export const SettingSidebar = ({ const loginStatus = useLiveData(useService(AuthService).session.status$); const generalList = useGeneralSettingList(); const onAccountSettingClick = useCallback(() => { - mixpanel.track('Button', { - resolve: 'AccountSetting', + mixpanel.track('AccountSettingsViewed', { + // page: + segment: 'settings panel', + module: 'settings menu', + control: 'menu item', }); onTabChange('account', null); }, [onTabChange]); const onWorkspaceSettingClick = useCallback( (subTab: WorkspaceSubTab, workspaceMetadata: WorkspaceMetadata) => { - mixpanel.track('Button', { - resolve: 'WorkspaceSetting', + mixpanel.track(`view workspace setting`, { + // page: + segment: 'settings panel', + module: 'settings menu', + control: 'menu item', + type: subTab, workspaceId: workspaceMetadata.id, }); onTabChange(`workspace:${subTab}`, workspaceMetadata); @@ -148,9 +155,21 @@ export const SettingSidebar = ({ key={key} title={title} onClick={() => { - mixpanel.track('Button', { - resolve: key, - }); + if (key === 'billing') { + mixpanel.track('BillingViewed', { + // page: + segment: 'settings panel', + module: 'settings menu', + control: 'menu item', + }); + } else if (key === 'plans') { + mixpanel.track('PlansViewed', { + // page: + segment: 'settings panel', + module: 'settings menu', + control: 'menu item', + }); + } onTabChange(key, null); }} data-testid={testId} diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx index cba6970738..3e862f0ee8 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx @@ -23,6 +23,7 @@ import { useMembers } from '@affine/core/hooks/affine/use-members'; import { useRevokeMemberPermission } from '@affine/core/hooks/affine/use-revoke-member-permission'; import { WorkspacePermissionService } from '@affine/core/modules/permissions'; import { WorkspaceQuotaService } from '@affine/core/modules/quota'; +import { mixpanel } from '@affine/core/utils'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { Permission } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; @@ -144,6 +145,12 @@ export const CloudWorkspaceMembersPanel = () => { open: true, activeTab: 'plans', }); + mixpanel.track('PlansViewed', { + // page: + segment: 'settings panel', + module: 'workspace setting', + control: 'invite member', + }); }, [setSettingModalAtom]); const listContainerRef = useRef(null); diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx index 9bf9bb7250..84dabcecb5 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx @@ -11,6 +11,7 @@ import { Button } from '@affine/component/ui/button'; import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { ShareService } from '@affine/core/modules/share-doc'; +import { mixpanel } from '@affine/core/utils'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { PublicPageMode } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; @@ -101,6 +102,12 @@ export const AffineSharePage = (props: ShareMenuProps) => { await shareService.share.enableShare( mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page ); + mixpanel.track('ShareCreated', { + segment: 'sharing panel', + module: 'public share', + control: 'share panel', + type: mode, + }); notify.success({ title: t[ diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/use-share-url.ts b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/use-share-url.ts index 2d8b6a1ace..83db694965 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/use-share-url.ts +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/use-share-url.ts @@ -1,5 +1,6 @@ import { toast } from '@affine/component'; import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch'; +import { mixpanel } from '@affine/core/utils'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useCallback, useMemo } from 'react'; @@ -52,10 +53,14 @@ export const useSharingUrl = ({ .catch(err => { console.error(err); }); + mixpanel.track('ShareLinkCopied', { + module: urlType === 'share' ? 'public share' : 'private share', + type: 'link', + }); } else { toast('Network not available'); } - }, [sharingUrl, t]); + }, [sharingUrl, t, urlType]); return { sharingUrl, diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/provider.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/provider.tsx index 549f059d59..57ca49d0b8 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/provider.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/provider.tsx @@ -1,5 +1,6 @@ import { notify } from '@affine/component'; import { authAtom, openSettingModalAtom } from '@affine/core/atoms'; +import { mixpanel } from '@affine/core/utils'; import { getBaseUrl } from '@affine/graphql'; import { Trans } from '@affine/i18n'; import { UnauthorizedError } from '@blocksuite/blocks'; @@ -345,6 +346,11 @@ Could you make a new website based on these notes and send back just the html fi getCurrentStore().set(openSettingModalAtom, { activeTab: 'billing', open: true, + scrollAnchor: 'aiPricingPlan', + }); + mixpanel.track('PlansViewed', { + segment: 'payment wall', + category: 'payment wall ai action count', }); }); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx index 72f1b744d8..23aa98e600 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx @@ -11,6 +11,8 @@ import { Export, MoveToTrash } from '@affine/core/components/page-list'; import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; import { useExportPage } from '@affine/core/hooks/affine/use-export-page'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; +import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; +import { mixpanel } from '@affine/core/utils'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { @@ -97,8 +99,34 @@ export const PageHeaderMenuButton = ({ const handleDuplicate = useCallback(() => { duplicate(pageId); + mixpanel.track('DocCreated', { + segment: 'editor header', + module: 'header menu', + control: 'copy doc', + type: 'doc duplicate', + category: 'doc', + }); }, [duplicate, pageId]); + const onImportFile = useAsyncCallback(async () => { + const options = await importFile(); + if (options.isWorkspaceFile) { + mixpanel.track('WorkspaceCreated', { + segment: 'editor header', + module: 'header menu', + control: 'import button', + type: 'imported workspace', + }); + } else { + mixpanel.track('DocCreated', { + segment: 'editor header', + module: 'header menu', + control: 'import button', + type: 'imported doc', + }); + } + }, [importFile]); + const EditMenu = ( <> {!isJournal && ( @@ -179,7 +207,7 @@ export const PageHeaderMenuButton = ({ } data-testid="editor-option-menu-import" - onSelect={importFile} + onSelect={onImportFile} style={menuItemStyle} > {t['Import']()} diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-page-list/utils.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-page-list/utils.tsx index c6e8499a4f..9886a12ecf 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-page-list/utils.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-page-list/utils.tsx @@ -36,30 +36,47 @@ export const usePageHelper = (docCollection: DocCollection) => { return createPageAndOpen('edgeless'); }, [createPageAndOpen]); - const importFileAndOpen = useAsyncCallback(async () => { - const { showImportModal } = await import('@blocksuite/blocks'); - const onSuccess = ( - pageIds: string[], - options: { isWorkspaceFile: boolean; importedCount: number } - ) => { - toast( - `Successfully imported ${options.importedCount} Page${ - options.importedCount > 1 ? 's' : '' - }.` - ); - if (options.isWorkspaceFile) { - jumpToSubPath(docCollection.id, WorkspaceSubPath.ALL); - return; - } + const importFileAndOpen = useMemo( + () => async () => { + const { showImportModal } = await import('@blocksuite/blocks'); + const { promise, resolve, reject } = + Promise.withResolvers< + Parameters< + NonNullable[0]['onSuccess']> + >[1] + >(); + const onSuccess = ( + pageIds: string[], + options: { isWorkspaceFile: boolean; importedCount: number } + ) => { + resolve(options); + toast( + `Successfully imported ${options.importedCount} Page${ + options.importedCount > 1 ? 's' : '' + }.` + ); + if (options.isWorkspaceFile) { + jumpToSubPath(docCollection.id, WorkspaceSubPath.ALL); + return; + } - if (pageIds.length === 0) { - return; - } - const pageId = pageIds[0]; - openPage(docCollection.id, pageId); - }; - showImportModal({ collection: docCollection, onSuccess }); - }, [docCollection, openPage, jumpToSubPath]); + if (pageIds.length === 0) { + return; + } + const pageId = pageIds[0]; + openPage(docCollection.id, pageId); + }; + showImportModal({ + collection: docCollection, + onSuccess, + onFail: message => { + reject(new Error(message)); + }, + }); + return await promise; + }, + [docCollection, openPage, jumpToSubPath] + ); const createLinkedPageAndOpen = useAsyncCallback( async (pageId: string) => { diff --git a/packages/frontend/core/src/components/page-list/components/new-page-button.tsx b/packages/frontend/core/src/components/page-list/components/new-page-button.tsx index 3a5f042464..e8688c4479 100644 --- a/packages/frontend/core/src/components/page-list/components/new-page-button.tsx +++ b/packages/frontend/core/src/components/page-list/components/new-page-button.tsx @@ -1,5 +1,6 @@ import { DropdownButton, Menu } from '@affine/component'; import { BlockCard } from '@affine/component/card/block-card'; +import { mixpanel } from '@affine/core/utils'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { EdgelessIcon, ImportIcon, PageIcon } from '@blocksuite/icons'; import type { PropsWithChildren } from 'react'; @@ -69,11 +70,27 @@ export const NewPageButton = ({ const handleCreateNewPage = useCallback(() => { createNewPage(); setOpen(false); + mixpanel.track('DocCreated', { + page: 'doc library', + segment: 'all doc', + module: 'doc list header', + control: 'new doc button', + type: 'doc', + category: 'page', + }); }, [createNewPage]); const handleCreateNewEdgeless = useCallback(() => { createNewEdgeless(); setOpen(false); + mixpanel.track('DocCreated', { + page: 'doc library', + segment: 'all doc', + module: 'doc list header', + control: 'new whiteboard button', + type: 'doc', + category: 'whiteboard', + }); }, [createNewEdgeless]); const handleImportFile = useCallback(() => { @@ -104,10 +121,7 @@ export const NewPageButton = ({ > { - createNewPage(); - setOpen(false); - }, [createNewPage])} + onClick={handleCreateNewPage} onClickDropDown={useCallback(() => setOpen(open => !open), [])} > {children} diff --git a/packages/frontend/core/src/components/page-list/docs/page-list-header.tsx b/packages/frontend/core/src/components/page-list/docs/page-list-header.tsx index 217498a2dd..9ed0453a1e 100644 --- a/packages/frontend/core/src/components/page-list/docs/page-list-header.tsx +++ b/packages/frontend/core/src/components/page-list/docs/page-list-header.tsx @@ -9,6 +9,7 @@ import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; import type { Tag } from '@affine/core/modules/tag'; import { TagService } from '@affine/core/modules/tag'; +import { mixpanel } from '@affine/core/utils'; import type { Collection } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { @@ -46,6 +47,28 @@ export const PageListHeader = () => { return t['com.affine.all-pages.header'](); }, [t]); + const onImportFile = useAsyncCallback(async () => { + const options = await importFile(); + if (options.isWorkspaceFile) { + mixpanel.track('WorkspaceCreated', { + page: 'doc library', + segment: 'all doc', + module: 'doc list header', + control: 'import button', + type: 'imported workspace', + }); + } else { + mixpanel.track('DocCreated', { + page: 'doc library', + segment: 'all doc', + module: 'doc list header', + control: 'import button', + type: 'imported doc', + // category + }); + } + }, [importFile]); + return (
{title}
@@ -54,7 +77,7 @@ export const PageListHeader = () => { testId="new-page-button-trigger" onCreateEdgeless={createEdgeless} onCreatePage={createPage} - onImportFile={importFile} + onImportFile={onImportFile} >
{t['New Page']()}
diff --git a/packages/frontend/core/src/components/page-list/operation-cell.tsx b/packages/frontend/core/src/components/page-list/operation-cell.tsx index ca59777487..58cac612b0 100644 --- a/packages/frontend/core/src/components/page-list/operation-cell.tsx +++ b/packages/frontend/core/src/components/page-list/operation-cell.tsx @@ -12,6 +12,7 @@ import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-sui import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; import { WorkbenchService } from '@affine/core/modules/workbench'; +import { mixpanel } from '@affine/core/utils'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { @@ -96,6 +97,13 @@ export const PageOperationCell = ({ const onDuplicate = useCallback(() => { duplicate(page.id, false); + mixpanel.track('DocCreated', { + segment: 'all doc', + module: 'doc item menu', + control: 'copy doc', + type: 'doc duplicate', + category: 'doc', + }); }, [duplicate, page.id]); const OperationMenu = ( diff --git a/packages/frontend/core/src/components/pure/cmdk/data-hooks.tsx b/packages/frontend/core/src/components/pure/cmdk/data-hooks.tsx index 5d2c9b6423..402c99cd75 100644 --- a/packages/frontend/core/src/components/pure/cmdk/data-hooks.tsx +++ b/packages/frontend/core/src/components/pure/cmdk/data-hooks.tsx @@ -3,6 +3,7 @@ import { useGetDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite import { useJournalHelper } from '@affine/core/hooks/use-journal'; import { CollectionService } from '@affine/core/modules/collection'; import { WorkspaceSubPath } from '@affine/core/shared'; +import { mixpanel } from '@affine/core/utils'; import type { Collection } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { @@ -235,6 +236,9 @@ export const usePageCommands = () => { page.id, blockId ); + mixpanel.track('AppendToJournal', { + control: 'cmdk', + }); }, icon: , }); @@ -250,6 +254,10 @@ export const usePageCommands = () => { const page = pageHelper.createPage(); page.load(); pageMetaHelper.setDocTitle(page.id, query); + mixpanel.track('DocCreated', { + control: 'cmdk', + type: 'doc', + }); }, icon: , }); @@ -265,6 +273,10 @@ export const usePageCommands = () => { const page = pageHelper.createEdgeless(); page.load(); pageMetaHelper.setDocTitle(page.id, query); + mixpanel.track('DocCreated', { + control: 'cmdk', + type: 'whiteboard', + }); }, icon: , }); diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/add-favourite-button.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/add-favourite-button.tsx index 7c99e1f4ab..d3f0dfbfd8 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/add-favourite-button.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/add-favourite-button.tsx @@ -1,6 +1,7 @@ import { IconButton } from '@affine/component/ui/button'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { mixpanel } from '@affine/core/utils'; import { PlusIcon } from '@blocksuite/icons'; import type { DocCollection } from '@blocksuite/store'; import { useService } from '@toeverything/infra'; @@ -24,10 +25,26 @@ export const AddFavouriteButton = ({ e.stopPropagation(); e.preventDefault(); createLinkedPage(pageId); + mixpanel.track('DocCreated', { + // page: + segment: 'all doc', + module: 'favorite', + control: 'new fav sub doc', + type: 'doc', + category: 'page', + }); } else { const page = createPage(); page.load(); favAdapter.set(page.id, 'doc', true); + mixpanel.track('DocCreated', { + // page: + segment: 'all doc', + module: 'favorite', + control: 'new fav doc', + type: 'doc', + category: 'page', + }); } }, [pageId, createLinkedPage, createPage, favAdapter] diff --git a/packages/frontend/core/src/components/root-app-sidebar/import-page.tsx b/packages/frontend/core/src/components/root-app-sidebar/import-page.tsx index a0c8639c18..7026b06753 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/import-page.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/import-page.tsx @@ -1,3 +1,5 @@ +import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; +import { mixpanel } from '@affine/core/utils'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { ImportIcon } from '@blocksuite/icons'; @@ -8,8 +10,31 @@ import { usePageHelper } from '../blocksuite/block-suite-page-list/utils'; const ImportPage = ({ docCollection }: { docCollection: DocCollection }) => { const t = useAFFiNEI18N(); const { importFile } = usePageHelper(docCollection); + + const onImportFile = useAsyncCallback(async () => { + const options = await importFile(); + if (options.isWorkspaceFile) { + mixpanel.track('WorkspaceCreated', { + page: 'doc library', + segment: 'navigation panel', + module: 'doc list header', + control: 'import button', + type: 'imported workspace', + }); + } else { + mixpanel.track('DocCreated', { + page: 'doc library', + segment: 'navigation panel', + module: 'doc list header', + control: 'import button', + type: 'imported doc', + // category + }); + } + }, [importFile]); + return ( - } onClick={importFile}> + } onClick={onImportFile}> {t['Import']()} ); diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.tsx b/packages/frontend/core/src/components/root-app-sidebar/index.tsx index a5afdd0a57..f0726f0fd6 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx @@ -2,6 +2,7 @@ import { AnimatedDeleteIcon } from '@affine/component'; import { getDNDId } from '@affine/core/hooks/affine/use-global-dnd-helper'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { CollectionService } from '@affine/core/modules/collection'; +import { mixpanel } from '@affine/core/utils'; import { apis, events } from '@affine/electron-api'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { FolderIcon, SettingsIcon } from '@blocksuite/icons'; @@ -106,11 +107,23 @@ export const RootAppSidebar = ({ ) ); + const allPageActive = currentPath === '/all'; + + const trashActive = currentPath === '/trash'; + const onClickNewPage = useAsyncCallback(async () => { const page = createPage(); page.load(); openPage(page.id); - }, [createPage, openPage]); + mixpanel.track('DocCreated', { + page: allPageActive ? 'all' : trashActive ? 'trash' : 'other', + segment: 'navigation panel', + module: 'bottom button', + control: 'new doc button', + category: 'page', + type: 'doc', + }); + }, [allPageActive, createPage, openPage, trashActive]); const { trashModal, setTrashModal, handleOnConfirm } = useTrashModalHelper(docCollection); @@ -166,10 +179,6 @@ export const RootAppSidebar = ({ }); }, [docCollection.id, collection, navigateHelper, open]); - const allPageActive = currentPath === '/all'; - - const trashActive = currentPath === '/trash'; - return ( { const setOpenSignOutModalAtom = useSetAtom(openSignOutModalAtom); const onOpenAccountSetting = useCallback(() => { + mixpanel.track('AccountSettingsViewed', { + // page: + segment: 'navigation panel', + module: 'profile and badge', + control: 'profile and email', + }); setSettingModalAtom(prev => ({ ...prev, open: true, diff --git a/packages/frontend/core/src/components/workspace/index.tsx b/packages/frontend/core/src/components/workspace/index.tsx index 86ec136113..1d312f7b8e 100644 --- a/packages/frontend/core/src/components/workspace/index.tsx +++ b/packages/frontend/core/src/components/workspace/index.tsx @@ -23,10 +23,12 @@ export const AppContainer = ({ useNoisyBackground, useBlurBackground, children, + ...rest }: WorkspaceRootProps) => { const noisyBackground = useNoisyBackground && environment.isDesktop; return (
('affine:page'); } + mixpanel.track('ShareCreated', { + type, + segment: 'sharing panel', + module: 'export share', + }); switch (type) { case 'html': await HtmlTransformer.exportDoc(page); diff --git a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx index daf796d1bc..ee11999e89 100644 --- a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx +++ b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx @@ -1,6 +1,7 @@ import { toast } from '@affine/component'; import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { mixpanel } from '@affine/core/utils'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { assertExists } from '@blocksuite/global/utils'; @@ -141,6 +142,11 @@ export function useRegisterBlocksuiteEditorCommands() { label: t['com.affine.header.option.duplicate'](), run() { duplicate(docId); + mixpanel.track('DocCreated', { + control: 'cmdk', + type: 'doc duplicate', + category: 'doc', + }); }, }) ); diff --git a/packages/frontend/core/src/layouts/workspace-layout.tsx b/packages/frontend/core/src/layouts/workspace-layout.tsx index b73d63447e..4f64f9e7ad 100644 --- a/packages/frontend/core/src/layouts/workspace-layout.tsx +++ b/packages/frontend/core/src/layouts/workspace-layout.tsx @@ -40,6 +40,7 @@ import { } from '../hooks/affine/use-global-dnd-helper'; import { useNavigateHelper } from '../hooks/use-navigate-helper'; import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands'; +import { WorkbenchService } from '../modules/workbench'; import { AllWorkspaceModals, CurrentWorkspaceModals, @@ -62,7 +63,6 @@ export const QuickSearch = () => { const onToggleQuickSearch = useCallback( (open: boolean) => { - mixpanel.track('QuickSearch', { open }); setOpenQuickSearchModalAtom(open); }, [setOpenQuickSearchModalAtom] @@ -113,6 +113,14 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => { const upgrading = useLiveData(currentWorkspace.upgrade.upgrading$); const needUpgrade = useLiveData(currentWorkspace.upgrade.needUpgrade$); + const workbench = useService(WorkbenchService).workbench; + + const basename = useLiveData(workbench.basename$); + + const currentPath = useLiveData( + workbench.location$.map(location => basename + location.pathname) + ); + useRegisterWorkspaceCommands(); useEffect(() => { @@ -143,6 +151,10 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => { const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom); const handleOpenQuickSearchModal = useCallback(() => { setOpenQuickSearchModalAtom(true); + mixpanel.track('QuickSearchOpened', { + segment: 'navigation panel', + control: 'search button', + }); }, [setOpenQuickSearchModalAtom]); const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom); @@ -152,6 +164,12 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => { activeTab: 'appearance', open: true, }); + mixpanel.track('SettingsViewed', { + // page: + segment: 'navigation panel', + module: 'general list', + control: 'settings button', + }); }, [setOpenSettingModalAtom]); const resizing = useAtomValue(appSidebarResizingAtom); @@ -171,7 +189,7 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => { <> {/* This DndContext is used for drag page from all-pages list into a folder in sidebar */} - + }> ( + 'WorkbenchLocationChanged' +); + export class WorkbenchService extends Service { + constructor() { + super(); + combineLatest([this.workbench.location$, this.workbench.basename$]) + .pipe( + map(([location, basename]) => basename + location.pathname), + distinctUntilChanged(), + skip(1) + ) + .subscribe(newLocation => { + this.eventBus.root.emit(WorkbenchLocationChanged, newLocation); + mixpanel.track_pageview({ + location: newLocation, + }); + }); + } + workbench = this.framework.createEntity(Workbench); } diff --git a/packages/frontend/core/src/pages/subscribe.tsx b/packages/frontend/core/src/pages/subscribe.tsx index fa5895b9d8..5d182459b7 100644 --- a/packages/frontend/core/src/pages/subscribe.tsx +++ b/packages/frontend/core/src/pages/subscribe.tsx @@ -8,6 +8,7 @@ import { EMPTY, mergeMap, switchMap } from 'rxjs'; import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper'; import { AuthService, SubscriptionService } from '../modules/cloud'; +import { mixpanel } from '../utils'; import { container } from './subscribe.css'; export const Component = () => { @@ -68,6 +69,16 @@ export const Component = () => { }); setMessage('Redirecting...'); location.href = checkout; + mixpanel.track('PlanChangeSucceeded', { + type: plan, + category: recurring, + }); + if (plan) { + mixpanel.people.set({ + [SubscriptionPlan.AI === plan ? 'ai plan' : plan]: plan, + recurring: recurring, + }); + } } catch (err) { console.error(err); setError('Something went wrong. Please try again.'); diff --git a/packages/frontend/core/src/pages/workspace/all-page/all-page-header.tsx b/packages/frontend/core/src/pages/workspace/all-page/all-page-header.tsx index fe01a92662..e9a5a6e84e 100644 --- a/packages/frontend/core/src/pages/workspace/all-page/all-page-header.tsx +++ b/packages/frontend/core/src/pages/workspace/all-page/all-page-header.tsx @@ -6,6 +6,8 @@ import { } from '@affine/core/components/page-list'; import { Header } from '@affine/core/components/pure/header'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; +import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; +import { mixpanel } from '@affine/core/utils'; import type { Filter } from '@affine/env/filter'; import { PlusIcon } from '@blocksuite/icons'; import { useService, WorkspaceService } from '@toeverything/infra'; @@ -27,6 +29,28 @@ export const AllPageHeader = ({ workspace.docCollection ); + const onImportFile = useAsyncCallback(async () => { + const options = await importFile(); + if (options.isWorkspaceFile) { + mixpanel.track('WorkspaceCreated', { + page: 'doc library', + segment: 'all page', + module: 'doc list header', + control: 'import button', + type: 'imported workspace', + }); + } else { + mixpanel.track('DocCreated', { + page: 'doc library', + segment: 'all page', + module: 'doc list header', + control: 'import button', + type: 'imported doc', + // category + }); + } + }, [importFile]); + return (
diff --git a/packages/frontend/core/src/router.tsx b/packages/frontend/core/src/router.tsx index eb53d49eb8..9bf4024957 100644 --- a/packages/frontend/core/src/router.tsx +++ b/packages/frontend/core/src/router.tsx @@ -5,17 +5,13 @@ import { createBrowserRouter as reactRouterCreateBrowserRouter, Outlet, redirect, - useLocation, // eslint-disable-next-line @typescript-eslint/no-restricted-imports useNavigate, } from 'react-router-dom'; -import { mixpanel } from './utils'; - export const NavigateContext = createContext(null); function RootRouter() { - const location = useLocation(); const navigate = useNavigate(); const [ready, setReady] = useState(false); useEffect(() => { @@ -23,16 +19,6 @@ function RootRouter() { setReady(true); }, []); - useEffect(() => { - mixpanel.track_pageview({ - page: location.pathname, - appVersion: runtimeConfig.appVersion, - environment: runtimeConfig.appBuildType, - editorVersion: runtimeConfig.editorVersion, - isSelfHosted: Boolean(runtimeConfig.isSelfHosted), - isDesktop: environment.isDesktop, - }); - }, [location]); return ( ready && ( diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 8b8d9d0d98..ae0ec5d564 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -574,6 +574,7 @@ "com.affine.cmdk.affine.navigation.goto-trash": "Go to Trash", "com.affine.cmdk.affine.navigation.goto-workspace": "Go to Workspace", "com.affine.cmdk.affine.navigation.open-settings": "Go to Settings", + "com.affine.cmdk.affine.navigation.open-account-settings": "Go to Account Settings", "com.affine.cmdk.affine.new-edgeless-page": "New Edgeless", "com.affine.cmdk.affine.new-page": "New Doc", "com.affine.cmdk.affine.new-workspace": "New Workspace",