diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx index 3e94a351c1236..cc26a83905dbb 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx @@ -24,6 +24,7 @@ import type { BlockSpec, WidgetElement } from '@blocksuite/block-std'; import { type AffineReference, AffineSlashMenuWidget, + EdgelessRootBlockComponent, EmbedLinkedDocBlockComponent, type ParagraphBlockService, type RootService, @@ -463,23 +464,28 @@ export function patchQuickSearchService( pageId: linkedDoc.id, }, ]); + const isEdgeless = + rootElement instanceof EdgelessRootBlockComponent; if (result.isNewDoc) { mixpanel.track('DocCreated', { control: 'linked doc', module: 'slash commands', type: 'linked doc', category: 'doc', + page: isEdgeless ? 'whiteboard editor' : 'page editor', }); mixpanel.track('LinkedDocCreated', { control: 'new doc', module: 'slash commands', type: 'doc', + page: isEdgeless ? 'whiteboard editor' : 'page editor', }); } else { mixpanel.track('LinkedDocCreated', { control: 'linked doc', module: 'slash commands', type: 'doc', + page: isEdgeless ? 'whiteboard editor' : 'page editor', }); } } else if ('userInput' in result) { 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 4c5aa28cda89a..5dc6b5a3b32f4 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 @@ -110,18 +110,20 @@ export const PageHeaderMenuButton = ({ duplicate(pageId); mixpanel.track('DocCreated', { segment: 'editor header', + page: doc.mode$.value === 'page' ? 'page editor' : 'edgeless editor', module: 'header menu', control: 'copy doc', type: 'doc duplicate', category: 'doc', }); - }, [duplicate, pageId]); + }, [doc.mode$.value, duplicate, pageId]); const onImportFile = useAsyncCallback(async () => { const options = await importFile(); if (options.isWorkspaceFile) { mixpanel.track('WorkspaceCreated', { segment: 'editor header', + page: doc.mode$.value === 'page' ? 'page editor' : 'edgeless editor', module: 'header menu', control: 'import button', type: 'imported workspace', @@ -129,12 +131,13 @@ export const PageHeaderMenuButton = ({ } else { mixpanel.track('DocCreated', { segment: 'editor header', + page: doc.mode$.value === 'page' ? 'page editor' : 'edgeless editor', module: 'header menu', control: 'import button', type: 'imported doc', }); } - }, [importFile]); + }, [doc.mode$.value, importFile]); const showResponsiveMenu = hideShare; const ResponsiveMenuItems = ( 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 6a265c5954d2b..ff18eb7fd4c8c 100644 --- a/packages/frontend/core/src/components/page-list/operation-cell.tsx +++ b/packages/frontend/core/src/components/page-list/operation-cell.tsx @@ -102,6 +102,7 @@ export const PageOperationCell = ({ control: 'copy doc', type: 'doc duplicate', category: 'doc', + page: 'doc library', }); }, [duplicate, page.id]); 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 4f52084a93d84..e441817abbcf7 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 { TelemetryWorkspaceContextService } from '@affine/core/modules/telemetry/services/telemetry'; import { mixpanel } from '@affine/core/utils'; import { PlusIcon } from '@blocksuite/icons/rc'; import { useService, useServices, WorkspaceService } from '@toeverything/infra'; @@ -19,8 +20,11 @@ export const AddFavouriteButton = ({ pageId }: AddFavouriteButtonProps) => { workspaceService.workspace.docCollection ); const favAdapter = useService(FavoriteItemsAdapter); + const telemetry = useService(TelemetryWorkspaceContextService); const handleAddFavorite = useAsyncCallback( async e => { + const page = telemetry.getPageContext(); + if (pageId) { e.stopPropagation(); e.preventDefault(); @@ -32,6 +36,7 @@ export const AddFavouriteButton = ({ pageId }: AddFavouriteButtonProps) => { control: 'new fav sub doc', type: 'doc', category: 'page', + page: page, }); } else { const page = createPage(); @@ -44,10 +49,11 @@ export const AddFavouriteButton = ({ pageId }: AddFavouriteButtonProps) => { control: 'new fav doc', type: 'doc', category: 'page', + page: page, }); } }, - [pageId, createLinkedPage, createPage, favAdapter] + [telemetry, pageId, createLinkedPage, createPage, favAdapter] ); return ( 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 2cde5a5ffc869..cf7f44a2ca8a4 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,7 +1,9 @@ import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; +import { TelemetryWorkspaceContextService } from '@affine/core/modules/telemetry/services/telemetry'; import { mixpanel } from '@affine/core/utils'; import { useI18n } from '@affine/i18n'; import { ImportIcon } from '@blocksuite/icons/rc'; +import { useService } from '@toeverything/infra'; import type { DocCollection } from '../../shared'; import { MenuItem } from '../app-sidebar'; @@ -10,12 +12,14 @@ import { usePageHelper } from '../blocksuite/block-suite-page-list/utils'; const ImportPage = ({ docCollection }: { docCollection: DocCollection }) => { const t = useI18n(); const { importFile } = usePageHelper(docCollection); + const telemetry = useService(TelemetryWorkspaceContextService); const onImportFile = useAsyncCallback(async () => { const options = await importFile(); + const page = telemetry.getPageContext(); if (options.isWorkspaceFile) { mixpanel.track('WorkspaceCreated', { - page: 'doc library', + page, segment: 'navigation panel', module: 'doc list header', control: 'import button', @@ -23,7 +27,7 @@ const ImportPage = ({ docCollection }: { docCollection: DocCollection }) => { }); } else { mixpanel.track('DocCreated', { - page: 'doc library', + page, segment: 'navigation panel', module: 'doc list header', control: 'import button', @@ -31,7 +35,7 @@ const ImportPage = ({ docCollection }: { docCollection: DocCollection }) => { // category }); } - }, [importFile]); + }, [importFile, telemetry]); return ( } onClick={onImportFile}> 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 6d13b827e1af0..fa19baabc39e2 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 { TelemetryWorkspaceContextService } from '@affine/core/modules/telemetry/services/telemetry'; import { mixpanel } from '@affine/core/utils'; import { apis, events } from '@affine/electron-api'; import { useI18n } from '@affine/i18n'; @@ -103,6 +104,8 @@ export const RootAppSidebar = memo( ) ); + const telemetry = useService(TelemetryWorkspaceContextService); + const allPageActive = currentPath === '/all'; const trashActive = currentPath === '/trash'; @@ -112,14 +115,14 @@ export const RootAppSidebar = memo( page.load(); openPage(page.id); mixpanel.track('DocCreated', { - page: allPageActive ? 'all' : trashActive ? 'trash' : 'other', + page: telemetry.getPageContext(), segment: 'navigation panel', module: 'bottom button', control: 'new doc button', category: 'page', type: 'doc', }); - }, [allPageActive, createPage, openPage, trashActive]); + }, [createPage, openPage, telemetry]); const navigateHelper = useNavigateHelper(); // Listen to the "New Page" action from the menu 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 19873f4a77303..ac2ed9b4097cc 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 @@ -4,6 +4,7 @@ import { registerAffineCommand, } from '@affine/core/commands'; import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { TelemetryWorkspaceContextService } from '@affine/core/modules/telemetry/services/telemetry'; import { mixpanel } from '@affine/core/utils'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useI18n } from '@affine/i18n'; @@ -58,6 +59,8 @@ export function useRegisterBlocksuiteEditorCommands() { [docId, setTrashModal] ); + const telemetry = useService(TelemetryWorkspaceContextService); + const isCloudWorkspace = workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD; useEffect(() => { @@ -144,6 +147,7 @@ export function useRegisterBlocksuiteEditorCommands() { control: 'cmdk', type: 'doc duplicate', category: 'doc', + page: telemetry.getPageContext(), }); }, }) @@ -275,5 +279,6 @@ export function useRegisterBlocksuiteEditorCommands() { favAdapter, docId, doc, + telemetry, ]); } diff --git a/packages/frontend/core/src/modules/navigation/index.ts b/packages/frontend/core/src/modules/navigation/index.ts index a67197d7c5e1c..616852ed6b6df 100644 --- a/packages/frontend/core/src/modules/navigation/index.ts +++ b/packages/frontend/core/src/modules/navigation/index.ts @@ -1,5 +1,5 @@ export { Navigator } from './entities/navigator'; -export { resolveLinkToDoc } from './utils'; +export { resolveLinkToDoc, resolveRouteLinkMeta } from './utils'; export { NavigationButtons } from './view/navigation-buttons'; import { type Framework, WorkspaceScope } from '@toeverything/infra'; diff --git a/packages/frontend/core/src/modules/navigation/utils.ts b/packages/frontend/core/src/modules/navigation/utils.ts index 47cdeba29e6f4..5017b40040e0e 100644 --- a/packages/frontend/core/src/modules/navigation/utils.ts +++ b/packages/frontend/core/src/modules/navigation/utils.ts @@ -8,7 +8,7 @@ function maybeAffineOrigin(origin: string) { ); } -export const resolveLinkToDoc = (href: string) => { +export const resolveRouteLinkMeta = (href: string) => { try { const url = new URL(href, location.origin); @@ -18,23 +18,49 @@ export const resolveLinkToDoc = (href: string) => { return null; } - // http://xxx/workspace/48__RTCSwASvWZxyAk3Jw/-Uge-K6SYcAbcNYfQ5U-j#xxxx + // http://xxx/workspace/all/yyy // to { workspaceId: '48__RTCSwASvWZxyAk3Jw', docId: '-Uge-K6SYcAbcNYfQ5U-j', blockId: 'xxxx' } - const [_, workspaceId, docId, blockId] = + const [_, workspaceId, moduleName, subModuleName] = url.toString().match(/\/workspace\/([^/]+)\/([^#]+)(?:#(.+))?/) || []; - /** - * @see /packages/frontend/core/src/router.tsx - */ - const excludedPaths = ['all', 'collection', 'tag', 'trash']; - - if (!docId || excludedPaths.includes(docId)) { - return null; + if (isRouteModulePath(moduleName)) { + return { + workspaceId, + moduleName, + subModuleName, + }; + } else if (moduleName) { + // for now we assume all other cases are doc links + return { + workspaceId, + moduleName: 'doc' as const, + docId: moduleName, + blockId: subModuleName, + }; } - - return { workspaceId, docId, blockId }; + return; } catch { return null; } }; + +/** + * @see /packages/frontend/core/src/router.tsx + */ +const routeModulePaths = ['all', 'collection', 'tag', 'trash'] as const; + +const isRouteModulePath = ( + path: string +): path is (typeof routeModulePaths)[number] => + routeModulePaths.includes(path as any); + +export const resolveLinkToDoc = (href: string) => { + const meta = resolveRouteLinkMeta(href); + if (!meta || meta.moduleName !== 'doc') return null; + return { + workspaceId: meta.workspaceId, + docId: meta.docId, + blockId: meta.blockId, + }; +}; diff --git a/packages/frontend/core/src/modules/telemetry/index.ts b/packages/frontend/core/src/modules/telemetry/index.ts index 9a1b05362bb06..8567ed995a23d 100644 --- a/packages/frontend/core/src/modules/telemetry/index.ts +++ b/packages/frontend/core/src/modules/telemetry/index.ts @@ -1,8 +1,14 @@ -import type { Framework } from '@toeverything/infra'; +import { type Framework, WorkspaceScope } from '@toeverything/infra'; import { AuthService } from '../cloud'; -import { TelemetryService } from './services/telemetry'; +import { + TelemetryService, + TelemetryWorkspaceContextService, +} from './services/telemetry'; export function configureTelemetryModule(framework: Framework) { framework.service(TelemetryService, [AuthService]); + framework + .scope(WorkspaceScope) + .service(TelemetryWorkspaceContextService, [WorkspaceScope]); } diff --git a/packages/frontend/core/src/modules/telemetry/services/telemetry.ts b/packages/frontend/core/src/modules/telemetry/services/telemetry.ts index 4d7dc1f105950..28b1498806b6b 100644 --- a/packages/frontend/core/src/modules/telemetry/services/telemetry.ts +++ b/packages/frontend/core/src/modules/telemetry/services/telemetry.ts @@ -1,6 +1,12 @@ import { mixpanel } from '@affine/core/utils'; import type { QuotaQuery } from '@affine/graphql'; -import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra'; +import type { WorkspaceScope } from '@toeverything/infra'; +import { + ApplicationStarted, + DocsService, + OnEvent, + Service, +} from '@toeverything/infra'; import { AccountChanged, @@ -9,6 +15,8 @@ import { } from '../../cloud'; import { AccountLoggedOut } from '../../cloud/services/auth'; import { UserQuotaChanged } from '../../cloud/services/user-quota'; +import { resolveRouteLinkMeta } from '../../navigation'; +import { WorkbenchService } from '../../workbench'; @OnEvent(ApplicationStarted, e => e.onApplicationStart) @OnEvent(AccountChanged, e => e.updateIdentity) @@ -67,3 +75,38 @@ export class TelemetryService extends Service { this.prevQuota = quota; } } + +// get telemetry related context in Workspace scope +export class TelemetryWorkspaceContextService extends Service { + constructor(private readonly provider: WorkspaceScope) { + super(); + } + + getPageContext() { + const workbench = this.provider?.getOptional(WorkbenchService)?.workbench; + const docs = this.provider?.getOptional(DocsService); + + if (!workbench || !docs) return ''; + + const basename = workbench.basename$.value; + const path = workbench.location$.value; + const fullPath = basename + path.pathname + path.search + path.hash; + const linkMeta = resolveRouteLinkMeta(fullPath); + return (() => { + const moduleName = + linkMeta?.moduleName === 'doc' + ? docs.list.getMode(linkMeta.docId) + : linkMeta?.moduleName; + switch (moduleName) { + case 'page': + return 'page editor'; + case 'edgeless': + return 'whiteboard editor'; + case 'trash': + return 'trash'; + default: + return 'doc library'; + } + })(); + } +}