diff --git a/packages/common/infra/src/modules/global-context/entities/global-context.ts b/packages/common/infra/src/modules/global-context/entities/global-context.ts index c022df6141..baa27f75ba 100644 --- a/packages/common/infra/src/modules/global-context/entities/global-context.ts +++ b/packages/common/infra/src/modules/global-context/entities/global-context.ts @@ -8,19 +8,36 @@ export class GlobalContext extends Entity { workspaceId = this.define('workspaceId'); + /** + * is in doc page + */ isDoc = this.define('isDoc'); + isTrashDoc = this.define('isTrashDoc'); docId = this.define('docId'); + docMode = this.define('docMode'); + /** + * is in collection page + */ isCollection = this.define('isCollection'); collectionId = this.define('collectionId'); + /** + * is in trash page + */ isTrash = this.define('isTrash'); - docMode = this.define('docMode'); - + /** + * is in tag page + */ isTag = this.define('isTag'); tagId = this.define('tagId'); + /** + * is in all docs page + */ + isAllDocs = this.define('isAllDocs'); + define(key: string) { this.memento.set(key, null); const livedata$ = LiveData.from(this.memento.watch(key), null); 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 1441365fb7..3b1bb22e80 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,9 +1,7 @@ import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { mixpanel } from '@affine/core/mixpanel'; -import { TelemetryWorkspaceContextService } from '@affine/core/modules/telemetry/services/telemetry'; 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'; @@ -12,14 +10,11 @@ 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, segment: 'navigation panel', module: 'doc list header', control: 'import button', @@ -27,7 +22,6 @@ const ImportPage = ({ docCollection }: { docCollection: DocCollection }) => { }); } else { mixpanel.track('DocCreated', { - page, segment: 'navigation panel', module: 'doc list header', control: 'import button', @@ -35,7 +29,7 @@ const ImportPage = ({ docCollection }: { docCollection: DocCollection }) => { // category }); } - }, [importFile, telemetry]); + }, [importFile]); 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 4d3d9b704f..55bf02aa95 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx @@ -11,7 +11,6 @@ import { } from '@affine/core/modules/explorer'; import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags'; import { CMDKQuickSearchService } from '@affine/core/modules/quicksearch/services/cmdk'; -import { TelemetryWorkspaceContextService } from '@affine/core/modules/telemetry/services/telemetry'; import { pathGenerator } from '@affine/core/shared'; import { apis, events } from '@affine/electron-api'; import { useI18n } from '@affine/i18n'; @@ -91,8 +90,6 @@ export const RootAppSidebar = (): ReactElement => { }); }, [cmdkQuickSearchService]); - const telemetry = useService(TelemetryWorkspaceContextService); - const allPageActive = currentPath === '/all'; const pageHelper = usePageHelper(currentWorkspace.docCollection); @@ -105,14 +102,13 @@ export const RootAppSidebar = (): ReactElement => { page.load(); openPage(currentWorkspaceId, page.id); mixpanel.track('DocCreated', { - page: telemetry.getPageContext(), segment: 'navigation panel', module: 'bottom button', control: 'new doc button', category: 'page', type: 'doc', }); - }, [createPage, currentWorkspaceId, openPage, telemetry]); + }, [createPage, currentWorkspaceId, openPage]); // Listen to the "New Page" action from the menu useEffect(() => { 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 5f1db7d042..0a9ed35e4d 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 @@ -6,7 +6,6 @@ import { } from '@affine/core/commands'; import { mixpanel } from '@affine/core/mixpanel'; import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; -import { TelemetryWorkspaceContextService } from '@affine/core/modules/telemetry/services/telemetry'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useI18n } from '@affine/i18n'; import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons/rc'; @@ -64,8 +63,6 @@ export function useRegisterBlocksuiteEditorCommands() { [docId, setTrashModal] ); - const telemetry = useService(TelemetryWorkspaceContextService); - const isCloudWorkspace = workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD; useEffect(() => { @@ -168,7 +165,6 @@ export function useRegisterBlocksuiteEditorCommands() { control: 'cmdk', type: 'doc duplicate', category: 'doc', - page: telemetry.getPageContext(), }); }, }) @@ -299,7 +295,6 @@ export function useRegisterBlocksuiteEditorCommands() { favAdapter, docId, doc, - telemetry, openInfoModal, ]); } diff --git a/packages/frontend/core/src/hooks/use-navigate-helper.ts b/packages/frontend/core/src/hooks/use-navigate-helper.ts index af52717ab1..95ba5c6d1a 100644 --- a/packages/frontend/core/src/hooks/use-navigate-helper.ts +++ b/packages/frontend/core/src/hooks/use-navigate-helper.ts @@ -18,6 +18,9 @@ function defaultNavigate(to: To, option?: { replace?: boolean }) { } // TODO(@eyhn): add a name -> path helper in the results +/** + * @deprecated use `WorkbenchService` instead + */ export function useNavigateHelper() { const navigate = useContext(NavigateContext) ?? defaultNavigate; diff --git a/packages/frontend/core/src/mixpanel/index.ts b/packages/frontend/core/src/mixpanel/index.ts index d2bc0f3876..6f3b73771d 100644 --- a/packages/frontend/core/src/mixpanel/index.ts +++ b/packages/frontend/core/src/mixpanel/index.ts @@ -6,6 +6,11 @@ import type { GeneralMixpanelEvent, MixpanelEvents } from './events'; const logger = new DebugLogger('mixpanel'); +type Middleware = ( + name: string, + properties?: Record +) => Record; + function createMixpanel() { let mixpanel; if (process.env.MIXPANEL_TOKEN) { @@ -22,6 +27,8 @@ function createMixpanel() { ); } + const middlewares = new Set(); + const wrapped = { reset() { mixpanel.reset(); @@ -40,8 +47,21 @@ function createMixpanel() { : Record) & GeneralMixpanelEvent, >(event_name: T, properties?: P) { - logger.debug('track', event_name, properties); - mixpanel.track(event_name, properties); + const middlewareProperties = Array.from(middlewares).reduce( + (acc, middleware) => { + return middleware(event_name, acc); + }, + properties as Record + ); + logger.debug('track', event_name, middlewareProperties); + + mixpanel.track(event_name as string, middlewareProperties); + }, + middleware(cb: Middleware): () => void { + middlewares.add(cb); + return () => { + middlewares.delete(cb); + }; }, opt_out_tracking() { mixpanel.opt_out_tracking(); diff --git a/packages/frontend/core/src/modules/telemetry/index.ts b/packages/frontend/core/src/modules/telemetry/index.ts index 8567ed995a..547b865f5e 100644 --- a/packages/frontend/core/src/modules/telemetry/index.ts +++ b/packages/frontend/core/src/modules/telemetry/index.ts @@ -1,14 +1,8 @@ -import { type Framework, WorkspaceScope } from '@toeverything/infra'; +import { type Framework, GlobalContextService } from '@toeverything/infra'; import { AuthService } from '../cloud'; -import { - TelemetryService, - TelemetryWorkspaceContextService, -} from './services/telemetry'; +import { TelemetryService } from './services/telemetry'; export function configureTelemetryModule(framework: Framework) { - framework.service(TelemetryService, [AuthService]); - framework - .scope(WorkspaceScope) - .service(TelemetryWorkspaceContextService, [WorkspaceScope]); + framework.service(TelemetryService, [AuthService, GlobalContextService]); } diff --git a/packages/frontend/core/src/modules/telemetry/services/telemetry.ts b/packages/frontend/core/src/modules/telemetry/services/telemetry.ts index 1fca0c7440..f0d37e823f 100644 --- a/packages/frontend/core/src/modules/telemetry/services/telemetry.ts +++ b/packages/frontend/core/src/modules/telemetry/services/telemetry.ts @@ -1,12 +1,7 @@ import { mixpanel } from '@affine/core/mixpanel'; import type { QuotaQuery } from '@affine/graphql'; -import type { WorkspaceScope } from '@toeverything/infra'; -import { - ApplicationStarted, - DocsService, - OnEvent, - Service, -} from '@toeverything/infra'; +import type { GlobalContextService } from '@toeverything/infra'; +import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra'; import { AccountChanged, @@ -15,8 +10,6 @@ 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) @@ -25,14 +18,19 @@ import { WorkbenchService } from '../../workbench'; export class TelemetryService extends Service { private prevQuota: NonNullable['quota'] | null = null; + private readonly disposables: (() => void)[] = []; - constructor(private readonly auth: AuthService) { + constructor( + private readonly auth: AuthService, + private readonly globalContextService: GlobalContextService + ) { super(); } onApplicationStart() { const account = this.auth.session.account$.value; this.updateIdentity(account); + this.registerMiddlewares(); } updateIdentity(account: AuthAccountInfo | null) { @@ -61,39 +59,41 @@ 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(); + registerMiddlewares() { + this.disposables.push( + mixpanel.middleware((_event, parameters) => { + const extraContext = this.extractGlobalContext(); + return { + ...extraContext, + ...parameters, + }; + }) + ); } - getPageContext() { - const workbench = this.provider?.getOptional(WorkbenchService)?.workbench; - const docs = this.provider?.getOptional(DocsService); + extractGlobalContext() { + const globalContext = this.globalContextService.globalContext; + const page = globalContext.isDoc.get() + ? globalContext.isTrashDoc.get() + ? 'trash' + : globalContext.docMode.get() === 'page' + ? 'doc editor' + : 'whiteboard editor' + : globalContext.isAllDocs.get() + ? 'doc library' + : globalContext.isTrash.get() + ? 'trash library' + : globalContext.isCollection.get() + ? 'collection detail' + : globalContext.isTag.get() + ? 'tag detail' + : 'unknown'; + return { page, activePage: page }; + } - 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 'doc editor'; - case 'edgeless': - return 'whiteboard editor'; - case 'trash': - return 'trash'; - default: - return 'doc library'; - } - })(); + override dispose(): void { + this.disposables.forEach(dispose => dispose()); + super.dispose(); } } diff --git a/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx b/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx index 3e34179741..8073b97d7a 100644 --- a/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx +++ b/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx @@ -6,10 +6,18 @@ import { import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { performanceRenderLogger } from '@affine/core/shared'; import type { Filter } from '@affine/env/filter'; -import { useService, WorkspaceService } from '@toeverything/infra'; -import { useState } from 'react'; +import { + GlobalContextService, + useService, + WorkspaceService, +} from '@toeverything/infra'; +import { useEffect, useState } from 'react'; -import { ViewBody, ViewHeader } from '../../../modules/workbench'; +import { + useIsActiveView, + ViewBody, + ViewHeader, +} from '../../../modules/workbench'; import { EmptyPageList } from '../page-list-empty'; import * as styles from './all-page.css'; import { FilterContainer } from './all-page-filter'; @@ -17,6 +25,7 @@ import { AllPageHeader } from './all-page-header'; export const AllPage = () => { const currentWorkspace = useService(WorkspaceService).workspace; + const globalContext = useService(GlobalContextService).globalContext; const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection); const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true); @@ -25,6 +34,19 @@ export const AllPage = () => { filters: filters, }); + const isActiveView = useIsActiveView(); + + useEffect(() => { + if (isActiveView) { + globalContext.isAllDocs.set(true); + + return () => { + globalContext.isAllDocs.set(false); + }; + } + return; + }, [globalContext, isActiveView]); + return ( <> diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index 6833cb3fe0..0352522ee9 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -76,6 +76,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { const activeSidebarTab = useLiveData(view.activeSidebarTab$); const doc = useService(DocService).doc; + const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash)); const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper(); const [editor, setEditor] = useState(null); const workspace = useService(WorkspaceService).workspace; @@ -139,7 +140,17 @@ const DetailPageImpl = memo(function DetailPageImpl() { } }, [doc.id, setDocReadonly]); - const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash)); + useEffect(() => { + if (isActiveView) { + globalContext.isTrashDoc.set(!!isInTrash); + + return () => { + globalContext.isTrashDoc.set(null); + }; + } + return; + }, [globalContext, isActiveView, isInTrash]); + useRegisterBlocksuiteEditorCommands(); const title = useLiveData(doc.title$); usePageDocumentTitle(title);