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
);
gfx.selection.set({
elements: [newBlockId],
editing: false,
});
gfx.tool.setTool(
// @ts-expect-error FIXME: resolve after gfx tool refactor
'default'
);
gfx.selection.set({
elements: [newBlockId],
editing: false,
});
}
if (!newBlockId) {

View File

@@ -1,5 +1,9 @@
import { createLitPortal } from '@blocksuite/affine-components/portal';
import type { EmbedIframeBlockModel } from '@blocksuite/affine-model';
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { WithDisposable } from '@blocksuite/global/lit';
import { EditIcon, InformationIcon, ResetIcon } from '@blocksuite/icons/lit';
@@ -191,6 +195,7 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
.model=${this.model}
.abortController=${this._editAbortController}
.std=${this.std}
.inSurface=${this.inSurface}
></embed-iframe-link-edit-popup>`,
portalStyles: {
zIndex: 'var(--affine-z-index-popover)',
@@ -210,6 +215,15 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
private readonly _handleRetry = (e: MouseEvent) => {
e.stopPropagation();
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() {
@@ -273,6 +287,16 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
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')
accessor _editButton: HTMLElement | null = null;
@@ -288,6 +312,9 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
@property({ attribute: false })
accessor std!: BlockStdScope;
@property({ attribute: false })
accessor inSurface = false;
@property({ attribute: false })
accessor options: EmbedIframeStatusCardOptions = {
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 { SignalWatcher } from '@blocksuite/global/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() {
const isInputEmpty = this.isInputEmpty();
const { url$ } = this.model.props;
@@ -94,4 +109,14 @@ export class EmbedIframeLinkEditPopup extends SignalWatcher(
</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';
import { isValidUrl, stopPropagation } from '@blocksuite/affine-shared/utils';
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 { property, query, state } from 'lit/decorators.js';
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() {
return this._linkInputValue.trim() === '';
}
@@ -33,9 +44,20 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
this.store.transact(() => {
const blockId = this.store.addBlock(flavour, { url }, parent, index);
this.store.deleteBlock(model);
this.std.selection.setGroup('note', [
this.std.selection.create(BlockSelection, { blockId }),
]);
if (this.inSurface) {
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();
@@ -50,6 +72,7 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
const embedIframeService = this.std.get(EmbedIframeService);
if (!embedIframeService) {
console.error('iframe EmbedIframeService not found');
this.track('failure');
return;
}
@@ -68,7 +91,9 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
title: '',
description: '',
});
this.track('success');
} catch (error) {
this.track('failure');
this.notificationService?.notify({
title: 'Error in embed iframe creation',
message: error instanceof Error ? error.message : 'Please try again',
@@ -129,4 +154,7 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
@property({ attribute: false })
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 { CloseIcon } from '@blocksuite/icons/lit';
import { baseTheme } from '@toeverything/theme';
@@ -16,6 +20,7 @@ export type EmbedLinkInputPopupOptions = {
title?: string;
description?: string;
placeholder?: string;
telemetrySegment?: string;
};
const DEFAULT_OPTIONS: EmbedLinkInputPopupOptions = {
@@ -24,6 +29,7 @@ const DEFAULT_OPTIONS: EmbedLinkInputPopupOptions = {
title: 'Embed Link',
description: 'Works with links of Google Drive, Spotify…',
placeholder: 'Paste the Embed link...',
telemetrySegment: 'editor',
};
export class EmbedIframeLinkInputPopup extends EmbedIframeLinkInputBase {
@@ -216,6 +222,17 @@ export class EmbedIframeLinkInputPopup extends EmbedIframeLinkInputBase {
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() {
const options = { ...DEFAULT_OPTIONS, ...this.options };
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 })
accessor options: EmbedLinkInputPopupOptions | undefined = undefined;
}

View File

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

View File

@@ -67,6 +67,11 @@ const openLinkAction = (id: string): ToolbarAction => {
run(ctx) {
const component = ctx.getCurrentBlockByType(EmbedIframeBlockComponent);
component?.open();
ctx.track('OpenLink', {
...trackBaseProps,
control: 'open original link',
});
},
};
};
@@ -263,6 +268,11 @@ export const builtinToolbarConfig = {
.copySlice(slice)
.then(() => toast(ctx.host, 'Copied to clipboard'))
.catch(console.error);
ctx.track('CopiedLink', {
...trackBaseProps,
control: 'copy link',
});
},
},
{
@@ -290,6 +300,11 @@ export const builtinToolbarConfig = {
run(ctx) {
const component = ctx.getCurrentBlockByType(EmbedIframeBlockComponent);
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}
.abortController=${this._linkInputAbortController}
.std=${this.std}
.inSurface=${this.inSurface}
.options=${options}
></embed-iframe-link-input-popup>`,
portalStyles: {
@@ -347,6 +348,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
.model=${this.model}
.onRetry=${this._handleRetry}
.std=${this.std}
.inSurface=${this.inSurface}
.options=${this._statusCardOptions}
></embed-iframe-error-card>`;
}