mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
feat(core): add globalcontext info to mixpanel track (#7681)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user