From dd529af14150bf709713c2ce27c7090384f64720 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Wed, 2 Oct 2024 13:41:03 -0400 Subject: [PATCH 1/8] Adds telemetry support for webview apps --- src/webviews/apps/shared/app.ts | 6 ++++- src/webviews/apps/shared/appBase.ts | 25 ++++++++++++++++++++- src/webviews/apps/shared/context.ts | 34 +++++++++++++++++++++++++++++ src/webviews/protocol.ts | 11 ++++++++++ src/webviews/webviewController.ts | 5 +++++ 5 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/webviews/apps/shared/app.ts b/src/webviews/apps/shared/app.ts index 2eba513249a9e..6fb21a87986be 100644 --- a/src/webviews/apps/shared/app.ts +++ b/src/webviews/apps/shared/app.ts @@ -7,7 +7,7 @@ import { debounce } from '../../../system/function'; import type { WebviewFocusChangedParams } from '../../protocol'; import { DidChangeWebviewFocusNotification, WebviewFocusChangedCommand, WebviewReadyCommand } from '../../protocol'; import { GlElement } from './components/element'; -import { ipcContext, loggerContext, LoggerContext } from './context'; +import { ipcContext, LoggerContext, loggerContext, telemetryContext, TelemetryContext } from './context'; import type { Disposable } from './events'; import { HostIpc } from './ipc'; @@ -31,6 +31,9 @@ export abstract class GlApp< @provide({ context: loggerContext }) protected _logger!: LoggerContext; + @provide({ context: telemetryContext }) + protected _telemetry!: TelemetryContext; + @property({ type: Object }) state!: State; @@ -65,6 +68,7 @@ export abstract class GlApp< } }), this._ipc, + (this._telemetry = new TelemetryContext(this._ipc)), ); this._ipc.sendCommand(WebviewReadyCommand, undefined); diff --git a/src/webviews/apps/shared/appBase.ts b/src/webviews/apps/shared/appBase.ts index c3632963a97ee..3232abf80c5a4 100644 --- a/src/webviews/apps/shared/appBase.ts +++ b/src/webviews/apps/shared/appBase.ts @@ -1,5 +1,7 @@ /*global window document*/ import { ContextProvider } from '@lit/context'; +import type { TimeInput } from '@opentelemetry/api'; +import type { Source, TelemetryEvents } from '../../../constants.telemetry'; import type { CustomEditorIds, WebviewIds, WebviewViewIds } from '../../../constants.views'; import { debounce } from '../../../system/function'; import type { LogScope } from '../../../system/logger.scope'; @@ -11,7 +13,12 @@ import type { IpcRequest, WebviewFocusChangedParams, } from '../../protocol'; -import { DidChangeWebviewFocusNotification, WebviewFocusChangedCommand, WebviewReadyCommand } from '../../protocol'; +import { + DidChangeWebviewFocusNotification, + TelemetrySendEventCommand, + WebviewFocusChangedCommand, + WebviewReadyCommand, +} from '../../protocol'; import { ipcContext, loggerContext, LoggerContext } from './context'; import { DOM } from './dom'; import type { Disposable } from './events'; @@ -191,6 +198,22 @@ export abstract class App< return this._hostIpc.sendRequest(requestType, params); } + protected sendTelemetryEvent( + name: T, + data?: TelemetryEvents[T], + source?: Source, + startTime?: TimeInput, + endTime?: TimeInput, + ): void { + this._hostIpc.sendCommand(TelemetrySendEventCommand, { + name: name, + data: data, + source: source, + startTime: startTime, + endTime: endTime, + }); + } + protected setState(state: Partial) { this._api.setState(state); } diff --git a/src/webviews/apps/shared/context.ts b/src/webviews/apps/shared/context.ts index 8b0791f7ff85a..c423e2bc130b8 100644 --- a/src/webviews/apps/shared/context.ts +++ b/src/webviews/apps/shared/context.ts @@ -1,8 +1,12 @@ import { createContext } from '@lit/context'; +import type { TimeInput } from '@opentelemetry/api'; +import type { Source, TelemetryEvents } from '../../../constants.telemetry'; import { Logger } from '../../../system/logger'; import type { LogScope } from '../../../system/logger.scope'; import { getNewLogScope } from '../../../system/logger.scope'; import { padOrTruncateEnd } from '../../../system/string'; +import { TelemetrySendEventCommand } from '../../protocol'; +import type { Disposable } from './events'; import type { HostIpc } from './ipc'; export class LoggerContext { @@ -35,5 +39,35 @@ export class LoggerContext { } } +export class TelemetryContext implements Disposable { + private readonly ipc: HostIpc; + private readonly disposables: Disposable[] = []; + + constructor(ipc: HostIpc) { + this.ipc = ipc; + } + + sendEvent( + name: T, + data?: TelemetryEvents[T], + source?: Source, + startTime?: TimeInput, + endTime?: TimeInput, + ): void { + this.ipc.sendCommand(TelemetrySendEventCommand, { + name: name, + data: data, + source: source, + startTime: startTime, + endTime: endTime, + }); + } + + dispose(): void { + this.disposables.forEach(d => d.dispose()); + } +} + export const ipcContext = createContext('ipc'); export const loggerContext = createContext('logger'); +export const telemetryContext = createContext('telemetry'); diff --git a/src/webviews/protocol.ts b/src/webviews/protocol.ts index d200208e46309..d5f1ecf4a95ba 100644 --- a/src/webviews/protocol.ts +++ b/src/webviews/protocol.ts @@ -1,4 +1,6 @@ +import type { TimeInput } from '@opentelemetry/api'; import type { Config } from '../config'; +import type { Source, TelemetryEvents } from '../constants.telemetry'; import type { CustomEditorIds, CustomEditorTypes, @@ -92,6 +94,15 @@ export interface UpdateConfigurationParams { } export const UpdateConfigurationCommand = new IpcCommand('core', 'configuration/update'); +export interface TelemetrySendEventParams { + name: T; + data?: TelemetryEvents[T]; + source?: Source; + startTime?: TimeInput; + endTime?: TimeInput; +} +export const TelemetrySendEventCommand = new IpcCommand('core', 'telemetry/sendEvent'); + // NOTIFICATIONS export interface DidChangeHostWindowFocusParams { diff --git a/src/webviews/webviewController.ts b/src/webviews/webviewController.ts index 89485d7fd4d56..e1a5171a23051 100644 --- a/src/webviews/webviewController.ts +++ b/src/webviews/webviewController.ts @@ -28,6 +28,7 @@ import { DidChangeHostWindowFocusNotification, DidChangeWebviewFocusNotification, ExecuteCommand, + TelemetrySendEventCommand, WebviewFocusChangedCommand, WebviewReadyCommand, } from './protocol'; @@ -387,6 +388,10 @@ export class WebviewController< } break; + case TelemetrySendEventCommand.is(e): + this.container.telemetry.sendEvent(e.params.name, e.params.data, e.params.source); + break; + default: this.provider.onMessageReceived?.(e); break; From 34c749bea35b9ba95fe3e8b6b75753d1ae5aef36 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Wed, 2 Oct 2024 17:36:14 -0400 Subject: [PATCH 2/8] Adds listener in appBase for custom telemetry events --- src/webviews/apps/shared/appBase.ts | 42 +++++++++++---------------- src/webviews/apps/shared/context.ts | 19 ++---------- src/webviews/apps/shared/telemetry.ts | 18 ++++++++++++ 3 files changed, 38 insertions(+), 41 deletions(-) create mode 100644 src/webviews/apps/shared/telemetry.ts diff --git a/src/webviews/apps/shared/appBase.ts b/src/webviews/apps/shared/appBase.ts index 3232abf80c5a4..c24c7b146f2c5 100644 --- a/src/webviews/apps/shared/appBase.ts +++ b/src/webviews/apps/shared/appBase.ts @@ -1,7 +1,5 @@ /*global window document*/ import { ContextProvider } from '@lit/context'; -import type { TimeInput } from '@opentelemetry/api'; -import type { Source, TelemetryEvents } from '../../../constants.telemetry'; import type { CustomEditorIds, WebviewIds, WebviewViewIds } from '../../../constants.views'; import { debounce } from '../../../system/function'; import type { LogScope } from '../../../system/logger.scope'; @@ -13,17 +11,13 @@ import type { IpcRequest, WebviewFocusChangedParams, } from '../../protocol'; -import { - DidChangeWebviewFocusNotification, - TelemetrySendEventCommand, - WebviewFocusChangedCommand, - WebviewReadyCommand, -} from '../../protocol'; -import { ipcContext, loggerContext, LoggerContext } from './context'; +import { DidChangeWebviewFocusNotification, WebviewFocusChangedCommand, WebviewReadyCommand } from '../../protocol'; +import { ipcContext, loggerContext, LoggerContext, telemetryContext, TelemetryContext } from './context'; import { DOM } from './dom'; import type { Disposable } from './events'; import type { HostIpcApi } from './ipc'; import { getHostIpcApi, HostIpc } from './ipc'; +import { telemetryEventName } from './telemetry'; import type { ThemeChangeEvent } from './theme'; import { computeThemeColors, onDidChangeTheme, watchThemeColors } from './theme'; @@ -36,6 +30,7 @@ export abstract class App< private readonly _api: HostIpcApi; private readonly _hostIpc: HostIpc; private readonly _logger: LoggerContext; + protected readonly _telemetry: TelemetryContext; protected state: State; protected readonly placement: 'editor' | 'view'; @@ -61,11 +56,18 @@ export abstract class App< this._hostIpc = new HostIpc(this.appName); disposables.push(this._hostIpc); + this._telemetry = new TelemetryContext(this._hostIpc); + disposables.push(this._telemetry); + new ContextProvider(document.body, { context: ipcContext, initialValue: this._hostIpc }); new ContextProvider(document.body, { context: loggerContext, initialValue: this._logger, }); + new ContextProvider(document.body, { + context: telemetryContext, + initialValue: this._telemetry, + }); if (this.state != null) { const state = this.getState(); @@ -123,6 +125,12 @@ export abstract class App< }), ); + disposables.push( + DOM.on(window, telemetryEventName, e => { + this._telemetry.sendEvent(e.detail); + }), + ); + this.log('opened'); } @@ -198,22 +206,6 @@ export abstract class App< return this._hostIpc.sendRequest(requestType, params); } - protected sendTelemetryEvent( - name: T, - data?: TelemetryEvents[T], - source?: Source, - startTime?: TimeInput, - endTime?: TimeInput, - ): void { - this._hostIpc.sendCommand(TelemetrySendEventCommand, { - name: name, - data: data, - source: source, - startTime: startTime, - endTime: endTime, - }); - } - protected setState(state: Partial) { this._api.setState(state); } diff --git a/src/webviews/apps/shared/context.ts b/src/webviews/apps/shared/context.ts index c423e2bc130b8..6883a7931c199 100644 --- a/src/webviews/apps/shared/context.ts +++ b/src/webviews/apps/shared/context.ts @@ -1,10 +1,9 @@ import { createContext } from '@lit/context'; -import type { TimeInput } from '@opentelemetry/api'; -import type { Source, TelemetryEvents } from '../../../constants.telemetry'; import { Logger } from '../../../system/logger'; import type { LogScope } from '../../../system/logger.scope'; import { getNewLogScope } from '../../../system/logger.scope'; import { padOrTruncateEnd } from '../../../system/string'; +import type { TelemetrySendEventParams } from '../../protocol'; import { TelemetrySendEventCommand } from '../../protocol'; import type { Disposable } from './events'; import type { HostIpc } from './ipc'; @@ -47,20 +46,8 @@ export class TelemetryContext implements Disposable { this.ipc = ipc; } - sendEvent( - name: T, - data?: TelemetryEvents[T], - source?: Source, - startTime?: TimeInput, - endTime?: TimeInput, - ): void { - this.ipc.sendCommand(TelemetrySendEventCommand, { - name: name, - data: data, - source: source, - startTime: startTime, - endTime: endTime, - }); + sendEvent(detail: TelemetrySendEventParams): void { + this.ipc.sendCommand(TelemetrySendEventCommand, detail); } dispose(): void { diff --git a/src/webviews/apps/shared/telemetry.ts b/src/webviews/apps/shared/telemetry.ts new file mode 100644 index 0000000000000..ff4624f31d4d6 --- /dev/null +++ b/src/webviews/apps/shared/telemetry.ts @@ -0,0 +1,18 @@ +import type { TelemetrySendEventParams } from '../../protocol'; + +export const telemetryEventName = 'gl-telemetry-fired'; + +export function emitTelemetrySentEvent(el: EventTarget, params: T) { + el.dispatchEvent( + new CustomEvent(telemetryEventName, { + bubbles: true, + detail: params, + }), + ); +} + +declare global { + interface WindowEventMap { + [telemetryEventName]: CustomEvent; + } +} From cf56d44d8a97327f39c4da9a889aa9b94c57a300 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Thu, 3 Oct 2024 16:40:06 -0400 Subject: [PATCH 3/8] Adds more telemetry to the commit graph --- src/constants.telemetry.ts | 15 +++++++++++ src/plus/webviews/graph/graphWebview.ts | 25 +++++++++++++++++++ src/webviews/apps/plus/graph/GraphWrapper.tsx | 6 +++++ src/webviews/apps/plus/graph/graph.tsx | 9 ++++++- .../apps/plus/graph/sidebar/sidebar.ts | 10 +++++++- 5 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index 8f0d716c7bd1b..ce70e9e1d7b47 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -1,3 +1,4 @@ +import type { GraphBranchesVisibility } from './config'; import type { AIModels, AIProviders } from './constants.ai'; import type { Commands } from './constants.commands'; import type { IntegrationId, SupportedCloudIntegrationIds } from './constants.integrations'; @@ -162,6 +163,20 @@ export type TelemetryEvents = { /** Sent when a "Graph" command is executed */ 'graph/command': Omit; + /** Sent when the user interacts with the graph */ + 'graph/column/changed': { column: string /* column props */ }; + 'graph/exclude/toggle': { key: string; value: boolean }; + 'graph/jump-to-ref': { alt: boolean }; + 'graph/minimap/daySelected': undefined; + 'graph/repository/change': undefined; + 'graph/repository/openOnRemote': undefined; + 'graph/row/hover': undefined; + 'graph/row/more': { duration: number }; + 'graph/row/selected': { rows: number }; + 'graph/sidebar/action': { action: string }; + 'graph/search': { types: string; duration: number }; + 'graph/visibility/changed': { branchesVisibility: GraphBranchesVisibility }; + /** Sent when the user takes an action on a launchpad item */ 'launchpad/title/action': LaunchpadEventData & { action: 'feedback' | 'open-on-gkdev' | 'refresh' | 'settings' | 'connect'; diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 147f965620807..9fcea1606ee33 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -916,6 +916,12 @@ export class GraphWebviewProvider implements WebviewProvider(requestType: T, msg: IpcCallMessageType) { try { const results = await this.getSearchResults(msg.params); + this.container.telemetry.sendEvent('graph/search', { + types: '', + duration: 0, + }); void this.host.respond(requestType, msg, results); } catch (ex) { void this.host.respond(requestType, msg, { @@ -1483,6 +1496,7 @@ export class GraphWebviewProvider implements WebviewProvider(requestType: T, msg: IpcCallMessageType) { @@ -2763,6 +2777,12 @@ export class GraphWebviewProvider implements WebviewProvider e.target && emitTelemetrySentEvent(e.target, { name: 'graph/minimap/daySelected' })); }; const handleOnMinimapToggle = (_e: React.MouseEvent) => { @@ -1007,6 +1010,9 @@ export function GraphWrapper({ style={{ marginRight: '-0.5rem' }} aria-label={`Open Repository on ${repo.provider.name}`} slot="anchor" + onClick={e => + emitTelemetrySentEvent(e.target, { name: 'graph/repository/openOnRemote' }) + } > { private async onHoverRowPromise(row: GraphRow) { try { - return await this.sendRequest(GetRowHoverRequest, { type: row.type as GitGraphRowType, id: row.sha }); + const request = await this.sendRequest(GetRowHoverRequest, { + type: row.type as GitGraphRowType, + id: row.sha, + }); + this._telemetry.sendEvent({ name: 'graph/row/hover' }); + return request; } catch (ex) { return { id: row.sha, markdown: { status: 'rejected' as const, reason: ex } }; } @@ -576,6 +581,7 @@ export class GraphApp extends App { try { // Assuming we have a command to get the ref details const rsp = await this.sendRequest(ChooseRefRequest, { alt: alt }); + this._telemetry.sendEvent({ name: 'graph/jump-to-ref', data: { alt: alt } }); return rsp; } catch { return undefined; @@ -650,6 +656,7 @@ export class GraphApp extends App { private onSelectionChanged(rows: GraphRow[]) { const selection = rows.filter(r => r != null).map(r => ({ id: r.sha, type: r.type as GitGraphRowType })); + this._telemetry.sendEvent({ name: 'graph/row/selected', data: { rows: selection.length } }); this.sendCommand(UpdateSelectionCommand, { selection: selection, }); diff --git a/src/webviews/apps/plus/graph/sidebar/sidebar.ts b/src/webviews/apps/plus/graph/sidebar/sidebar.ts index ed18e116e8cb0..cbf7a89de9075 100644 --- a/src/webviews/apps/plus/graph/sidebar/sidebar.ts +++ b/src/webviews/apps/plus/graph/sidebar/sidebar.ts @@ -9,6 +9,7 @@ import type { Disposable } from '../../../shared/events'; import type { HostIpc } from '../../../shared/ipc'; import '../../../shared/components/code-icon'; import '../../../shared/components/overlays/tooltip'; +import { emitTelemetrySentEvent } from '../../../shared/telemetry'; interface Icon { type: IconTypes; @@ -147,7 +148,7 @@ export class GlGraphSideBar extends LitElement { if (this.include != null && !this.include.includes(icon.type)) return; return html` - + this.sendTelemetry(icon.command)}> ${this._countsTask.render({ pending: () => @@ -160,6 +161,13 @@ export class GlGraphSideBar extends LitElement { `; } + + private sendTelemetry(command: string) { + emitTelemetrySentEvent(this, { + name: 'graph/sidebar/action', + data: { action: command }, + }); + } } function renderCount(count: number | undefined) { From 7549b6fda04792339881be759af59aa5590037ba Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Thu, 3 Oct 2024 16:51:56 -0400 Subject: [PATCH 4/8] Adds telemetry to visual file history --- src/constants.telemetry.ts | 7 +++++++ src/plus/webviews/timeline/timelineWebview.ts | 5 +++++ src/webviews/apps/plus/timeline/chart.ts | 11 +++++++++++ 3 files changed, 23 insertions(+) diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index ce70e9e1d7b47..88e6d3d82e2bb 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -177,6 +177,13 @@ export type TelemetryEvents = { 'graph/search': { types: string; duration: number }; 'graph/visibility/changed': { branchesVisibility: GraphBranchesVisibility }; + /** Sent when the user interacts with the visual file history */ + 'timeline/period/change': { period: string }; + 'timeline/chart/selectCommit': undefined; + 'timeline/chart/toggleLegend': undefined; + 'timeline/openInEditor': undefined; + 'timeline/editorChanged': undefined; + /** Sent when the user takes an action on a launchpad item */ 'launchpad/title/action': LaunchpadEventData & { action: 'feedback' | 'open-on-gkdev' | 'refresh' | 'settings' | 'connect'; diff --git a/src/plus/webviews/timeline/timelineWebview.ts b/src/plus/webviews/timeline/timelineWebview.ts index 7f279307a6456..5048d584d77b8 100644 --- a/src/plus/webviews/timeline/timelineWebview.ts +++ b/src/plus/webviews/timeline/timelineWebview.ts @@ -162,6 +162,7 @@ export class TimelineWebviewProvider implements WebviewProvider Date: Mon, 7 Oct 2024 15:29:01 -0400 Subject: [PATCH 5/8] Adds tracking when showing webviews --- src/constants.telemetry.ts | 30 +++++++++++++++-- src/plus/webviews/graph/graphWebview.ts | 6 ++-- src/plus/webviews/graph/registration.ts | 2 ++ .../patchDetails/patchDetailsWebview.ts | 6 ++-- .../webviews/patchDetails/registration.ts | 2 ++ src/plus/webviews/timeline/registration.ts | 2 ++ src/plus/webviews/timeline/timelineWebview.ts | 4 +-- .../commitDetails/commitDetailsWebview.ts | 9 ++++-- src/webviews/commitDetails/registration.ts | 2 ++ src/webviews/home/homeWebview.ts | 6 ++-- src/webviews/home/registration.ts | 1 + src/webviews/settings/registration.ts | 1 + src/webviews/settings/settingsWebview.ts | 6 ++-- src/webviews/webviewController.ts | 32 +++++++++++++++---- src/webviews/webviewProvider.ts | 4 ++- src/webviews/webviewsController.ts | 2 ++ src/webviews/welcome/registration.ts | 1 + 17 files changed, 90 insertions(+), 26 deletions(-) diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index 88e6d3d82e2bb..16822fa80648e 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -3,7 +3,14 @@ import type { AIModels, AIProviders } from './constants.ai'; import type { Commands } from './constants.commands'; import type { IntegrationId, SupportedCloudIntegrationIds } from './constants.integrations'; import type { SubscriptionState } from './constants.subscription'; -import type { CustomEditorTypes, TreeViewTypes, WebviewTypes, WebviewViewTypes } from './constants.views'; +import type { + CustomEditorTypes, + TreeViewTypes, + WebviewIds, + WebviewTypes, + WebviewViewIds, + WebviewViewTypes, +} from './constants.views'; import type { GitContributionTiers } from './git/models/contributor'; export type TelemetryGlobalContext = { @@ -370,7 +377,26 @@ export type TelemetryEvents = { | 'integrations' | 'more'; }; -}; +} & Record< + `${WebviewTypes | WebviewViewTypes}/showAborted`, + { + id: WebviewIds | WebviewViewIds; + instanceId: string | undefined; + host: 'editor' | 'view'; + duration: number; + loading: boolean; + } +> & + Record< + `${WebviewTypes | WebviewViewTypes}/shown`, + { + id: WebviewIds | WebviewViewIds; + instanceId: string | undefined; + host: 'editor' | 'view'; + duration: number; + loading: boolean; + } & Record<`context.${string}`, string | number | boolean> + >; type AIEventDataBase = { 'model.id': AIModels; diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 9fcea1606ee33..4a93a57010d92 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -356,7 +356,7 @@ export class GraphWebviewProvider implements WebviewProvider - ): Promise { + ): Promise<[boolean, Record<`context.${string}`, string | number | boolean> | undefined]> { this._firstSelection = true; this._etag = this.container.git.etag; @@ -386,7 +386,7 @@ export class GraphWebviewProvider implements WebviewProvider { + ): Promise<[boolean, Record<`context.${string}`, string | number | boolean> | undefined]> { const [arg] = args; if (arg?.mode === 'view' && arg.draft != null) { await this.updateViewDraftState(arg.draft); @@ -201,9 +201,9 @@ export class PatchDetailsWebviewProvider this.updateCreateDraftState(create); } - if (options?.preserveVisibility && !this.host.visible) return false; + if (options?.preserveVisibility && !this.host.visible) return [false, undefined]; - return true; + return [true, undefined]; } includeBootstrap(): Promise> { diff --git a/src/plus/webviews/patchDetails/registration.ts b/src/plus/webviews/patchDetails/registration.ts index e86d291bb4960..2c4fc677c7ef6 100644 --- a/src/plus/webviews/patchDetails/registration.ts +++ b/src/plus/webviews/patchDetails/registration.ts @@ -30,6 +30,7 @@ export function registerPatchDetailsWebviewView(controller: WebviewsController) title: 'Patch', contextKeyPrefix: `gitlens:webviewView:patchDetails`, trackingFeature: 'patchDetailsView', + type: 'patchDetails', plusFeature: true, webviewHostOptions: { retainContextWhenHidden: false, @@ -66,6 +67,7 @@ export function registerPatchDetailsWebviewPanel(controller: WebviewsController) title: 'Patch', contextKeyPrefix: `gitlens:webview:patchDetails`, trackingFeature: 'patchDetailsWebview', + type: 'patchDetails', plusFeature: true, column: ViewColumn.Active, webviewHostOptions: { diff --git a/src/plus/webviews/timeline/registration.ts b/src/plus/webviews/timeline/registration.ts index 767ecf5985e7e..f8f065f130b87 100644 --- a/src/plus/webviews/timeline/registration.ts +++ b/src/plus/webviews/timeline/registration.ts @@ -19,6 +19,7 @@ export function registerTimelineWebviewPanel(controller: WebviewsController) { title: 'Visual File History', contextKeyPrefix: `gitlens:webview:timeline`, trackingFeature: 'timelineWebview', + type: 'timeline', plusFeature: true, column: ViewColumn.Active, webviewHostOptions: { @@ -44,6 +45,7 @@ export function registerTimelineWebviewView(controller: WebviewsController) { title: 'Visual File History', contextKeyPrefix: `gitlens:webviewView:timeline`, trackingFeature: 'timelineView', + type: 'timeline', plusFeature: true, webviewHostOptions: { retainContextWhenHidden: false, diff --git a/src/plus/webviews/timeline/timelineWebview.ts b/src/plus/webviews/timeline/timelineWebview.ts index 5048d584d77b8..2b421e676f881 100644 --- a/src/plus/webviews/timeline/timelineWebview.ts +++ b/src/plus/webviews/timeline/timelineWebview.ts @@ -114,7 +114,7 @@ export class TimelineWebviewProvider implements WebviewProvider - ): boolean { + ): [boolean, Record<`context.${string}`, string | number | boolean> | undefined] { const [arg] = args; if (arg != null) { if (arg instanceof Uri) { @@ -138,7 +138,7 @@ export class TimelineWebviewProvider implements WebviewProvider { diff --git a/src/webviews/commitDetails/commitDetailsWebview.ts b/src/webviews/commitDetails/commitDetailsWebview.ts index 4952ce1914717..c458574cb6bbe 100644 --- a/src/webviews/commitDetails/commitDetailsWebview.ts +++ b/src/webviews/commitDetails/commitDetailsWebview.ts @@ -226,12 +226,15 @@ export class CommitDetailsWebviewProvider _loading: boolean, options?: WebviewShowOptions, ...args: WebviewShowingArgs> - ): Promise { + ): Promise<[boolean, Record<`context.${string}`, string | number | boolean> | undefined]> { const [arg] = args; if ((arg as ShowWipArgs)?.type === 'wip') { - return this.onShowingWip(arg as ShowWipArgs); + return [await this.onShowingWip(arg as ShowWipArgs), undefined]; } - return this.onShowingCommit(arg as Partial | undefined, options); + return [ + await this.onShowingCommit(arg as Partial | undefined, options), + undefined, + ]; } private get inReview(): boolean { diff --git a/src/webviews/commitDetails/registration.ts b/src/webviews/commitDetails/registration.ts index aa5cc7c113aca..ebeeeba2ec8a4 100644 --- a/src/webviews/commitDetails/registration.ts +++ b/src/webviews/commitDetails/registration.ts @@ -13,6 +13,7 @@ export function registerCommitDetailsWebviewView(controller: WebviewsController) title: 'Inspect', contextKeyPrefix: `gitlens:webviewView:commitDetails`, trackingFeature: 'commitDetailsView', + type: 'commitDetails', plusFeature: false, webviewHostOptions: { retainContextWhenHidden: false, @@ -35,6 +36,7 @@ export function registerGraphDetailsWebviewView(controller: WebviewsController) title: 'Commit Graph Inspect', contextKeyPrefix: `gitlens:webviewView:graphDetails`, trackingFeature: 'graphDetailsView', + type: 'graphDetails', plusFeature: false, webviewHostOptions: { retainContextWhenHidden: false, diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts index 7f4e9ba83737e..948f0c40c4062 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -53,17 +53,17 @@ export class HomeWebviewProvider implements WebviewProvider - ) { + ): [boolean, Record<`context.${string}`, string | number | boolean> | undefined] { const [arg] = args as HomeWebviewShowingArgs; if (arg?.focusAccount === true) { if (!loading && this.host.ready && this.host.visible) { queueMicrotask(() => void this.host.notify(DidFocusAccount, undefined)); - return true; + return [true, undefined]; } this._pendingFocusAccount = true; } - return true; + return [true, undefined]; } private onChangeConnectionState() { diff --git a/src/webviews/home/registration.ts b/src/webviews/home/registration.ts index bbb2397721194..7696c821dfbd2 100644 --- a/src/webviews/home/registration.ts +++ b/src/webviews/home/registration.ts @@ -11,6 +11,7 @@ export function registerHomeWebviewView(controller: WebviewsController) { title: 'Home', contextKeyPrefix: `gitlens:webviewView:home`, trackingFeature: 'homeView', + type: 'home', plusFeature: false, webviewHostOptions: { retainContextWhenHidden: false, diff --git a/src/webviews/settings/registration.ts b/src/webviews/settings/registration.ts index 9ffdae6e4173a..87ec4c5e8beae 100644 --- a/src/webviews/settings/registration.ts +++ b/src/webviews/settings/registration.ts @@ -16,6 +16,7 @@ export function registerSettingsWebviewPanel(controller: WebviewsController) { title: 'GitLens Settings', contextKeyPrefix: `gitlens:webview:settings`, trackingFeature: 'settingsWebview', + type: 'settings', plusFeature: false, column: ViewColumn.Active, webviewHostOptions: { diff --git a/src/webviews/settings/settingsWebview.ts b/src/webviews/settings/settingsWebview.ts index ee2b49063e191..5930b504c6949 100644 --- a/src/webviews/settings/settingsWebview.ts +++ b/src/webviews/settings/settingsWebview.ts @@ -95,7 +95,7 @@ export class SettingsWebviewProvider implements WebviewProvider { + ): [boolean, Record<`context.${string}`, string | number | boolean> | undefined] { const anchor = args[0]; if (anchor && typeof anchor === 'string') { if (!loading && this.host.ready && this.host.visible) { @@ -106,13 +106,13 @@ export class SettingsWebviewProvider implements WebviewProvider - ): boolean | Promise; + ): + | [boolean, Record<`context.${string}`, string | number | boolean> | undefined] + | Promise<[boolean, Record<`context.${string}`, string | number | boolean> | undefined]>; registerCommands?(): Disposable[]; includeBootstrap?(): SerializedState | Promise; diff --git a/src/webviews/webviewsController.ts b/src/webviews/webviewsController.ts index eb7e1d66efcde..165a55a908a1e 100644 --- a/src/webviews/webviewsController.ts +++ b/src/webviews/webviewsController.ts @@ -29,6 +29,7 @@ export interface WebviewPanelDescriptor { readonly title: string; readonly contextKeyPrefix: `gitlens:webview:${WebviewTypes}`; readonly trackingFeature: TrackedUsageFeatures; + readonly type: WebviewTypes; readonly plusFeature: boolean; readonly column?: ViewColumn; readonly webviewOptions?: WebviewOptions; @@ -79,6 +80,7 @@ export interface WebviewViewDescriptor { readonly title: string; readonly contextKeyPrefix: `gitlens:webviewView:${WebviewViewTypes}`; readonly trackingFeature: TrackedUsageFeatures; + readonly type: WebviewViewTypes; readonly plusFeature: boolean; readonly webviewOptions?: WebviewOptions; readonly webviewHostOptions?: { diff --git a/src/webviews/welcome/registration.ts b/src/webviews/welcome/registration.ts index c968d968e70ab..5462a11255e42 100644 --- a/src/webviews/welcome/registration.ts +++ b/src/webviews/welcome/registration.ts @@ -13,6 +13,7 @@ export function registerWelcomeWebviewPanel(controller: WebviewsController) { title: 'Welcome to GitLens', contextKeyPrefix: `gitlens:webview:welcome`, trackingFeature: 'welcomeWebview', + type: 'welcome', plusFeature: false, column: ViewColumn.Active, webviewHostOptions: { From 6dc4ac0c52f1a26f360174596e87625282f5cf22 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Mon, 7 Oct 2024 16:30:21 -0400 Subject: [PATCH 6/8] Updates event name for jump to ref --- src/constants.telemetry.ts | 2 +- src/webviews/apps/plus/graph/graph.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index 16822fa80648e..e8f60f2e13748 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -173,7 +173,7 @@ export type TelemetryEvents = { /** Sent when the user interacts with the graph */ 'graph/column/changed': { column: string /* column props */ }; 'graph/exclude/toggle': { key: string; value: boolean }; - 'graph/jump-to-ref': { alt: boolean }; + 'graph/jumpToRef': { alt: boolean }; 'graph/minimap/daySelected': undefined; 'graph/repository/change': undefined; 'graph/repository/openOnRemote': undefined; diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index 78e0e3de72193..1876e8ca5c6c9 100644 --- a/src/webviews/apps/plus/graph/graph.tsx +++ b/src/webviews/apps/plus/graph/graph.tsx @@ -581,7 +581,7 @@ export class GraphApp extends App { try { // Assuming we have a command to get the ref details const rsp = await this.sendRequest(ChooseRefRequest, { alt: alt }); - this._telemetry.sendEvent({ name: 'graph/jump-to-ref', data: { alt: alt } }); + this._telemetry.sendEvent({ name: 'graph/jumpToRef', data: { alt: alt } }); return rsp; } catch { return undefined; From c4381075605f8cfd3013a4e5b3106104aba0cc94 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Mon, 7 Oct 2024 16:34:49 -0400 Subject: [PATCH 7/8] Updates column config --- src/constants.telemetry.ts | 2 +- src/plus/webviews/graph/graphWebview.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index e8f60f2e13748..4a37c10f7aea9 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -171,7 +171,7 @@ export type TelemetryEvents = { 'graph/command': Omit; /** Sent when the user interacts with the graph */ - 'graph/column/changed': { column: string /* column props */ }; + 'graph/columns/changed': Record<`column.${string}`, boolean | string | number>; 'graph/exclude/toggle': { key: string; value: boolean }; 'graph/jumpToRef': { alt: boolean }; 'graph/minimap/daySelected': undefined; diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 4a93a57010d92..1fb1b1fbc17b9 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -917,11 +917,13 @@ export class GraphWebviewProvider implements WebviewProvider = {}; + for (const [key, config] of Object.entries(e.config)) { + for (const [prop, value] of Object.entries(config)) { + sendEvent[`column.${key}.${prop}`] = value; + } } + this.container.telemetry.sendEvent('graph/columns/changed', sendEvent); } private onRefsVisibilityChanged(e: UpdateRefsVisibilityParams) { From 11fc243357e8243a67a6978535e732d601b4a588 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Mon, 7 Oct 2024 20:53:10 -0400 Subject: [PATCH 8/8] Updates webview telemetry - expands telemetry data captured in graph - adds getTelemetryContext in provider and host Co-authored-by: Eric Amodio --- docs/telemetry-events.md | 361 +++++++++++++++++- src/constants.telemetry.ts | 99 ++--- src/plus/webviews/graph/graphWebview.ts | 77 +++- src/system/object.ts | 2 +- src/webviews/apps/plus/graph/GraphWrapper.tsx | 13 +- src/webviews/apps/plus/graph/graph.tsx | 2 +- src/webviews/apps/plus/timeline/chart.ts | 4 +- src/webviews/protocol.ts | 4 +- src/webviews/webviewController.ts | 19 +- src/webviews/webviewProvider.ts | 8 +- 10 files changed, 511 insertions(+), 78 deletions(-) diff --git a/docs/telemetry-events.md b/docs/telemetry-events.md index aa3723df781c8..feeb0a3dd0379 100644 --- a/docs/telemetry-events.md +++ b/docs/telemetry-events.md @@ -335,6 +335,179 @@ or } ``` +### graph/shown + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true, + 'context.repository.id': string, + 'context.repository.scheme': string, + 'context.repository.closed': false | true, + 'context.repository.folder.scheme': string, + 'context.repository.provider.id': string, + 'context.config.allowMultiple': false | true, + 'context.config.avatars': false | true, + 'context.config.branchesVisibility': 'all' | 'smart' | 'current', + 'context.config.commitOrdering': 'date' | 'author-date' | 'topo', + 'context.config.dateFormat': string, + 'context.config.dateStyle': 'absolute' | 'relative', + 'context.config.defaultItemLimit': number, + 'context.config.dimMergeCommits': false | true, + 'context.config.highlightRowsOnRefHover': false | true, + 'context.config.layout': 'editor' | 'panel', + 'context.config.minimap.enabled': false | true, + 'context.config.minimap.dataType': 'commits' | 'lines', + 'context.config.minimap.additionalTypes': string, + 'context.config.onlyFollowFirstParent': false | true, + 'context.config.pageItemLimit': number, + 'context.config.pullRequests.enabled': false | true, + 'context.config.scrollMarkers.enabled': false | true, + 'context.config.scrollMarkers.additionalTypes': string, + 'context.config.scrollRowPadding': number, + 'context.config.searchItemLimit': number, + 'context.config.showDetailsView': false | 'open' | 'selection', + 'context.config.showGhostRefsOnRowHover': false | true, + 'context.config.showRemoteNames': false | true, + 'context.config.showUpstreamStatus': false | true, + 'context.config.sidebar.enabled': false | true, + 'context.config.statusBar.enabled': false | true +} +``` + +### graph/columns/changed + +> Sent when the user interacts with the graph + +```typescript +{} +``` + +### graph/exclude/toggled + +```typescript +{ + 'key': string, + 'value': false | true +} +``` + +### graph/jumpToRef + +```typescript +{ + 'alt': false | true +} +``` + +### graph/minimap/daySelected + +```typescript +undefined +``` + +### graph/repository/changed + +```typescript +undefined +``` + +### graph/repository/openOnRemote + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view' +} +``` + +### graph/row/hovered + +```typescript +undefined +``` + +### graph/row/selected + +```typescript +{ + 'rows': number +} +``` + +### graph/rows/loaded + +```typescript +{ + 'duration': number, + 'rows': number +} +``` + +### graph/sidebar/action + +```typescript +{ + 'action': string +} +``` + +### graph/searched + +```typescript +{ + 'types': string, + 'duration': number, + 'matches': number +} +``` + +### graph/branchesVisibility/changed + +```typescript +{ + 'branchesVisibility': 'all' | 'smart' | 'current' +} +``` + +### timeline/period/change + +> Sent when the user interacts with the visual file history + +```typescript +{ + 'period': string +} +``` + +### timeline/chart/selectCommit + +```typescript +undefined +``` + +### timeline/chart/toggleLegend + +```typescript +undefined +``` + +### timeline/openInEditor + +```typescript +undefined +``` + +### timeline/editorChanged + +```typescript +undefined +``` + ### launchpad/title/action > Sent when the user takes an action on a launchpad item @@ -345,7 +518,7 @@ or 'initialState.group': string, 'initialState.selectTopItem': false | true, 'items.error': string, - 'action': 'feedback' | 'open-on-gkdev' | 'refresh' | 'settings' | 'connect' + 'action': 'settings' | 'feedback' | 'open-on-gkdev' | 'refresh' | 'connect' } ``` @@ -381,7 +554,7 @@ or 'groups.draft.collapsed': false | true, 'groups.other.collapsed': false | true, 'groups.snoozed.collapsed': false | true, - 'action': 'feedback' | 'open-on-gkdev' | 'refresh' | 'settings' | 'connect' + 'action': 'settings' | 'feedback' | 'open-on-gkdev' | 'refresh' | 'connect' } ``` @@ -757,7 +930,7 @@ void 'repository.visibility': 'private' | 'public' | 'local', 'repoPrivacy': 'private' | 'public' | 'local', 'filesChanged': number, - 'source': 'settings' | 'code-suggest' | 'account' | 'cloud-patches' | 'commandPalette' | 'deeplink' | 'graph' | 'home' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'notification' | 'patchDetails' | 'prompt' | 'quick-wizard' | 'remoteProvider' | 'timeline' | 'trial-indicator' | 'scm-input' | 'subscription' | 'walkthrough' | 'welcome' | 'worktrees' + 'source': 'graph' | 'patchDetails' | 'settings' | 'timeline' | 'welcome' | 'home' | 'code-suggest' | 'account' | 'cloud-patches' | 'commandPalette' | 'deeplink' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'notification' | 'prompt' | 'quick-wizard' | 'remoteProvider' | 'trial-indicator' | 'scm-input' | 'subscription' | 'walkthrough' | 'worktrees' } ``` @@ -911,7 +1084,7 @@ or ```typescript { - 'usage.key': 'settingsWebview:shown' | 'graphWebview:shown' | 'patchDetailsWebview:shown' | 'timelineWebview:shown' | 'welcomeWebview:shown' | 'launchpadView:shown' | 'worktreesView:shown' | 'branchesView:shown' | 'commitsView:shown' | 'contributorsView:shown' | 'draftsView:shown' | 'fileHistoryView:shown' | 'lineHistoryView:shown' | 'pullRequestView:shown' | 'remotesView:shown' | 'repositoriesView:shown' | 'searchAndCompareView:shown' | 'stashesView:shown' | 'tagsView:shown' | 'workspacesView:shown' | 'graphView:shown' | 'homeView:shown' | 'patchDetailsView:shown' | 'timelineView:shown' | 'commitDetailsView:shown' | 'graphDetailsView:shown' | 'rebaseEditor:shown', + 'usage.key': 'graphWebview:shown' | 'patchDetailsWebview:shown' | 'settingsWebview:shown' | 'timelineWebview:shown' | 'welcomeWebview:shown' | 'graphView:shown' | 'patchDetailsView:shown' | 'timelineView:shown' | 'commitDetailsView:shown' | 'graphDetailsView:shown' | 'homeView:shown' | 'commitsView:shown' | 'stashesView:shown' | 'tagsView:shown' | 'launchpadView:shown' | 'worktreesView:shown' | 'branchesView:shown' | 'contributorsView:shown' | 'draftsView:shown' | 'fileHistoryView:shown' | 'lineHistoryView:shown' | 'pullRequestView:shown' | 'remotesView:shown' | 'repositoriesView:shown' | 'searchAndCompareView:shown' | 'workspacesView:shown' | 'rebaseEditor:shown', 'usage.count': number } ``` @@ -926,3 +1099,183 @@ or } ``` +### graph/showAborted + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### patchDetails/showAborted + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### settings/showAborted + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### timeline/showAborted + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### welcome/showAborted + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### commitDetails/showAborted + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### graphDetails/showAborted + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### home/showAborted + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### patchDetails/shown + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### settings/shown + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### timeline/shown + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### welcome/shown + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### commitDetails/shown + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### graphDetails/shown + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + +### home/shown + +```typescript +{ + 'id': string, + 'instanceId': string, + 'host': 'editor' | 'view', + 'duration': number, + 'loading': false | true +} +``` + diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index 4a37c10f7aea9..1a910f5dff9cb 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -1,17 +1,11 @@ -import type { GraphBranchesVisibility } from './config'; +import type { GraphBranchesVisibility, GraphConfig } from './config'; import type { AIModels, AIProviders } from './constants.ai'; import type { Commands } from './constants.commands'; import type { IntegrationId, SupportedCloudIntegrationIds } from './constants.integrations'; import type { SubscriptionState } from './constants.subscription'; -import type { - CustomEditorTypes, - TreeViewTypes, - WebviewIds, - WebviewTypes, - WebviewViewIds, - WebviewViewTypes, -} from './constants.views'; +import type { CustomEditorTypes, TreeViewTypes, WebviewTypes, WebviewViewTypes } from './constants.views'; import type { GitContributionTiers } from './git/models/contributor'; +import type { Flatten } from './system/object'; export type TelemetryGlobalContext = { 'cloudIntegrations.connected.count': number; @@ -170,19 +164,21 @@ export type TelemetryEvents = { /** Sent when a "Graph" command is executed */ 'graph/command': Omit; + 'graph/shown': WebviewShownEventData & GraphShownEventData; + /** Sent when the user interacts with the graph */ 'graph/columns/changed': Record<`column.${string}`, boolean | string | number>; - 'graph/exclude/toggle': { key: string; value: boolean }; + 'graph/exclude/toggled': { key: string; value: boolean }; 'graph/jumpToRef': { alt: boolean }; 'graph/minimap/daySelected': undefined; - 'graph/repository/change': undefined; - 'graph/repository/openOnRemote': undefined; - 'graph/row/hover': undefined; - 'graph/row/more': { duration: number }; + 'graph/repository/changed': undefined; + 'graph/repository/openOnRemote': WebviewEventData; + 'graph/row/hovered': undefined; 'graph/row/selected': { rows: number }; + 'graph/rows/loaded': { duration: number; rows: number }; 'graph/sidebar/action': { action: string }; - 'graph/search': { types: string; duration: number }; - 'graph/visibility/changed': { branchesVisibility: GraphBranchesVisibility }; + 'graph/searched': { types: string; duration: number; matches: number }; + 'graph/branchesVisibility/changed': { branchesVisibility: GraphBranchesVisibility }; /** Sent when the user interacts with the visual file history */ 'timeline/period/change': { period: string }; @@ -311,12 +307,7 @@ export type TelemetryEvents = { }; /** Sent when a repository is opened */ - 'repository/opened': { - 'repository.id': string; - 'repository.scheme': string; - 'repository.closed': boolean; - 'repository.folder.scheme': string | undefined; - 'repository.provider.id': string; + 'repository/opened': RepositoryEventData & { 'repository.remoteProviders': string; 'repository.contributors.commits.count': number | undefined; 'repository.contributors.commits.avgPerContributor': number | undefined; @@ -324,12 +315,7 @@ export type TelemetryEvents = { 'repository.contributors.since': '1.year.ago'; } & Record<`repository.contributors.distribution.${GitContributionTiers}`, number>; /** Sent when a repository's visibility is first requested */ - 'repository/visibility': { - 'repository.id': string | undefined; - 'repository.scheme': string | undefined; - 'repository.closed': boolean | undefined; - 'repository.folder.scheme': string | undefined; - 'repository.provider.id': string | undefined; + 'repository/visibility': Partial & { 'repository.visibility': 'private' | 'public' | 'local' | undefined; }; @@ -377,25 +363,10 @@ export type TelemetryEvents = { | 'integrations' | 'more'; }; -} & Record< - `${WebviewTypes | WebviewViewTypes}/showAborted`, - { - id: WebviewIds | WebviewViewIds; - instanceId: string | undefined; - host: 'editor' | 'view'; - duration: number; - loading: boolean; - } -> & +} & Record<`${WebviewTypes | WebviewViewTypes}/showAborted`, WebviewShownEventData> & Record< - `${WebviewTypes | WebviewViewTypes}/shown`, - { - id: WebviewIds | WebviewViewIds; - instanceId: string | undefined; - host: 'editor' | 'view'; - duration: number; - loading: boolean; - } & Record<`context.${string}`, string | number | boolean> + `${Exclude}/shown`, + WebviewShownEventData & Record<`context.${string}`, string | number | boolean | undefined> >; type AIEventDataBase = { @@ -471,6 +442,30 @@ type LaunchpadGroups = | 'other' | 'snoozed'; +type FlattenedGraphConfig = { + [K in keyof Flatten]: Flatten[K]; +}; +type GraphContextEventData = {} & WebviewTelemetryContext & + Partial<{ + [K in keyof RepositoryEventData as `context.${K}`]: RepositoryEventData[K]; + }>; + +type GraphShownEventData = GraphContextEventData & + FlattenedGraphConfig & + Partial> & + Partial>; + +export type GraphTelemetryContext = GraphContextEventData; +export type GraphShownTelemetryContext = GraphShownEventData; + +type RepositoryEventData = { + 'repository.id': string; + 'repository.scheme': string; + 'repository.closed': boolean; + 'repository.folder.scheme': string | undefined; + 'repository.provider.id': string; +}; + type SubscriptionEventData = { 'subscription.state'?: SubscriptionState; 'subscription.status'?: @@ -492,6 +487,18 @@ type SubscriptionEventData = { Record<`previous.subscription.previewTrial.${string}`, string | number | boolean | undefined> >; +type WebviewEventData = { + id: string; + instanceId: string | undefined; + host: 'editor' | 'view'; +}; +export type WebviewTelemetryContext = WebviewEventData; + +type WebviewShownEventData = WebviewEventData & { + duration: number; + loading: boolean; +}; + export type LoginContext = 'start_trial'; export type ConnectIntegrationContext = 'launchpad'; export type Context = LoginContext | ConnectIntegrationContext; diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 1fb1b1fbc17b9..13360fae43485 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -19,6 +19,7 @@ import type { import { GlyphChars } from '../../../constants'; import { Commands } from '../../../constants.commands'; import type { StoredGraphFilters, StoredGraphRefType } from '../../../constants.storage'; +import type { GraphShownTelemetryContext, GraphTelemetryContext } from '../../../constants.telemetry'; import type { Container } from '../../../container'; import { CancellationError } from '../../../errors'; import type { CommitSelectedEvent } from '../../../eventBus'; @@ -87,7 +88,7 @@ import { } from '../../../git/models/repository'; import { getWorktreesByBranch } from '../../../git/models/worktree'; import type { GitSearch } from '../../../git/search'; -import { getSearchQueryComparisonKey } from '../../../git/search'; +import { getSearchQueryComparisonKey, parseSearchQuery } from '../../../git/search'; import { splitGitCommitMessage } from '../../../git/utils/commit-utils'; import { ReferencesQuickPickIncludes, showReferencePicker } from '../../../quickpicks/referencePicker'; import { showRepositoryPicker } from '../../../quickpicks/repositoryPicker'; @@ -96,12 +97,13 @@ import { debug, log } from '../../../system/decorators/log'; import type { Deferrable } from '../../../system/function'; import { debounce, disposableInterval } from '../../../system/function'; import { count, find, last, map } from '../../../system/iterable'; -import { updateRecordValue } from '../../../system/object'; +import { flatten, updateRecordValue } from '../../../system/object'; import { getSettledValue, pauseOnCancelOrTimeout, pauseOnCancelOrTimeoutMapTuplePromise, } from '../../../system/promise'; +import { Stopwatch } from '../../../system/stopwatch'; import { executeActionCommand, executeCommand, @@ -159,6 +161,7 @@ import type { GraphRefMetadataType, GraphRepository, GraphScrollMarkerTypes, + GraphSearchResults, GraphSelectedRows, GraphStashContextValue, GraphTagContextValue, @@ -352,11 +355,48 @@ export class GraphWebviewProvider implements WebviewProvider]: GraphShownTelemetryContext[K]; + }> = {}; + const columns = this.getColumns(); + if (columns != null) { + for (const [name, config] of Object.entries(columns)) { + if (!config.isHidden) { + columnContext[`context.column.${name}.visible`] = true; + } + if (config.mode != null) { + columnContext[`context.column.${name}.mode`] = config.mode; + } + } + } + + const cfg = flatten(configuration.get('graph'), 'context.config', { joinArrays: true }); + const context: GraphShownTelemetryContext = { + ...this.getTelemetryContext(), + ...columnContext, + ...cfg, + }; + + return context; + } + async onShowing( loading: boolean, _options?: WebviewShowOptions, ...args: WebviewShowingArgs - ): Promise<[boolean, Record<`context.${string}`, string | number | boolean> | undefined]> { + ): Promise<[boolean, Record<`context.${string}`, string | number | boolean | undefined> | undefined]> { this._firstSelection = true; this._etag = this.container.git.etag; @@ -386,7 +426,7 @@ export class GraphWebviewProvider implements WebviewProvider(requestType: T, msg: IpcCallMessageType) { try { + using sw = new Stopwatch(`GraphWebviewProvider.onSearchRequest(${this.host.id})`); const results = await this.getSearchResults(msg.params); - this.container.telemetry.sendEvent('graph/search', { - types: '', - duration: 0, + const query = msg.params.search ? parseSearchQuery(msg.params.search) : undefined; + const types = new Set(); + if (query != null) { + for (const [_, values] of query) { + values.forEach(v => types.add(v)); + } + } + this.container.telemetry.sendEvent('graph/searched', { + types: [...types].join(','), + duration: sw.elapsed(), + matches: (results.results as GraphSearchResults)?.count ?? 0, }); void this.host.respond(requestType, msg, results); } catch (ex) { @@ -1498,7 +1549,7 @@ export class GraphWebviewProvider implements WebviewProvider(requestType: T, msg: IpcCallMessageType) { @@ -2780,7 +2831,7 @@ export class GraphWebviewProvider implements WebviewProvider = { }; }[keyof T]; -type Flatten< +export type Flatten< T extends object | null | undefined, P extends string | undefined, JoinArrays extends boolean, diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index 6d5fae1d23a44..59005e48e0e01 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -67,7 +67,7 @@ import { import { createCommandLink } from '../../../../system/commands'; import { filterMap, first, groupByFilterMap, join } from '../../../../system/iterable'; import { createWebviewCommandLink } from '../../../../system/webview'; -import type { IpcNotification } from '../../../protocol'; +import type { IpcNotification, TelemetrySendEventParams } from '../../../protocol'; import { DidChangeHostWindowFocusNotification } from '../../../protocol'; import { GlButton } from '../../shared/components/button.react'; import { GlCheckbox } from '../../shared/components/checkbox'; @@ -466,7 +466,9 @@ export function GraphWrapper({ graphRef.current?.selectCommits([sha], false, true); - queueMicrotask(() => e.target && emitTelemetrySentEvent(e.target, { name: 'graph/minimap/daySelected' })); + queueMicrotask( + () => e.target && emitTelemetrySentEvent(e.target, { name: 'graph/minimap/daySelected', data: {} }), + ); }; const handleOnMinimapToggle = (_e: React.MouseEvent) => { @@ -1011,7 +1013,12 @@ export function GraphWrapper({ aria-label={`Open Repository on ${repo.provider.name}`} slot="anchor" onClick={e => - emitTelemetrySentEvent(e.target, { name: 'graph/repository/openOnRemote' }) + emitTelemetrySentEvent< + TelemetrySendEventParams<'graph/repository/openOnRemote'> + >(e.target, { + name: 'graph/repository/openOnRemote', + data: {}, + }) } > { type: row.type as GitGraphRowType, id: row.sha, }); - this._telemetry.sendEvent({ name: 'graph/row/hover' }); + this._telemetry.sendEvent({ name: 'graph/row/hovered', data: {} }); return request; } catch (ex) { return { id: row.sha, markdown: { status: 'rejected' as const, reason: ex } }; diff --git a/src/webviews/apps/plus/timeline/chart.ts b/src/webviews/apps/plus/timeline/chart.ts index 683c918616de6..190bff2f1706d 100644 --- a/src/webviews/apps/plus/timeline/chart.ts +++ b/src/webviews/apps/plus/timeline/chart.ts @@ -506,10 +506,10 @@ export class TimelineChart implements Disposable { }); } - private onToggleLegend(id: string) { - console.log('timeline/chart/toggleLegend', id); + private onToggleLegend(_id: string) { emitTelemetrySentEvent(this.$container, { name: 'timeline/chart/toggleLegend', + data: {}, }); } } diff --git a/src/webviews/protocol.ts b/src/webviews/protocol.ts index d5f1ecf4a95ba..8240167ec70be 100644 --- a/src/webviews/protocol.ts +++ b/src/webviews/protocol.ts @@ -1,6 +1,6 @@ import type { TimeInput } from '@opentelemetry/api'; import type { Config } from '../config'; -import type { Source, TelemetryEvents } from '../constants.telemetry'; +import type { Source, TelemetryEvents, WebviewTelemetryContext } from '../constants.telemetry'; import type { CustomEditorIds, CustomEditorTypes, @@ -96,7 +96,7 @@ export const UpdateConfigurationCommand = new IpcCommand { name: T; - data?: TelemetryEvents[T]; + data: Omit; source?: Source; startTime?: TimeInput; endTime?: TimeInput; diff --git a/src/webviews/webviewController.ts b/src/webviews/webviewController.ts index 23526f2de5597..a1ebc2fb1c574 100644 --- a/src/webviews/webviewController.ts +++ b/src/webviews/webviewController.ts @@ -2,6 +2,7 @@ import { getNonce } from '@env/crypto'; import type { ViewBadge, Webview, WebviewPanel, WebviewView, WindowState } from 'vscode'; import { Disposable, EventEmitter, Uri, ViewColumn, window, workspace } from 'vscode'; import type { Commands } from '../constants.commands'; +import type { WebviewTelemetryContext } from '../constants.telemetry'; import type { CustomEditorTypes, WebviewTypes, WebviewViewTypes } from '../constants.views'; import type { Container } from '../container'; import { getScopedCounter } from '../system/counter'; @@ -197,6 +198,14 @@ export class WebviewController< this._initializing = undefined; } + getTelemetryContext(): WebviewTelemetryContext { + return { + id: this.id, + instanceId: this.instanceId, + host: this.isHost('editor') ? ('editor' as const) : ('view' as const), + }; + } + isHost(type: 'editor'): this is WebviewPanelController; isHost(type: 'view'): this is WebviewViewController; isHost( @@ -282,9 +291,7 @@ export class WebviewController< } const eventBase = { - id: this.id, - instanceId: this.instanceId, - host: this.isHost('editor') ? ('editor' as const) : ('view' as const), + ...this.getTelemetryContext(), loading: loading, }; @@ -409,7 +416,11 @@ export class WebviewController< break; case TelemetrySendEventCommand.is(e): - this.container.telemetry.sendEvent(e.params.name, e.params.data, e.params.source); + this.container.telemetry.sendEvent( + e.params.name, + { ...e.params.data, ...(this.provider.getTelemetryContext?.() ?? this.getTelemetryContext()) }, + e.params.source, + ); break; default: diff --git a/src/webviews/webviewProvider.ts b/src/webviews/webviewProvider.ts index 3fe79673874d5..c80b29e36544b 100644 --- a/src/webviews/webviewProvider.ts +++ b/src/webviews/webviewProvider.ts @@ -1,4 +1,5 @@ import type { Disposable, Uri, ViewBadge } from 'vscode'; +import type { WebviewTelemetryContext } from '../constants.telemetry'; import type { WebviewContext } from '../system/webview'; import type { IpcCallMessageType, @@ -22,13 +23,15 @@ export interface WebviewProvider): boolean | undefined; getSplitArgs?(): WebviewShowingArgs; + getTelemetryContext?(): Record<`context.${string}`, string | number | boolean | undefined> & + WebviewTelemetryContext; onShowing?( loading: boolean, options: WebviewShowOptions, ...args: WebviewShowingArgs ): - | [boolean, Record<`context.${string}`, string | number | boolean> | undefined] - | Promise<[boolean, Record<`context.${string}`, string | number | boolean> | undefined]>; + | [boolean, Record<`context.${string}`, string | number | boolean | undefined> | undefined] + | Promise<[boolean, Record<`context.${string}`, string | number | boolean | undefined> | undefined]>; registerCommands?(): Disposable[]; includeBootstrap?(): SerializedState | Promise; @@ -73,6 +76,7 @@ export interface WebviewHost< clearPendingIpcNotifications(): void; sendPendingIpcNotifications(): void; + getTelemetryContext(): WebviewTelemetryContext; isHost(type: 'editor'): this is WebviewHost; isHost(type: 'view'): this is WebviewHost;