Skip to content

Commit 41de0b1

Browse files
committed
Unifies AI reporting
1 parent 3d0cff8 commit 41de0b1

File tree

6 files changed

+245
-120
lines changed

6 files changed

+245
-120
lines changed

src/ai/aiProviderService.ts

Lines changed: 177 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import type { CancellationToken, Disposable, MessageItem, ProgressOptions, QuickInputButton } from 'vscode';
22
import { env, ThemeIcon, Uri, window } from 'vscode';
3-
import type { AIModels, AIProviders, SupportedAIModels } from '../constants';
3+
import type {
4+
AIGenerateDraftEvent,
5+
AIModels,
6+
AIProviders,
7+
Sources,
8+
SupportedAIModels,
9+
TelemetryEvents,
10+
} from '../constants';
411
import type { Container } from '../container';
12+
import { CancellationError } from '../errors';
513
import type { GitCommit } from '../git/models/commit';
614
import { assertsCommitHasFullDetails, isCommit } from '../git/models/commit';
715
import { uncommitted, uncommittedStaged } from '../git/models/constants';
@@ -13,6 +21,7 @@ import { configuration } from '../system/configuration';
1321
import { getSettledValue } from '../system/promise';
1422
import type { Storage } from '../system/storage';
1523
import { supportedInVSCodeVersion } from '../system/utils';
24+
import type { TelemetryService } from '../telemetry/telemetry';
1625
import { AnthropicProvider } from './anthropicProvider';
1726
import { GeminiProvider } from './geminiProvider';
1827
import { OpenAIProvider } from './openaiProvider';
@@ -195,18 +204,22 @@ export class AIProviderService implements Disposable {
195204

196205
async generateCommitMessage(
197206
changes: string[],
207+
sourceContext: { source: Sources },
198208
options?: { cancellation?: CancellationToken; context?: string; progress?: ProgressOptions },
199209
): Promise<string | undefined>;
200210
async generateCommitMessage(
201211
repoPath: Uri,
212+
sourceContext: { source: Sources },
202213
options?: { cancellation?: CancellationToken; context?: string; progress?: ProgressOptions },
203214
): Promise<string | undefined>;
204215
async generateCommitMessage(
205216
repository: Repository,
217+
sourceContext: { source: Sources },
206218
options?: { cancellation?: CancellationToken; context?: string; progress?: ProgressOptions },
207219
): Promise<string | undefined>;
208220
async generateCommitMessage(
209221
changesOrRepoOrPath: string[] | Repository | Uri,
222+
sourceContext: { source: Sources },
210223
options?: { cancellation?: CancellationToken; context?: string; progress?: ProgressOptions },
211224
): Promise<string | undefined> {
212225
const changes: string | undefined = await this.getChanges(changesOrRepoOrPath);
@@ -217,26 +230,68 @@ export class AIProviderService implements Disposable {
217230

218231
const provider = this._provider!;
219232

233+
const payload: TelemetryEvents['ai/generate'] = {
234+
type: 'commitMessage',
235+
model: { id: model.id, provider: { id: model.provider.id, name: model.provider.name } },
236+
};
237+
const source: Parameters<TelemetryService['sendEvent']>[2] = { source: sourceContext.source };
238+
220239
const confirmed = await confirmAIProviderToS(model, this.container.storage);
221-
if (!confirmed) return undefined;
222-
if (options?.cancellation?.isCancellationRequested) return undefined;
223-
224-
if (options?.progress != null) {
225-
return window.withProgress(options.progress, async () =>
226-
provider.generateCommitMessage(model, changes, {
227-
cancellation: options?.cancellation,
228-
context: options?.context,
229-
}),
240+
if (!confirmed) {
241+
this.container.telemetry.sendEvent(
242+
'ai/generate',
243+
{ ...payload, failed: { reason: 'user-declined' } },
244+
source,
230245
);
246+
247+
return undefined;
231248
}
232-
return provider.generateCommitMessage(model, changes, {
249+
250+
if (options?.cancellation?.isCancellationRequested) {
251+
this.container.telemetry.sendEvent(
252+
'ai/generate',
253+
{ ...payload, failed: { reason: 'user-cancelled' } },
254+
source,
255+
);
256+
257+
return undefined;
258+
}
259+
260+
const promise = provider.generateCommitMessage(model, changes, {
233261
cancellation: options?.cancellation,
234262
context: options?.context,
235263
});
264+
265+
const start = Date.now();
266+
try {
267+
const result = await (options?.progress != null
268+
? window.withProgress(options.progress, () => promise)
269+
: promise);
270+
271+
this.container.telemetry.sendEvent('ai/generate', { ...payload, duration: Date.now() - start }, source);
272+
273+
return result;
274+
} catch (ex) {
275+
this.container.telemetry.sendEvent(
276+
'ai/generate',
277+
{
278+
...payload,
279+
duration: Date.now() - start,
280+
failed:
281+
ex instanceof CancellationError
282+
? { reason: 'user-cancelled' }
283+
: { reason: 'error', error: String(ex) },
284+
},
285+
source,
286+
);
287+
288+
throw ex;
289+
}
236290
}
237291

238292
async generateDraftMessage(
239293
changesOrRepoOrPath: string[] | Repository | Uri,
294+
sourceContext: { source: Sources; type: AIGenerateDraftEvent['draftType'] },
240295
options?: {
241296
cancellation?: CancellationToken;
242297
context?: string;
@@ -252,24 +307,65 @@ export class AIProviderService implements Disposable {
252307

253308
const provider = this._provider!;
254309

310+
const payload: TelemetryEvents['ai/generate'] = {
311+
type: 'draftMessage',
312+
draftType: sourceContext.type,
313+
model: { id: model.id, provider: { id: model.provider.id, name: model.provider.name } },
314+
};
315+
const source: Parameters<TelemetryService['sendEvent']>[2] = { source: sourceContext.source };
316+
255317
const confirmed = await confirmAIProviderToS(model, this.container.storage);
256-
if (!confirmed) return undefined;
257-
if (options?.cancellation?.isCancellationRequested) return undefined;
258-
259-
if (options?.progress != null) {
260-
return window.withProgress(options.progress, async () =>
261-
provider.generateDraftMessage(model, changes, {
262-
cancellation: options?.cancellation,
263-
context: options?.context,
264-
codeSuggestion: options?.codeSuggestion,
265-
}),
318+
if (!confirmed) {
319+
this.container.telemetry.sendEvent(
320+
'ai/generate',
321+
{ ...payload, failed: { reason: 'user-declined' } },
322+
source,
266323
);
324+
325+
return undefined;
326+
}
327+
328+
if (options?.cancellation?.isCancellationRequested) {
329+
this.container.telemetry.sendEvent(
330+
'ai/generate',
331+
{ ...payload, failed: { reason: 'user-cancelled' } },
332+
source,
333+
);
334+
335+
return undefined;
267336
}
268-
return provider.generateDraftMessage(model, changes, {
337+
338+
const promise = provider.generateDraftMessage(model, changes, {
269339
cancellation: options?.cancellation,
270340
context: options?.context,
271341
codeSuggestion: options?.codeSuggestion,
272342
});
343+
344+
const start = Date.now();
345+
try {
346+
const result = await (options?.progress != null
347+
? window.withProgress(options.progress, () => promise)
348+
: promise);
349+
350+
this.container.telemetry.sendEvent('ai/generate', { ...payload, duration: Date.now() - start }, source);
351+
352+
return result;
353+
} catch (ex) {
354+
this.container.telemetry.sendEvent(
355+
'ai/generate',
356+
{
357+
...payload,
358+
duration: Date.now() - start,
359+
failed:
360+
ex instanceof CancellationError
361+
? { reason: 'user-cancelled' }
362+
: { reason: 'error', error: String(ex) },
363+
},
364+
source,
365+
);
366+
367+
throw ex;
368+
}
273369
}
274370

275371
private async getChanges(
@@ -299,60 +395,85 @@ export class AIProviderService implements Disposable {
299395
}
300396

301397
async explainCommit(
302-
repoPath: string | Uri,
303-
sha: string,
304-
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
305-
): Promise<string | undefined>;
306-
async explainCommit(
307-
commit: GitRevisionReference | GitCommit,
308-
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
309-
): Promise<string | undefined>;
310-
async explainCommit(
311-
commitOrRepoPath: string | Uri | GitRevisionReference | GitCommit,
312-
shaOrOptions?: string | { progress?: ProgressOptions },
398+
commitOrRevision: GitRevisionReference | GitCommit,
399+
sourceContext: { source: Sources; type: TelemetryEvents['ai/explain']['changeType'] },
313400
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
314401
): Promise<string | undefined> {
315-
let commit: GitCommit | undefined;
316-
if (typeof commitOrRepoPath === 'string' || commitOrRepoPath instanceof Uri) {
317-
if (typeof shaOrOptions !== 'string' || !shaOrOptions) throw new Error('Invalid arguments provided');
318-
319-
commit = await this.container.git.getCommit(commitOrRepoPath, shaOrOptions);
320-
} else {
321-
if (typeof shaOrOptions === 'string') throw new Error('Invalid arguments provided');
322-
323-
commit = isCommit(commitOrRepoPath)
324-
? commitOrRepoPath
325-
: await this.container.git.getCommit(commitOrRepoPath.repoPath, commitOrRepoPath.ref);
326-
options = shaOrOptions;
327-
}
328-
if (commit == null) throw new Error('Unable to find commit');
329-
330-
const diff = await this.container.git.getDiff(commit.repoPath, commit.sha);
402+
const diff = await this.container.git.getDiff(commitOrRevision.repoPath, commitOrRevision.ref);
331403
if (!diff?.contents) throw new Error('No changes found to explain.');
332404

333405
const model = await this.getModel();
334406
if (model == null) return undefined;
335407

336408
const provider = this._provider!;
337409

410+
const payload: TelemetryEvents['ai/explain'] = {
411+
type: 'change',
412+
changeType: sourceContext.type,
413+
model: { id: model.id, provider: { id: model.provider.id, name: model.provider.name } },
414+
};
415+
const source: Parameters<TelemetryService['sendEvent']>[2] = { source: sourceContext.source };
416+
338417
const confirmed = await confirmAIProviderToS(model, this.container.storage);
339-
if (!confirmed) return undefined;
418+
if (!confirmed) {
419+
this.container.telemetry.sendEvent(
420+
'ai/explain',
421+
{ ...payload, failed: { reason: 'user-declined' } },
422+
source,
423+
);
424+
425+
return undefined;
426+
}
427+
428+
const commit = isCommit(commitOrRevision)
429+
? commitOrRevision
430+
: await this.container.git.getCommit(commitOrRevision.repoPath, commitOrRevision.ref);
431+
if (commit == null) throw new Error('Unable to find commit');
340432

341433
if (!commit.hasFullDetails()) {
342434
await commit.ensureFullDetails();
343435
assertsCommitHasFullDetails(commit);
344436
}
345437

346-
if (options?.progress != null) {
347-
return window.withProgress(options.progress, async () =>
348-
provider.explainChanges(model, commit.message, diff.contents, {
349-
cancellation: options?.cancellation,
350-
}),
438+
if (options?.cancellation?.isCancellationRequested) {
439+
this.container.telemetry.sendEvent(
440+
'ai/explain',
441+
{ ...payload, failed: { reason: 'user-cancelled' } },
442+
source,
351443
);
444+
445+
return undefined;
352446
}
353-
return provider.explainChanges(model, commit.message, diff.contents, {
447+
448+
const promise = provider.explainChanges(model, commit.message, diff.contents, {
354449
cancellation: options?.cancellation,
355450
});
451+
452+
const start = Date.now();
453+
try {
454+
const result = await (options?.progress != null
455+
? window.withProgress(options.progress, () => promise)
456+
: promise);
457+
458+
this.container.telemetry.sendEvent('ai/explain', { ...payload, duration: Date.now() - start }, source);
459+
460+
return result;
461+
} catch (ex) {
462+
this.container.telemetry.sendEvent(
463+
'ai/explain',
464+
{
465+
...payload,
466+
duration: Date.now() - start,
467+
failed:
468+
ex instanceof CancellationError
469+
? { reason: 'user-cancelled' }
470+
: { reason: 'error', error: String(ex) },
471+
},
472+
source,
473+
);
474+
475+
throw ex;
476+
}
356477
}
357478

358479
async reset(all?: boolean) {

src/commands/generateCommitMessage.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,14 @@ export class GenerateCommitMessageCommand extends ActiveEditorCommand {
4141
const currentMessage = scmRepo.inputBox.value;
4242
const message = await (
4343
await this.container.ai
44-
)?.generateCommitMessage(repository, {
45-
context: currentMessage,
46-
progress: { location: ProgressLocation.Notification, title: 'Generating commit message...' },
47-
});
44+
)?.generateCommitMessage(
45+
repository,
46+
{ source: 'commandPalette' },
47+
{
48+
context: currentMessage,
49+
progress: { location: ProgressLocation.Notification, title: 'Generating commit message...' },
50+
},
51+
);
4852
if (message == null) return;
4953

5054
void executeCoreCommand('workbench.view.scm');

0 commit comments

Comments
 (0)