Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/node/nextEditProviderTelemetry.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 { ChatFetchResponseType } from '../../../platform/chat/common/commonTypes';
7
import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService';
8
import { DebugRecorderBookmark } from '../../../platform/inlineEdits/common/debugRecorderBookmark';
9
import { IObservableDocument, ObservableWorkspace } from '../../../platform/inlineEdits/common/observableWorkspace';
10
import { IStatelessNextEditTelemetry, StatelessNextEditRequest } from '../../../platform/inlineEdits/common/statelessNextEditProvider';
11
import { autorunWithChanges } from '../../../platform/inlineEdits/common/utils/observable';
12
import { APIUsage } from '../../../platform/networking/common/openai';
13
import { INotebookService } from '../../../platform/notebook/common/notebookService';
14
import { ITelemetryService, multiplexProperties, TelemetryEventMeasurements, TelemetryEventProperties } from '../../../platform/telemetry/common/telemetry';
15
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
16
import { LogEntry } from '../../../platform/workspaceRecorder/common/workspaceLog';
17
import { findNotebook } from '../../../util/common/notebooks';
18
import { RunOnceScheduler } from '../../../util/vs/base/common/async';
19
import { Disposable, DisposableStore, IDisposable, RefCountedDisposable } from '../../../util/vs/base/common/lifecycle';
20
import { Schemas } from '../../../util/vs/base/common/network';
21
import { autorun, autorunHandleChanges } from '../../../util/vs/base/common/observableInternal';
22
import { StringEdit, StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit';
23
import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';
24
import { StringText } from '../../../util/vs/editor/common/core/text/abstractText';
25
import { Uri } from '../../../vscodeTypes';
26
import { DebugRecorder } from './debugRecorder';
27
import { INesConfigs } from './nesConfigs';
28
import { INextEditDisplayLocation, INextEditResult } from './nextEditResult';
29
30
export type NextEditTelemetryStatus = 'new' | 'requested' | `noEdit:${string}` | 'docChanged' | 'emptyEdits' | 'emptyEditsButHasNextCursorPosition' | 'previouslyRejected' | 'previouslyRejectedCache' | 'accepted' | 'notAccepted' | 'rejected';
31
32
export type NesAcceptance = 'accepted' | 'notAccepted' | 'rejected';
33
34
export type EnhancedTelemetrySendingReasonKind = 'idle' | 'hard_cap' | 'user_jump';
35
36
export interface IEnhancedTelemetrySendingReason {
37
readonly reason: EnhancedTelemetrySendingReasonKind;
38
readonly details: {
39
readonly idleTimeoutMs?: number;
40
readonly hardCapTimeoutMs?: number;
41
readonly from?: { readonly file: string; readonly line: number };
42
readonly to?: { readonly file: string; readonly line: number | undefined };
43
};
44
}
45
46
export interface IAlternativeAction {
47
readonly text: string | undefined; // undefined if the text is too long
48
readonly textLength: number;
49
readonly selection: ITelemetryRange[];
50
readonly edits: ITelemetryEdit[];
51
readonly tags: string[];
52
readonly recording: ITelemetryRecording | undefined;
53
}
54
55
export interface ITelemetryEdit {
56
readonly time: string;
57
readonly start: number;
58
readonly endExclusive: number;
59
readonly newText: string;
60
}
61
62
export interface ITelemetryRange {
63
readonly start: number;
64
readonly endExclusive: number;
65
}
66
67
export interface ITelemetryRecording {
68
readonly entries: LogEntry[] | undefined;
69
readonly entriesSize: number;
70
readonly requestTime: number;
71
}
72
73
export const enum ReusedRequestKind {
74
Speculative = 'speculative',
75
Async = 'async',
76
}
77
78
export interface ILlmNESTelemetry extends Partial<IStatelessNextEditTelemetry> { // it's partial because the next edit can be pulled from cache resulting in no stateless provider telemetry
79
readonly providerId: string;
80
readonly headerRequestId: string | undefined;
81
readonly nextEditProviderDuration: number | undefined;
82
readonly fetchStartedAfterMs: number | undefined;
83
readonly isFromCache: boolean;
84
readonly reusedRequest: ReusedRequestKind | undefined;
85
readonly subsequentEditOrder: number | undefined;
86
readonly activeDocumentOriginalLineCount: number | undefined;
87
readonly activeDocumentEditsCount: number | undefined;
88
readonly activeDocumentLanguageId: string | undefined;
89
readonly activeDocumentRepository: string | undefined;
90
readonly hasNextEdit: boolean;
91
readonly wasPreviouslyRejected: boolean;
92
readonly status: NextEditTelemetryStatus;
93
readonly nextEditProviderError: string | undefined;
94
readonly nesConfigs: INesConfigs | undefined;
95
readonly repositoryUrls: string[] | undefined;
96
readonly documentsCount: number | undefined;
97
readonly editsCount: number | undefined;
98
readonly isNotebook: boolean;
99
readonly notebookType: string | undefined;
100
readonly alternativeAction: IAlternativeAction | undefined;
101
}
102
103
export interface IDiagnosticsTelemetry {
104
readonly diagnosticType: string | undefined;
105
readonly diagnosticDroppedReasons: string | undefined;
106
readonly diagnosticDistanceToUnknownDiagnostic: number | undefined;
107
readonly diagnosticDistanceToAlternativeDiagnostic: number | undefined;
108
readonly diagnosticHasAlternativeDiagnosticForSameRange: boolean | undefined;
109
110
// imports
111
readonly diagnosticHasExistingSameFileImport: boolean | undefined;
112
readonly diagnosticIsLocalImport: boolean | undefined;
113
readonly diagnosticAlternativeImportsCount: number | undefined;
114
}
115
116
export interface INextEditProviderTelemetry extends ILlmNESTelemetry, IDiagnosticsTelemetry {
117
readonly opportunityId: string;
118
readonly requestN: number;
119
readonly isShown: boolean;
120
readonly acceptance: NesAcceptance;
121
readonly disposalReason: string | undefined;
122
readonly supersededByOpportunityId: string | undefined;
123
readonly status: NextEditTelemetryStatus;
124
readonly nextEditProviderError: string | undefined;
125
readonly activeDocumentRepository: string | undefined;
126
readonly repositoryUrls: string[] | undefined;
127
readonly alternativeAction: IAlternativeAction | undefined;
128
readonly postProcessingOutcome: string | undefined;
129
readonly isNESForAnotherDoc: boolean;
130
readonly notebookCellMarkerCount: number;
131
readonly notebookCellMarkerIndex: number;
132
readonly notebookId: string | undefined;
133
readonly notebookCellLines: string | undefined;
134
readonly isActiveDocument?: boolean;
135
readonly isMultilineEdit?: boolean;
136
readonly isEolDifferent?: boolean;
137
readonly isNextEditorVisible?: boolean;
138
readonly isNextEditorRangeVisible?: boolean;
139
readonly isNaturalLanguageDominated: boolean;
140
141
readonly hadLlmNES: boolean;
142
readonly hadDiagnosticsNES: boolean;
143
readonly pickedNES: 'llm' | 'diagnostics' | undefined;
144
readonly configIsDiagnosticsNESEnabled: boolean;
145
146
readonly userTypingDisagreed: boolean | undefined;
147
}
148
149
export class LlmNESTelemetryBuilder extends Disposable {
150
151
public build(includeAlternativeAction: boolean): ILlmNESTelemetry {
152
let documentsCount: number | undefined = undefined;
153
let editsCount: number | undefined = undefined;
154
let activeDocumentEditsCount: number | undefined = undefined;
155
let activeDocumentLanguageId: string | undefined = undefined;
156
let activeDocumentOriginalLineCount: number | undefined = undefined;
157
let isNotebook: boolean = false;
158
let notebookType: string | undefined = undefined;
159
let activeDocumentRepository: string | undefined = undefined;
160
let repositoryUrls: string[] | undefined = undefined;
161
162
if (this._request) {
163
const activeDoc = this._request.getActiveDocument();
164
documentsCount = this._request.documents.length;
165
editsCount = this._request.documents.reduce((acc, doc) => acc + doc.recentEdits.edits.length, 0);
166
activeDocumentEditsCount = activeDoc.recentEdits.edits.length;
167
activeDocumentLanguageId = activeDoc.languageId;
168
activeDocumentOriginalLineCount = activeDoc.documentAfterEditsLines.length;
169
isNotebook = activeDoc.id.toUri().scheme === Schemas.vscodeNotebookCell || this._notebookService?.hasSupportedNotebooks(activeDoc.id.toUri()) || false;
170
notebookType = this._workspaceService === undefined ? undefined : findNotebook(activeDoc.id.toUri(), this._workspaceService.notebookDocuments)?.notebookType;
171
const git = this._gitExtensionService?.getExtensionApi();
172
if (git) {
173
const activeDocRepository = git.getRepository(Uri.parse(activeDoc.id.uri));
174
if (activeDocRepository) {
175
const remoteName = activeDocRepository.state.HEAD?.upstream?.remote;
176
const remote = activeDocRepository.state.remotes.find(r => r.name === remoteName);
177
if (remote?.fetchUrl) {
178
activeDocumentRepository = remote.pushUrl || remote.fetchUrl;
179
}
180
}
181
182
const remoteUrlSet = new Set<string>();
183
const repositories = [...new Set(this._request.documents.map(doc => git.getRepository(Uri.parse(doc.id.uri))).filter(Boolean))];
184
for (const repository of repositories) {
185
const remoteName = repository?.state.HEAD?.upstream?.remote;
186
const remote = repository?.state.remotes.find(r => r.name === remoteName);
187
if (remote?.fetchUrl) {
188
remoteUrlSet.add(remote.fetchUrl);
189
}
190
if (remote?.pushUrl) {
191
remoteUrlSet.add(remote.pushUrl);
192
}
193
}
194
repositoryUrls = [...remoteUrlSet];
195
}
196
}
197
198
let alternativeAction: IAlternativeAction | undefined;
199
if (includeAlternativeAction && this.editCollectingInfo !== undefined) {
200
const originalText = this.editCollectingInfo.originalDoc.value;
201
let recording: ITelemetryRecording | undefined;
202
if (this._debugRecorder && this._requestBookmark) {
203
const entries = this._debugRecorder.getRecentLog();
204
const entriesSize = JSON.stringify(entries)?.length || 0;
205
recording = {
206
entries: entriesSize > 200 * 1024 ? undefined : entries,
207
entriesSize: entriesSize,
208
requestTime: this._requestBookmark.timeMs,
209
};
210
}
211
alternativeAction = {
212
text: originalText.length > 200 * 1024 ? undefined : originalText,
213
textLength: originalText.length,
214
selection: this.editCollectingInfo.originalSelection.map(range => ({
215
start: range.start,
216
endExclusive: range.endExclusive,
217
})),
218
edits: this.editCollectingInfo.edits.map(edit => edit.edit.replacements.map(e => ({
219
time: edit.time.toISOString(),
220
start: e.replaceRange.start,
221
endExclusive: e.replaceRange.endExclusive,
222
newText: e.newText,
223
}))).flat(),
224
tags: [],
225
recording,
226
};
227
}
228
229
const fetchStartedAfterMs = this._statelessNextEditTelemetry?.fetchStartedAt === undefined ? undefined : this._statelessNextEditTelemetry.fetchStartedAt - this._startTime;
230
231
return {
232
providerId: this._providerId,
233
headerRequestId: this._headerRequestId,
234
nextEditProviderDuration: this._duration,
235
isFromCache: this._isFromCache,
236
reusedRequest: this._reusedRequest,
237
subsequentEditOrder: this._subsequentEditOrder,
238
documentsCount,
239
editsCount,
240
activeDocumentEditsCount,
241
activeDocumentLanguageId,
242
activeDocumentOriginalLineCount,
243
fetchStartedAfterMs,
244
hasNextEdit: this._hasNextEdit,
245
wasPreviouslyRejected: this._wasPreviouslyRejected,
246
isNotebook,
247
notebookType,
248
status: this._status,
249
nextEditProviderError: this._nextEditProviderError,
250
alternativeAction,
251
252
...this._statelessNextEditTelemetry,
253
254
activeDocumentRepository,
255
repositoryUrls,
256
257
nesConfigs: this._nesConfigs,
258
};
259
}
260
261
private _startTime: number;
262
263
/** Dependent on the observable document to track edits and selections */
264
private editCollectingInfo: undefined | {
265
originalDoc: StringText;
266
originalSelection: readonly OffsetRange[];
267
originalSelectionLine: number | undefined;
268
edits: { time: Date; edit: StringEdit }[];
269
};
270
271
public get originalSelectionLine(): number | undefined {
272
return this.editCollectingInfo?.originalSelectionLine;
273
}
274
275
/**
276
* @param _doc passing an observable document allows to track edits and selections
277
*/
278
constructor(
279
private readonly _gitExtensionService: IGitExtensionService | undefined,
280
private readonly _notebookService: INotebookService | undefined,
281
private readonly _workspaceService: IWorkspaceService | undefined,
282
private readonly _providerId: string,
283
private readonly _doc: IObservableDocument | undefined,
284
private readonly _debugRecorder?: DebugRecorder,
285
private readonly _requestBookmark?: DebugRecorderBookmark,
286
) {
287
super();
288
this._startTime = Date.now();
289
290
if (this._doc) {
291
this.editCollectingInfo = {
292
originalDoc: this._doc.value.get(),
293
originalSelection: this._doc.selection.get(),
294
originalSelectionLine: this._doc.primarySelectionLine.get(),
295
edits: [],
296
};
297
298
this._store.add(autorunWithChanges(this, {
299
value: this._doc.value,
300
}, (data) => {
301
const time = new Date();
302
data.value.changes.forEach(change => {
303
this.editCollectingInfo?.edits.push({
304
time,
305
edit: change,
306
});
307
});
308
}));
309
}
310
}
311
312
private _nesConfigs: INesConfigs | undefined;
313
public setNESConfigs(nesConfigs: INesConfigs): this {
314
this._nesConfigs = nesConfigs;
315
return this;
316
}
317
318
private _headerRequestId: string | undefined;
319
public setHeaderRequestId(uuid: string): this {
320
this._headerRequestId = uuid;
321
return this;
322
}
323
324
private _isFromCache: boolean = false;
325
public setIsFromCache(): this {
326
this._isFromCache = true;
327
return this;
328
}
329
330
private _reusedRequest: ReusedRequestKind | undefined;
331
public setReusedRequest(kind: ReusedRequestKind): this {
332
this._reusedRequest = kind;
333
return this;
334
}
335
336
private _subsequentEditOrder: number | undefined;
337
public setSubsequentEditOrder(subsequentEditOrder: number | undefined): this {
338
this._subsequentEditOrder = subsequentEditOrder;
339
return this;
340
}
341
342
private _request: StatelessNextEditRequest | undefined;
343
public setRequest(request: StatelessNextEditRequest): this {
344
this._request = request;
345
return this;
346
}
347
348
private _statelessNextEditTelemetry: IStatelessNextEditTelemetry | undefined;
349
public setStatelessNextEditTelemetry(statelessNextEditTelemetry: IStatelessNextEditTelemetry): this {
350
this._statelessNextEditTelemetry = statelessNextEditTelemetry;
351
return this;
352
}
353
354
private _hasNextEdit: boolean = false;
355
public setHasNextEdit(hasNextEdit: boolean): this {
356
this._hasNextEdit = hasNextEdit;
357
return this;
358
}
359
360
private _wasPreviouslyRejected: boolean = false;
361
public setWasPreviouslyRejected(): this {
362
this._wasPreviouslyRejected = true;
363
return this;
364
}
365
366
private _duration: number | undefined;
367
public markEndTime(): this {
368
this._duration = Date.now() - this._startTime;
369
return this;
370
}
371
372
private _status: NextEditTelemetryStatus = 'new';
373
public setStatus(status: NextEditTelemetryStatus): this {
374
this._status = status;
375
return this;
376
}
377
378
private _nextEditProviderError: string | undefined;
379
public setNextEditProviderError(nextEditProviderError: string | undefined): this {
380
this._nextEditProviderError = nextEditProviderError;
381
return this;
382
}
383
}
384
385
interface IDiagnosticTelemetryRun {
386
alternativeImportsCount?: number;
387
hasExistingSameFileImport?: boolean;
388
isLocalImport?: boolean;
389
distanceToUnknownDiagnostic?: number;
390
distanceToAlternativeDiagnostic?: number;
391
hasAlternativeDiagnosticForSameRange?: boolean;
392
}
393
394
export class DiagnosticsTelemetryBuilder {
395
396
public build(): IDiagnosticsTelemetry {
397
const diagnosticDroppedReasons = this._droppedReasons.length > 0 ? JSON.stringify(this._droppedReasons) : undefined;
398
return {
399
diagnosticType: this._type,
400
diagnosticDroppedReasons,
401
diagnosticAlternativeImportsCount: this._diagnosticRunTelemetry?.alternativeImportsCount,
402
diagnosticHasExistingSameFileImport: this._diagnosticRunTelemetry?.hasExistingSameFileImport,
403
diagnosticIsLocalImport: this._diagnosticRunTelemetry?.isLocalImport,
404
diagnosticDistanceToUnknownDiagnostic: this._diagnosticRunTelemetry?.distanceToUnknownDiagnostic,
405
diagnosticDistanceToAlternativeDiagnostic: this._diagnosticRunTelemetry?.distanceToAlternativeDiagnostic,
406
diagnosticHasAlternativeDiagnosticForSameRange: this._diagnosticRunTelemetry?.hasAlternativeDiagnosticForSameRange
407
};
408
}
409
410
public populate(telemetry: DiagnosticsTelemetryBuilder) {
411
this._droppedReasons.forEach(reason => telemetry.addDroppedReason(reason));
412
if (this._type) {
413
telemetry.setType(this._type);
414
}
415
if (this._diagnosticRunTelemetry) {
416
telemetry.setDiagnosticRunTelemetry(this._diagnosticRunTelemetry);
417
}
418
}
419
420
private _type: string | undefined;
421
setType(type: string): this {
422
this._type = type;
423
return this;
424
}
425
426
private _droppedReasons: string[] = [];
427
addDroppedReason(reason: string): this {
428
this._droppedReasons.push(reason);
429
return this;
430
}
431
432
private _diagnosticRunTelemetry: IDiagnosticTelemetryRun | undefined;
433
setDiagnosticRunTelemetry(diagnosticRun: IDiagnosticTelemetryRun): this {
434
this._diagnosticRunTelemetry = diagnosticRun;
435
return this;
436
}
437
}
438
439
export class NextEditProviderTelemetryBuilder extends Disposable {
440
441
private static providerIdToReqN = new Map<string /* providerId */, number>();
442
443
/**
444
* Whether telemetry for this builder has been sent -- only for ordinary telemetry, not enhanced telemetry
445
*/
446
private _isSent: boolean = false;
447
public get isSent(): boolean {
448
return this._isSent;
449
}
450
public markAsSent(): void {
451
this._isSent = true;
452
}
453
454
public build(includeAlternativeAction: boolean): INextEditProviderTelemetry {
455
456
const nesTelemetry = this._nesBuilder.build(includeAlternativeAction);
457
const diagnosticsTelemetry = this._diagnosticsBuilder.build();
458
459
return {
460
...nesTelemetry,
461
...diagnosticsTelemetry,
462
463
opportunityId: this._opportunityId || '',
464
requestN: this._requestN,
465
isShown: this._isShown,
466
acceptance: this._acceptance,
467
disposalReason: this._disposalReason,
468
supersededByOpportunityId: this._supersededByOpportunityId,
469
pickedNES: this._nesTypePicked,
470
hadLlmNES: this._hadLlmNES,
471
isMultilineEdit: this._isMultilineEdit,
472
isEolDifferent: this._isEolDifferent,
473
isActiveDocument: this._isActiveDocument,
474
isNextEditorVisible: this._isNextEditorVisible,
475
isNextEditorRangeVisible: this._isNextEditorRangeVisible,
476
isNESForAnotherDoc: this._isNESForAnotherDoc,
477
notebookId: this._notebookId,
478
notebookCellLines: this._notebookCellLines,
479
notebookCellMarkerCount: this._notebookCellMarkerCount,
480
notebookCellMarkerIndex: this._notebookCellMarkerIndex,
481
hadDiagnosticsNES: this._hadDiagnosticsNES,
482
configIsDiagnosticsNESEnabled: this._configIsDiagnosticsNESEnabled,
483
isNaturalLanguageDominated: this._isNaturalLanguageDominated,
484
postProcessingOutcome: this._postProcessingOutcome,
485
userTypingDisagreed: this._userTypingDisagreed,
486
};
487
}
488
489
private _requestN: number;
490
491
private readonly _nesBuilder: LlmNESTelemetryBuilder;
492
public get nesBuilder(): LlmNESTelemetryBuilder {
493
return this._nesBuilder;
494
}
495
private readonly _diagnosticsBuilder: DiagnosticsTelemetryBuilder;
496
public get diagnosticsBuilder(): DiagnosticsTelemetryBuilder {
497
return this._diagnosticsBuilder;
498
}
499
500
/**
501
* @param _doc passing an observable document allows to track edits and selections
502
*/
503
constructor(
504
gitExtensionService: IGitExtensionService | undefined,
505
notebookService: INotebookService | undefined,
506
workspaceService: IWorkspaceService | undefined,
507
providerId: string,
508
public readonly doc: IObservableDocument | undefined,
509
debugRecorder?: DebugRecorder,
510
requestBookmark?: DebugRecorderBookmark,
511
) {
512
super();
513
514
let requestN = NextEditProviderTelemetryBuilder.providerIdToReqN.get(providerId) || 0;
515
this._requestN = ++requestN;
516
NextEditProviderTelemetryBuilder.providerIdToReqN.set(providerId, requestN);
517
518
this._nesBuilder = this._register(new LlmNESTelemetryBuilder(gitExtensionService, notebookService, workspaceService, providerId, doc, debugRecorder, requestBookmark));
519
this._diagnosticsBuilder = new DiagnosticsTelemetryBuilder();
520
}
521
522
private _opportunityId: string | undefined;
523
public setOpportunityId(uuid: string): this {
524
this._opportunityId = uuid;
525
return this;
526
}
527
528
private _isShown: boolean = false;
529
public setAsShown(): this {
530
this._isShown = true;
531
return this;
532
}
533
534
private _acceptance: NesAcceptance = 'notAccepted';
535
public setAcceptance(acceptance: NesAcceptance): this {
536
this._acceptance = acceptance;
537
return this;
538
}
539
540
private _disposalReason: string | undefined = undefined;
541
public setDisposalReason(disposalReason: string | undefined): this {
542
this._disposalReason = disposalReason;
543
return this;
544
}
545
546
private _supersededByOpportunityId: string | undefined = undefined;
547
public setSupersededBy(opportunityId: string | undefined): this {
548
this._supersededByOpportunityId = opportunityId;
549
return this;
550
}
551
552
private _userTypingDisagreed: boolean | undefined = undefined;
553
public setUserTypingDisagreed(userTypingDisagreed: boolean): this {
554
this._userTypingDisagreed = userTypingDisagreed;
555
return this;
556
}
557
558
private _nesTypePicked: 'llm' | 'diagnostics' | undefined;
559
public setPickedNESType(nesTypePicked: 'llm' | 'diagnostics'): this {
560
this._nesTypePicked = nesTypePicked;
561
return this;
562
}
563
564
private _isActiveDocument?: boolean;
565
public setIsActiveDocument(isActive: boolean): this {
566
this._isActiveDocument = isActive;
567
return this;
568
}
569
570
private _notebookCellMarkerCount: number = 0;
571
public setNotebookCellMarkerCount(count: number): this {
572
this._notebookCellMarkerCount = count;
573
return this;
574
}
575
576
private _isMultilineEdit?: boolean;
577
public setIsMultilineEdit(isMultiLine: boolean): this {
578
this._isMultilineEdit = isMultiLine;
579
return this;
580
}
581
582
private _isEolDifferent?: boolean;
583
public setIsEolDifferent(isEolDifferent: boolean): this {
584
this._isEolDifferent = isEolDifferent;
585
return this;
586
}
587
588
private _isNextEditorVisible?: boolean;
589
public setIsNextEditorVisible(isVisible: boolean): this {
590
this._isNextEditorVisible = isVisible;
591
return this;
592
}
593
594
private _isNextEditorRangeVisible?: boolean;
595
public setIsNextEditorRangeVisible(isVisible: boolean): this {
596
this._isNextEditorRangeVisible = isVisible;
597
return this;
598
}
599
600
private _notebookId?: string;
601
public setNotebookId(notebookId: string): this {
602
this._notebookId = notebookId;
603
return this;
604
}
605
606
private _notebookCellLines?: string;
607
public setNotebookCellLines(notebookCellLines: string): this {
608
this._notebookCellLines = notebookCellLines;
609
return this;
610
}
611
612
private _notebookCellMarkerIndex: number = -1;
613
public setNotebookCellMarkerIndex(index: number): this {
614
this._notebookCellMarkerIndex = index;
615
return this;
616
}
617
618
private _isNESForAnotherDoc: boolean = false;
619
public setIsNESForOtherEditor(isForAnotherDoc: boolean): this {
620
this._isNESForAnotherDoc = isForAnotherDoc;
621
return this;
622
}
623
624
private _hadLlmNES: boolean = false;
625
public setHadLlmNES(boolean: boolean): this {
626
this._hadLlmNES = boolean;
627
return this;
628
}
629
630
private _hadDiagnosticsNES: boolean = false;
631
public setHadDiagnosticsNES(boolean: boolean): this {
632
this._hadDiagnosticsNES = boolean;
633
return this;
634
}
635
636
public setStatus(status: NextEditTelemetryStatus): this {
637
this._nesBuilder.setStatus(status);
638
return this;
639
}
640
641
private _configIsDiagnosticsNESEnabled: boolean = false;
642
public setConfigIsDiagnosticsNESEnabled(boolean: boolean): this {
643
this._configIsDiagnosticsNESEnabled = boolean;
644
return this;
645
}
646
647
private _isNaturalLanguageDominated: boolean = false;
648
public setIsNaturalLanguageDominated(isNaturalLanguageDominated: boolean): this {
649
this._isNaturalLanguageDominated = isNaturalLanguageDominated;
650
return this;
651
}
652
653
private _postProcessingOutcome: string | undefined;
654
public setPostProcessingOutcome(suggestion: {
655
edit: StringReplacement;
656
isInlineCompletion: boolean;
657
displayLocation?: INextEditDisplayLocation;
658
}): this {
659
const displayLocation = suggestion.displayLocation ? {
660
label: suggestion.displayLocation.label,
661
range: suggestion.displayLocation.range.toString()
662
} : undefined;
663
664
this._postProcessingOutcome = JSON.stringify({
665
suggestedEdit: suggestion.edit.toString(),
666
isInlineCompletion: suggestion.isInlineCompletion,
667
displayLocation
668
});
669
670
return this;
671
}
672
}
673
674
/**
675
* Watches all documents in the {@link ObservableWorkspace} for idle periods and cursor jumps.
676
*
677
* Only documents tracked by the workspace are monitored. Documents in languages where
678
* Copilot completions are disabled (e.g. markdown, plaintext), non-file URI schemes,
679
* and copilot-ignored files are excluded. This matches the scope of {@link DebugRecorder}.
680
*
681
* Fires `onIdle` after 5 seconds of no document edits across the workspace,
682
* and `onUserJump` when the user moves their cursor to a different line or file
683
* (ignoring selection changes within 200ms of an edit, which are likely side-effects of typing).
684
*
685
* Ref-counted via {@link RefCountedDisposable}: call {@link acquire} when a telemetry entry
686
* starts using this detector, {@link release} when it's done. Auto-disposes when all
687
* references are released. Use {@link forceDispose} on owner shutdown.
688
*/
689
class IdleDetector {
690
private readonly _store = new DisposableStore();
691
private readonly _disposalTracker = new RefCountedDisposable(this._store);
692
693
/** Snapshot of each document's primarySelectionLine to detect which doc's cursor actually moved. */
694
private readonly _selectionSnapshots = new Map<string, number | undefined>();
695
696
/** Timestamp of the last document edit, used to suppress selection changes caused by typing. */
697
private _lastEditTime = 0;
698
699
get isDisposed(): boolean { return this._store.isDisposed; }
700
701
constructor(
702
workspace: ObservableWorkspace,
703
private readonly _onIdle: (idleTimeoutMs: number) => void,
704
private readonly _onUserJump: (toDocId: string, toLine: number | undefined) => void,
705
) {
706
const idleTimeMs = 5_000;
707
708
// Idle timer: resets each time any tracked document changes, fires after 5s of inactivity
709
const idleScheduler = this._store.add(new RunOnceScheduler(() => {
710
this._onIdle(idleTimeMs);
711
}, idleTimeMs));
712
this._idleScheduler = idleScheduler;
713
714
// Watch for document content changes across the workspace.
715
// Skip scheduling on the first (initialization) run — the idle timer is started
716
// explicitly via scheduleIdleTimer() when the first entry acquires the detector.
717
let isFirstDocRun = true;
718
this._store.add(autorun(reader => {
719
workspace.onDidOpenDocumentChange.read(reader);
720
if (isFirstDocRun) {
721
isFirstDocRun = false;
722
return;
723
}
724
this._lastEditTime = Date.now();
725
idleScheduler.schedule();
726
}));
727
728
// Watch for selection (cursor) changes across all documents to detect user jumps.
729
// Uses autorunHandleChanges to get the `removed` list from openDocuments change data
730
// so we can clean up stale selection snapshots when documents are closed.
731
let isFirstSelectionRun = true;
732
this._store.add(autorunHandleChanges({
733
owner: this,
734
changeTracker: {
735
createChangeSummary: () => ({ removed: [] as readonly IObservableDocument[] }),
736
handleChange: (ctx, summary) => {
737
if (ctx.didChange(workspace.openDocuments)) {
738
summary.removed = ctx.change.removed;
739
}
740
return true;
741
}
742
}
743
}, (reader, changeSummary) => {
744
if (this._store.isDisposed) { return; }
745
746
// Subscribe to all document primarySelectionLine observables to detect line changes
747
const docs = workspace.openDocuments.read(reader);
748
for (const doc of docs) {
749
doc.primarySelectionLine.read(reader);
750
}
751
752
// On the first run, snapshot all current selection lines as baseline
753
if (isFirstSelectionRun) {
754
isFirstSelectionRun = false;
755
for (const doc of docs) {
756
// eslint-disable-next-line local/code-no-observable-get-in-reactive-context
757
this._selectionSnapshots.set(doc.id.uri, doc.primarySelectionLine.get());
758
}
759
return;
760
}
761
762
// Clean up snapshots for closed documents
763
for (const removed of changeSummary.removed) {
764
this._selectionSnapshots.delete(removed.id.uri);
765
}
766
767
// If a document was edited very recently (within 200ms), this selection change
768
// is likely a side-effect of the edit (e.g. cursor moves when typing) — not a deliberate jump
769
if (Date.now() - this._lastEditTime < 200) { return; }
770
771
// Find the doc whose selection line actually changed from what we last saw
772
for (const doc of docs) {
773
const currentDocId = doc.id.uri;
774
// eslint-disable-next-line local/code-no-observable-get-in-reactive-context
775
const currentLine = doc.primarySelectionLine.get();
776
const previousLine = this._selectionSnapshots.get(currentDocId);
777
778
if (previousLine === currentLine) { continue; }
779
780
this._selectionSnapshots.set(currentDocId, currentLine);
781
this._onUserJump(currentDocId, currentLine);
782
return;
783
}
784
}));
785
}
786
787
private _idleScheduler: RunOnceScheduler | undefined;
788
789
/** Start the idle timer. Called when an entry first acquires this detector. */
790
scheduleIdleTimer(): void { this._idleScheduler?.schedule(); }
791
792
acquire(): void { this._disposalTracker.acquire(); }
793
release(): void { this._disposalTracker.release(); }
794
forceDispose(): void { this._store.dispose(); }
795
}
796
797
export class TelemetrySender implements IDisposable {
798
799
private readonly _map = new Map<INextEditResult, { builder: NextEditProviderTelemetryBuilder; timeout: TimeoutHandle; hardCapTimeout?: TimeoutHandle }>();
800
private _idleDetector: IdleDetector | undefined;
801
802
constructor(
803
private readonly _workspace: ObservableWorkspace | undefined,
804
@ITelemetryService private readonly _telemetryService: ITelemetryService,
805
) {
806
}
807
808
/**
809
* Schedule sending enhanced telemetry for a NES suggestion.
810
*
811
* After a 2-minute initial delay, enters an idle-detection phase that monitors all workspace documents
812
* and finally sends the telemetry event when one of these conditions is met:
813
*
814
* - **idle** (5s): No document edits across the entire workspace for 5 seconds.
815
* - **user_jump**: User moves cursor to a different line or different file (detected via
816
* {@link IObservableDocument.primarySelectionLine} snapshot diffs.
817
* - **hard_cap** (30s): Forced send after 30 seconds regardless of activity.
818
*
819
* Note: only documents tracked by the {@link ObservableWorkspace} are monitored. Documents in
820
* languages where Copilot completions are disabled (e.g. markdown) and copilot-ignored files are excluded,
821
* so activity in those files won't reset the idle timer. This matches the scope of {@link DebugRecorder}.
822
*/
823
public scheduleSendingEnhancedTelemetry(nextEditResult: INextEditResult, builder: NextEditProviderTelemetryBuilder): void {
824
const existing = this._map.get(nextEditResult);
825
if (existing) {
826
if (existing.builder !== builder) {
827
existing.builder.dispose();
828
}
829
this._removeEntry(nextEditResult, existing);
830
}
831
832
const timeout = setTimeout(() => {
833
this._enterIdleDetection(nextEditResult, builder);
834
}, /* 2 minutes */ 2 * 60 * 1000);
835
this._map.set(nextEditResult, { builder, timeout });
836
}
837
838
private _enterIdleDetection(nextEditResult: INextEditResult, builder: NextEditProviderTelemetryBuilder): void {
839
const workspace = this._workspace;
840
if (!workspace) {
841
this._buildAndSendEnhancedTelemetry(nextEditResult, builder, { reason: 'idle', details: { idleTimeoutMs: 0 } });
842
return;
843
}
844
845
if (!this._idleDetector) {
846
this._idleDetector = new IdleDetector(
847
workspace,
848
idleTimeoutMs => this._sendAllPendingInIdlePhase({ reason: 'idle', details: { idleTimeoutMs } }),
849
(toDocId, toLine) => this._sendAllPendingInIdlePhaseWithJump(toDocId, toLine),
850
);
851
// RefCountedDisposable starts at count=1, which covers this first entry.
852
// Only subsequent entries need acquire().
853
} else {
854
this._idleDetector.acquire();
855
}
856
// Start/restart the idle timer so this entry gets a fresh 5s window
857
this._idleDetector.scheduleIdleTimer();
858
859
const hardCapMs = 30_000;
860
const hardCapTimeout = setTimeout(() => {
861
this._sendForEntry(nextEditResult, { reason: 'hard_cap', details: { hardCapTimeoutMs: hardCapMs } });
862
}, hardCapMs);
863
864
const entry = this._map.get(nextEditResult);
865
if (entry) {
866
entry.hardCapTimeout = hardCapTimeout;
867
}
868
}
869
870
private _releaseIdleDetector(): void {
871
this._idleDetector?.release();
872
if (this._idleDetector?.isDisposed) {
873
this._idleDetector = undefined;
874
}
875
}
876
877
/** Send all entries that are in the idle-detection phase (have no initial timeout pending) with a shared reason. */
878
private _sendAllPendingInIdlePhase(reason: IEnhancedTelemetrySendingReason): void {
879
const entriesToSend: INextEditResult[] = [];
880
for (const [result, data] of this._map) {
881
if (data.hardCapTimeout !== undefined) {
882
entriesToSend.push(result);
883
}
884
}
885
for (const result of entriesToSend) {
886
this._sendForEntry(result, reason);
887
}
888
}
889
890
/** Send all entries in idle-detection phase with user_jump, using per-entry `from` positions. */
891
private _sendAllPendingInIdlePhaseWithJump(toDocId: string, toLine: number | undefined): void {
892
const entriesToSend: [INextEditResult, NextEditProviderTelemetryBuilder][] = [];
893
for (const [result, data] of this._map) {
894
if (data.hardCapTimeout !== undefined) {
895
entriesToSend.push([result, data.builder]);
896
}
897
}
898
for (const [result, builder] of entriesToSend) {
899
const nesDocId: string | undefined = builder.doc?.id.uri;
900
const nesDocLine: number | undefined = builder.nesBuilder.originalSelectionLine;
901
const from = nesDocId !== undefined && nesDocLine !== undefined
902
? { file: nesDocId, line: nesDocLine }
903
: undefined;
904
this._sendForEntry(result, {
905
reason: 'user_jump',
906
details: {
907
from,
908
to: { file: toDocId, line: toLine },
909
},
910
});
911
}
912
}
913
914
/** Send enhanced telemetry for a single entry that's in the idle-detection phase. */
915
private _sendForEntry(nextEditResult: INextEditResult, reason: IEnhancedTelemetrySendingReason): void {
916
const data = this._map.get(nextEditResult);
917
if (!data) { return; }
918
919
if (data.hardCapTimeout !== undefined) {
920
clearTimeout(data.hardCapTimeout);
921
this._releaseIdleDetector();
922
}
923
this._map.delete(nextEditResult);
924
925
let telemetry: INextEditProviderTelemetry;
926
try {
927
telemetry = data.builder.build(true);
928
} finally {
929
data.builder.dispose();
930
}
931
this._doSendEnhancedTelemetry(telemetry, reason);
932
}
933
934
private _removeEntry(nextEditResult: INextEditResult, data: { builder: NextEditProviderTelemetryBuilder; timeout: TimeoutHandle; hardCapTimeout?: TimeoutHandle }): void {
935
clearTimeout(data.timeout);
936
if (data.hardCapTimeout !== undefined) {
937
clearTimeout(data.hardCapTimeout);
938
this._releaseIdleDetector();
939
}
940
this._map.delete(nextEditResult);
941
}
942
943
private _buildAndSendEnhancedTelemetry(nextEditResult: INextEditResult, builder: NextEditProviderTelemetryBuilder, sendingReason: IEnhancedTelemetrySendingReason): void {
944
let telemetry: INextEditProviderTelemetry;
945
this._map.delete(nextEditResult);
946
try {
947
telemetry = builder.build(true);
948
} finally {
949
builder.dispose();
950
}
951
this._doSendEnhancedTelemetry(telemetry, sendingReason);
952
}
953
954
/**
955
* Send telemetry for the next edit result in case it has already been rejected or contains no edits to be shown.
956
*/
957
public sendTelemetry(nextEditResult: INextEditResult | undefined, builder: NextEditProviderTelemetryBuilder): void {
958
if (nextEditResult) {
959
const data = this._map.get(nextEditResult);
960
if (data) {
961
this._removeEntry(nextEditResult, data);
962
}
963
}
964
const telemetry = builder.build(true);
965
if (!builder.isSent) {
966
this._doSendTelemetry(telemetry);
967
builder.markAsSent();
968
}
969
this._doSendEnhancedTelemetry(telemetry, undefined);
970
}
971
972
public sendTelemetryForBuilder(builder: NextEditProviderTelemetryBuilder): void {
973
if (builder.isSent) {
974
return;
975
}
976
const telemetry = builder.build(false); // disposal is done by enhanced telemetry sending in a setTimeout callback
977
this._doSendTelemetry(telemetry);
978
builder.markAsSent();
979
}
980
981
private async _doSendTelemetry(telemetry: INextEditProviderTelemetry): Promise<void> {
982
const {
983
opportunityId,
984
headerRequestId,
985
requestN,
986
providerId,
987
modelName,
988
hadStatelessNextEditProviderCall,
989
statelessNextEditProviderDuration,
990
nextEditProviderDuration,
991
isFromCache,
992
reusedRequest,
993
subsequentEditOrder,
994
activeDocumentLanguageId,
995
activeDocumentOriginalLineCount,
996
nLinesOfCurrentFileInPrompt,
997
wasPreviouslyRejected,
998
isShown,
999
isNotebook,
1000
notebookType,
1001
isNESForAnotherDoc,
1002
isActiveDocument,
1003
isEolDifferent,
1004
isMultilineEdit,
1005
isNextEditorRangeVisible,
1006
isNextEditorVisible,
1007
acceptance,
1008
disposalReason,
1009
logProbThreshold,
1010
documentsCount,
1011
editsCount,
1012
activeDocumentEditsCount,
1013
promptLineCount,
1014
promptCharCount,
1015
hadLowLogProbSuggestion,
1016
nEditsSuggested,
1017
lineDistanceToMostRecentEdit,
1018
isCursorAtEndOfLine,
1019
isInlineSuggestion,
1020
debounceTime,
1021
artificialDelay,
1022
hasNextEdit,
1023
notebookCellMarkerCount,
1024
notebookCellMarkerIndex,
1025
notebookId,
1026
notebookCellLines,
1027
nextEditLogprob,
1028
supersededByOpportunityId,
1029
noNextEditReasonKind,
1030
noNextEditReasonMessage,
1031
fetchStartedAfterMs,
1032
response: responseWithStats,
1033
configIsDiagnosticsNESEnabled,
1034
isNaturalLanguageDominated,
1035
diagnosticType,
1036
diagnosticDroppedReasons,
1037
diagnosticHasExistingSameFileImport,
1038
diagnosticIsLocalImport,
1039
diagnosticAlternativeImportsCount,
1040
diagnosticDistanceToUnknownDiagnostic,
1041
diagnosticDistanceToAlternativeDiagnostic,
1042
diagnosticHasAlternativeDiagnosticForSameRange,
1043
hadDiagnosticsNES,
1044
hadLlmNES,
1045
pickedNES,
1046
xtabAggressivenessLevel,
1047
xtabUserHappinessScore,
1048
userAggressivenessSetting,
1049
modelConfig,
1050
} = telemetry;
1051
1052
let usage: APIUsage | undefined;
1053
let ttft_: number | undefined;
1054
let fetchResult_: ChatFetchResponseType | undefined;
1055
let fetchTime_: number | undefined;
1056
if (responseWithStats !== undefined) {
1057
const { response, ttft, fetchResult, fetchTime } = await responseWithStats;
1058
if (response.type === ChatFetchResponseType.Success) {
1059
usage = response.usage;
1060
}
1061
ttft_ = ttft;
1062
fetchResult_ = fetchResult;
1063
fetchTime_ = fetchTime;
1064
}
1065
1066
/* __GDPR__
1067
"provideInlineEdit" : {
1068
"owner": "ulugbekna",
1069
"comment": "Telemetry for inline edit (NES) provided",
1070
"opportunityId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Unique identifier for an opportunity to show an NES." },
1071
"headerRequestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Unique identifier of the network request which is also included in the fetch request header." },
1072
"providerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "NES provider identifier (StatelessNextEditProvider)" },
1073
"modelName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Name of the model used to provide the NES" },
1074
"activeDocumentLanguageId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "LanguageId of the active document" },
1075
"mergeConflictExpanded": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "If and how edit window expanded to include merge conflict lines ('normal' or 'only' or undefined if not expanded)" },
1076
"acceptance": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "User acceptance of the edit" },
1077
"disposalReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Reason for disposal of NES" },
1078
"supersededByOpportunityId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "UUID of the opportunity that superseded this edit" },
1079
"endpoint": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Endpoint for the request" },
1080
"noNextEditReasonKind": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Reason kind for no next edit" },
1081
"noNextEditReasonMessage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Reason message for no next edit" },
1082
"fetchResult": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Fetch result" },
1083
"fetchError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Fetch error message" },
1084
"pickedNES": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request had picked NES" },
1085
"nextEditProviderError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Error message from next edit provider" },
1086
"diagnosticType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Type of diagnostics" },
1087
"diagnosticDroppedReasons": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Reasons for dropping diagnostics NES suggestions" },
1088
"requestN": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Request number", "isMeasurement": true },
1089
"hadStatelessNextEditProviderCall": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request had a stateless next edit provider call", "isMeasurement": true },
1090
"statelessNextEditProviderDuration": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Duration of stateless next edit provider", "isMeasurement": true },
1091
"nextEditProviderDuration": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Duration of next edit provider", "isMeasurement": true },
1092
"isFromCache": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the edit was provided from cache", "isMeasurement": true },
1093
"reusedRequest": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the result was obtained by joining a pending request ('speculative' or 'async'), undefined for fresh requests and cache hits" },
1094
"subsequentEditOrder": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Order of the subsequent edit", "isMeasurement": true },
1095
"activeDocumentOriginalLineCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of lines in the active document before shortening", "isMeasurement": true },
1096
"activeDocumentNLinesInPrompt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of lines in the active document included in prompt", "isMeasurement": true },
1097
"wasPreviouslyRejected": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the edit was previously rejected", "isMeasurement": true },
1098
"isShown": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the edit was shown", "isMeasurement": true },
1099
"isNotebook": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the document is a notebook", "isMeasurement": true },
1100
"isNESForAnotherDoc": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the NES if for another document", "isMeasurement": true },
1101
"isMultilineEdit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the NES is for a multiline edit", "isMeasurement": true },
1102
"isEolDifferent": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the NES edit and original text have different end of lines", "isMeasurement": true },
1103
"isNextEditorVisible": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the next editor is visible", "isMeasurement": true },
1104
"isNextEditorRangeVisible": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the next editor range is visible", "isMeasurement": true },
1105
"notebookCellMarkerIndex": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Index of the notebook cell marker in the edit", "isMeasurement": true },
1106
"isActiveDocument": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the document is the active document", "isMeasurement": true },
1107
"hasNotebookCellMarker": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the edit has a notebook cell marker", "isMeasurement": true },
1108
"notebookCellMarkerCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Count of notebook cell markers in the edit", "isMeasurement": true },
1109
"notebookId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Id of notebook" },
1110
"notebookCellLines": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Line counts of notebook cells" },
1111
"notebookType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Type of notebook, if any" },
1112
"logProbThreshold": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Log probability threshold for the edit", "isMeasurement": true },
1113
"documentsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of documents", "isMeasurement": true },
1114
"editsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of edits", "isMeasurement": true },
1115
"activeDocumentEditsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of edits in the active document", "isMeasurement": true },
1116
"promptLineCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of lines in the prompt", "isMeasurement": true },
1117
"promptCharCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of characters in the prompt", "isMeasurement": true },
1118
"nDiffsInPrompt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of diffs included in the prompt", "isMeasurement": true },
1119
"diffTokensInPrompt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens consumed by diffs in the prompt", "isMeasurement": true },
1120
"nNeighborSnippetsComputed": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Total number of neighbor (similar files) snippets computed before budget filtering", "isMeasurement": true },
1121
"nNeighborSnippetsInPrompt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of neighbor (similar files) snippets actually included in the prompt", "isMeasurement": true },
1122
"neighborSnippetIndicesInPrompt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "JSON-encoded array of original input indices (ascending) of neighbor snippets included in the prompt" },
1123
"hadLowLogProbSuggestion": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the suggestion had low log probability", "isMeasurement": true },
1124
"nEditsSuggested": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of edits suggested", "isMeasurement": true },
1125
"hasNextEdit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether next edit provider returned an edit (if an edit was previously rejected, this field is false)", "isMeasurement": true },
1126
"nextEditLogprob": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Log probability of the next edit", "isMeasurement": true },
1127
"lineDistanceToMostRecentEdit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Line distance to most recent edit", "isMeasurement": true },
1128
"isCursorAtEndOfLine": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the cursor is at the end of the line", "isMeasurement": true },
1129
"isInlineSuggestion": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the cursor is at a valid inline suggestion position (middle of line with valid trailing characters)", "isMeasurement": true },
1130
"debounceTime": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Debounce time", "isMeasurement": true },
1131
"artificialDelay": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Artificial delay (aka backoff) on the response based on previous user acceptance/rejection in milliseconds", "isMeasurement": true },
1132
"fetchStartedAfterMs": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Time from inline edit provider invocation to fetch init", "isMeasurement": true },
1133
"ttft": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Time to first token", "isMeasurement": true },
1134
"fetchTime": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Time from fetch init to end of stream", "isMeasurement": true },
1135
"promptTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the prompt", "isMeasurement": true },
1136
"responseTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the response", "isMeasurement": true },
1137
"cachedTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of cached tokens in the response", "isMeasurement": true },
1138
"acceptedPredictionTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the prediction that appeared in the completion", "isMeasurement": true },
1139
"rejectedPredictionTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the prediction that appeared in the completion", "isMeasurement": true },
1140
"hadDiagnosticsNES": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request had diagnostics NES", "isMeasurement": true },
1141
"hadLlmNES": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request had LLM NES", "isMeasurement": true },
1142
"configIsDiagnosticsNESEnabled": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether diagnostics NES is enabled", "isMeasurement": true },
1143
"isNaturalLanguageDominated": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the context is dominated by natural language", "isMeasurement": true },
1144
"diagnosticHasExistingSameFileImport": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the diagnostic has an existing same file import", "isMeasurement": true },
1145
"diagnosticIsLocalImport": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the diagnostic is a local import", "isMeasurement": true },
1146
"diagnosticAlternativeImportsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of alternative imports for the diagnostic", "isMeasurement": true },
1147
"diagnosticDistanceToUnknownDiagnostic": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Distance to the unknown diagnostic", "isMeasurement": true },
1148
"diagnosticDistanceToAlternativeDiagnostic": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Distance to the alternative diagnostic", "isMeasurement": true },
1149
"diagnosticHasAlternativeDiagnosticForSameRange": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether there is an alternative diagnostic for the same range", "isMeasurement": true },
1150
"nextCursorLineDistance": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Distance from next cursor line to current cursor line: newCursorLineNumber - currentCursorLineNumber", "isMeasurement": true },
1151
"nextCursorLineError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Error in the predicted next cursor line" },
1152
"xtabAggressivenessLevel": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The aggressiveness level used for xtabAggressiveness prompting strategy (low, medium, high)" },
1153
"userAggressivenessSetting": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The raw user-facing aggressiveness setting value (only set when user changed from default)" },
1154
"xtabUserHappinessScore": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "User happiness score (0-1) when using xtabAggressiveness prompting strategy", "isMeasurement": true },
1155
"userTypingDisagreed": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the user typing disagreed with the suggestion", "isMeasurement": true },
1156
"modelConfig": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "JSON-encoded model configuration from the model service" }
1157
}
1158
*/
1159
this._sendTelemetryToBoth(
1160
{
1161
opportunityId,
1162
headerRequestId,
1163
providerId,
1164
modelName,
1165
activeDocumentLanguageId,
1166
mergeConflictExpanded: telemetry.mergeConflictExpanded,
1167
acceptance,
1168
disposalReason,
1169
supersededByOpportunityId,
1170
noNextEditReasonKind,
1171
noNextEditReasonMessage,
1172
fetchResult: fetchResult_,
1173
nextEditProviderError: telemetry.nextEditProviderError,
1174
reusedRequest,
1175
diagnosticType,
1176
diagnosticDroppedReasons,
1177
pickedNES,
1178
notebookType,
1179
notebookId,
1180
notebookCellLines,
1181
nextCursorLineError: telemetry.nextCursorPrediction?.nextCursorLineError,
1182
xtabAggressivenessLevel,
1183
userAggressivenessSetting,
1184
modelConfig,
1185
neighborSnippetIndicesInPrompt: telemetry.neighborSnippetIndicesInPrompt,
1186
},
1187
{
1188
requestN,
1189
hadStatelessNextEditProviderCall: this._boolToNum(hadStatelessNextEditProviderCall),
1190
statelessNextEditProviderDuration,
1191
nextEditProviderDuration,
1192
isFromCache: this._boolToNum(isFromCache),
1193
subsequentEditOrder,
1194
activeDocumentOriginalLineCount,
1195
activeDocumentNLinesInPrompt: nLinesOfCurrentFileInPrompt,
1196
wasPreviouslyRejected: this._boolToNum(wasPreviouslyRejected),
1197
isShown: this._boolToNum(isShown),
1198
isNotebook: this._boolToNum(isNotebook),
1199
isNESForAnotherDoc: this._boolToNum(isNESForAnotherDoc),
1200
isActiveDocument: this._boolToNum(isActiveDocument),
1201
isEolDifferent: this._boolToNum(isEolDifferent),
1202
isMultilineEdit: this._boolToNum(isMultilineEdit),
1203
isNextEditorRangeVisible: this._boolToNum(isNextEditorRangeVisible),
1204
isNextEditorVisible: this._boolToNum(isNextEditorVisible),
1205
hasNotebookCellMarker: notebookCellMarkerCount > 0 ? 1 : 0,
1206
notebookCellMarkerCount,
1207
notebookCellMarkerIndex,
1208
logProbThreshold,
1209
documentsCount,
1210
editsCount,
1211
activeDocumentEditsCount,
1212
promptLineCount,
1213
promptCharCount,
1214
hadLowLogProbSuggestion: this._boolToNum(hadLowLogProbSuggestion),
1215
nEditsSuggested,
1216
lineDistanceToMostRecentEdit,
1217
isCursorAtEndOfLine: this._boolToNum(isCursorAtEndOfLine),
1218
isInlineSuggestion: this._boolToNum(isInlineSuggestion),
1219
debounceTime,
1220
artificialDelay,
1221
fetchStartedAfterMs,
1222
ttft: ttft_,
1223
fetchTime: fetchTime_,
1224
promptTokens: usage?.prompt_tokens,
1225
responseTokens: usage?.completion_tokens,
1226
cachedTokens: usage?.prompt_tokens_details?.cached_tokens,
1227
acceptedPredictionTokens: usage?.completion_tokens_details?.accepted_prediction_tokens,
1228
rejectedPredictionTokens: usage?.completion_tokens_details?.rejected_prediction_tokens,
1229
hasNextEdit: this._boolToNum(hasNextEdit),
1230
userTypingDisagreed: this._boolToNum(telemetry.userTypingDisagreed),
1231
nextEditLogprob,
1232
hadDiagnosticsNES: this._boolToNum(hadDiagnosticsNES),
1233
hadLlmNES: this._boolToNum(hadLlmNES),
1234
configIsDiagnosticsNESEnabled: this._boolToNum(configIsDiagnosticsNESEnabled),
1235
isNaturalLanguageDominated: this._boolToNum(isNaturalLanguageDominated),
1236
diagnosticHasExistingSameFileImport: this._boolToNum(diagnosticHasExistingSameFileImport),
1237
diagnosticIsLocalImport: this._boolToNum(diagnosticIsLocalImport),
1238
diagnosticAlternativeImportsCount: diagnosticAlternativeImportsCount,
1239
diagnosticDistanceToUnknownDiagnostic: diagnosticDistanceToUnknownDiagnostic,
1240
diagnosticDistanceToAlternativeDiagnostic: diagnosticDistanceToAlternativeDiagnostic,
1241
diagnosticHasAlternativeDiagnosticForSameRange: this._boolToNum(diagnosticHasAlternativeDiagnosticForSameRange),
1242
nextCursorLineDistance: telemetry.nextCursorPrediction?.nextCursorLineDistance,
1243
xtabUserHappinessScore,
1244
nDiffsInPrompt: telemetry.nDiffsInPrompt,
1245
diffTokensInPrompt: telemetry.diffTokensInPrompt,
1246
nNeighborSnippetsComputed: telemetry.nNeighborSnippetsComputed,
1247
nNeighborSnippetsInPrompt: telemetry.nNeighborSnippetsInPrompt,
1248
}
1249
);
1250
}
1251
1252
private _sendTelemetryToBoth(properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements): void {
1253
this._telemetryService.sendMSFTTelemetryEvent('provideInlineEdit', properties, measurements);
1254
this._telemetryService.sendGHTelemetryEvent('copilot-nes/provideInlineEdit', properties, measurements);
1255
}
1256
1257
private async _doSendEnhancedTelemetry(telemetry: INextEditProviderTelemetry, sendingReason: IEnhancedTelemetrySendingReason | undefined): Promise<void> {
1258
1259
const {
1260
opportunityId,
1261
headerRequestId,
1262
providerId,
1263
activeDocumentLanguageId,
1264
status: suggestionStatus,
1265
modelName,
1266
prompt,
1267
response,
1268
alternativeAction,
1269
postProcessingOutcome,
1270
activeDocumentRepository,
1271
repositoryUrls,
1272
cursorJumpModelName,
1273
cursorJumpPrompt,
1274
cursorJumpResponse,
1275
lintErrors,
1276
terminalOutput,
1277
similarFilesContext,
1278
modelConfig,
1279
isFromCache,
1280
} = telemetry;
1281
1282
const modelResponse = response === undefined ? response : await response;
1283
const resolvedSimilarFilesContext = await similarFilesContext?.catch(() => undefined);
1284
1285
this._telemetryService.sendEnhancedGHTelemetryEvent('copilot-nes/provideInlineEdit',
1286
multiplexProperties({
1287
opportunityId,
1288
headerRequestId,
1289
providerId,
1290
activeDocumentLanguageId,
1291
suggestionStatus,
1292
modelName,
1293
prompt,
1294
modelResponse: modelResponse === undefined || modelResponse.response.type !== ChatFetchResponseType.Success ? undefined : modelResponse.response.value,
1295
alternativeAction: alternativeAction ? JSON.stringify({ ...alternativeAction, enhancedTelemetrySendingReason: sendingReason }) : undefined,
1296
enhancedTelemetrySendingReason: !alternativeAction && sendingReason ? JSON.stringify(sendingReason) : undefined,
1297
postProcessingOutcome,
1298
activeDocumentRepository,
1299
repositories: JSON.stringify(repositoryUrls),
1300
cursorJumpModelName,
1301
cursorJumpPrompt,
1302
cursorJumpResponse,
1303
lintErrors,
1304
terminalOutput,
1305
similarFilesContext: resolvedSimilarFilesContext,
1306
modelConfig,
1307
}),
1308
{
1309
isFromCache: this._boolToNum(isFromCache),
1310
}
1311
);
1312
}
1313
1314
/**
1315
* If `value` is undefined, return undefined, otherwise return 1 if `value` is true, 0 otherwise.
1316
*/
1317
private _boolToNum(value: boolean | undefined): number | undefined {
1318
return value === undefined ? undefined : (value ? 1 : 0);
1319
}
1320
1321
dispose(): void {
1322
for (const data of this._map.values()) {
1323
clearTimeout(data.timeout);
1324
if (data.hardCapTimeout !== undefined) {
1325
clearTimeout(data.hardCapTimeout);
1326
}
1327
data.builder.dispose();
1328
}
1329
this._map.clear();
1330
1331
if (this._idleDetector) {
1332
this._idleDetector.forceDispose();
1333
this._idleDetector = undefined;
1334
}
1335
}
1336
}
1337
1338