diff --git a/src/extension.ts b/src/extension.ts index 8995a390..5db3e22d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -14,6 +14,9 @@ import {SfCli} from './lib/sf-cli'; import {Displayable, ProgressNotification, UxDisplay} from './lib/display'; import {DiagnosticManager, DiagnosticConvertible, DiagnosticManagerImpl} from './lib/diagnostics'; +import {DiffCreateAction} from './lib/actions/diff-create-action'; +import { DiffAcceptAction } from './lib/actions/diff-accept-action'; +import { DiffRejectAction } from './lib/actions/diff-reject-action'; import {ScannerAction} from './lib/actions/scanner-action'; import { CliScannerV4Strategy } from './lib/scanner-strategies/v4-scanner'; import { CliScannerV5Strategy } from './lib/scanner-strategies/v5-scanner'; @@ -225,21 +228,33 @@ export async function activate(context: vscode.ExtensionContext): Promise { + vscode.commands.registerCommand(Constants.UNIFIED_DIFF, async (source: string, code: string, file?: string) => { + await (new DiffCreateAction(`${source}.${Constants.UNIFIED_DIFF}`, { + callback: (code: string, file?: string) => VSCodeUnifiedDiff.singleton.unifiedDiff(code, file), + telemetryService + })).run(code, file); await VSCodeUnifiedDiff.singleton.unifiedDiff(code, file); }) ); context.subscriptions.push( vscode.commands.registerCommand(CODEGENIE_UNIFIED_DIFF_ACCEPT, async (hunk: DiffHunk) => { - await VSCodeUnifiedDiff.singleton.unifiedDiffAccept(hunk); + // TODO: The use of the prefix shouldn't be hardcoded. Ideally, it should be passed in as an argument to the command. + // But that would require us to make changes to the underlying UnifiedDiff code that we're not currently in a position to make. + await (new DiffAcceptAction(`${Constants.A4D_PREFIX}.${CODEGENIE_UNIFIED_DIFF_ACCEPT}`, { + callback: async (diffHunk: DiffHunk) => { + await VSCodeUnifiedDiff.singleton.unifiedDiffAccept(diffHunk); + return diffHunk.lines.length; + }, + telemetryService + })).run(hunk); // For accept & accept all, it is tricky to track the diagnostics and the changed lines as multiple fixes are requested. // Hence, we save the file and rerun the scan instead. await vscode.window.activeTextEditor.document.save(); @@ -252,12 +267,22 @@ function setupUnifiedDiff(context: vscode.ExtensionContext, diagnosticManager: D ); context.subscriptions.push( vscode.commands.registerCommand(CODEGENIE_UNIFIED_DIFF_REJECT, async (hunk: DiffHunk) => { - await VSCodeUnifiedDiff.singleton.unifiedDiffReject(hunk); + // TODO: The use of the prefix shouldn't be hardcoded. Ideally, it should be passed in as an argument to the command. + // But that would require us to make changes to the underlying UnifiedDiff code that we're not currently in a position to make. + await (new DiffRejectAction(`${Constants.A4D_PREFIX}.${CODEGENIE_UNIFIED_DIFF_REJECT}`, { + callback: (diffHunk: DiffHunk) => VSCodeUnifiedDiff.singleton.unifiedDiffReject(diffHunk), + telemetryService + })).run(hunk); }) ); context.subscriptions.push( vscode.commands.registerCommand(CODEGENIE_UNIFIED_DIFF_ACCEPT_ALL, async () => { - await VSCodeUnifiedDiff.singleton.unifiedDiffAcceptAll(); + // TODO: The use of the prefix shouldn't be hardcoded. Ideally, it should be passed in as an argument to the command. + // But that would require us to make changes to the underlying UnifiedDiff code that we're not currently in a position to make. + await (new DiffAcceptAction(`${Constants.A4D_PREFIX}.${CODEGENIE_UNIFIED_DIFF_ACCEPT_ALL}`, { + callback: () => VSCodeUnifiedDiff.singleton.unifiedDiffAcceptAll(), + telemetryService + })).run(); await vscode.window.activeTextEditor.document.save(); return _runAndDisplayScanner(Constants.COMMAND_RUN_ON_ACTIVE_FILE, [vscode.window.activeTextEditor.document.fileName], { telemetryService, @@ -268,7 +293,12 @@ function setupUnifiedDiff(context: vscode.ExtensionContext, diagnosticManager: D ); context.subscriptions.push( vscode.commands.registerCommand(CODEGENIE_UNIFIED_DIFF_REJECT_ALL, async () => { - await VSCodeUnifiedDiff.singleton.unifiedDiffRejectAll(); + // TODO: The use of the prefix shouldn't be hardcoded. Ideally, it should be passed in as an argument to the command. + // But that would require us to make changes to the underlying UnifiedDiff code that we're not currently in a position to make. + await (new DiffRejectAction(`${Constants.A4D_PREFIX}.${CODEGENIE_UNIFIED_DIFF_REJECT_ALL}`, { + callback: () => VSCodeUnifiedDiff.singleton.unifiedDiffRejectAll(), + telemetryService + })).run(); }) ); VSCodeUnifiedDiff.singleton.activate(context); @@ -498,7 +528,6 @@ export async function _runAndDisplayScanner(commandName: string, targets: string }); } catch (e) { const errMsg = e instanceof Error ? e.message : e as string; - console.log(errMsg); telemetryService.sendException(Constants.TELEM_FAILED_STATIC_ANALYSIS, errMsg, { executedCommand: commandName, duration: (Date.now() - startTime).toString() @@ -547,7 +576,6 @@ export async function _runAndDisplayDfa(context:vscode.ExtensionContext ,runInfo }) } catch (e) { const errMsg = e instanceof Error ? e.message : e as string; - console.log(errMsg); telemetryService.sendException(Constants.TELEM_FAILED_DFA_ANALYSIS, errMsg, { executedCommand: commandName, duration: (Date.now() - startTime).toString() @@ -600,7 +628,6 @@ export async function _clearDiagnosticsForSelectedFiles(selections: vscode.Uri[] }); } catch (e) { const errMsg = e instanceof Error ? e.message : e as string; - console.log(errMsg); telemetryService.sendException(Constants.TELEM_FAILED_STATIC_ANALYSIS, errMsg, { executedCommand: commandName, duration: (Date.now() - startTime).toString() diff --git a/src/lib/actions/diff-accept-action.ts b/src/lib/actions/diff-accept-action.ts new file mode 100644 index 00000000..f81c3b3f --- /dev/null +++ b/src/lib/actions/diff-accept-action.ts @@ -0,0 +1,40 @@ +import { DiffHunk } from "../../shared/UnifiedDiff"; +import { TelemetryService } from "../core-extension-service"; +import * as Constants from '../constants'; + +export type DiffAcceptCallback = (diffHunk?: DiffHunk) => Promise; + +export type DiffAcceptDependencies = { + callback: DiffAcceptCallback; + telemetryService: TelemetryService; +}; + +export class DiffAcceptAction { + private readonly commandName: string; + private readonly callback: DiffAcceptCallback; + private readonly telemetryService: TelemetryService; + + public constructor(commandName: string, dependencies: DiffAcceptDependencies) { + this.commandName = commandName; + this.callback = dependencies.callback; + this.telemetryService = dependencies.telemetryService; + } + + public async run(diffHunk?: DiffHunk): Promise { + const startTime = Date.now(); + try { + const lines: number = await this.callback(diffHunk); + this.telemetryService.sendCommandEvent(Constants.TELEM_DIFF_ACCEPT, { + commandSource: this.commandName, + completionNumLines: lines.toString(), + languageType: 'apex' // The only rules that the CodeAnalyzer A4D integration supports are Apex-based + }); + } catch (e) { + const errMsg = e instanceof Error ? e.message : e as string; + this.telemetryService.sendException(Constants.TELEM_DIFF_ACCEPT_FAILED, errMsg, { + executedCommand: this.commandName, + duration: (Date.now() - startTime).toString() + }) + } + } +} diff --git a/src/lib/actions/diff-create-action.ts b/src/lib/actions/diff-create-action.ts new file mode 100644 index 00000000..ecf10cee --- /dev/null +++ b/src/lib/actions/diff-create-action.ts @@ -0,0 +1,38 @@ +import {TelemetryService} from '../core-extension-service'; +import * as Constants from '../constants'; + +export type DiffCreateCallback = (code: string, file?: string) => Promise; + +export type DiffCreateDependencies = { + callback: DiffCreateCallback; + telemetryService: TelemetryService; +}; + +export class DiffCreateAction { + private readonly commandName: string; + private readonly callback: DiffCreateCallback; + private readonly telemetryService: TelemetryService; + + public constructor(commandName: string, dependencies: DiffCreateDependencies) { + this.commandName = commandName; + this.callback = dependencies.callback; + this.telemetryService = dependencies.telemetryService; + } + + public async run(code: string, file?: string): Promise { + const startTime = Date.now(); + try { + await this.callback(code, file); + this.telemetryService.sendCommandEvent(Constants.TELEM_DIFF_SUGGESTION, { + commandSource: this.commandName, + languageType: 'apex' // The only rules that the CodeAnalyzer A4D integration supports are Apex-based + }); + } catch (e) { + const errMsg = e instanceof Error ? e.message : e as string; + this.telemetryService.sendException(Constants.TELEM_DIFF_SUGGESTION_FAILED, errMsg, { + executedCommand: this.commandName, + duration: (Date.now() - startTime).toString() + }); + } + } +} diff --git a/src/lib/actions/diff-reject-action.ts b/src/lib/actions/diff-reject-action.ts new file mode 100644 index 00000000..41465154 --- /dev/null +++ b/src/lib/actions/diff-reject-action.ts @@ -0,0 +1,39 @@ +import { DiffHunk } from "../../shared/UnifiedDiff"; +import { TelemetryService } from "../core-extension-service"; +import * as Constants from '../constants'; + +export type DiffRejectCallback = (diffHunk?: DiffHunk) => Promise; + +export type DiffRejectDependencies = { + callback: DiffRejectCallback; + telemetryService: TelemetryService; +}; + +export class DiffRejectAction { + private readonly commandName: string; + private readonly callback: DiffRejectCallback; + private readonly telemetryService: TelemetryService; + + public constructor(commandName: string, dependencies: DiffRejectDependencies) { + this.commandName = commandName; + this.callback = dependencies.callback; + this.telemetryService = dependencies.telemetryService; + } + + public async run(diffHunk?: DiffHunk): Promise { + const startTime = Date.now(); + try { + await this.callback(diffHunk); + this.telemetryService.sendCommandEvent(Constants.TELEM_DIFF_REJECT, { + commandSource: this.commandName, + languageType: 'apex' // The only rules that the CodeAnalyzer A4D integration supports are Apex-based + }); + } catch (e) { + const errMsg = e instanceof Error ? e.message : e as string; + this.telemetryService.sendException(Constants.TELEM_DIFF_REJECT_FAILED, errMsg, { + executedCommand: this.commandName, + duration: (Date.now() - startTime).toString() + }) + } + } +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index fce7fee1..a9e7dc4f 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -28,6 +28,12 @@ export const TELEM_FAILED_STATIC_ANALYSIS = 'sfdx__codeanalyzer_static_run_faile export const TELEM_SUCCESSFUL_DFA_ANALYSIS = 'sfdx__codeanalyzer_dfa_run_complete'; export const TELEM_FAILED_DFA_ANALYSIS = 'sfdx__codeanalyzer_dfa_run_failed'; export const TELEM_SUCCESSFUL_APEX_GURU_FILE_ANALYSIS = 'sfdx__apexguru_file_run_complete'; +export const TELEM_DIFF_SUGGESTION = 'sfdx__eGPT_suggest'; +export const TELEM_DIFF_SUGGESTION_FAILED = 'sfdx__eGPT_suggest_failure'; +export const TELEM_DIFF_ACCEPT = 'sfdx__eGPT_accept'; +export const TELEM_DIFF_ACCEPT_FAILED = 'sfdx__eGPT_accept_failure'; +export const TELEM_DIFF_REJECT = 'sfdx__eGPT_clear'; +export const TELEM_DIFF_REJECT_FAILED = 'sfdx__eGPT_clear_failure'; // versioning export const MINIMUM_REQUIRED_VERSION_CORE_EXTENSION = '58.4.1'; @@ -46,3 +52,4 @@ export const APEX_GURU_RETRY_INTERVAL_MILLIS = 1000; export const ENABLE_A4D_INTEGRATION = false; export const A4D_FIX_AVAILABLE_RULES = ['ApexCRUDViolation', 'ApexSharingViolations', 'EmptyCatchBlock', 'EmptyTryOrFinallyBlock', 'EmptyWhileStmt', 'EmptyIfStmt']; export const UNIFIED_DIFF = 'unifiedDiff'; +export const A4D_PREFIX = 'SFCA_A4D_FIX'; diff --git a/src/modelBasedFixers/apex-pmd-violations-fixer.ts b/src/modelBasedFixers/apex-pmd-violations-fixer.ts index 25bbb258..aea87da5 100644 --- a/src/modelBasedFixers/apex-pmd-violations-fixer.ts +++ b/src/modelBasedFixers/apex-pmd-violations-fixer.ts @@ -83,12 +83,12 @@ export class ApexPmdViolationsFixer implements vscode.CodeActionProvider { const updatedFileContent = this.replaceCodeInFile(document.getText(), codeSnippet.trim(), diagnostic.range.start.line + 1, diagnostic.range.end.line + 1, document); // Update the command arguments with the resolved code snippet - codeAction.command.arguments = [updatedFileContent, document.uri]; + codeAction.command.arguments = [Constants.A4D_PREFIX, updatedFileContent, document.uri]; return codeAction; } catch (error) { const errorMessage = '***Failed to resolve code action:***' - const detailedMessage = error instanceof Error + const detailedMessage = error instanceof Error ? error.message : String(error); console.error(errorMessage, error); @@ -198,17 +198,17 @@ export class ApexPmdViolationsFixer implements vscode.CodeActionProvider { if (!document) { return replaceCode; } - + // Get the indentation of the first line in the range const startLine = range && range.start.line > 0 ? range.start.line - 1 : 0; const baseIndentation = this.getLineIndentation(document, startLine); - + // Split the replacement code into lines const lines = replaceCode.split(/\r?\n/); - + // First, normalize the code by removing all existing indentation const normalizedLines = lines.map(line => line.trimStart()); - + let indentLevel = 0; let braceLevel = 0; let parenLevel = 0; @@ -250,10 +250,10 @@ export class ApexPmdViolationsFixer implements vscode.CodeActionProvider { return indentation + line; }); - + return formattedLines.join(document.eol === vscode.EndOfLine.CRLF ? '\r\n' : '\n'); } - + // Helper to get the indentation of a specific line private getLineIndentation(document: vscode.TextDocument, lineNumber: number): string { const lineText = document.lineAt(lineNumber).text; @@ -267,7 +267,7 @@ export class ApexPmdViolationsFixer implements vscode.CodeActionProvider { // This is a known issue and we will fix this as we learn more about how the model sends the responses for other fixes. const updatedDiagnostics = currentDiagnostics.filter( diagnostic => ( - !Constants.A4D_FIX_AVAILABLE_RULES.includes(this.extractDiagnosticCode(diagnostic)) || + !Constants.A4D_FIX_AVAILABLE_RULES.includes(this.extractDiagnosticCode(diagnostic)) || (diagnostic.range.end.line < range.start.line || diagnostic.range.start.line > range.end.line ) ) ); diff --git a/src/shared/UnifiedDiff.ts b/src/shared/UnifiedDiff.ts index d480d264..94db9a90 100644 --- a/src/shared/UnifiedDiff.ts +++ b/src/shared/UnifiedDiff.ts @@ -461,14 +461,16 @@ export class VSCodeUnifiedDiff implements vscode.CodeLensProvider, vscode.CodeAc /** * Accept all changes in the unified diff. */ - public async unifiedDiffAcceptAll() { + public async unifiedDiffAcceptAll(): Promise { const editor = vscode.window.activeTextEditor; - if (!editor) return; + if (!editor) return 0; const diff = this.unifiedDiffs.get(editor.document.uri.toString()); - if (!diff) return; + if (!diff) return 0; + const diffLines: number = diff.getHunks().reduce((prev, curr) => prev + curr.lines.length, 0); diff.setSourceCode(diff.getTargetCode()); await this.renderUnifiedDiff(editor.document); this.checkRedundantUnifiedDiff(editor.document); + return diffLines; } /** diff --git a/src/test/unit/actions/diff-accept-action.test.ts b/src/test/unit/actions/diff-accept-action.test.ts new file mode 100644 index 00000000..df7390a3 --- /dev/null +++ b/src/test/unit/actions/diff-accept-action.test.ts @@ -0,0 +1,87 @@ +import { Properties, TelemetryService } from "../../../lib/core-extension-service"; +import { DiffHunk } from "../../../shared/UnifiedDiff"; +import { DiffAcceptAction } from '../../../lib/actions/diff-accept-action'; +import * as Constants from '../../../lib/constants'; + + +describe('DiffAcceptAction', () => { + describe('Telemetry events', () => { + it('Generates telemetry event for failed suggestion acceptance', async () => { + const stubTelemetryService: StubTelemetryService = new StubTelemetryService(); + const diffAcceptAction: DiffAcceptAction = new DiffAcceptAction('fakeName', { + callback: (_diffHunk?: DiffHunk) => Promise.reject(new Error('Forced error')), + telemetryService: stubTelemetryService + }); + await diffAcceptAction.run(); + + expect(stubTelemetryService.getSentCommandEvents()).toHaveLength(0); + expect(stubTelemetryService.getSentExceptions()).toHaveLength(1); + expect(stubTelemetryService.getSentExceptions()[0].name).toEqual(Constants.TELEM_DIFF_ACCEPT_FAILED); + expect(stubTelemetryService.getSentExceptions()[0].message).toEqual('Forced error'); + expect(stubTelemetryService.getSentExceptions()[0].data).toHaveProperty('executedCommand', 'fakeName'); + }); + + it('Generates telemetry event for successful suggestion acceptance', async () => { + const stubTelemetryService: StubTelemetryService = new StubTelemetryService(); + const diffAcceptAction: DiffAcceptAction = new DiffAcceptAction('fakeName', { + callback: (_diffHunk?: DiffHunk) => Promise.resolve(5), + telemetryService: stubTelemetryService + }); + await diffAcceptAction.run(); + + expect(stubTelemetryService.getSentExceptions()).toHaveLength(0); + expect(stubTelemetryService.getSentCommandEvents()).toHaveLength(1); + expect(stubTelemetryService.getSentCommandEvents()[0].key).toEqual(Constants.TELEM_DIFF_ACCEPT); + expect(stubTelemetryService.getSentCommandEvents()[0].data.commandSource).toEqual('fakeName'); + expect(stubTelemetryService.getSentCommandEvents()[0].data.completionNumLines).toEqual('5'); + expect(stubTelemetryService.getSentCommandEvents()[0].data.languageType).toEqual('apex'); + }); + }); +}); + +type TelemetryCommandEventData = { + key: string; + data?: Properties; +}; + +type TelemetryExceptionData = { + name: string; + message: string; + data?: Record; +}; + +class StubTelemetryService implements TelemetryService { + private commandEventCalls: TelemetryCommandEventData[] = []; + private exceptionCalls: TelemetryExceptionData[] = []; + + public sendExtensionActivationEvent(_hrStart: [number, number]): void { + // NO-OP + } + + public sendCommandEvent(key: string, data: Properties): void { + this.commandEventCalls.push({ + key, + data + }); + } + + public sendException(name: string, message: string, data?: Record): void { + this.exceptionCalls.push({ + name, + message, + data + }); + } + + public getSentCommandEvents(): TelemetryCommandEventData[] { + return this.commandEventCalls; + } + + public getSentExceptions(): TelemetryExceptionData[] { + return this.exceptionCalls; + } + + public dispose(): void { + // NO-OP + } +} diff --git a/src/test/unit/actions/diff-create-action.test.ts b/src/test/unit/actions/diff-create-action.test.ts new file mode 100644 index 00000000..b387fb53 --- /dev/null +++ b/src/test/unit/actions/diff-create-action.test.ts @@ -0,0 +1,84 @@ +import { Properties, TelemetryService } from "../../../lib/core-extension-service"; +import { DiffCreateAction } from "../../../lib/actions/diff-create-action"; +import * as Constants from '../../../lib/constants'; + +describe('DiffCreateAction', () => { + describe('Telemetry events', () => { + it('Generates telemetry event for successful suggestion creation', async () => { + const stubTelemetryService: StubTelemetryService = new StubTelemetryService(); + const diffCreateAction = new DiffCreateAction('fakeName', { + callback: (_code: string, _file?: string) => Promise.reject(new Error('Forced error')), + telemetryService: stubTelemetryService + }); + await diffCreateAction.run('This arg is irrelevant', 'So is this one'); + + expect(stubTelemetryService.getSentCommandEvents()).toHaveLength(0); + expect(stubTelemetryService.getSentExceptions()).toHaveLength(1); + expect(stubTelemetryService.getSentExceptions()[0].name).toEqual(Constants.TELEM_DIFF_SUGGESTION_FAILED); + expect(stubTelemetryService.getSentExceptions()[0].message).toEqual('Forced error'); + expect(stubTelemetryService.getSentExceptions()[0].data).toHaveProperty('executedCommand', 'fakeName'); + }); + + it('Generates telemetry event for unsuccessful suggestion generation', async () => { + const stubTelemetryService: StubTelemetryService = new StubTelemetryService(); + const diffCreateAction = new DiffCreateAction('fakeName', { + callback: (_code: string, _file?: string) => Promise.resolve(), + telemetryService: stubTelemetryService + }); + await diffCreateAction.run('This arg is irrelevant', 'So is this one'); + + expect(stubTelemetryService.getSentExceptions()).toHaveLength(0); + expect(stubTelemetryService.getSentCommandEvents()).toHaveLength(1); + expect(stubTelemetryService.getSentCommandEvents()[0].key).toEqual(Constants.TELEM_DIFF_SUGGESTION); + expect(stubTelemetryService.getSentCommandEvents()[0].data.commandSource).toEqual('fakeName'); + expect(stubTelemetryService.getSentCommandEvents()[0].data.languageType).toEqual('apex'); + }); + }); +}) + +type TelemetryCommandEventData = { + key: string; + data?: Properties; +}; + +type TelemetryExceptionData = { + name: string; + message: string; + data?: Record; +}; + +class StubTelemetryService implements TelemetryService { + private commandEventCalls: TelemetryCommandEventData[] = []; + private exceptionCalls: TelemetryExceptionData[] = []; + + public sendExtensionActivationEvent(_hrStart: [number, number]): void { + // NO-OP + } + + public sendCommandEvent(key: string, data: Properties): void { + this.commandEventCalls.push({ + key, + data + }); + } + + public sendException(name: string, message: string, data?: Record): void { + this.exceptionCalls.push({ + name, + message, + data + }); + } + + public getSentCommandEvents(): TelemetryCommandEventData[] { + return this.commandEventCalls; + } + + public getSentExceptions(): TelemetryExceptionData[] { + return this.exceptionCalls; + } + + public dispose(): void { + // NO-OP + } +} diff --git a/src/test/unit/actions/diff-reject-action.test.ts b/src/test/unit/actions/diff-reject-action.test.ts new file mode 100644 index 00000000..63e4e525 --- /dev/null +++ b/src/test/unit/actions/diff-reject-action.test.ts @@ -0,0 +1,87 @@ +import { Properties, TelemetryService } from "../../../lib/core-extension-service"; +import { DiffHunk } from "../../../shared/UnifiedDiff"; +import { DiffRejectAction } from '../../../lib/actions/diff-reject-action'; +import * as Constants from '../../../lib/constants'; + + +describe('DiffRejectAction', () => { + describe('Telemetry events', () => { + it('Generates telemetry event for failed suggestion rejection', async () => { + const stubTelemetryService: StubTelemetryService = new StubTelemetryService(); + const diffRejectAction: DiffRejectAction = new DiffRejectAction('fakeName', { + callback: (_diffHunk?: DiffHunk) => Promise.reject(new Error('Forced error')), + telemetryService: stubTelemetryService + }); + await diffRejectAction.run(); + + expect(stubTelemetryService.getSentCommandEvents()).toHaveLength(0); + expect(stubTelemetryService.getSentExceptions()).toHaveLength(1); + expect(stubTelemetryService.getSentExceptions()[0].name).toEqual(Constants.TELEM_DIFF_REJECT_FAILED); + expect(stubTelemetryService.getSentExceptions()[0].message).toEqual('Forced error'); + expect(stubTelemetryService.getSentExceptions()[0].data).toHaveProperty('executedCommand', 'fakeName'); + }); + + it('Generates telemetry event for successful suggestion rejection', async () => { + const stubTelemetryService: StubTelemetryService = new StubTelemetryService(); + const diffRejectAction: DiffRejectAction = new DiffRejectAction('fakeName', { + callback: (_diffHunk?: DiffHunk) => Promise.resolve(), + telemetryService: stubTelemetryService + }); + await diffRejectAction.run(); + + expect(stubTelemetryService.getSentExceptions()).toHaveLength(0); + expect(stubTelemetryService.getSentCommandEvents()).toHaveLength(1); + expect(stubTelemetryService.getSentCommandEvents()[0].key).toEqual(Constants.TELEM_DIFF_REJECT); + expect(stubTelemetryService.getSentCommandEvents()[0].data.commandSource).toEqual('fakeName'); + expect(stubTelemetryService.getSentCommandEvents()[0].data.languageType).toEqual('apex'); + + }); + }); +}); + +type TelemetryCommandEventData = { + key: string; + data?: Properties; +}; + +type TelemetryExceptionData = { + name: string; + message: string; + data?: Record; +}; + +class StubTelemetryService implements TelemetryService { + private commandEventCalls: TelemetryCommandEventData[] = []; + private exceptionCalls: TelemetryExceptionData[] = []; + + public sendExtensionActivationEvent(_hrStart: [number, number]): void { + // NO-OP + } + + public sendCommandEvent(key: string, data: Properties): void { + this.commandEventCalls.push({ + key, + data + }); + } + + public sendException(name: string, message: string, data?: Record): void { + this.exceptionCalls.push({ + name, + message, + data + }); + } + + public getSentCommandEvents(): TelemetryCommandEventData[] { + return this.commandEventCalls; + } + + public getSentExceptions(): TelemetryExceptionData[] { + return this.exceptionCalls; + } + + public dispose(): void { + // NO-OP + } +}