Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts
3296 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 { TimeoutTimer } from '../../../../../base/common/async.js';
7
import { onUnexpectedError } from '../../../../../base/common/errors.js';
8
import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
9
import { IObservableWithChange, runOnChange } from '../../../../../base/common/observable.js';
10
import { generateUuid } from '../../../../../base/common/uuid.js';
11
import { AnnotatedStringEdit, BaseStringEdit } from '../../../../../editor/common/core/edits/stringEdit.js';
12
import { StringText } from '../../../../../editor/common/core/text/abstractText.js';
13
import { EditDeltaInfo, EditSuggestionId, ITextModelEditSourceMetadata } from '../../../../../editor/common/textModelEditSource.js';
14
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
15
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
16
import { EditSourceData, IDocumentWithAnnotatedEdits, createDocWithJustReason } from '../helpers/documentWithAnnotatedEdits.js';
17
import { IAiEditTelemetryService } from './aiEditTelemetry/aiEditTelemetryService.js';
18
import { ArcTracker } from '../../common/arcTracker.js';
19
import type { ScmRepoBridge } from './editSourceTrackingImpl.js';
20
import { forwardToChannelIf, isCopilotLikeExtension } from './forwardingTelemetryService.js';
21
22
export class InlineEditArcTelemetrySender extends Disposable {
23
constructor(
24
docWithAnnotatedEdits: IDocumentWithAnnotatedEdits<EditSourceData>,
25
scmRepoBridge: ScmRepoBridge | undefined,
26
@IInstantiationService private readonly _instantiationService: IInstantiationService
27
) {
28
super();
29
30
this._register(runOnChange(docWithAnnotatedEdits.value, (_val, _prev, changes) => {
31
const edit = AnnotatedStringEdit.compose(changes.map(c => c.edit));
32
33
if (!edit.replacements.some(r => r.data.editSource.metadata.source === 'inlineCompletionAccept')) {
34
return;
35
}
36
if (!edit.replacements.every(r => r.data.editSource.metadata.source === 'inlineCompletionAccept')) {
37
onUnexpectedError(new Error('ArcTelemetrySender: Not all edits are inline completion accept edits!'));
38
return;
39
}
40
if (edit.replacements[0].data.editSource.metadata.source !== 'inlineCompletionAccept') {
41
return;
42
}
43
const data = edit.replacements[0].data.editSource.metadata;
44
45
const docWithJustReason = createDocWithJustReason(docWithAnnotatedEdits, this._store);
46
const reporter = this._instantiationService.createInstance(ArcTelemetryReporter, [0, 30, 120, 300, 600, 900].map(s => s * 1000), _prev, docWithJustReason, scmRepoBridge, edit, res => {
47
res.telemetryService.publicLog2<{
48
extensionId: string;
49
extensionVersion: string;
50
opportunityId: string;
51
languageId: string;
52
didBranchChange: number;
53
timeDelayMs: number;
54
55
originalCharCount: number;
56
originalLineCount: number;
57
originalDeletedLineCount: number;
58
arc: number;
59
currentLineCount: number;
60
currentDeletedLineCount: number;
61
}, {
62
owner: 'hediet';
63
comment: 'Reports the accepted and retained character count for an inline completion/edit.';
64
65
extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id (copilot or copilot-chat); which provided this inline completion.' };
66
extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the extension.' };
67
opportunityId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for an opportunity to show an inline completion or NES.' };
68
languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' };
69
70
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.' };
71
timeDelayMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The time delay between the user accepting the edit and measuring the survival rate.' };
72
73
originalCharCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original character count before any edits.' };
74
originalLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original line count before any edits.' };
75
originalDeletedLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original deleted line count before any edits.' };
76
arc: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The accepted and restrained character count.' };
77
currentLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The current line count after edits.' };
78
currentDeletedLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The current deleted line count after edits.' };
79
}>('editTelemetry.reportInlineEditArc', {
80
extensionId: data.$extensionId ?? '',
81
extensionVersion: data.$extensionVersion ?? '',
82
opportunityId: data.$$requestUuid ?? 'unknown',
83
languageId: data.$$languageId,
84
didBranchChange: res.didBranchChange ? 1 : 0,
85
timeDelayMs: res.timeDelayMs,
86
87
originalCharCount: res.originalCharCount,
88
originalLineCount: res.originalLineCount,
89
originalDeletedLineCount: res.originalDeletedLineCount,
90
arc: res.arc,
91
currentLineCount: res.currentLineCount,
92
currentDeletedLineCount: res.currentDeletedLineCount,
93
94
...forwardToChannelIf(isCopilotLikeExtension(data.$extensionId)),
95
});
96
});
97
98
this._register(toDisposable(() => {
99
reporter.cancel();
100
}));
101
}));
102
}
103
}
104
105
export class AiEditTelemetryAdapter extends Disposable {
106
constructor(
107
docWithAnnotatedEdits: IDocumentWithAnnotatedEdits<EditSourceData>,
108
@IAiEditTelemetryService private readonly _aiEditTelemetryService: IAiEditTelemetryService,
109
) {
110
super();
111
112
this._register(runOnChange(docWithAnnotatedEdits.value, (_val, _prev, changes) => {
113
const edit = AnnotatedStringEdit.compose(changes.map(c => c.edit));
114
115
const supportedSource = new Set(['Chat.applyEdits', 'inlineChat.applyEdits'] as ITextModelEditSourceMetadata['source'][]);
116
117
if (!edit.replacements.some(r => supportedSource.has(r.data.editSource.metadata.source))) {
118
return;
119
}
120
if (!edit.replacements.every(r => supportedSource.has(r.data.editSource.metadata.source))) {
121
onUnexpectedError(new Error(`ArcTelemetrySender: Not all edits are ${edit.replacements[0].data.editSource.metadata.source}!`));
122
return;
123
}
124
let applyCodeBlockSuggestionId: EditSuggestionId | undefined = undefined;
125
const data = edit.replacements[0].data.editSource;
126
let feature: 'inlineChat' | 'sideBarChat';
127
if (data.metadata.source === 'Chat.applyEdits') {
128
feature = 'sideBarChat';
129
if (data.metadata.$$mode === 'applyCodeBlock') {
130
applyCodeBlockSuggestionId = data.metadata.$$codeBlockSuggestionId;
131
}
132
} else {
133
feature = 'inlineChat';
134
}
135
136
// TODO@hediet tie this suggestion id to hunks, so acceptance can be correlated.
137
this._aiEditTelemetryService.createSuggestionId({
138
applyCodeBlockSuggestionId,
139
languageId: data.props.$$languageId,
140
presentation: 'highlightedEdit',
141
feature,
142
modelId: data.props.$modelId,
143
modeId: data.props.$$mode as any,
144
editDeltaInfo: EditDeltaInfo.fromEdit(edit, _prev),
145
});
146
}));
147
}
148
}
149
150
export class ChatArcTelemetrySender extends Disposable {
151
constructor(
152
docWithAnnotatedEdits: IDocumentWithAnnotatedEdits<EditSourceData>,
153
scmRepoBridge: ScmRepoBridge | undefined,
154
@IInstantiationService private readonly _instantiationService: IInstantiationService
155
) {
156
super();
157
158
this._register(runOnChange(docWithAnnotatedEdits.value, (_val, _prev, changes) => {
159
const edit = AnnotatedStringEdit.compose(changes.map(c => c.edit));
160
161
const supportedSource = new Set(['Chat.applyEdits', 'inlineChat.applyEdits'] as ITextModelEditSourceMetadata['source'][]);
162
163
if (!edit.replacements.some(r => supportedSource.has(r.data.editSource.metadata.source))) {
164
return;
165
}
166
if (!edit.replacements.every(r => supportedSource.has(r.data.editSource.metadata.source))) {
167
onUnexpectedError(new Error(`ArcTelemetrySender: Not all edits are ${edit.replacements[0].data.editSource.metadata.source}!`));
168
return;
169
}
170
const data = edit.replacements[0].data.editSource;
171
172
const uniqueEditId = generateUuid();
173
174
const docWithJustReason = createDocWithJustReason(docWithAnnotatedEdits, this._store);
175
const reporter = this._instantiationService.createInstance(ArcTelemetryReporter, [0, 60, 300].map(s => s * 1000), _prev, docWithJustReason, scmRepoBridge, edit, res => {
176
res.telemetryService.publicLog2<{
177
sourceKeyCleaned: string;
178
extensionId: string | undefined;
179
extensionVersion: string | undefined;
180
opportunityId: string | undefined;
181
editSessionId: string | undefined;
182
requestId: string | undefined;
183
modelId: string | undefined;
184
languageId: string | undefined;
185
mode: string | undefined;
186
uniqueEditId: string | undefined;
187
188
didBranchChange: number;
189
timeDelayMs: number;
190
191
originalCharCount: number;
192
originalLineCount: number;
193
originalDeletedLineCount: number;
194
arc: number;
195
currentLineCount: number;
196
currentDeletedLineCount: number;
197
}, {
198
owner: 'hediet';
199
comment: 'Reports the accepted and retained character count for an inline completion/edit.';
200
201
sourceKeyCleaned: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The key of the edit source.' };
202
extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id (copilot or copilot-chat); which provided this inline completion.' };
203
extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the extension.' };
204
opportunityId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for an opportunity to show an inline completion or NES.' };
205
editSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The session id.' };
206
requestId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The request id.' };
207
modelId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The model id.' };
208
languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' };
209
mode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The mode chat was in.' };
210
uniqueEditId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The unique identifier for the edit.' };
211
212
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.' };
213
timeDelayMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The time delay between the user accepting the edit and measuring the survival rate.' };
214
215
originalCharCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original character count before any edits.' };
216
originalLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original line count before any edits.' };
217
originalDeletedLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original deleted line count before any edits.' };
218
arc: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The accepted and restrained character count.' };
219
currentLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The current line count after edits.' };
220
currentDeletedLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The current deleted line count after edits.' };
221
}>('editTelemetry.reportEditArc', {
222
sourceKeyCleaned: data.toKey(Number.MAX_SAFE_INTEGER, {
223
$extensionId: false,
224
$extensionVersion: false,
225
$$requestUuid: false,
226
$$sessionId: false,
227
$$requestId: false,
228
$$languageId: false,
229
$modelId: false,
230
}),
231
extensionId: data.props.$extensionId,
232
extensionVersion: data.props.$extensionVersion,
233
opportunityId: data.props.$$requestUuid,
234
editSessionId: data.props.$$sessionId,
235
requestId: data.props.$$requestId,
236
modelId: data.props.$modelId,
237
languageId: data.props.$$languageId,
238
mode: data.props.$$mode,
239
uniqueEditId,
240
241
didBranchChange: res.didBranchChange ? 1 : 0,
242
timeDelayMs: res.timeDelayMs,
243
244
originalCharCount: res.originalCharCount,
245
originalLineCount: res.originalLineCount,
246
originalDeletedLineCount: res.originalDeletedLineCount,
247
arc: res.arc,
248
currentLineCount: res.currentLineCount,
249
currentDeletedLineCount: res.currentDeletedLineCount,
250
251
...forwardToChannelIf(isCopilotLikeExtension(data.props.$extensionId)),
252
});
253
});
254
255
this._register(toDisposable(() => {
256
reporter.cancel();
257
}));
258
}));
259
}
260
}
261
262
263
export interface EditTelemetryData {
264
telemetryService: ITelemetryService;
265
timeDelayMs: number;
266
didBranchChange: boolean;
267
arc: number;
268
originalCharCount: number;
269
270
currentLineCount: number;
271
currentDeletedLineCount: number;
272
originalLineCount: number;
273
originalDeletedLineCount: number;
274
}
275
276
export class ArcTelemetryReporter {
277
private readonly _store = new DisposableStore();
278
private readonly _arcTracker;
279
private readonly _initialBranchName: string | undefined;
280
281
private readonly _initialLineCounts;
282
283
constructor(
284
private readonly _timesMs: number[],
285
private readonly _documentValueBeforeTrackedEdit: StringText,
286
private readonly _document: { value: IObservableWithChange<StringText, { edit: BaseStringEdit }> },
287
// _markedEdits -> document.value
288
private readonly _gitRepo: ScmRepoBridge | undefined,
289
private readonly _trackedEdit: BaseStringEdit,
290
private readonly _sendTelemetryEvent: (res: EditTelemetryData) => void,
291
292
@ITelemetryService private readonly _telemetryService: ITelemetryService
293
) {
294
this._arcTracker = new ArcTracker(this._documentValueBeforeTrackedEdit, this._trackedEdit);
295
296
this._store.add(runOnChange(this._document.value, (_val, _prevVal, changes) => {
297
const edit = BaseStringEdit.composeOrUndefined(changes.map(c => c.edit));
298
if (edit) {
299
this._arcTracker.handleEdits(edit);
300
}
301
}));
302
303
this._initialLineCounts = this._arcTracker.getLineCountInfo();
304
305
this._initialBranchName = this._gitRepo?.headBranchNameObs.get();
306
307
for (let i = 0; i < this._timesMs.length; i++) {
308
const timeMs = this._timesMs[i];
309
310
if (timeMs <= 0) {
311
this._report(timeMs);
312
} else {
313
this._reportAfter(timeMs, i === this._timesMs.length - 1 ? () => {
314
this._store.dispose();
315
} : undefined);
316
}
317
}
318
}
319
320
private _reportAfter(timeoutMs: number, cb?: () => void) {
321
const timer = new TimeoutTimer(() => {
322
this._report(timeoutMs);
323
timer.dispose();
324
if (cb) {
325
cb();
326
}
327
}, timeoutMs);
328
this._store.add(timer);
329
}
330
331
private _report(timeMs: number): void {
332
const currentBranch = this._gitRepo?.headBranchNameObs.get();
333
const didBranchChange = currentBranch !== this._initialBranchName;
334
const currentLineCounts = this._arcTracker.getLineCountInfo();
335
336
this._sendTelemetryEvent({
337
telemetryService: this._telemetryService,
338
timeDelayMs: timeMs,
339
didBranchChange,
340
arc: this._arcTracker.getAcceptedRestrainedCharactersCount(),
341
originalCharCount: this._arcTracker.getOriginalCharacterCount(),
342
343
currentLineCount: currentLineCounts.insertedLineCounts,
344
currentDeletedLineCount: currentLineCounts.deletedLineCounts,
345
originalLineCount: this._initialLineCounts.insertedLineCounts,
346
originalDeletedLineCount: this._initialLineCounts.deletedLineCounts,
347
});
348
}
349
350
public cancel(): void {
351
this._store.dispose();
352
}
353
}
354
355