diff --git a/packages/frontend/core/src/commands/affine-navigation.tsx b/packages/frontend/core/src/commands/affine-navigation.tsx
index 106bdb8fa8c20..026a8a7079ac9 100644
--- a/packages/frontend/core/src/commands/affine-navigation.tsx
+++ b/packages/frontend/core/src/commands/affine-navigation.tsx
@@ -6,7 +6,7 @@ import type { createStore } from 'jotai';
import { openSettingModalAtom, openWorkspaceListModalAtom } from '../atoms';
import type { useNavigateHelper } from '../hooks/use-navigate-helper';
-import { mixpanel } from '../mixpanel';
+import { mixpanel, track } from '../mixpanel';
import { registerAffineCommand } from './registry';
export function registerAffineNavigationCommands({
@@ -77,10 +77,7 @@ export function registerAffineNavigationCommands({
label: t['com.affine.cmdk.affine.navigation.open-settings'](),
keyBinding: '$mod+,',
run() {
- mixpanel.track('SettingsViewed', {
- // page:
- segment: 'cmdk',
- });
+ track.$.cmdk.settings.openSettings();
store.set(openSettingModalAtom, s => ({
activeTab: 'appearance',
open: !s.open,
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/actions.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/actions.tsx
index 2f14c3c00d6c4..1dfbb5c885fdf 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/actions.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/actions.tsx
@@ -1,7 +1,6 @@
import { getDowngradeQuestionnaireLink } from '@affine/core/hooks/affine/use-subscription-notify';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { mixpanel } from '@affine/core/mixpanel';
-import type { MixpanelEvents } from '@affine/core/mixpanel/events';
import { SubscriptionPlan } from '@affine/graphql';
import { useLiveData, useService } from '@toeverything/infra';
import { nanoid } from 'nanoid';
@@ -25,7 +24,7 @@ export const CancelAction = ({
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
- module: MixpanelEvents['PlanChangeStarted']['module'];
+ module: string;
} & PropsWithChildren) => {
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
const [isMutating, setIsMutating] = useState(false);
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/cancel.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/cancel.tsx
index 50c9ffb80db29..a0652f836f42f 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/cancel.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/cancel.tsx
@@ -3,7 +3,6 @@ import { useDowngradeNotify } from '@affine/core/components/affine/subscription-
import { getDowngradeQuestionnaireLink } from '@affine/core/hooks/affine/use-subscription-notify';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { mixpanel } from '@affine/core/mixpanel';
-import type { MixpanelEvents } from '@affine/core/mixpanel/events';
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
import { SubscriptionPlan } from '@affine/graphql';
import { useI18n } from '@affine/i18n';
@@ -12,7 +11,7 @@ import { nanoid } from 'nanoid';
import { useState } from 'react';
export interface AICancelProps extends ButtonProps {
- module: MixpanelEvents['PlanChangeStarted']['module'];
+ module: string;
}
export const AICancel = ({ module, ...btnProps }: AICancelProps) => {
const t = useI18n();
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/resume.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/resume.tsx
index b5813bd18fe9e..b84cad8d05943 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/resume.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/resume.tsx
@@ -6,7 +6,6 @@ import {
} from '@affine/component';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { mixpanel } from '@affine/core/mixpanel';
-import type { MixpanelEvents } from '@affine/core/mixpanel/events';
import { SubscriptionService } from '@affine/core/modules/cloud';
import { SubscriptionPlan } from '@affine/graphql';
import { useI18n } from '@affine/i18n';
@@ -17,7 +16,7 @@ import { nanoid } from 'nanoid';
import { useState } from 'react';
export interface AIResumeProps extends ButtonProps {
- module: MixpanelEvents['PlanChangeStarted']['module'];
+ module: string;
}
export const AIResume = ({ module, ...btnProps }: AIResumeProps) => {
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 1e0d0599b2f20..5c740525c58bb 100644
--- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx
+++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx
@@ -81,10 +81,6 @@ export const RootAppSidebar = (): ReactElement => {
const cmdkQuickSearchService = useService(CMDKQuickSearchService);
const onOpenQuickSearchModal = useCallback(() => {
cmdkQuickSearchService.toggle();
- mixpanel.track('QuickSearchOpened', {
- segment: 'navigation panel',
- control: 'search button',
- });
}, [cmdkQuickSearchService]);
const allPageActive = currentPath === '/all';
@@ -157,6 +153,7 @@ export const RootAppSidebar = (): ReactElement => {
diff --git a/packages/frontend/core/src/mixpanel/__tests__/auto.spec.ts b/packages/frontend/core/src/mixpanel/__tests__/auto.spec.ts
new file mode 100644
index 0000000000000..3fdb241a8630c
--- /dev/null
+++ b/packages/frontend/core/src/mixpanel/__tests__/auto.spec.ts
@@ -0,0 +1,148 @@
+/**
+ * @vitest-environment happy-dom
+ */
+import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
+
+import { enableAutoTrack, makeTracker } from '../auto';
+
+describe('callable events chain', () => {
+ const call = vi.fn();
+ const track = makeTracker(call);
+
+ beforeEach(() => {
+ call.mockClear();
+ });
+
+ test('should call track with event and props', () => {
+ // @ts-expect-error fake chain
+ track.pageA.segmentA.moduleA.eventA();
+
+ expect(call).toBeCalledWith('eventA', {
+ page: 'pageA',
+ segment: 'segmentA',
+ module: 'moduleA',
+ });
+ });
+
+ test('should be able to override props', () => {
+ // @ts-expect-error fake chain
+ track.pageA.segmentA.moduleA.eventA({ page: 'pageB', control: 'controlA' });
+
+ expect(call).toBeCalledWith('eventA', {
+ page: 'pageB',
+ segment: 'segmentA',
+ module: 'moduleA',
+ control: 'controlA',
+ });
+ });
+
+ test('should be able to append custom props', () => {
+ // @ts-expect-error fake chain
+ track.pageA.segmentA.moduleA.eventA({ custom: 'prop' });
+
+ expect(call).toBeCalledWith('eventA', {
+ page: 'pageA',
+ segment: 'segmentA',
+ module: 'moduleA',
+ custom: 'prop',
+ });
+ });
+
+ test('should be able to ignore matrix named with placeholder `$`', () => {
+ // @ts-expect-error fake chain
+ track.$.segmentA.moduleA.eventA();
+ // @ts-expect-error fake chain
+ track.pageA.$.moduleA.eventA();
+ // @ts-expect-error fake chain
+ track.pageA.segmentA.$.eventA();
+ // @ts-expect-error fake chain
+ track.$.$.$.eventA();
+
+ const args = [
+ {
+ segment: 'segmentA',
+ module: 'moduleA',
+ },
+ {
+ page: 'pageA',
+ module: 'moduleA',
+ },
+ {
+ page: 'pageA',
+ segment: 'segmentA',
+ },
+ {},
+ ];
+
+ args.forEach((arg, i) => {
+ expect(call).toHaveBeenNthCalledWith(i + 1, 'eventA', arg);
+ });
+ });
+});
+
+describe('auto track with dom dataset', () => {
+ const root = document.createElement('div');
+ const call = vi.fn();
+ beforeAll(() => {
+ call.mockReset();
+ root.innerHTML = '';
+ return enableAutoTrack(root, call);
+ });
+
+ test('should ignore if data-event-props not set', () => {
+ const nonTrackBtn = document.createElement('button');
+ root.append(nonTrackBtn);
+
+ nonTrackBtn.click();
+
+ expect(call).not.toBeCalled();
+ });
+
+ test('should track event with props', () => {
+ const btn = document.createElement('button');
+ btn.dataset.eventProps = 'allDocs.header.actions.createDoc';
+ root.append(btn);
+
+ btn.click();
+
+ expect(call).toBeCalledWith('createDoc', {
+ page: 'allDocs',
+ segment: 'header',
+ module: 'actions',
+ });
+ });
+
+ test('should track event with single', () => {
+ const btn = document.createElement('button');
+ btn.dataset.eventProps = 'allDocs.header.actions.createDoc';
+ btn.dataset.eventArg = 'test';
+ root.append(btn);
+
+ btn.click();
+
+ expect(call).toBeCalledWith('createDoc', {
+ page: 'allDocs',
+ segment: 'header',
+ module: 'actions',
+ arg: 'test',
+ });
+ });
+
+ test('should track event with multiple args', () => {
+ const btn = document.createElement('button');
+ btn.dataset.eventProps = 'allDocs.header.actions.createDoc';
+ btn.dataset.eventArgsFoo = 'bar';
+ btn.dataset.eventArgsBaz = 'qux';
+ root.append(btn);
+
+ btn.click();
+
+ expect(call).toBeCalledWith('createDoc', {
+ page: 'allDocs',
+ segment: 'header',
+ module: 'actions',
+ foo: 'bar',
+ baz: 'qux',
+ });
+ });
+});
diff --git a/packages/frontend/core/src/mixpanel/auto.ts b/packages/frontend/core/src/mixpanel/auto.ts
new file mode 100644
index 0000000000000..ea63313331ee3
--- /dev/null
+++ b/packages/frontend/core/src/mixpanel/auto.ts
@@ -0,0 +1,118 @@
+import { DebugLogger } from '@affine/debug';
+
+import type { CallableEventsChain, EventsUnion } from './types';
+
+const logger = new DebugLogger('mixpanel');
+
+interface TrackFn {
+ (event: string, props: Record): void;
+}
+
+const levels = ['page', 'segment', 'module', 'event'] as const;
+export function makeTracker(trackFn: TrackFn): CallableEventsChain {
+ function makeTrackerInner(level: number, info: Record) {
+ const proxy = new Proxy({} as Record, {
+ get(target, prop) {
+ if (
+ typeof prop !== 'string' ||
+ prop === '$$typeof' /* webpack hot load reading this prop */
+ ) {
+ return undefined;
+ }
+
+ if (levels[level] === 'event') {
+ return (arg: string | Record) => {
+ trackFn(prop, {
+ ...info,
+ ...(typeof arg === 'string' ? { arg } : arg),
+ });
+ };
+ } else {
+ let levelProxy = target[prop];
+ if (levelProxy) {
+ return levelProxy;
+ }
+
+ levelProxy = makeTrackerInner(
+ level + 1,
+ prop === '$' ? { ...info } : { ...info, [levels[level]]: prop }
+ );
+ target[prop] = levelProxy;
+ return levelProxy;
+ }
+ },
+ });
+
+ return proxy;
+ }
+
+ return makeTrackerInner(0, {}) as CallableEventsChain;
+}
+
+/**
+ * listen on clicking on all subtree elements and auto track events if defined
+ *
+ * @example
+ *
+ * ```html
+ *
+ * ```
+ */
+export function enableAutoTrack(root: HTMLElement, trackFn: TrackFn) {
+ const listener = (e: Event) => {
+ const el = e.target as HTMLElement | null;
+ if (!el) {
+ return;
+ }
+ const dataset = el.dataset;
+
+ if (dataset['eventProps']) {
+ const args: Record = {};
+ if (dataset['eventArg'] !== undefined) {
+ args['arg'] = dataset['event-arg'];
+ } else {
+ for (const argName of Object.keys(dataset)) {
+ if (argName.startsWith('eventArgs')) {
+ args[argName.slice(9).toLowerCase()] = dataset[argName];
+ }
+ }
+ }
+
+ const props = dataset['eventProps']
+ .split('.')
+ .map(name => (name === '$' ? undefined : name));
+ if (props.length !== levels.length) {
+ logger.error('Invalid event props on element', el);
+ return;
+ }
+
+ const event = props[3];
+
+ if (!event) {
+ logger.error('Invalid event props on element', el);
+ return;
+ }
+
+ trackFn(event, {
+ page: props[0] as any,
+ segment: props[1],
+ module: props[2],
+ ...args,
+ });
+ }
+ };
+
+ root.addEventListener('click', listener, {});
+ return () => {
+ root.removeEventListener('click', listener);
+ };
+}
+
+declare module 'react' {
+ // we have to declare `T` but it's actually not used
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ interface HTMLAttributes {
+ 'data-event-props'?: EventsUnion;
+ 'data-event-arg'?: string;
+ }
+}
diff --git a/packages/frontend/core/src/mixpanel/events.ts b/packages/frontend/core/src/mixpanel/events.ts
new file mode 100644
index 0000000000000..464ec9796b53b
--- /dev/null
+++ b/packages/frontend/core/src/mixpanel/events.ts
@@ -0,0 +1,43 @@
+// let '$' stands for unspecific matrix
+/* eslint-disable rxjs/finnish */
+export interface Events {
+ $: {
+ cmdk: {
+ settings: ['openSettings', 'changeLanguage'];
+ };
+ navigationPanel: {
+ generalFunction: [
+ 'quickSearch',
+ 'createDoc',
+ 'goToAllPage',
+ 'goToJournals',
+ 'openSettings',
+ ];
+ collection: ['createDoc'];
+ bottomButtong: ['downloadApp', 'restartAndInstallUpdate'];
+ others: ['openTrash', 'export'];
+ };
+ };
+ doc: {
+ editor: {
+ formatToolbar: ['bold'];
+ };
+ };
+ edgeless: {
+ editor: {
+ formatToolbar: ['drawConnector'];
+ };
+ };
+ // remove when type added
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ allDocs: {};
+ // remove when type added
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ collection: {};
+ // remove when type added
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ tag: {};
+ // remove when type added
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ trash: {};
+}
diff --git a/packages/frontend/core/src/mixpanel/events/index.ts b/packages/frontend/core/src/mixpanel/events/index.ts
deleted file mode 100644
index 41a748d09ce7a..0000000000000
--- a/packages/frontend/core/src/mixpanel/events/index.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { PlanChangeStartedEvent } from './plan-change-started';
-import type { PlanChangeSucceededEvent } from './plan-change-succeed';
-
-export interface MixpanelEvents {
- PlanChangeStarted: PlanChangeStartedEvent;
- PlanChangeSucceeded: PlanChangeSucceededEvent;
- OAuth: {
- provider: string;
- };
-}
-
-export interface GeneralMixpanelEvent {
- // location
- page?: string | null;
- segment?: string | null;
- module?: string | null;
- control?: string | null;
-
- // entity
- type?: string | null;
- category?: string | null;
- id?: string | null;
-}
diff --git a/packages/frontend/core/src/mixpanel/events/plan-change-started.ts b/packages/frontend/core/src/mixpanel/events/plan-change-started.ts
deleted file mode 100644
index cc2e3aec81cf5..0000000000000
--- a/packages/frontend/core/src/mixpanel/events/plan-change-started.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
-
-/**
- * Before subscription plan changed
- */
-export interface PlanChangeStartedEvent {
- segment?: 'settings panel';
- module?: 'pricing plan list' | 'billing subscription list';
- control?:
- | 'new subscription' // no subscription before
- | 'cancel'
- | 'paying' // resume: subscribed before
- | 'plan cancel action';
- type?: SubscriptionPlan;
- category?: SubscriptionRecurring;
-}
diff --git a/packages/frontend/core/src/mixpanel/events/plan-change-succeed.ts b/packages/frontend/core/src/mixpanel/events/plan-change-succeed.ts
deleted file mode 100644
index 3d96022434ca1..0000000000000
--- a/packages/frontend/core/src/mixpanel/events/plan-change-succeed.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { PlanChangeStartedEvent } from './plan-change-started';
-
-/**
- * Subscription plan changed successfully
- */
-export type PlanChangeSucceededEvent = Pick<
- PlanChangeStartedEvent,
- 'control' | 'type' | 'category' | 'segment'
->;
diff --git a/packages/frontend/core/src/mixpanel/index.ts b/packages/frontend/core/src/mixpanel/index.ts
index 6f3b73771d335..6d962ddc64260 100644
--- a/packages/frontend/core/src/mixpanel/index.ts
+++ b/packages/frontend/core/src/mixpanel/index.ts
@@ -1,108 +1,8 @@
-import { DebugLogger } from '@affine/debug';
-import type { OverridedMixpanel } from 'mixpanel-browser';
-import mixpanelBrowser from 'mixpanel-browser';
+import { enableAutoTrack, makeTracker } from './auto';
+import { mixpanel } from './mixpanel';
-import type { GeneralMixpanelEvent, MixpanelEvents } from './events';
+export const track = makeTracker((event, props) => {
+ mixpanel.track(event, props);
+});
-const logger = new DebugLogger('mixpanel');
-
-type Middleware = (
- name: string,
- properties?: Record
-) => Record;
-
-function createMixpanel() {
- let mixpanel;
- if (process.env.MIXPANEL_TOKEN) {
- mixpanelBrowser.init(process.env.MIXPANEL_TOKEN || '', {
- track_pageview: true,
- persistence: 'localStorage',
- api_host: 'https://telemetry.affine.run',
- });
- mixpanel = mixpanelBrowser;
- } else {
- mixpanel = new Proxy(
- function () {} as unknown as OverridedMixpanel,
- createProxyHandler()
- );
- }
-
- const middlewares = new Set();
-
- const wrapped = {
- reset() {
- mixpanel.reset();
- mixpanel.register({
- appVersion: runtimeConfig.appVersion,
- environment: runtimeConfig.appBuildType,
- editorVersion: runtimeConfig.editorVersion,
- isSelfHosted: Boolean(runtimeConfig.isSelfHosted),
- isDesktop: environment.isDesktop,
- });
- },
- track<
- T extends string,
- P extends (T extends keyof MixpanelEvents
- ? MixpanelEvents[T]
- : Record) &
- GeneralMixpanelEvent,
- >(event_name: T, properties?: P) {
- 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();
- },
- opt_in_tracking() {
- mixpanel.opt_in_tracking();
- },
- has_opted_in_tracking() {
- mixpanel.has_opted_in_tracking();
- },
- has_opted_out_tracking() {
- mixpanel.has_opted_out_tracking();
- },
- identify(unique_id?: string) {
- mixpanel.identify(unique_id);
- },
- get people() {
- return mixpanel.people;
- },
- track_pageview(properties?: { location?: string }) {
- logger.debug('track_pageview', properties);
- mixpanel.track_pageview(properties);
- },
- };
-
- wrapped.reset();
-
- return wrapped;
-}
-
-export const mixpanel = createMixpanel();
-
-function createProxyHandler() {
- const handler = {
- get: () => {
- return new Proxy(
- function () {} as unknown as OverridedMixpanel,
- createProxyHandler()
- );
- },
- apply: () => {},
- } as ProxyHandler;
- return handler;
-}
+export { enableAutoTrack, mixpanel };
diff --git a/packages/frontend/core/src/mixpanel/mixpanel.d.ts b/packages/frontend/core/src/mixpanel/mixpanel.d.ts
deleted file mode 100644
index 0903c124541ad..0000000000000
--- a/packages/frontend/core/src/mixpanel/mixpanel.d.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports
-import * as mixpanel from 'mixpanel-browser';
-
-import type { GeneralMixpanelEvent, MixpanelEvents } from './events';
-
-declare module 'mixpanel-browser' {
- export interface OverridedMixpanel {
- track<
- T extends string,
- P extends (T extends keyof MixpanelEvents
- ? MixpanelEvents[T]
- : Record) &
- GeneralMixpanelEvent,
- >(
- event_name: T,
- properties?: P
- ): void;
- }
-}
diff --git a/packages/frontend/core/src/mixpanel/mixpanel.ts b/packages/frontend/core/src/mixpanel/mixpanel.ts
new file mode 100644
index 0000000000000..fe92998507067
--- /dev/null
+++ b/packages/frontend/core/src/mixpanel/mixpanel.ts
@@ -0,0 +1,101 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports
+import { DebugLogger } from '@affine/debug';
+import type { OverridedMixpanel } from 'mixpanel-browser';
+import mixpanelBrowser from 'mixpanel-browser';
+
+const logger = new DebugLogger('mixpanel');
+
+type Middleware = (
+ name: string,
+ properties?: Record
+) => Record;
+
+function createMixpanel() {
+ let mixpanel;
+ if (process.env.MIXPANEL_TOKEN) {
+ mixpanelBrowser.init(process.env.MIXPANEL_TOKEN || '', {
+ track_pageview: true,
+ persistence: 'localStorage',
+ api_host: 'https://telemetry.affine.run',
+ });
+ mixpanel = mixpanelBrowser;
+ } else {
+ mixpanel = new Proxy(
+ function () {} as unknown as OverridedMixpanel,
+ createProxyHandler()
+ );
+ }
+
+ const middlewares = new Set();
+
+ const wrapped = {
+ reset() {
+ mixpanel.reset();
+ mixpanel.register({
+ appVersion: runtimeConfig.appVersion,
+ environment: runtimeConfig.appBuildType,
+ editorVersion: runtimeConfig.editorVersion,
+ isSelfHosted: Boolean(runtimeConfig.isSelfHosted),
+ isDesktop: environment.isDesktop,
+ });
+ },
+ track(event_name: string, properties?: Record) {
+ 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();
+ },
+ opt_in_tracking() {
+ mixpanel.opt_in_tracking();
+ },
+ has_opted_in_tracking() {
+ mixpanel.has_opted_in_tracking();
+ },
+ has_opted_out_tracking() {
+ mixpanel.has_opted_out_tracking();
+ },
+ identify(unique_id?: string) {
+ mixpanel.identify(unique_id);
+ },
+ get people() {
+ return mixpanel.people;
+ },
+ track_pageview(properties?: { location?: string }) {
+ logger.debug('track_pageview', properties);
+ mixpanel.track_pageview(properties);
+ },
+ };
+
+ wrapped.reset();
+
+ return wrapped;
+}
+
+export const mixpanel = createMixpanel();
+
+function createProxyHandler() {
+ const handler = {
+ get: () => {
+ return new Proxy(
+ function () {} as unknown as OverridedMixpanel,
+ createProxyHandler()
+ );
+ },
+ apply: () => {},
+ } as ProxyHandler;
+ return handler;
+}
diff --git a/packages/frontend/core/src/mixpanel/types.ts b/packages/frontend/core/src/mixpanel/types.ts
new file mode 100644
index 0000000000000..97ad213a522d2
--- /dev/null
+++ b/packages/frontend/core/src/mixpanel/types.ts
@@ -0,0 +1,60 @@
+import type { Events } from './events';
+
+export type CallableEventsChain = {
+ [Page in keyof Events]: {
+ [Segment in keyof Events[Page]]: {
+ [Module in keyof Events[Page][Segment]]: {
+ // @ts-expect-error ignore `symbol | number` as key
+ [Control in Events[Page][Segment][Module][number]]: (
+ arg?: string
+ ) => void;
+ };
+ };
+ };
+};
+
+export type EventsUnion = {
+ [Page in keyof Events]: {
+ [Segment in keyof Events[Page]]: {
+ [Module in keyof Events[Page][Segment]]: {
+ // @ts-expect-error ignore `symbol | number` as key
+ [Control in Events[Page][Segment][Module][number]]: `${Page}.${Segment}.${Module}.${Control}`;
+ // @ts-expect-error ignore `symbol | number` as key
+ }[Events[Page][Segment][Module][number]];
+ }[keyof Events[Page][Segment]];
+ }[keyof Events[Page]];
+}[keyof Events];
+
+// page > segment > module > [controls]
+type IsFourLevelsDeep<
+ T,
+ Depth extends number[] = [],
+> = Depth['length'] extends 3
+ ? T extends Array
+ ? true
+ : false
+ : T extends object
+ ? {
+ [K in keyof T]: IsFourLevelsDeep;
+ }[keyof T] extends true
+ ? true
+ : false
+ : false;
+
+// for type checking
+export const _assertIsAllEventsDefinedInFourLevels: IsFourLevelsDeep =
+ true;
+
+export interface EventProps {
+ // location
+ page?: keyof Events;
+ segment?: string;
+ module?: string;
+ control?: string;
+ arg?: string;
+
+ // entity
+ type?: string;
+ category?: string;
+ id?: string;
+}
diff --git a/packages/frontend/core/src/modules/telemetry/services/telemetry.ts b/packages/frontend/core/src/modules/telemetry/services/telemetry.ts
index f0d37e823f318..538303022edcf 100644
--- a/packages/frontend/core/src/modules/telemetry/services/telemetry.ts
+++ b/packages/frontend/core/src/modules/telemetry/services/telemetry.ts
@@ -1,4 +1,5 @@
import { mixpanel } from '@affine/core/mixpanel';
+import type { EventProps } from '@affine/core/mixpanel/types';
import type { QuotaQuery } from '@affine/graphql';
import type { GlobalContextService } from '@toeverything/infra';
import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra';
@@ -72,24 +73,24 @@ export class TelemetryService extends Service {
);
}
- extractGlobalContext() {
+ extractGlobalContext(): EventProps {
const globalContext = this.globalContextService.globalContext;
const page = globalContext.isDoc.get()
? globalContext.isTrashDoc.get()
? 'trash'
: globalContext.docMode.get() === 'page'
- ? 'doc editor'
- : 'whiteboard editor'
+ ? 'doc'
+ : 'edgeless'
: globalContext.isAllDocs.get()
- ? 'doc library'
+ ? 'allDocs'
: globalContext.isTrash.get()
- ? 'trash library'
+ ? 'trash'
: globalContext.isCollection.get()
- ? 'collection detail'
+ ? 'collection'
: globalContext.isTag.get()
- ? 'tag detail'
- : 'unknown';
- return { page, activePage: page };
+ ? 'tag'
+ : undefined;
+ return { page };
}
override dispose(): void {
diff --git a/packages/frontend/core/src/telemetry.tsx b/packages/frontend/core/src/telemetry.tsx
index aeceddbb2ee74..868da531446c6 100644
--- a/packages/frontend/core/src/telemetry.tsx
+++ b/packages/frontend/core/src/telemetry.tsx
@@ -2,13 +2,16 @@ import { appSettingAtom } from '@toeverything/infra';
import { useAtomValue } from 'jotai/react';
import { useLayoutEffect } from 'react';
-import { mixpanel } from './mixpanel';
+import { enableAutoTrack, mixpanel } from './mixpanel';
export function Telemetry() {
const settings = useAtomValue(appSettingAtom);
useLayoutEffect(() => {
if (settings.enableTelemetry === false) {
mixpanel.opt_out_tracking();
+ return;
+ } else {
+ return enableAutoTrack(document.body, mixpanel.track);
}
}, [settings.enableTelemetry]);
return null;