diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 873b5c9bb306f..717619c9bb589 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -954,6 +954,7 @@ export interface ITerminalInstance { } export interface ITerminalQuickFixOptions { + id: string; commandLineMatcher: string | RegExp; outputMatcher?: ITerminalOutputMatcher; getQuickFixes: TerminalQuickFixCallback; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts index 04fc6eb724a1f..5f4e870f69f92 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts @@ -21,6 +21,7 @@ export const GitCreatePrOutputRegex = /remote:\s*(https:\/\/github\.com\/.+\/.+\ export function gitSimilarCommand(): ITerminalQuickFixOptions { return { + id: 'Git Similar', commandLineMatcher: GitCommandLineRegex, outputMatcher: { lineMatcher: GitSimilarOutputRegex, @@ -51,6 +52,7 @@ export function gitSimilarCommand(): ITerminalQuickFixOptions { } export function gitTwoDashes(): ITerminalQuickFixOptions { return { + id: 'Git Two Dashes', commandLineMatcher: GitCommandLineRegex, outputMatcher: { lineMatcher: GitTwoDashesRegex, @@ -74,6 +76,7 @@ export function gitTwoDashes(): ITerminalQuickFixOptions { } export function freePort(terminalInstance?: Partial): ITerminalQuickFixOptions { return { + id: 'Free Port', commandLineMatcher: AnyCommandLineRegex, outputMatcher: { lineMatcher: FreePortOutputRegex, @@ -101,6 +104,7 @@ export function freePort(terminalInstance?: Partial): ITermin } export function gitPushSetUpstream(): ITerminalQuickFixOptions { return { + id: 'Git Push Set Upstream', commandLineMatcher: GitPushCommandLineRegex, outputMatcher: { lineMatcher: GitPushOutputRegex, @@ -125,6 +129,7 @@ export function gitPushSetUpstream(): ITerminalQuickFixOptions { export function gitCreatePr(): ITerminalQuickFixOptions { return { + id: 'Git Create Pr', commandLineMatcher: GitPushCommandLineRegex, outputMatcher: { lineMatcher: GitCreatePrOutputRegex, diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts index 44ce6c7858f6c..0c7b0e156b031 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts @@ -26,8 +26,21 @@ import { IDecoration, Terminal } from 'xterm'; // Importing types is safe in any layer // eslint-disable-next-line local/code-import-patterns import type { ITerminalAddon } from 'xterm-headless'; - - +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ILogService } from 'vs/platform/log/common/log'; +const quickFixTelemetryTitle = 'terminal/quick-fix'; +type QuickFixResultTelemetryEvent = { + id: string; + fixesShown: boolean; + ranQuickFixCommand?: boolean; +}; +type QuickFixClassification = { + owner: 'meganrogge'; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The quick fix ID' }; + fixesShown: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the fixes were shown by the user' }; + ranQuickFixCommand?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If the command that was executed matched a quick fix suggested one. Undefined if no command is expected.' }; + comment: 'Terminal quick fixes'; +}; const quickFixSelectors = [DecorationSelector.QuickFix, DecorationSelector.LightBulb, DecorationSelector.Codicon, DecorationSelector.CommandDecoration, DecorationSelector.XtermDecoration]; export interface ITerminalQuickFix { showMenu(): void; @@ -56,13 +69,18 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, private readonly _terminalDecorationHoverService: TerminalDecorationHoverManager; + private _fixesShown: boolean = false; + private _expectedCommands: string[] | undefined; + constructor(private readonly _capabilities: ITerminalCapabilityStore, @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService, @IAudioCueService private readonly _audioCueService: IAudioCueService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IOpenerService private readonly _openerService: IOpenerService + @IOpenerService private readonly _openerService: IOpenerService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILogService private readonly _logService: ILogService ) { super(); const commandDetectionCapability = this._capabilities.get(TerminalCapability.CommandDetection); @@ -83,6 +101,7 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, } showMenu(): void { + this._fixesShown = true; this._decoration?.element?.click(); } @@ -99,7 +118,25 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, if (!terminal || !commandDetection) { return; } - this._register(commandDetection.onCommandFinished(command => this._resolveQuickFixes(command))); + this._register(commandDetection.onCommandFinished(command => { + if (this._expectedCommands) { + const ranQuickFixCommand = this._expectedCommands.includes(command.command); + this._logService.debug(quickFixTelemetryTitle, { + id: this._expectedCommands.join(' '), + fixesShown: this._fixesShown, + ranQuickFixCommand + }); + this._telemetryService?.publicLog2(quickFixTelemetryTitle, { + id: this._expectedCommands.join(' '), + fixesShown: this._fixesShown, + ranQuickFixCommand + }); + this._expectedCommands = undefined; + } + this._resolveQuickFixes(command); + this._fixesShown = false; + })); + // The buffer is not ready by the time command finish // is called. Add the decoration on command start if there are corresponding quick fixes this._register(commandDetection.onCommandStarted(() => { @@ -120,9 +157,24 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, if (!result) { return; } - const { fixes, onDidRunQuickFix } = result; + const { fixes, onDidRunQuickFix, expectedCommands } = result; + this._expectedCommands = expectedCommands; this._quickFixes = fixes; - onDidRunQuickFix(() => this._disposeQuickFix()); + this._register(onDidRunQuickFix((id) => { + const ranQuickFixCommand = (this._expectedCommands?.includes(command.command) || false); + this._logService.debug(quickFixTelemetryTitle, { + id, + fixesShown: this._fixesShown, + ranQuickFixCommand + }); + this._telemetryService?.publicLog2(quickFixTelemetryTitle, { + id, + fixesShown: this._fixesShown, + ranQuickFixCommand + }); + this._disposeQuickFix(); + this._fixesShown = false; + })); } private _disposeQuickFix(): void { @@ -177,11 +229,12 @@ export function getQuickFixesForCommand( quickFixOptions: Map, openerService: IOpenerService, onDidRequestRerunCommand?: Emitter<{ command: string; addNewLine?: boolean }> -): { fixes: IAction[]; onDidRunQuickFix: Event } | undefined { - const onDidRunQuickFixEmitter = new Emitter(); +): { fixes: IAction[]; onDidRunQuickFix: Event; expectedCommands?: string[] } | undefined { + const onDidRunQuickFixEmitter = new Emitter(); const onDidRunQuickFix = onDidRunQuickFixEmitter.event; const fixes: IAction[] = []; const newCommand = command.command; + const expectedCommands = []; for (const options of quickFixOptions.values()) { for (const option of options) { if (option.exitStatus !== undefined && option.exitStatus !== (command.exitCode === 0)) { @@ -196,6 +249,7 @@ export function getQuickFixesForCommand( if (outputMatcher) { outputMatch = command.getOutputMatch(outputMatcher); } + const id = option.id; const quickFixes = option.getQuickFixes({ commandLineMatch, outputMatch }, command); if (quickFixes) { for (const quickFix of asArray(quickFixes)) { @@ -218,6 +272,7 @@ export function getQuickFixesForCommand( tooltip: label, command: quickFix.command } as IAction; + expectedCommands.push(quickFix.command); break; } case 'opener': { @@ -231,7 +286,7 @@ export function getQuickFixesForCommand( openerService.open(quickFix.uri); // since no command gets run here, need to // clear the decoration and quick fix - onDidRunQuickFixEmitter.fire(); + onDidRunQuickFixEmitter.fire(id); }, tooltip: label, uri: quickFix.uri @@ -245,7 +300,10 @@ export function getQuickFixesForCommand( label: quickFix.label, class: quickFix.class, enabled: quickFix.enabled, - run: () => quickFix.run(), + run: () => { + quickFix.run(); + onDidRunQuickFixEmitter.fire(id); + }, tooltip: quickFix.tooltip }; } @@ -256,7 +314,7 @@ export function getQuickFixesForCommand( } } } - return fixes.length > 0 ? { fixes, onDidRunQuickFix } : undefined; + return fixes.length > 0 ? { fixes, onDidRunQuickFix, expectedCommands } : undefined; }