feat(editor): embed iframe block event tracking (#11313)

Close [BS-2958](https://linear.app/affine-design/issue/BS-2958/埋点相关)
This commit is contained in:
donteatfriedrice
2025-04-01 02:50:23 +00:00
parent 3467515ae9
commit 1dbd34177e
12 changed files with 157 additions and 12 deletions

View File

@@ -90,15 +90,15 @@ export const insertEmbedIframeWithUrlCommand: Command<
surfaceBlock.model surfaceBlock.model
); );
gfx.selection.set({
elements: [newBlockId],
editing: false,
});
gfx.tool.setTool( gfx.tool.setTool(
// @ts-expect-error FIXME: resolve after gfx tool refactor // @ts-expect-error FIXME: resolve after gfx tool refactor
'default' 'default'
); );
gfx.selection.set({
elements: [newBlockId],
editing: false,
});
} }
if (!newBlockId) { if (!newBlockId) {

View File

@@ -1,5 +1,9 @@
import { createLitPortal } from '@blocksuite/affine-components/portal'; import { createLitPortal } from '@blocksuite/affine-components/portal';
import type { EmbedIframeBlockModel } from '@blocksuite/affine-model'; import type { EmbedIframeBlockModel } from '@blocksuite/affine-model';
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { WithDisposable } from '@blocksuite/global/lit'; import { WithDisposable } from '@blocksuite/global/lit';
import { EditIcon, InformationIcon, ResetIcon } from '@blocksuite/icons/lit'; import { EditIcon, InformationIcon, ResetIcon } from '@blocksuite/icons/lit';
@@ -191,6 +195,7 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
.model=${this.model} .model=${this.model}
.abortController=${this._editAbortController} .abortController=${this._editAbortController}
.std=${this.std} .std=${this.std}
.inSurface=${this.inSurface}
></embed-iframe-link-edit-popup>`, ></embed-iframe-link-edit-popup>`,
portalStyles: { portalStyles: {
zIndex: 'var(--affine-z-index-popover)', zIndex: 'var(--affine-z-index-popover)',
@@ -210,6 +215,15 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
private readonly _handleRetry = (e: MouseEvent) => { private readonly _handleRetry = (e: MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
this.onRetry(); this.onRetry();
// track retry event
this.telemetryService?.track('ReloadLink', {
type: 'embed iframe block',
page: this.editorMode === 'page' ? 'doc editor' : 'whiteboard editor',
segment: 'editor',
module: 'embed block',
control: 'reload button',
});
}; };
override render() { override render() {
@@ -273,6 +287,16 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
return this.model.doc.readonly; return this.model.doc.readonly;
} }
get telemetryService() {
return this.std.getOptional(TelemetryProvider);
}
get editorMode() {
const docModeService = this.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
return mode ?? 'page';
}
@query('.button.edit') @query('.button.edit')
accessor _editButton: HTMLElement | null = null; accessor _editButton: HTMLElement | null = null;
@@ -288,6 +312,9 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor std!: BlockStdScope; accessor std!: BlockStdScope;
@property({ attribute: false })
accessor inSurface = false;
@property({ attribute: false }) @property({ attribute: false })
accessor options: EmbedIframeStatusCardOptions = { accessor options: EmbedIframeStatusCardOptions = {
layout: 'horizontal', layout: 'horizontal',

View File

@@ -1,3 +1,7 @@
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { SignalWatcher } from '@blocksuite/global/lit'; import { SignalWatcher } from '@blocksuite/global/lit';
import { DoneIcon } from '@blocksuite/icons/lit'; import { DoneIcon } from '@blocksuite/icons/lit';
@@ -67,6 +71,17 @@ export class EmbedIframeLinkEditPopup extends SignalWatcher(
} }
`; `;
protected override track(status: 'success' | 'failure') {
this.telemetryService?.track('EditLink', {
type: 'embed iframe block',
page: this.editorMode === 'page' ? 'doc editor' : 'whiteboard editor',
segment: 'editor',
module: 'embed block',
control: 'edit button',
other: status,
});
}
override render() { override render() {
const isInputEmpty = this.isInputEmpty(); const isInputEmpty = this.isInputEmpty();
const { url$ } = this.model.props; const { url$ } = this.model.props;
@@ -94,4 +109,14 @@ export class EmbedIframeLinkEditPopup extends SignalWatcher(
</div> </div>
`; `;
} }
get telemetryService() {
return this.std.getOptional(TelemetryProvider);
}
get editorMode() {
const docModeService = this.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
return mode ?? 'page';
}
} }

View File

@@ -5,11 +5,22 @@ import {
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { isValidUrl, stopPropagation } from '@blocksuite/affine-shared/utils'; import { isValidUrl, stopPropagation } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/lit'; import { WithDisposable } from '@blocksuite/global/lit';
import { BlockSelection, type BlockStdScope } from '@blocksuite/std'; import { noop } from '@blocksuite/global/utils';
import {
BlockSelection,
type BlockStdScope,
SurfaceSelection,
} from '@blocksuite/std';
import { LitElement } from 'lit'; import { LitElement } from 'lit';
import { property, query, state } from 'lit/decorators.js'; import { property, query, state } from 'lit/decorators.js';
export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) { export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
// this method is used to track the event when the user inputs the link
// it should be overridden by the subclass
protected track(status: 'success' | 'failure') {
noop(status);
}
protected isInputEmpty() { protected isInputEmpty() {
return this._linkInputValue.trim() === ''; return this._linkInputValue.trim() === '';
} }
@@ -33,9 +44,20 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
this.store.transact(() => { this.store.transact(() => {
const blockId = this.store.addBlock(flavour, { url }, parent, index); const blockId = this.store.addBlock(flavour, { url }, parent, index);
this.store.deleteBlock(model); this.store.deleteBlock(model);
this.std.selection.setGroup('note', [ if (this.inSurface) {
this.std.selection.create(BlockSelection, { blockId }), this.std.selection.setGroup('gfx', [
]); this.std.selection.create(
SurfaceSelection,
blockId,
[blockId],
false
),
]);
} else {
this.std.selection.setGroup('note', [
this.std.selection.create(BlockSelection, { blockId }),
]);
}
}); });
this.abortController?.abort(); this.abortController?.abort();
@@ -50,6 +72,7 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
const embedIframeService = this.std.get(EmbedIframeService); const embedIframeService = this.std.get(EmbedIframeService);
if (!embedIframeService) { if (!embedIframeService) {
console.error('iframe EmbedIframeService not found'); console.error('iframe EmbedIframeService not found');
this.track('failure');
return; return;
} }
@@ -68,7 +91,9 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
title: '', title: '',
description: '', description: '',
}); });
this.track('success');
} catch (error) { } catch (error) {
this.track('failure');
this.notificationService?.notify({ this.notificationService?.notify({
title: 'Error in embed iframe creation', title: 'Error in embed iframe creation',
message: error instanceof Error ? error.message : 'Please try again', message: error instanceof Error ? error.message : 'Please try again',
@@ -129,4 +154,7 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor abortController: AbortController | undefined = undefined; accessor abortController: AbortController | undefined = undefined;
@property({ attribute: false })
accessor inSurface = false;
} }

View File

@@ -1,3 +1,7 @@
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { CloseIcon } from '@blocksuite/icons/lit'; import { CloseIcon } from '@blocksuite/icons/lit';
import { baseTheme } from '@toeverything/theme'; import { baseTheme } from '@toeverything/theme';
@@ -16,6 +20,7 @@ export type EmbedLinkInputPopupOptions = {
title?: string; title?: string;
description?: string; description?: string;
placeholder?: string; placeholder?: string;
telemetrySegment?: string;
}; };
const DEFAULT_OPTIONS: EmbedLinkInputPopupOptions = { const DEFAULT_OPTIONS: EmbedLinkInputPopupOptions = {
@@ -24,6 +29,7 @@ const DEFAULT_OPTIONS: EmbedLinkInputPopupOptions = {
title: 'Embed Link', title: 'Embed Link',
description: 'Works with links of Google Drive, Spotify…', description: 'Works with links of Google Drive, Spotify…',
placeholder: 'Paste the Embed link...', placeholder: 'Paste the Embed link...',
telemetrySegment: 'editor',
}; };
export class EmbedIframeLinkInputPopup extends EmbedIframeLinkInputBase { export class EmbedIframeLinkInputPopup extends EmbedIframeLinkInputBase {
@@ -216,6 +222,17 @@ export class EmbedIframeLinkInputPopup extends EmbedIframeLinkInputBase {
this.abortController?.abort(); this.abortController?.abort();
}; };
protected override track(status: 'success' | 'failure') {
this.telemetryService?.track('CreateEmbedBlock', {
type: 'embed iframe block',
page: this.editorMode === 'page' ? 'doc editor' : 'whiteboard editor',
segment: this.options?.telemetrySegment ?? 'editor',
module: 'embed block',
control: 'confirm embed link',
other: status,
});
}
override render() { override render() {
const options = { ...DEFAULT_OPTIONS, ...this.options }; const options = { ...DEFAULT_OPTIONS, ...this.options };
const { showCloseButton, variant, title, description, placeholder } = const { showCloseButton, variant, title, description, placeholder } =
@@ -261,6 +278,16 @@ export class EmbedIframeLinkInputPopup extends EmbedIframeLinkInputBase {
`; `;
} }
get telemetryService() {
return this.std.getOptional(TelemetryProvider);
}
get editorMode() {
const docModeService = this.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
return mode ?? 'page';
}
@property({ attribute: false }) @property({ attribute: false })
accessor options: EmbedLinkInputPopupOptions | undefined = undefined; accessor options: EmbedLinkInputPopupOptions | undefined = undefined;
} }

View File

@@ -31,6 +31,9 @@ export const embedIframeSlashMenuConfig: SlashMenuConfig = {
.pipe(insertEmptyEmbedIframeCommand, { .pipe(insertEmptyEmbedIframeCommand, {
place: 'after', place: 'after',
removeEmptyLine: true, removeEmptyLine: true,
linkInputPopupOptions: {
telemetrySegment: 'slash menu',
},
}) })
.run(); .run();
}, },

View File

@@ -67,6 +67,11 @@ const openLinkAction = (id: string): ToolbarAction => {
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockByType(EmbedIframeBlockComponent); const component = ctx.getCurrentBlockByType(EmbedIframeBlockComponent);
component?.open(); component?.open();
ctx.track('OpenLink', {
...trackBaseProps,
control: 'open original link',
});
}, },
}; };
}; };
@@ -263,6 +268,11 @@ export const builtinToolbarConfig = {
.copySlice(slice) .copySlice(slice)
.then(() => toast(ctx.host, 'Copied to clipboard')) .then(() => toast(ctx.host, 'Copied to clipboard'))
.catch(console.error); .catch(console.error);
ctx.track('CopiedLink', {
...trackBaseProps,
control: 'copy link',
});
}, },
}, },
{ {
@@ -290,6 +300,11 @@ export const builtinToolbarConfig = {
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockByType(EmbedIframeBlockComponent); const component = ctx.getCurrentBlockByType(EmbedIframeBlockComponent);
component?.refreshData().catch(console.error); component?.refreshData().catch(console.error);
ctx.track('ReloadLink', {
...trackBaseProps,
control: 'reload link',
});
}, },
}, },
{ {

View File

@@ -225,6 +225,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
.model=${this.model} .model=${this.model}
.abortController=${this._linkInputAbortController} .abortController=${this._linkInputAbortController}
.std=${this.std} .std=${this.std}
.inSurface=${this.inSurface}
.options=${options} .options=${options}
></embed-iframe-link-input-popup>`, ></embed-iframe-link-input-popup>`,
portalStyles: { portalStyles: {
@@ -347,6 +348,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
.model=${this.model} .model=${this.model}
.onRetry=${this._handleRetry} .onRetry=${this._handleRetry}
.std=${this.std} .std=${this.std}
.inSurface=${this.inSurface}
.options=${this._statusCardOptions} .options=${this._statusCardOptions}
></embed-iframe-error-card>`; ></embed-iframe-error-card>`;
} }

View File

@@ -493,6 +493,7 @@ const embedToolGroup: KeyboardToolPanelGroup = {
linkInputPopupOptions: { linkInputPopupOptions: {
showCloseButton: true, showCloseButton: true,
variant: 'mobile', variant: 'mobile',
telemetrySegment: 'keyboard toolbar',
}, },
}) })
.run(); .run();

View File

@@ -11,6 +11,9 @@ export type LinkEventType =
| 'OpenedCardStyleSelector' | 'OpenedCardStyleSelector'
| 'SelectedCardStyle' | 'SelectedCardStyle'
| 'OpenedCardScaleSelector' | 'OpenedCardScaleSelector'
| 'SelectedCardScale'; | 'SelectedCardScale'
| 'OpenLink'
| 'EditLink'
| 'ReloadLink';
export type LinkToolbarEvents = Record<LinkEventType, TelemetryEvent>; export type LinkToolbarEvents = Record<LinkEventType, TelemetryEvent>;

View File

@@ -30,6 +30,7 @@ export type TelemetryEventMap = OutDatabaseAllEvents &
AttachmentUploadedEvent: AttachmentUploadedEvent; AttachmentUploadedEvent: AttachmentUploadedEvent;
BlockCreated: BlockCreationEvent; BlockCreated: BlockCreationEvent;
EdgelessToolPicked: EdgelessToolPickedEvent; EdgelessToolPicked: EdgelessToolPickedEvent;
CreateEmbedBlock: TelemetryEvent;
}; };
export interface TelemetryService { export interface TelemetryService {

View File

@@ -24,6 +24,7 @@ import {
import { import {
DndApiExtensionIdentifier, DndApiExtensionIdentifier,
DocModeProvider, DocModeProvider,
EmbedIframeService,
TelemetryProvider, TelemetryProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { import {
@@ -1058,8 +1059,20 @@ export class DragEventWatcher {
new Bound(0, 0, 0, 0); new Bound(0, 0, 0, 0);
if (block.flavour === 'affine:embed-iframe') { if (block.flavour === 'affine:embed-iframe') {
blockBound.w = EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE; let width = EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE;
blockBound.h = EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE; let height = EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE;
if (block.props.url && typeof block.props.url === 'string') {
const embedIframeService = this.std.get(EmbedIframeService);
const options = embedIframeService.getConfig(
block.props.url
)?.options;
if (options) {
width = options.widthInSurface;
height = options.heightInSurface;
}
}
blockBound.w = width;
blockBound.h = height;
} else if ( } else if (
block.flavour === 'affine:attachment' || block.flavour === 'affine:attachment' ||
block.flavour === 'affine:bookmark' || block.flavour === 'affine:bookmark' ||