diff --git a/src/commands/gitCommands.ts b/src/commands/gitCommands.ts index a6158e6ea8185..1da5df1977299 100644 --- a/src/commands/gitCommands.ts +++ b/src/commands/gitCommands.ts @@ -218,6 +218,17 @@ export class GitCommandsCommand extends Command { break; case Commands.ShowLaunchpad: args = { command: 'focus', ...args }; + // TODO: Improve this. Args do not come in with a command property, + // but the contextual typing relies on it to recognize the other args. + if (args.command === 'focus') { + args.source ??= 'commandPalette'; + this.container.telemetry.sendEvent('launchpad/opened', { + source: args.source, + group: args.state?.initialGroup ?? null, + selectTopItem: args.state?.selectTopItem ?? false, + }); + } + break; } diff --git a/src/constants.ts b/src/constants.ts index caba8d6681047..d03b271df84e1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -811,7 +811,13 @@ export type TelemetryEvents = | 'usage/track' | 'openReviewMode' | 'codeSuggestionCreated' - | 'codeSuggestionViewed'; + | 'codeSuggestionViewed' + | 'launchpad/opened' + | 'launchpad/configurationChanged' + | 'launchpad/groupToggled' + | 'launchpad/actionTaken' + | 'launchpad/indicatorHidden' + | 'launchpad/indicatorFirstDataReceived'; export type AIProviders = 'anthropic' | 'gemini' | 'openai'; export type AIModels = Provider extends 'openai' @@ -879,6 +885,7 @@ export type GlobalStorage = { 'views:welcome:visible': boolean; 'confirm:draft:storage': boolean; 'home:sections:collapsed': string[]; + 'launchpad:indicator:dataReceived': boolean; } & { [key in `confirm:ai:tos:${AIProviders}`]: boolean } & { [key in `provider:authentication:skip:${string}`]: boolean; } & { [key in `gk:${string}:checkin`]: Stored } & { diff --git a/src/plus/focus/focus.ts b/src/plus/focus/focus.ts index c0c4609d096fb..e89837543f103 100644 --- a/src/plus/focus/focus.ts +++ b/src/plus/focus/focus.ts @@ -1,3 +1,4 @@ +import type { QuickInputButton } from 'vscode'; import { commands, Uri } from 'vscode'; import { getAvatarUri } from '../../avatars'; import type { @@ -74,7 +75,9 @@ const groupMap = new Map([ ['snoozed', ['Snoozed', 'bell-slash']], ]); -export interface FocusItemQuickPickItem extends QuickPickItemOfT {} +export interface FocusItemQuickPickItem extends QuickPickItemOfT { + group: FocusGroup; +} interface Context { items: FocusItem[]; @@ -82,8 +85,12 @@ interface Context { collapsed: Map; } +interface GroupedFocusItem extends FocusItem { + group: FocusGroup; +} + interface State { - item?: FocusItem; + item?: GroupedFocusItem; action?: FocusAction | FocusTargetAction; initialGroup?: FocusGroup; selectTopItem?: boolean; @@ -92,6 +99,7 @@ interface State { export interface FocusCommandArgs { readonly command: 'focus'; confirm?: boolean; + source?: 'indicator' | 'home' | 'commandPalette' | 'welcome'; state?: Partial; } @@ -166,7 +174,7 @@ export class FocusCommand extends QuickCommand { context.items = await this.container.focus.getCategorizedItems(); if (state.counter < 2 || state.item == null) { - const result = yield* this.pickFocusItemStep(state, context, { + const result = yield* this.pickFocusItemStep(state, context, this.container, { picked: state.item?.id, selectTopItem: state.selectTopItem, }); @@ -179,12 +187,17 @@ export class FocusCommand extends QuickCommand { if (this.confirm(state.confirm)) { await this.container.focus.ensureFocusItemCodeSuggestions(state.item); + this.sendItemActionTelemetry('select', state.item, state.item.group); const result = yield* this.confirmStep(state, context); if (result === StepResultBreak) continue; state.action = result; } + if (state.action) { + this.sendItemActionTelemetry(state.action, state.item, state.item.group); + } + if (typeof state.action === 'string') { switch (state.action) { case 'merge': { @@ -235,8 +248,9 @@ export class FocusCommand extends QuickCommand { private *pickFocusItemStep( state: StepState, context: Context, + container: Container, { picked, selectTopItem }: { picked?: string; selectTopItem?: boolean }, - ): StepResultGenerator { + ): StepResultGenerator { function getItems(categorizedItems: FocusItem[]) { const items: (FocusItemQuickPickItem | DirectiveQuickPickItem)[] = []; @@ -260,7 +274,13 @@ export class FocusCommand extends QuickCommand { })\u00a0\u00a0${groupMap.get(ui)![0]?.toUpperCase()}`, //'\u00a0', //detail: groupMap.get(group)?.[0].toUpperCase(), onDidSelect: () => { - context.collapsed.set(ui, !context.collapsed.get(ui)); + const collapse = !context.collapsed.get(ui); + context.collapsed.set(ui, collapse); + container.telemetry.sendEvent('launchpad/groupToggled', { + group: ui, + expanded: !collapse, + itemsCount: groupItems.length, + }); }, }), ); @@ -299,6 +319,7 @@ export class FocusCommand extends QuickCommand { iconPath: i.author?.avatarUrl != null ? Uri.parse(i.author.avatarUrl) : undefined, item: i, picked: i.id === picked || i.id === topItem?.id, + group: ui, }; }), ); @@ -351,7 +372,7 @@ export class FocusCommand extends QuickCommand { } }, - onDidClickItemButton: async (quickpick, button, { item }) => { + onDidClickItemButton: async (quickpick, button, { group, item }) => { switch (button) { case OpenOnGitHubQuickInputButton: this.container.focus.open(item); @@ -377,6 +398,7 @@ export class FocusCommand extends QuickCommand { break; } + this.sendItemActionTelemetry(button, item, group); quickpick.busy = true; try { @@ -392,7 +414,9 @@ export class FocusCommand extends QuickCommand { }); const selection: StepSelection = yield step; - return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak; + return canPickStepContinue(step, state, selection) + ? { ...selection[0].item, group: selection[0].group } + : StepResultBreak; } private *confirmStep( @@ -548,6 +572,8 @@ export class FocusCommand extends QuickCommand { } break; } + + this.sendItemActionTelemetry(button, state.item, state.item.group); }, }, ); @@ -780,6 +806,68 @@ export class FocusCommand extends QuickCommand { return 'Open'; } } + + private sendItemActionTelemetry( + buttonOrAction: QuickInputButton | FocusAction | FocusTargetAction | 'select', + item: FocusItem, + group: FocusGroup, + ) { + let action: + | FocusAction + | 'pin' + | 'unpin' + | 'snooze' + | 'unsnooze' + | 'open-suggestion' + | 'open-suggestion-browser' + | 'select' + | undefined; + if (typeof buttonOrAction !== 'string' && 'action' in buttonOrAction) { + action = buttonOrAction.action; + } else { + switch (buttonOrAction) { + case MergeQuickInputButton: + action = 'merge'; + break; + case OpenOnGitHubQuickInputButton: + action = 'soft-open'; + break; + case PinQuickInputButton: + action = 'pin'; + break; + case UnpinQuickInputButton: + action = 'unpin'; + break; + case SnoozeQuickInputButton: + action = 'snooze'; + break; + case UnsnoozeQuickInputButton: + action = 'unsnooze'; + break; + case OpenCodeSuggestionBrowserQuickInputButton: + action = 'open-suggestion-browser'; + break; + case 'open': + case 'merge': + case 'soft-open': + case 'switch': + case 'select': + action = buttonOrAction; + break; + } + } + + if (action == null) return; + + this.container.telemetry.sendEvent('launchpad/actionTaken', { + action: action, + itemType: item.type, + itemProvider: item.provider.id, + itemActionableCategory: item.actionableCategory, + itemGroup: group, + itemCodeSuggestionCount: item.codeSuggestionsCount, + }); + } } function isFocusTargetActionQuickPickItem(item: any): item is QuickPickItemOfT { diff --git a/src/plus/focus/focusIndicator.ts b/src/plus/focus/focusIndicator.ts index c18c6e9de195f..de203f2968e2c 100644 --- a/src/plus/focus/focusIndicator.ts +++ b/src/plus/focus/focusIndicator.ts @@ -125,7 +125,7 @@ export class FocusIndicator implements Disposable { : { title: 'Open Launchpad', command: Commands.ShowLaunchpad, - arguments: [{ state: { selectTopItem: label === 'item' } }], + arguments: [{ source: 'indicator', state: { selectTopItem: label === 'item' } }], }; } @@ -221,6 +221,7 @@ export class FocusIndicator implements Disposable { ); this._statusBarFocus.color = undefined; } else if (state === 'data') { + void this.maybeSendFirstDataEvent(); this._lastDataUpdate = new Date(); const useColors = configuration.get('launchpad.indicator.useColors'); const groups = configuration.get('launchpad.indicator.groups') ?? ([] satisfies FocusGroup[]); @@ -259,6 +260,7 @@ export class FocusIndicator implements Disposable { : pluralize('pull request', items.length) } can be merged.](command:gitlens.showLaunchpad?${encodeURIComponent( JSON.stringify({ + source: 'indicator', state: { initialGroup: 'mergeable', selectTopItem: labelText === 'item' }, }), )} "Open Ready to Merge in Launchpad")`, @@ -313,6 +315,7 @@ export class FocusIndicator implements Disposable { hasMultipleCategories ? 'are blocked' : actionMessage }.](command:gitlens.showLaunchpad?${encodeURIComponent( JSON.stringify({ + source: 'indicator', state: { initialGroup: 'blocked', selectTopItem: labelText === 'item' }, }), )} "Open Blocked in Launchpad")`, @@ -346,6 +349,7 @@ export class FocusIndicator implements Disposable { items.length > 1 ? 'require' : 'requires' } follow-up.](command:gitlens.showLaunchpad?${encodeURIComponent( JSON.stringify({ + source: 'indicator', state: { initialGroup: 'follow-up', selectTopItem: labelText === 'item' }, }), )} "Open Follow-Up in Launchpad")`, @@ -364,6 +368,7 @@ export class FocusIndicator implements Disposable { items.length > 1 ? 'need' : 'needs' } your review.](command:gitlens.showLaunchpad?${encodeURIComponent( JSON.stringify({ + source: 'indicator', state: { initialGroup: 'needs-review', selectTopItem: labelText === 'item', @@ -427,4 +432,13 @@ export class FocusIndicator implements Disposable { : '' }`; } + + private async maybeSendFirstDataEvent() { + const firstTimeDataReceived = this.container.storage.get('launchpad:indicator:dataReceived') ?? false; + if (!firstTimeDataReceived) { + void this.container.storage.store('launchpad:indicator:dataReceived', true); + const userId = (await this.container.subscription.getSubscription())?.account?.id; + this.container.telemetry.sendEvent('launchpad/indicatorFirstDataReceived', { userId: userId }); + } + } } diff --git a/src/plus/focus/focusProvider.ts b/src/plus/focus/focusProvider.ts index 47d25d226359a..7529ccb9f5bf4 100644 --- a/src/plus/focus/focusProvider.ts +++ b/src/plus/focus/focusProvider.ts @@ -589,6 +589,23 @@ export class FocusProvider implements Disposable { private onConfigurationChanged(e: ConfigurationChangeEvent) { if (!configuration.changed(e, 'launchpad')) return; + const launchpadConfig = configuration.get('launchpad'); + this.container.telemetry.sendEvent('launchpad/configurationChanged', { + staleThreshold: launchpadConfig.staleThreshold, + ignoredRepositories: launchpadConfig.ignoredRepositories, + indicatorEnabled: launchpadConfig.indicator.enabled, + indicatorOpenInEditor: launchpadConfig.indicator.openInEditor, + indicatorIcon: launchpadConfig.indicator.icon, + indicatorLabel: launchpadConfig.indicator.label, + indicatorUseColors: launchpadConfig.indicator.useColors, + indicatorGroups: launchpadConfig.indicator.groups, + indicatorPollingEnabled: launchpadConfig.indicator.polling.enabled, + indicatorPollingInterval: launchpadConfig.indicator.polling.interval, + }); + + if (configuration.changed(e, 'launchpad.indicator.enabled') && !launchpadConfig.indicator.enabled) { + this.container.telemetry.sendEvent('launchpad/indicatorHidden'); + } if ( configuration.changed(e, 'launchpad.ignoredRepositories') || diff --git a/src/webviews/apps/home/home.html b/src/webviews/apps/home/home.html index b4415119be348..8d1ad086c2a70 100644 --- a/src/webviews/apps/home/home.html +++ b/src/webviews/apps/home/home.html @@ -227,7 +227,7 @@