Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/multiFileEdit/common/multiFileEditQualityTelemetry.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import type { NotebookDocument, TextDocument, Uri } from 'vscode';
7
import { createServiceIdentifier } from '../../../util/common/services';
8
import { Disposable } from '../../../util/vs/base/common/lifecycle';
9
import { ResourceMap } from '../../../util/vs/base/common/map';
10
import { IChatSessionService } from '../../chat/common/chatSessionService';
11
import { IGitService } from '../../git/common/gitService';
12
import { ILogService } from '../../log/common/logService';
13
import { IAlternativeNotebookContentService } from '../../notebook/common/alternativeContent';
14
import { INotebookService } from '../../notebook/common/notebookService';
15
import { resolveWorkspaceOTelMetadata } from '../../otel/common/workspaceOTelMetadata';
16
import { ITelemetryService, multiplexProperties } from '../../telemetry/common/telemetry';
17
import { IWorkspaceService } from '../../workspace/common/workspaceService';
18
19
export interface IMultiFileEdit {
20
readonly isAgent?: boolean;
21
readonly uri: Uri;
22
readonly prompt: string;
23
readonly document?: TextDocument | NotebookDocument;
24
}
25
26
export interface IMultiFileEditRequestInfo {
27
readonly chatRequestId: string;
28
}
29
30
export interface IMultiFileEditTelemetry {
31
readonly mapper: string;
32
readonly chatSessionId?: string;
33
readonly chatRequestId: string;
34
readonly speculationRequestId: string;
35
}
36
37
export const IMultiFileEditInternalTelemetryService = createServiceIdentifier<IMultiFileEditInternalTelemetryService>('IMultiFileEditInternalTelemetryService');
38
export interface IMultiFileEditInternalTelemetryService {
39
_serviceBrand: undefined;
40
/**
41
* Store telemetry info for a multi-file edit
42
*/
43
storeEditPrompt(edit: IMultiFileEdit, telemetryOptions: IMultiFileEditTelemetry): void;
44
/**
45
* Send a telemetry event with the outcome of a multi-file edit
46
* @param chatRequestId The chat request id of the multi-file edit
47
* @param uri The uri of the file that was accepted
48
* Note: we do NOT track partial accepts and rejects
49
*/
50
sendEditPromptAndResult(telemetry: IMultiFileEditRequestInfo, uri: Uri, outcome: 'accept' | 'reject'): Promise<void>;
51
}
52
53
export class MultiFileEditInternalTelemetryService extends Disposable implements IMultiFileEditInternalTelemetryService {
54
55
declare _serviceBrand: undefined;
56
57
// URI -> chatResponseId -> edits
58
private readonly editedFiles = new ResourceMap<Map<string, (IMultiFileEdit & IMultiFileEditTelemetry)[]>>();
59
// sessionId -> (URI -> TextDocument | NotebookDocument)
60
private readonly editedDocuments = new Map<string, ResourceMap<TextDocument | NotebookDocument>>();
61
62
constructor(
63
@ITelemetryService private readonly telemetryService: ITelemetryService,
64
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
65
@INotebookService private readonly notebookService: INotebookService,
66
@ILogService private readonly logService: ILogService,
67
@IAlternativeNotebookContentService private readonly alternativeNotebookContent: IAlternativeNotebookContentService,
68
@IChatSessionService private readonly chatSessionService: IChatSessionService,
69
@IGitService private readonly gitService: IGitService,
70
) {
71
super();
72
this._register(this.chatSessionService.onDidDisposeChatSession(sessionId => {
73
this.editedDocuments.delete(sessionId);
74
}));
75
}
76
77
storeEditPrompt(edit: IMultiFileEdit, telemetryOptions: IMultiFileEditTelemetry): void {
78
this.logService.debug(`Storing edit prompt for ${edit.uri.toString()} with request ID ${telemetryOptions.chatRequestId}`);
79
80
const existingEditsForUri = this.editedFiles.get(edit.uri) ?? new Map();
81
const existingEditsForUriInRequest = existingEditsForUri.get(telemetryOptions.chatRequestId) ?? [];
82
existingEditsForUriInRequest.push({ ...edit, ...telemetryOptions });
83
existingEditsForUri.set(telemetryOptions.chatRequestId, existingEditsForUriInRequest);
84
this.editedFiles.set(edit.uri, existingEditsForUri);
85
if (edit.document && telemetryOptions.chatSessionId) {
86
let sessionMap = this.editedDocuments.get(telemetryOptions.chatSessionId);
87
if (!sessionMap) {
88
sessionMap = new ResourceMap<TextDocument | NotebookDocument>();
89
this.editedDocuments.set(telemetryOptions.chatSessionId, sessionMap);
90
}
91
sessionMap.set(edit.uri, edit.document);
92
}
93
}
94
95
async sendEditPromptAndResult(telemetry: IMultiFileEditRequestInfo, uri: Uri, outcome: 'accept' | 'reject'): Promise<void> {
96
const editsForUri = this.editedFiles.get(uri);
97
if (!editsForUri) {
98
return;
99
}
100
if (editsForUri.size > 1) {
101
// Multiple edit turns have affected this file
102
// i.e. edit -> edit -> accept/reject
103
// Skip sending telemetry for files which originated from multiple SD prompts
104
// and reset our tracking
105
this.logService.debug(`Skipping telemetry for ${uri.toString()} with request ID ${telemetry.chatRequestId} due to multiple edit turns`);
106
this.editedFiles.delete(uri);
107
return;
108
}
109
110
const editsForUriInChatRequest = editsForUri.get(telemetry.chatRequestId);
111
if (!editsForUriInChatRequest) {
112
return;
113
}
114
115
if (editsForUriInChatRequest.length > 1) {
116
// This file has been edited twice in one edit turn,
117
// which can happen if the LLM iterates on a file in agentic edit mode
118
// and can also happen when the LLM ignores instructions in non-agentic edits.
119
// Again, skip sending telemetry for files which originated from multiple SD prompts
120
// and reset our tracking
121
this.logService.debug(`Skipping telemetry for ${uri.toString()} with request ID ${telemetry.chatRequestId} due to multiple edits in one turn`);
122
this.editedFiles.delete(uri);
123
return;
124
}
125
126
try {
127
const edit = editsForUriInChatRequest[0];
128
129
// NOTE: this may not be what's on disk, but should reflect the outcome of accepting/rejecting
130
// regardless of whether the user is an autosave user / has saved the edits by now
131
let languageId: string | undefined = undefined;
132
let documentText: string | undefined = undefined;
133
if (edit.chatSessionId) {
134
const editedDocument = this.editedDocuments.get(edit.chatSessionId)?.get(uri);
135
if (editedDocument && 'getText' in editedDocument) {
136
languageId = editedDocument.languageId;
137
documentText = editedDocument.getText();
138
}
139
}
140
if (!documentText && !languageId) {
141
if (this.notebookService.hasSupportedNotebooks(uri)) {
142
const snapshot = await this.workspaceService.openNotebookDocumentAndSnapshot(uri, this.alternativeNotebookContent.getFormat(undefined));
143
languageId ??= snapshot.languageId;
144
documentText ??= snapshot.getText();
145
}
146
else {
147
const textDocument = await this.workspaceService.openTextDocument(uri);
148
languageId = textDocument.languageId;
149
documentText = textDocument.getText();
150
}
151
}
152
153
this.telemetryService.sendInternalMSFTTelemetryEvent('multiFileEditQuality',
154
{
155
requestId: telemetry.chatRequestId,
156
speculationRequestId: edit.speculationRequestId,
157
// NOTE: for now this will always be false because in agent mode the edits are invoked via the MappedEditsProvider, so we lose the turn ID
158
isAgent: String(edit.isAgent),
159
outcome,
160
prompt: edit.prompt,
161
languageId,
162
file: documentText, // Note that this is not necessarily the same as the model output because the user may have made manual edits
163
mapper: edit.mapper
164
},
165
{
166
isNotebook: this.notebookService.hasSupportedNotebooks(uri) ? 1 : 0
167
}
168
);
169
170
const workspace = resolveWorkspaceOTelMetadata(this.gitService, uri);
171
const gitHubEnhancedTelemetryProperties = multiplexProperties({
172
headerRequestId: edit.speculationRequestId,
173
providerId: edit.mapper,
174
languageId: languageId,
175
messageText: edit.prompt,
176
suggestion: outcome,
177
completionTextJson: documentText, // Note that this is not necessarily the same as the model output because the user may have made manual edits
178
conversationId: edit.chatSessionId,
179
messageId: edit.chatRequestId,
180
headBranchName: workspace.headBranchName,
181
headCommitHash: workspace.headCommitHash,
182
remoteUrl: workspace.remoteUrl,
183
fileRelativePath: workspace.fileRelativePath,
184
});
185
this.telemetryService.sendEnhancedGHTelemetryEvent('fastApply/editOutcome', gitHubEnhancedTelemetryProperties);
186
this.logService.debug(`Sent telemetry for ${uri.toString()} with request ID ${edit.chatRequestId}, SD request ID ${edit.speculationRequestId}, and outcome ${outcome}`);
187
} catch (e) {
188
this.logService.error('Error sending multi-file edit telemetry', JSON.stringify(e));
189
} finally {
190
this.editedFiles.delete(uri);
191
}
192
}
193
}
194
195