feat(core): add globalcontext info to mixpanel track (#7681)

This commit is contained in:
EYHN
2024-08-01 09:29:31 +00:00
parent bb767a6cdc
commit 553fbed60f
10 changed files with 126 additions and 74 deletions

View File

@@ -8,19 +8,36 @@ export class GlobalContext extends Entity {
workspaceId = this.define<string>('workspaceId');
/**
* is in doc page
*/
isDoc = this.define<boolean>('isDoc');
isTrashDoc = this.define<boolean>('isTrashDoc');
docId = this.define<string>('docId');
docMode = this.define<DocMode>('docMode');
/**
* is in collection page
*/
isCollection = this.define<boolean>('isCollection');
collectionId = this.define<string>('collectionId');
/**
* is in trash page
*/
isTrash = this.define<boolean>('isTrash');
docMode = this.define<DocMode>('docMode');
/**
* is in tag page
*/
isTag = this.define<boolean>('isTag');
tagId = this.define<string>('tagId');
/**
* is in all docs page
*/
isAllDocs = this.define<boolean>('isAllDocs');
define<T>(key: string) {
this.memento.set(key, null);
const livedata$ = LiveData.from(this.memento.watch<T>(key), null);

View File

@@ -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 (
<MenuItem icon={<ImportIcon />} onClick={onImportFile}>

View File

@@ -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(() => {

View File

@@ -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,
]);
}

View File

@@ -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;

View File

@@ -6,6 +6,11 @@ import type { GeneralMixpanelEvent, MixpanelEvents } from './events';
const logger = new DebugLogger('mixpanel');
type Middleware = (
name: string,
properties?: Record<string, unknown>
) => Record<string, unknown>;
function createMixpanel() {
let mixpanel;
if (process.env.MIXPANEL_TOKEN) {
@@ -22,6 +27,8 @@ function createMixpanel() {
);
}
const middlewares = new Set<Middleware>();
const wrapped = {
reset() {
mixpanel.reset();
@@ -40,8 +47,21 @@ function createMixpanel() {
: Record<string, unknown>) &
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<string, unknown>
);
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();

View File

@@ -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]);
}

View File

@@ -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<QuotaQuery['currentUser']>['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();
}
}

View File

@@ -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 (
<>
<ViewHeader>

View File

@@ -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<AffineEditorContainer | null>(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);