diff --git a/src/abstract/managers/TelemetryManager.ts b/src/abstract/managers/TelemetryManager.ts index 90ebe4795..e9b5aa1e2 100644 --- a/src/abstract/managers/TelemetryManager.ts +++ b/src/abstract/managers/TelemetryManager.ts @@ -2,16 +2,18 @@ import type { TelemetryRequest } from '@uploadcare/quality-insights'; import { TelemetryAPIService } from '@uploadcare/quality-insights'; import { Queue } from '@uploadcare/upload-client'; import { initialConfig } from '../../blocks/Config/initialConfig'; -import type { EventKey } from '../../blocks/UploadCtxProvider/EventEmitter'; -import { EventType } from '../../blocks/UploadCtxProvider/EventEmitter'; +import type { EventKey, InternalEventKey } from '../../blocks/UploadCtxProvider/EventEmitter'; +import { EventType, InternalEventType } from '../../blocks/UploadCtxProvider/EventEmitter'; import { PACKAGE_NAME, PACKAGE_VERSION } from '../../env'; import type { ConfigType } from '../../types/index'; import type { Block } from '../Block'; +type CommonEventType = InternalEventKey | EventKey; + type TelemetryState = TelemetryRequest & { eventTimestamp: number }; type TelemetryEventBody = Partial> & { modalId?: string; - eventType?: EventKey; + eventType?: CommonEventType; }; export class TelemetryManager { @@ -31,7 +33,9 @@ export class TelemetryManager { for (const key of Object.keys(this._config) as (keyof ConfigType)[]) { this._block.subConfigValue(key, (value) => { if (this._initialized && this._config[key] !== value) { - this._block.emit(EventType.CHANGE_CONFIG, undefined); + this.sendEvent({ + eventType: InternalEventType.CHANGE_CONFIG, + }); } this._setConfig(key, value); @@ -39,8 +43,8 @@ export class TelemetryManager { } } - private _init(type: EventKey | undefined): void { - if (type === EventType.INIT_SOLUTION && !this._initialized) { + private _init(type: CommonEventType | undefined): void { + if (type === InternalEventType.INIT_SOLUTION && !this._initialized) { this._initialized = true; } } @@ -60,7 +64,7 @@ export class TelemetryManager { } const result: Partial> = { ...body }; - if (body.eventType === EventType.INIT_SOLUTION || body.eventType === EventType.CHANGE_CONFIG) { + if (body.eventType === InternalEventType.INIT_SOLUTION || body.eventType === InternalEventType.CHANGE_CONFIG) { result.config = this._config as TelemetryState['config']; } @@ -83,7 +87,7 @@ export class TelemetryManager { } as TelemetryState; } - private _excludedEvents(type: EventKey | undefined): boolean { + private _excludedEvents(type: CommonEventType | undefined): boolean { if ( type && [ @@ -95,8 +99,6 @@ export class TelemetryManager { EventType.FILE_UPLOAD_PROGRESS, EventType.FILE_UPLOAD_SUCCESS, EventType.FILE_UPLOAD_FAILED, - EventType.FILE_URL_CHANGED, - EventType.GROUP_CREATED, ].includes(type) ) { return true; @@ -132,6 +134,7 @@ export class TelemetryManager { sendEventError(error: unknown, context = 'unknown'): void { this.sendEvent({ + eventType: InternalEventType.ERROR_EVENT, payload: { metadata: { event: 'error', @@ -147,6 +150,7 @@ export class TelemetryManager { */ sendEventCloudImageEditor(e: MouseEvent, tabId: string, options: Record = {}): void { this.sendEvent({ + eventType: InternalEventType.ACTION_EVENT, payload: { metadata: { tabId, diff --git a/src/blocks/CameraSource/CameraSource.ts b/src/blocks/CameraSource/CameraSource.ts index 67c8c4f7b..758855a69 100644 --- a/src/blocks/CameraSource/CameraSource.ts +++ b/src/blocks/CameraSource/CameraSource.ts @@ -5,6 +5,7 @@ import { deserializeCsv } from '../../utils/comma-separated'; import { debounce } from '../../utils/debounce'; import { stringToArray } from '../../utils/stringToArray'; import { UploadSource } from '../../utils/UploadSource'; +import { InternalEventType } from '../UploadCtxProvider/EventEmitter'; import './camera-source.css'; import { CameraSourceEvents, CameraSourceTypes } from './constants'; @@ -174,12 +175,36 @@ export class CameraSource extends UploaderBlock { this.historyBack(); }, - onShot: () => this._shot(), + onShot: () => { + this.telemetryManager.sendEvent({ + eventType: InternalEventType.ACTION_EVENT, + payload: { + metadata: { + event: 'shot-camera', + node: this.tagName, + tabId: this._activeTab, + }, + }, + }); + this._shot(); + }, onRequestPermissions: () => this._capture(), /** General method for photo and video capture */ - onStartCamera: () => this._chooseActionWithCamera(), + onStartCamera: () => { + this.telemetryManager.sendEvent({ + eventType: InternalEventType.ACTION_EVENT, + payload: { + metadata: { + event: 'start-camera', + node: this.tagName, + tabId: this._activeTab, + }, + }, + }); + this._chooseActionWithCamera(); + }, onStartRecording: () => this._startRecording(), @@ -189,9 +214,33 @@ export class CameraSource extends UploaderBlock { onToggleAudio: () => this._toggleEnableAudio(), - onRetake: () => this._retake(), + onRetake: () => { + this.telemetryManager.sendEvent({ + eventType: InternalEventType.ACTION_EVENT, + payload: { + metadata: { + event: 'retake-camera', + node: this.tagName, + tabId: this._activeTab, + }, + }, + }); + this._retake(); + }, - onAccept: () => this._accept(), + onAccept: () => { + this.telemetryManager.sendEvent({ + eventType: InternalEventType.ACTION_EVENT, + payload: { + metadata: { + event: 'accept-camera', + node: this.tagName, + tabId: this._activeTab, + }, + }, + }); + this._accept(); + }, onClickTab: (event: MouseEvent) => { const target = event.currentTarget as HTMLElement | null; @@ -330,6 +379,17 @@ export class CameraSource extends UploaderBlock { this._mediaRecorder?.stop(); this.classList.remove('uc-recording'); + + this.telemetryManager.sendEvent({ + eventType: InternalEventType.ACTION_EVENT, + payload: { + metadata: { + event: 'stop-camera', + node: this.tagName, + tabId: this._activeTab, + }, + }, + }); }; /** This method is used to toggle recording pause/resume */ @@ -562,6 +622,17 @@ export class CameraSource extends UploaderBlock { }); } + this.telemetryManager.sendEvent({ + eventType: InternalEventType.ACTION_EVENT, + payload: { + metadata: { + event: 'camera-tab-switch', + node: this.tagName, + tabId, + }, + }, + }); + this._activeTab = tabId; }; diff --git a/src/blocks/FileItem/FileItem.ts b/src/blocks/FileItem/FileItem.ts index 0e71c61b3..399223ab9 100644 --- a/src/blocks/FileItem/FileItem.ts +++ b/src/blocks/FileItem/FileItem.ts @@ -13,6 +13,7 @@ import { parseShrink } from '../../utils/parseShrink'; import { throttle } from '../../utils/throttle'; import { ExternalUploadSource } from '../../utils/UploadSource'; import './file-item.css'; +import { EventType, InternalEventType } from '../UploadCtxProvider/EventEmitter'; import { FileItemConfig } from './FileItemConfig'; const FileItemState = Object.freeze({ @@ -85,6 +86,7 @@ export class FileItem extends FileItemConfig { ariaLabelStatusFile: '', onEdit: this._withEntry((entry) => { this.telemetryManager.sendEvent({ + eventType: InternalEventType.ACTION_EVENT, payload: { metadata: { event: 'edit-file', @@ -100,6 +102,7 @@ export class FileItem extends FileItemConfig { }), onRemove: () => { this.telemetryManager.sendEvent({ + eventType: InternalEventType.ACTION_EVENT, payload: { metadata: { event: 'remove-file', @@ -107,6 +110,7 @@ export class FileItem extends FileItemConfig { }, }, }); + this.uploadCollection.remove(this.$.uid); }, onUpload: () => { diff --git a/src/blocks/UploadCtxProvider/EventEmitter.ts b/src/blocks/UploadCtxProvider/EventEmitter.ts index 78ed24886..961712393 100644 --- a/src/blocks/UploadCtxProvider/EventEmitter.ts +++ b/src/blocks/UploadCtxProvider/EventEmitter.ts @@ -8,6 +8,8 @@ const DEFAULT_DEBOUNCE_TIMEOUT = 20; export const InternalEventType = Object.freeze({ INIT_SOLUTION: 'init-solution', CHANGE_CONFIG: 'change-config', + ACTION_EVENT: 'action-event', + ERROR_EVENT: 'error-event', } as const); export const EventType = Object.freeze({ @@ -32,12 +34,12 @@ export const EventType = Object.freeze({ CHANGE: 'change', GROUP_CREATED: 'group-created', - - ...InternalEventType, } as const); export type EventKey = (typeof EventType)[keyof typeof EventType]; +export type InternalEventKey = (typeof InternalEventType)[keyof typeof InternalEventType]; + export type EventPayload = { [EventType.FILE_ADDED]: OutputFileEntry<'idle'>; [EventType.FILE_REMOVED]: OutputFileEntry<'removed'>; @@ -62,8 +64,6 @@ export type EventPayload = { [EventType.COMMON_UPLOAD_FAILED]: OutputCollectionState<'failed'>; [EventType.CHANGE]: OutputCollectionState; [EventType.GROUP_CREATED]: OutputCollectionState<'success', 'has-group'>; - [EventType.INIT_SOLUTION]: void; - [EventType.CHANGE_CONFIG]: void; }; export class EventEmitter { diff --git a/src/blocks/UploadList/UploadList.ts b/src/blocks/UploadList/UploadList.ts index 92d2d3177..dec9e5df6 100644 --- a/src/blocks/UploadList/UploadList.ts +++ b/src/blocks/UploadList/UploadList.ts @@ -3,7 +3,7 @@ import { ActivityBlock } from '../../abstract/ActivityBlock'; import { UploaderBlock } from '../../abstract/UploaderBlock'; import type { OutputCollectionErrorType, OutputError } from '../../types'; import { throttle } from '../../utils/throttle'; -import { EventType } from '../UploadCtxProvider/EventEmitter'; +import { EventType, InternalEventType } from '../UploadCtxProvider/EventEmitter'; import './upload-list.css'; export type FilesViewMode = 'grid' | 'list'; @@ -38,6 +38,16 @@ export class UploadList extends UploaderBlock { hasFiles: false, onAdd: () => { + this.telemetryManager.sendEvent({ + eventType: InternalEventType.ACTION_EVENT, + payload: { + metadata: { + event: 'add-more', + node: this.tagName, + }, + }, + }); + this.api.initFlow(true); }, onUpload: () => { @@ -50,6 +60,15 @@ export class UploadList extends UploaderBlock { this.api.doneFlow(); }, onCancel: () => { + this.telemetryManager.sendEvent({ + eventType: InternalEventType.ACTION_EVENT, + payload: { + metadata: { + event: 'clear-all', + node: this.tagName, + }, + }, + }); this.uploadCollection.clearAll(); }, } as any; diff --git a/src/blocks/UrlSource/UrlSource.ts b/src/blocks/UrlSource/UrlSource.ts index f0032aa1f..4aae34424 100644 --- a/src/blocks/UrlSource/UrlSource.ts +++ b/src/blocks/UrlSource/UrlSource.ts @@ -2,6 +2,7 @@ import type { ActivityType } from '../../abstract/ActivityBlock'; import { ActivityBlock } from '../../abstract/ActivityBlock'; import { UploaderBlock } from '../../abstract/UploaderBlock'; import { UploadSource } from '../../utils/UploadSource'; +import { EventType, InternalEventType } from '../UploadCtxProvider/EventEmitter'; import './url-source.css'; type BaseInitState = InstanceType['init$']; @@ -25,6 +26,15 @@ export class UrlSource extends UploaderBlock { importDisabled: true, onUpload: (event: Event) => { event.preventDefault(); + this.telemetryManager.sendEvent({ + eventType: InternalEventType.ACTION_EVENT, + payload: { + metadata: { + event: 'upload-from-url', + node: this.tagName, + }, + }, + }); const url = this.ref.input['value'] as string; this.api.addFileFromUrl(url, { source: UploadSource.URL }); diff --git a/src/solutions/cloud-image-editor/CloudImageEditor.ts b/src/solutions/cloud-image-editor/CloudImageEditor.ts index 0a4899b9b..378e63813 100644 --- a/src/solutions/cloud-image-editor/CloudImageEditor.ts +++ b/src/solutions/cloud-image-editor/CloudImageEditor.ts @@ -21,7 +21,9 @@ export class CloudImageEditor extends CloudImageEditorBlock { override initCallback(): void { super.initCallback(); - this.emit(InternalEventType.INIT_SOLUTION, undefined); + this.telemetryManager.sendEvent({ + eventType: InternalEventType.INIT_SOLUTION, + }); this.a11y?.registerBlock(this); } diff --git a/src/solutions/file-uploader/inline/FileUploaderInline.ts b/src/solutions/file-uploader/inline/FileUploaderInline.ts index 77cc2df35..2b8cc8c55 100644 --- a/src/solutions/file-uploader/inline/FileUploaderInline.ts +++ b/src/solutions/file-uploader/inline/FileUploaderInline.ts @@ -52,7 +52,9 @@ export class FileUploaderInline extends SolutionBlock { override initCallback(): void { super.initCallback(); - this.emit(InternalEventType.INIT_SOLUTION, undefined); + this.telemetryManager.sendEvent({ + eventType: InternalEventType.INIT_SOLUTION, + }); const uBlock = this.ref.uBlock as UploaderBlock | undefined; if (!uBlock) { diff --git a/src/solutions/file-uploader/minimal/FileUploaderMinimal.ts b/src/solutions/file-uploader/minimal/FileUploaderMinimal.ts index 325a2bd34..69bc88fda 100644 --- a/src/solutions/file-uploader/minimal/FileUploaderMinimal.ts +++ b/src/solutions/file-uploader/minimal/FileUploaderMinimal.ts @@ -74,7 +74,9 @@ export class FileUploaderMinimal extends SolutionBlock { override initCallback(): void { super.initCallback(); - this.emit(InternalEventType.INIT_SOLUTION, undefined); + this.telemetryManager.sendEvent({ + eventType: InternalEventType.INIT_SOLUTION, + }); const uBlock = this.ref.uBlock as UploaderBlock | undefined; if (!uBlock) { diff --git a/src/solutions/file-uploader/regular/FileUploaderRegular.ts b/src/solutions/file-uploader/regular/FileUploaderRegular.ts index cd88abada..080dbdf45 100644 --- a/src/solutions/file-uploader/regular/FileUploaderRegular.ts +++ b/src/solutions/file-uploader/regular/FileUploaderRegular.ts @@ -27,7 +27,9 @@ export class FileUploaderRegular extends SolutionBlock { override initCallback(): void { super.initCallback(); - this.emit(InternalEventType.INIT_SOLUTION, undefined); + this.telemetryManager.sendEvent({ + eventType: InternalEventType.INIT_SOLUTION, + }); this.defineAccessor('headless', (value: unknown) => { this.set$({ isHidden: asBoolean(value) });