From 731a4c952f833d8dff2c2e127cfa0c523f9fe595 Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Mon, 31 Mar 2025 11:45:32 +0000 Subject: [PATCH] feat(core): track for integration (#11128) --- .../integration/readwise/connect.tsx | 4 ++ .../integration/readwise/connected.tsx | 3 + .../integration/readwise/import-dialog.tsx | 57 +++++++++++++++---- .../integration/readwise/index.tsx | 11 +++- .../integration/readwise/setting-dialog.tsx | 34 ++++++++++- .../integration/readwise/track.ts | 25 ++++++++ packages/frontend/track/src/events.ts | 57 ++++++++++++++++++- 7 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/track.ts diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connect.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connect.tsx index 6b71aa5474..014ff3c6cf 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connect.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connect.tsx @@ -17,6 +17,7 @@ import { getTokenLink, inputErrorMsg, } from './index.css'; +import { readwiseTrack } from './track'; const ConnectDialog = ({ onClose, @@ -48,6 +49,9 @@ const ConnectDialog = ({ const handleResult = useCallback( (success: boolean, token: string) => { + readwiseTrack.connectIntegration({ + result: success ? 'success' : 'failed', + }); if (success) { onSuccess(token); } else { diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connected.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connected.tsx index 3af093653e..bca2822039 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connected.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connected.tsx @@ -7,6 +7,7 @@ import { useCallback, useState } from 'react'; import * as styles from './connected.css'; import { actionButton } from './index.css'; +import { readwiseTrack } from './track'; export const DisconnectDialog = ({ onClose }: { onClose: () => void }) => { const t = useI18n(); @@ -21,11 +22,13 @@ export const DisconnectDialog = ({ onClose }: { onClose: () => void }) => { const handleCancel = useCallback(() => onClose(), [onClose]); const handleKeep = useCallback(() => { readwise.disconnect(); + readwiseTrack.disconnectIntegration({ method: 'keep' }); onClose(); }, [onClose, readwise]); const handleDelete = useAsyncCallback(async () => { await readwise.deleteAll(); readwise.disconnect(); + readwiseTrack.disconnectIntegration({ method: 'delete' }); onClose(); }, [onClose, readwise]); diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/import-dialog.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/import-dialog.tsx index 4e916188f2..08aa07861e 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/import-dialog.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/import-dialog.tsx @@ -14,6 +14,7 @@ import { InformationFillDuotoneIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; import clsx from 'clsx'; import { + type ChangeEvent, type Dispatch, forwardRef, type SetStateAction, @@ -26,6 +27,7 @@ import { import { Virtuoso } from 'react-virtuoso'; import * as styles from './import-dialog.css'; +import { readwiseTrack } from './track'; export const ImportDialog = ({ onClose }: { onClose: () => void }) => { const t = useI18n(); @@ -54,6 +56,13 @@ export const ImportDialog = ({ onClose }: { onClose: () => void }) => { ); const handleConfirmImport = useCallback( (ids: string[]) => { + readwiseTrack.confirmIntegrationImport({ + control: 'Readwise import list', + method: readwise.setting$('lastImportedAt').value + ? 'withtimestamp' + : 'new', + }); + if (ids.length === 0) { onClose(); return; @@ -65,15 +74,26 @@ export const ImportDialog = ({ onClose }: { onClose: () => void }) => { abortControllerRef.current = abortController; const signal = abortController.signal; + const startTime = Date.now(); readwise .highlightsToAffineDocs(selectedHighlights.reverse(), books, { signal, onProgress: setImportProgress, onComplete: () => { + readwiseTrack.completeIntegrationImport({ + total: selectedHighlights.length, + done: selectedHighlights.length, + time: (Date.now() - startTime) / 1000, + }); readwise.updateSetting('lastImportedAt', timestamp); onClose(); }, onAbort: finished => { + readwiseTrack.abortIntegrationImport({ + total: selectedHighlights.length, + done: finished, + time: (Date.now() - startTime) / 1000, + }); notify({ icon: , style: 'normal', @@ -184,6 +204,10 @@ const SelectStage = ({ const [selected, setSelected] = useState([]); const handleResetLastImportedAt = useCallback(() => { + readwiseTrack.startIntegrationImport({ + method: 'cleartimestamp', + control: 'Readwise import list', + }); readwise.updateSetting('lastImportedAt', undefined); onResetLastImportedAt(); }, [onResetLastImportedAt, readwise]); @@ -315,11 +339,17 @@ const HighlightTable = ({ .catch(console.error); }, [readwise]); - const handleToggleSelectAll = useCallback(() => { - setSelected(prev => - prev.length === highlights.length ? [] : highlights.map(h => h.id) - ); - }, [highlights, setSelected]); + const handleToggleSelectAll = useCallback( + (_: ChangeEvent, checked: boolean) => { + readwiseTrack.selectIntegrationImport({ + method: 'all', + option: checked ? 'on' : 'off', + control: 'Readwise import list', + }); + setSelected(checked ? highlights.map(h => h.id) : []); + }, + [highlights, setSelected] + ); return (
@@ -358,14 +388,17 @@ const HighlightTable = ({
{ - setSelected(prev => { - if (prev.includes(highlight.id)) { - return prev.filter(id => id !== highlight.id); - } else { - return [...prev, highlight.id]; - } + onChange={(_: ChangeEvent, checked: boolean) => { + readwiseTrack.selectIntegrationImport({ + method: 'single', + option: checked ? 'on' : 'off', + control: 'Readwise import list', }); + setSelected( + checked + ? [...selected, highlight.id] + : selected.filter(id => id !== highlight.id) + ); }} />
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/index.tsx index 4a0c211047..f9042f5578 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/index.tsx @@ -16,6 +16,7 @@ import { ConnectButton } from './connect'; import { ConnectedActions } from './connected'; import { ImportDialog } from './import-dialog'; import { SettingDialog } from './setting-dialog'; +import { readwiseTrack } from './track'; export const ReadwiseIntegration = () => { const t = useI18n(); @@ -42,6 +43,14 @@ export const ReadwiseIntegration = () => { setOpenImportDialog(true); }, []); + const onImportClick = useCallback(() => { + readwiseTrack.startIntegrationImport({ + method: settings?.lastImportedAt ? 'withtimestamp' : 'new', + control: 'Readwise Card', + }); + handleImport(); + }, [handleImport, settings?.lastImportedAt]); + return ( { {token ? ( <> - + {openSetting && ( { + readwiseTrack.modifyIntegrationSettings({ + item, + option, + method, + }); +}; + const Divider = () => { return
  • ; }; @@ -72,6 +85,7 @@ const NewHighlightSetting = () => { const toggle = useCallback( (value: boolean) => { + trackModifySetting('New', value ? 'on' : 'off'); readwise.updateSetting('syncNewHighlights', value); }, [readwise] @@ -98,6 +112,7 @@ const UpdateStrategySetting = () => { const toggle = useCallback( (value: boolean) => { + trackModifySetting('Update', value ? 'on' : 'off', 'append'); if (!value) readwise.updateSetting('updateStrategy', undefined); else readwise.updateSetting('updateStrategy', 'append'); }, @@ -106,6 +121,7 @@ const UpdateStrategySetting = () => { const handleUpdate = useCallback( (value: ReadwiseConfig['updateStrategy']) => { + trackModifySetting('Update', 'on', value); readwise.updateSetting('updateStrategy', value); }, [readwise] @@ -167,12 +183,23 @@ const UpdateStrategySetting = () => { const StartImport = ({ onImport }: { onImport: () => void }) => { const t = useI18n(); + const readwise = useService(IntegrationService).readwise; + + const handleImport = useCallback(() => { + const lastImportedAt = readwise.setting$('lastImportedAt').value; + readwiseTrack.startIntegrationImport({ + method: lastImportedAt ? 'withtimestamp' : 'new', + control: 'Readwise settings', + }); + onImport(); + }, [onImport, readwise]); + return ( - @@ -226,18 +253,23 @@ const TagsSetting = () => { ); const onSelectTag = useCallback( (tagId: string) => { + trackModifySetting('Tag', 'on'); updateReadwiseTags([...(tagIds ?? []), tagId]); }, [tagIds, updateReadwiseTags] ); const onDeselectTag = useCallback( (tagId: string) => { + trackModifySetting('Tag', 'off'); updateReadwiseTags(tagIds?.filter(id => id !== tagId) ?? []); }, [tagIds, updateReadwiseTags] ); const onDeleteTag = useCallback( (tagId: string) => { + if (tagIds?.includes(tagId)) { + trackModifySetting('Tag', 'off'); + } tagService.tagList.deleteTag(tagId); updateReadwiseTags(tagIds ?? []); }, diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/track.ts b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/track.ts new file mode 100644 index 0000000000..fc430ddc58 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/track.ts @@ -0,0 +1,25 @@ +import track from '@affine/track'; + +/** + * Wrap the track function to add default properties to the first argument + */ +export const readwiseTrack = new Proxy(track.$.settingsPanel.integrationList, { + get(target, key, receiver) { + const original = Reflect.get(target, key, receiver); + + if (typeof original !== 'function') { + return original; + } + + return function (this: unknown, ...args: unknown[]) { + if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) { + args[0] = { + type: 'readwise', + control: 'Readwise Card', + ...args[0], + }; + } + return original.apply(this, args); + }; + }, +}); diff --git a/packages/frontend/track/src/events.ts b/packages/frontend/track/src/events.ts index 4a2ba279e7..1f383f8450 100644 --- a/packages/frontend/track/src/events.ts +++ b/packages/frontend/track/src/events.ts @@ -149,6 +149,18 @@ type TemplateEvents = 'openTemplateListMenu'; type NotificationEvents = 'openInbox' | 'clickNotification'; // END SECTION +// SECTION: Integration +type IntegrationEvents = + | 'connectIntegration' + | 'disconnectIntegration' + | 'modifyIntegrationSettings' + | 'startIntegrationImport' + | 'selectIntegrationImport' + | 'confirmIntegrationImport' + | 'abortIntegrationImport' + | 'completeIntegrationImport'; +// END SECTION + type UserEvents = | GeneralEvents | AppEvents @@ -167,8 +179,8 @@ type UserEvents = | DNDEvents | AttachmentEvents | TemplateEvents - | NotificationEvents; - + | NotificationEvents + | IntegrationEvents; interface PageDivision { [page: string]: { [segment: string]: { @@ -235,6 +247,16 @@ const PageEvents = { ], billing: ['viewPlans', 'bookDemo'], about: ['checkUpdates', 'downloadUpdate', 'changeAppSetting'], + integrationList: [ + 'connectIntegration', + 'disconnectIntegration', + 'modifyIntegrationSettings', + 'startIntegrationImport', + 'selectIntegrationImport', + 'confirmIntegrationImport', + 'abortIntegrationImport', + 'completeIntegrationImport', + ], }, cmdk: { recent: ['recentDocs'], @@ -485,6 +507,10 @@ type ImportArgs = { docCount: number; }; }; +type IntegrationArgs> = { + type: string; + control: 'Readwise Card' | 'Readwise settings' | 'Readwise import list'; +} & T; export type EventArgs = { createWorkspace: { flavour: string }; @@ -556,6 +582,33 @@ export type EventArgs = { item: 'read' | 'button' | 'dismiss'; button?: string; }; + connectIntegration: IntegrationArgs<{ result: 'success' | 'failed' }>; + disconnectIntegration: IntegrationArgs<{ method: 'keep' | 'delete' }>; + modifyIntegrationSettings: IntegrationArgs<{ + item: string; + option: any; + method: any; + }>; + startIntegrationImport: IntegrationArgs<{ + method: 'new' | 'withtimestamp' | 'cleartimestamp'; + }>; + selectIntegrationImport: IntegrationArgs<{ + method: 'single' | 'all'; + option: 'on' | 'off'; + }>; + confirmIntegrationImport: IntegrationArgs<{ + method: 'new' | 'withtimestamp'; + }>; + abortIntegrationImport: IntegrationArgs<{ + time: number; + done: number; + total: number; + }>; + completeIntegrationImport: IntegrationArgs<{ + time: number; + done: number; + total: number; + }>; }; // for type checking