Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/node/feedbackGenerator.ts
13399 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 * as l10n from '@vscode/l10n';
7
8
import { TelemetryEventMeasurements, TelemetryEventProperties } from '@vscode/extension-telemetry';
9
import { RenderPromptResult } from '@vscode/prompt-tsx';
10
import type { CancellationToken, Progress } from 'vscode';
11
import { ChatLocation } from '../../../platform/chat/common/commonTypes';
12
import { EditSurvivalReporter, EditSurvivalResult } from '../../../platform/editSurvivalTracking/common/editSurvivalReporter';
13
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
14
import { IIgnoreService } from '../../../platform/ignore/common/ignoreService';
15
import { ILogService } from '../../../platform/log/common/logService';
16
import { ReviewComment, ReviewRequest } from '../../../platform/review/common/reviewService';
17
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
18
import { isNotebookCellOrNotebookChatInput } from '../../../util/common/notebooks';
19
import { coalesce } from '../../../util/vs/base/common/arrays';
20
import * as path from '../../../util/vs/base/common/path';
21
import { generateUuid } from '../../../util/vs/base/common/uuid';
22
import { StringEdit } from '../../../util/vs/editor/common/core/edits/stringEdit';
23
import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';
24
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
25
import { MarkdownString, Range } from '../../../vscodeTypes';
26
import { PromptRenderer } from '../../prompts/node/base/promptRenderer';
27
import { CurrentChangeInput } from '../../prompts/node/feedback/currentChange';
28
import { ProvideFeedbackPrompt } from '../../prompts/node/feedback/provideFeedback';
29
import { sendUserActionTelemetry } from './telemetry';
30
31
export type FeedbackResult = { type: 'success'; comments: ReviewComment[]; excludedComments?: ReviewComment[]; reason?: string } | { type: 'error'; severity?: 'info'; reason: string } | { type: 'cancelled' };
32
33
export class FeedbackGenerator {
34
constructor(
35
@ITelemetryService private readonly telemetryService: ITelemetryService,
36
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
37
@ILogService private readonly logService: ILogService,
38
@IInstantiationService private readonly instantiationService: IInstantiationService,
39
@IIgnoreService private readonly ignoreService: IIgnoreService,
40
) { }
41
42
async generateComments(input: CurrentChangeInput[], token: CancellationToken, progress?: Progress<ReviewComment[]>): Promise<FeedbackResult> {
43
const startTime = Date.now();
44
45
const ignoreService = this.ignoreService;
46
const ignored = await Promise.all(input.map(i => ignoreService.isCopilotIgnored(i.document.uri)));
47
const filteredInput = input.filter((_, i) => !ignored[i]);
48
if (filteredInput.length === 0) {
49
this.logService.info('All input documents are ignored. Skipping feedback generation.');
50
return {
51
type: 'error',
52
severity: 'info',
53
reason: l10n.t('All input documents are ignored by configuration. Check your .copilotignore file.')
54
};
55
}
56
57
const endpoint = await this.endpointProvider.getChatEndpoint('copilot-base');
58
59
const prompts: RenderPromptResult[] = [];
60
const batches = [filteredInput];
61
while (batches.length) {
62
const batch = batches.shift()!;
63
try {
64
const promptRenderer = PromptRenderer.create(this.instantiationService, endpoint, ProvideFeedbackPrompt, {
65
input: batch,
66
logService: this.logService,
67
});
68
const prompt = await promptRenderer.render();
69
this.logService.debug(`[FeedbackGenerator] Rendered batch of ${batch.length} inputs.`);
70
prompts.push(prompt);
71
} catch (err) {
72
if (err.code === 'split_input') {
73
const i = Math.floor(batch.length / 2);
74
batches.unshift(batch.slice(0, i), batch.slice(i));
75
this.logService.debug(`[FeedbackGenerator] Splitting in batches of ${batches[0].length} and ${batches[1].length} inputs due to token limit.`);
76
} else {
77
throw err;
78
}
79
}
80
}
81
82
if (token.isCancellationRequested) {
83
return { type: 'cancelled' };
84
}
85
86
const inputType = filteredInput[0]?.selection ? 'selection' : 'change';
87
const maxPrompts = 10;
88
if (prompts.length > maxPrompts) {
89
return {
90
type: 'error',
91
reason: inputType === 'selection' ? l10n.t('There is too much text to review, try reviewing a smaller selection.') : l10n.t('There are too many changes to review, try reviewing a smaller set of changes.'),
92
};
93
}
94
95
const request: ReviewRequest = {
96
source: 'vscodeCopilotChat',
97
promptCount: prompts.length,
98
messageId: generateUuid(),
99
inputType,
100
inputRanges: filteredInput.map(input => ({
101
uri: input.document.uri,
102
ranges: input.selection ? [input.selection] : input.change?.hunks.map(hunk => hunk.range) || [],
103
})),
104
};
105
106
const requestStartTime = Date.now();
107
const results = await Promise.all(prompts.map(async prompt => {
108
let receivedComments: ReviewComment[] = [];
109
const finishedCb = progress ? async (text: string) => {
110
const comments = parseReviewComments(request, filteredInput, text, true);
111
if (comments.length > receivedComments.length) {
112
progress.report(comments.slice(receivedComments.length));
113
receivedComments = comments;
114
}
115
return undefined;
116
} : undefined;
117
118
const fetchResult = await endpoint
119
.makeChatRequest(
120
'feedbackGenerator',
121
prompt.messages,
122
finishedCb,
123
token,
124
ChatLocation.Other,
125
undefined,
126
undefined,
127
false,
128
{
129
messageId: request.messageId,
130
}
131
);
132
133
const comments = fetchResult.type === 'success' ? parseReviewComments(request, filteredInput, fetchResult.value, false) : [];
134
135
if (progress && comments && comments.length > receivedComments.length) {
136
progress.report(comments.slice(receivedComments.length));
137
receivedComments = comments;
138
}
139
140
return {
141
fetchResult,
142
comments,
143
};
144
}));
145
146
const fetchResult = results.find(r => r.fetchResult.type !== 'success')?.fetchResult || results[0].fetchResult;
147
const comments = results.map(r => r.comments).flat();
148
149
/* __GDPR__
150
"feedback.generateDiagnostics" : {
151
"owner": "chrmarti",
152
"comment": "Metadata about the code feedback generation",
153
"model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that is used in the endpoint." },
154
"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." },
155
"source": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Which backend generated the comment." },
156
"messageId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request." },
157
"responseType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The result type of the response." },
158
"documentType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What kind of document (e.g., text or notebook)." },
159
"languageId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The current file language." },
160
"inputType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What type of input (e.g., selection or change)." },
161
"commentTypes": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What kind of comment (e.g., correctness or performance)." },
162
"promptCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of prompts run." },
163
"numberOfDiagnostics": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of diagnostics." },
164
"inputDocumentCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many documents were part of the input." },
165
"inputLineCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many (selected or changed) lines were part of the input." },
166
"timeToRequest": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "How long it took to start the request." },
167
"timeToComplete": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "How long it took to complete the request." }
168
}
169
*/
170
this.telemetryService.sendMSFTTelemetryEvent('feedback.generateDiagnostics', {
171
model: endpoint.model,
172
requestId: fetchResult.requestId,
173
responseType: fetchResult.type,
174
source: request.source,
175
messageId: request.messageId,
176
documentType: filteredInput[0] && isNotebookCellOrNotebookChatInput(filteredInput[0]?.document.uri) ? 'notebook' : 'text',
177
languageId: filteredInput[0]?.document.languageId,
178
inputType: request.inputType,
179
commentTypes: [...new Set(
180
comments?.map(c => knownKinds.has(c.kind) ? c.kind : 'unknown')).values()
181
].sort().join(',') || undefined,
182
}, {
183
promptCount: prompts.length,
184
numberOfDiagnostics: comments?.length ?? -1,
185
inputDocumentCount: request.inputRanges.length,
186
inputLineCount: request.inputRanges
187
.reduce((acc, r) => acc + r.ranges
188
.reduce((acc, r) => acc + (r.end.line - r.start.line), 0), 0),
189
timeToRequest: requestStartTime - startTime,
190
timeToComplete: Date.now() - startTime
191
});
192
193
return token.isCancellationRequested
194
? { type: 'cancelled' }
195
: fetchResult.type === 'success'
196
? { type: 'success', comments: comments || [] }
197
: { type: 'error', reason: fetchResult.reason };
198
}
199
}
200
201
const knownKinds = new Set(['bug', 'performance', 'consistency', 'documentation', 'naming', 'readability', 'style', 'other']);
202
203
export function parseReviewComments(request: ReviewRequest, input: CurrentChangeInput[], message: string, dropPartial = false): ReviewComment[] {
204
const comments: ReviewComment[] = [];
205
206
// Extract the messages from the comment
207
for (const match of parseFeedbackResponse(message, dropPartial)) {
208
const { relativeDocumentPath, from, to, kind, severity, content } = match;
209
if (!knownKinds.has(kind)) {
210
continue;
211
}
212
213
const i = relativeDocumentPath && input.find(i => i.relativeDocumentPath === relativeDocumentPath);
214
if (!i) {
215
continue;
216
}
217
218
const document = i.document;
219
const filterRanges = i.selection ? [i.selection!] : i.change?.hunks.map(hunk => hunk.range);
220
221
const fromLine = document.lineAt(from >= 0 ? from : 0);
222
const toLine = document.lineAt((to <= document.lineCount ? to : document.lineCount) - 1);
223
const lastNonWhitespaceCharacterIndex = toLine.text.trimEnd().length;
224
225
// Create a Diagnostic object for each message
226
const range = new Range(fromLine.lineNumber, fromLine.firstNonWhitespaceCharacterIndex, toLine.lineNumber, lastNonWhitespaceCharacterIndex);
227
if (filterRanges && !filterRanges.some(r => r.intersection(range))) {
228
continue;
229
}
230
const comment: ReviewComment = {
231
request,
232
document,
233
uri: document.uri,
234
languageId: document.languageId,
235
range,
236
body: new MarkdownString(content),
237
kind,
238
severity,
239
originalIndex: comments.length,
240
actionCount: 0,
241
};
242
comments.push(comment);
243
}
244
245
return comments;
246
}
247
248
export function parseFeedbackResponse(response: string, dropPartial = false) {
249
const regex = /(?<num>\d+)\. Line (?<from>\d+)(-(?<to>\d+))?([^:]*)( in `?(?<relativeDocumentPath>[^,:`]+))`?(, (?<kind>\w+))?(, (?<severity>\w+) severity)?: (?<content>.+?)((?=\n\d+\.|\n\n)|(?<earlyEnd>$))/gs;
250
return coalesce(Array.from(response.matchAll(regex), match => {
251
const groups = match.groups!;
252
if (dropPartial && typeof groups.earlyEnd === 'string') {
253
return undefined;
254
}
255
const from = parseInt(groups.from) - 1;
256
const to = groups.to ? parseInt(groups.to) : from + 1;
257
const relativeDocumentPath = groups.relativeDocumentPath?.replaceAll(path.sep === '/' ? '\\' : '/', path.sep);
258
const kind = groups.kind || 'other';
259
const severity = groups.severity || 'unknown';
260
let content = groups.content.trim();
261
// Remove trailing code block (which sometimes suggests a fix) because that interfers with the suggestion rendering later.
262
if (content.endsWith('```')) {
263
const i = content.lastIndexOf('```', content.length - 4);
264
if (i !== -1) {
265
content = content.substring(0, i)
266
.trim();
267
}
268
}
269
// Remove broken block.
270
const blockBorders = [...content.matchAll(/```/g)];
271
if (blockBorders.length % 2) {
272
const odd = blockBorders[blockBorders.length - 1];
273
content = content.substring(0, odd.index)
274
.trim();
275
}
276
return {
277
relativeDocumentPath,
278
from,
279
to,
280
linkOffset: match.index! + groups.num.length + 2,
281
linkLength: 5 + groups.from.length + (groups.to ? groups.to.length + 1 : 0),
282
kind,
283
severity,
284
content
285
};
286
}));
287
}
288
289
export function sendReviewActionTelemetry(reviewCommentOrComments: ReviewComment | ReviewComment[], totalComments: number, userAction: 'helpful' | 'unhelpful' | string, logService: ILogService, telemetryService: ITelemetryService, instantiationService: IInstantiationService): void {
290
logService.debug('[FeedbackGenerator] user feedback received');
291
const reviewComments = Array.isArray(reviewCommentOrComments) ? reviewCommentOrComments : [reviewCommentOrComments];
292
const reviewComment = reviewComments[0];
293
if (!reviewComment) {
294
logService.warn('[FeedbackGenerator] No review comment found for user feedback');
295
return;
296
}
297
298
const userActionProperties = {
299
source: reviewComment.request.source,
300
messageId: reviewComment.request.messageId,
301
userAction,
302
};
303
304
const commentType = knownKinds.has(reviewComment.kind) ? reviewComment.kind : 'unknown';
305
const sharedProps: TelemetryEventProperties = {
306
source: reviewComment.request.source,
307
requestId: reviewComment.request.messageId,
308
documentType: isNotebookCellOrNotebookChatInput(reviewComment.uri) ? 'notebook' : 'text',
309
languageId: reviewComment.languageId,
310
inputType: reviewComment.request.inputType,
311
commentType,
312
userAction,
313
};
314
const sharedMeasures: TelemetryEventMeasurements = {
315
commentIndex: reviewComment.originalIndex,
316
actionCount: reviewComment.actionCount,
317
inputDocumentCount: reviewComment.request.inputRanges.length,
318
inputLineCount: reviewComment.request.inputRanges
319
.reduce((acc, r) => acc + r.ranges
320
.reduce((acc, r) => acc + (r.end.line - r.start.line), 0), 0),
321
promptCount: reviewComment.request.promptCount,
322
totalComments,
323
comments: reviewComments.length,
324
commentLength: reviewComments.reduce((acc, c) => acc + (typeof c.body === 'string' ? c.body.length : c.body.value.length), 0),
325
};
326
327
if (userAction === 'helpful' || userAction === 'unhelpful') {
328
/* __GDPR__
329
"review.comment.vote" : {
330
"owner": "chrmarti",
331
"comment": "Metadata about votes on review comments",
332
"source": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Which backend generated the comment." },
333
"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." },
334
"documentType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What kind of document (e.g., text or notebook)." },
335
"languageId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The current file language." },
336
"inputType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What type of input (e.g., selection or change)." },
337
"commentType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What kind of comment (e.g., correctness or performance)." },
338
"userAction": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What action the user triggered (e.g., helpful, unhelpful, apply or discard)." },
339
"commentIndex": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Original index of the comment in the generated comments." },
340
"actionCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of previously logged actions on the comment." },
341
"inputDocumentCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many documents were part of the input." },
342
"inputLineCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many (selected or changed) lines were part of the input." },
343
"promptCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of prompts run." },
344
"totalComments": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of comments." },
345
"comments": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many comments are affected by the action." },
346
"commentLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many characters long the review comment is." }
347
}
348
*/
349
telemetryService.sendMSFTTelemetryEvent('review.comment.vote', sharedProps, sharedMeasures);
350
telemetryService.sendInternalMSFTTelemetryEvent('review.comment.vote', sharedProps);
351
sendUserActionTelemetry(telemetryService, undefined, userActionProperties, {}, 'review.comment.vote');
352
} else {
353
reviewComment.actionCount++;
354
/* __GDPR__
355
"review.comment.action" : {
356
"owner": "chrmarti",
357
"comment": "Metadata about actions on review comments",
358
"source": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Which backend generated the comment." },
359
"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." },
360
"documentType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What kind of document (e.g., text or notebook)." },
361
"languageId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The current file language." },
362
"inputType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What type of input (e.g., selection or change)." },
363
"commentType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What kind of comment (e.g., correctness or performance)." },
364
"userAction": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What action the user triggered (e.g., helpful, unhelpful, apply or discard)." },
365
"commentIndex": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Original index of the comment in the generated comments." },
366
"actionCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of previously logged actions on the comment." },
367
"inputDocumentCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many documents were part of the input." },
368
"inputLineCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many (selected or changed) lines were part of the input." },
369
"promptCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of prompts run." },
370
"totalComments": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of comments." },
371
"comments": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many comments are affected by the action." },
372
"commentLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many characters long the review comment is." }
373
}
374
*/
375
telemetryService.sendMSFTTelemetryEvent('review.comment.action', sharedProps, sharedMeasures);
376
telemetryService.sendInternalMSFTTelemetryEvent('review.comment.action', sharedProps);
377
sendUserActionTelemetry(telemetryService, undefined, userActionProperties, {}, 'review.comment.action');
378
}
379
if (userAction === 'discardComment') {
380
const { document, range } = reviewComment;
381
const from = document.offsetAt(range.start);
382
const to = document.offsetAt(range.end);
383
const text = document.getText(range);
384
instantiationService.createInstance(EditSurvivalReporter, document.document, document.getText(), StringEdit.replace(OffsetRange.ofStartAndLength(from, to - from), text), StringEdit.empty, {}, discardCommentSurvivalEvent(sharedProps, sharedMeasures));
385
}
386
}
387
388
function discardCommentSurvivalEvent(sharedProps: TelemetryEventProperties | undefined, sharedMeasures: TelemetryEventMeasurements | undefined) {
389
return (res: EditSurvivalResult) => {
390
/* __GDPR__
391
"review.discardCommentRangeSurvival" : {
392
"owner": "chrmarti",
393
"comment": "Tracks how much percent of the commented range surived after 5 minutes of discarding",
394
"survivalRateFourGram": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The rate between 0 and 1 of how much of the AI edit is still present in the document." },
395
"survivalRateNoRevert": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The rate between 0 and 1 of how much of the ranges the AI touched ended up being reverted." },
396
"didBranchChange": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Indicates if the branch changed in the meantime. If the branch changed (value is 1), this event should probably be ignored." },
397
"timeDelayMs": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The time delay between the user accepting the edit and measuring the survival rate." },
398
"source": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Which backend generated the comment." },
399
"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." },
400
"documentType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What kind of document (e.g., text or notebook)." },
401
"languageId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The current file language." },
402
"inputType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What type of input (e.g., selection or change)." },
403
"commentType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What kind of comment (e.g., correctness or performance)." },
404
"userAction": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "What action the user triggered (e.g., helpful, unhelpful, apply or discard)." },
405
"commentIndex": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Original index of the comment in the generated comments." },
406
"actionCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of previously logged actions on the comment." },
407
"inputDocumentCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many documents were part of the input." },
408
"inputLineCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many (selected or changed) lines were part of the input." },
409
"promptCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of prompts run." },
410
"totalComments": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of comments." },
411
"comments": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many comments are affected by the action." },
412
"commentLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many characters long the review comment is." }
413
}
414
*/
415
res.telemetryService.sendMSFTTelemetryEvent('review.discardCommentRangeSurvival', sharedProps, {
416
...sharedMeasures,
417
survivalRateFourGram: res.fourGram,
418
survivalRateNoRevert: res.noRevert,
419
timeDelayMs: res.timeDelayMs,
420
didBranchChange: res.didBranchChange ? 1 : 0,
421
});
422
};
423
}
424
425