feat(editor): embed iframe block event tracking (#11313)
Close [BS-2958](https://linear.app/affine-design/issue/BS-2958/埋点相关)
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -493,6 +493,7 @@ const embedToolGroup: KeyboardToolPanelGroup = {
|
|||||||
linkInputPopupOptions: {
|
linkInputPopupOptions: {
|
||||||
showCloseButton: true,
|
showCloseButton: true,
|
||||||
variant: 'mobile',
|
variant: 'mobile',
|
||||||
|
telemetrySegment: 'keyboard toolbar',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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' ||
|
||||||
|
|||||||
Reference in New Issue
Block a user