feat(editor): track citation events (#12664)

Closes: [BS-3551](https://linear.app/affine-design/issue/BS-3551/citation埋点)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced citation tracking across attachments, bookmarks, embedded documents, paragraphs, footnotes, rename modals, and toolbars for actions like editing, deleting, expanding, and hovering on citations.
  - Introduced a centralized citation service to unify citation detection and telemetry event management.
- **Chores**
  - Updated service exports and telemetry modules to include the new citation service and citation-related event types.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
donteatfriedrice
2025-06-05 03:09:24 +00:00
parent 053efb61f0
commit 1bdccdbd57
14 changed files with 249 additions and 11 deletions
@@ -17,6 +17,7 @@ import {
AttachmentBlockStyles,
} from '@blocksuite/affine-model';
import {
CitationProvider,
DocModeProvider,
FileSizeLimitProvider,
TelemetryProvider,
@@ -37,6 +38,7 @@ import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { guard } from 'lit/directives/guard.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
import { filter } from 'rxjs/operators';
import { AttachmentEmbedProvider } from './embed';
import { styles } from './styles';
@@ -79,8 +81,12 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
return this.std.get(FileSizeLimitProvider).maxFileSize;
}
get citationService() {
return this.std.get(CitationProvider);
}
get isCitation() {
return !!this.model.props.footnoteIdentifier;
return this.citationService.isCitationModel(this.model);
}
convertTo = () => {
@@ -139,6 +145,34 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
selectionManager.setGroup('note', [blockSelection]);
}
private readonly _trackCitationDeleteEvent = () => {
// Check citation delete event
this._disposables.add(
this.std.store.slots.blockUpdated
.pipe(
filter(payload => {
if (!payload.isLocal) return false;
const { flavour, id, type } = payload;
if (
type !== 'delete' ||
flavour !== this.model.flavour ||
id !== this.model.id
)
return false;
const { model } = payload;
if (!this.citationService.isCitationModel(model)) return false;
return true;
})
)
.subscribe(() => {
this.citationService.trackEvent('Delete');
})
);
};
override connectedCallback() {
super.connectedCallback();
@@ -162,6 +196,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
});
});
}
this._trackCitationDeleteEvent();
}
override firstUpdated() {
@@ -1,6 +1,7 @@
import { ConfirmIcon } from '@blocksuite/affine-components/icons';
import { toast } from '@blocksuite/affine-components/toast';
import type { AttachmentBlockModel } from '@blocksuite/affine-model';
import { CitationProvider } from '@blocksuite/affine-shared/services';
import type { EditorHost } from '@blocksuite/std';
import { html } from 'lit';
import { createRef, ref } from 'lit/directives/ref.js';
@@ -33,6 +34,7 @@ export const RenameModal = ({
let fileName = includeExtension ? nameWithoutExtension : originalName;
const extension = includeExtension ? originalExtension : '';
const citationService = editorHost.std.get(CitationProvider);
const abort = () => abortController.abort();
const onConfirm = () => {
@@ -44,6 +46,9 @@ export const RenameModal = ({
model.store.updateBlock(model, {
name: newFileName,
});
if (citationService.isCitationModel(model)) {
citationService.trackEvent('Edit');
}
abort();
};
const onInput = (e: InputEvent) => {
@@ -8,6 +8,7 @@ import type {
} from '@blocksuite/affine-model';
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
import {
CitationProvider,
DocModeProvider,
LinkPreviewServiceIdentifier,
} from '@blocksuite/affine-shared/services';
@@ -18,6 +19,7 @@ import { html } from 'lit';
import { property, query } from 'lit/decorators.js';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { filter } from 'rxjs/operators';
import { refreshBookmarkUrlData } from './utils.js';
@@ -114,11 +116,12 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
);
};
get citationService() {
return this.std.get(CitationProvider);
}
get isCitation() {
return (
!!this.model.props.footnoteIdentifier &&
this.model.props.style === 'citation'
);
return this.citationService.isCitationModel(this.model);
}
get imageProxyService() {
@@ -166,6 +169,31 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
></bookmark-card>`;
};
private readonly _trackCitationDeleteEvent = () => {
// Check citation delete event
this._disposables.add(
this.std.store.slots.blockUpdated
.pipe(
filter(payload => {
if (!payload.isLocal) return false;
const { flavour, id, type } = payload;
if (
type !== 'delete' ||
flavour !== this.model.flavour ||
id !== this.model.id
)
return false;
const { model } = payload;
if (!this.citationService.isCitationModel(model)) return false;
return true;
})
)
.subscribe(() => {
this.citationService.trackEvent('Delete');
})
);
};
override connectedCallback() {
super.connectedCallback();
@@ -203,6 +231,8 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
}
})
);
this._trackCitationDeleteEvent();
}
override disconnectedCallback(): void {
@@ -17,6 +17,7 @@ import {
REFERENCE_NODE,
} from '@blocksuite/affine-shared/consts';
import {
CitationProvider,
DocDisplayMetaProvider,
DocModeProvider,
OpenDocExtensionIdentifier,
@@ -43,6 +44,7 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
import throttle from 'lodash-es/throttle';
import { filter } from 'rxjs/operators';
import * as Y from 'yjs';
import { renderLinkedDocInCard } from '../common/render-linked-doc';
@@ -254,11 +256,12 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
return this.store.readonly;
}
get citationService() {
return this.std.get(CitationProvider);
}
get isCitation() {
return (
!!this.model.props.footnoteIdentifier &&
this.model.props.style === 'citation'
);
return this.citationService.isCitationModel(this.model);
}
private readonly _handleDoubleClick = (event: MouseEvent) => {
@@ -454,6 +457,31 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
);
};
private readonly _trackCitationDeleteEvent = () => {
// Check citation delete event
this._disposables.add(
this.std.store.slots.blockUpdated
.pipe(
filter(payload => {
if (!payload.isLocal) return false;
const { flavour, id, type } = payload;
if (
type !== 'delete' ||
flavour !== this.model.flavour ||
id !== this.model.id
)
return false;
const { model } = payload;
if (!this.citationService.isCitationModel(model)) return false;
return true;
})
)
.subscribe(() => {
this.citationService.trackEvent('Delete');
})
);
};
override connectedCallback() {
super.connectedCallback();
@@ -532,6 +560,8 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
}
})
);
this._trackCitationDeleteEvent();
}
getInitialState(): {
@@ -7,7 +7,10 @@ import {
BLOCK_CHILDREN_CONTAINER_PADDING_LEFT,
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
} from '@blocksuite/affine-shared/consts';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import {
CitationProvider,
DocModeProvider,
} from '@blocksuite/affine-shared/services';
import {
calculateCollapsedSiblings,
getNearestHeadingBefore,
@@ -63,6 +66,10 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
?.getPlaceholder(this.model);
}
get citationService() {
return this.std.get(CitationProvider);
}
get attributeRenderer() {
return this.inlineManager.getRenderer();
}
@@ -94,6 +101,12 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
return this.std.get(DefaultInlineManagerExtension.identifier);
}
get hasCitationSiblings() {
return this.collapsedSiblings.some(sibling =>
this.citationService.isCitationModel(sibling)
);
}
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
@@ -286,6 +299,13 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
collapsed: value,
});
}
if (this.hasCitationSiblings) {
this.citationService.trackEvent('Expand', {
control: 'Source Button',
type: value ? 'Hide' : 'Show',
});
}
}}
></blocksuite-toggle-button>
`