Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.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 { reverseOrder, compareBy, numberComparator, sumBy } from '../../../../../base/common/arrays.js';
7
import { IntervalTimer, TimeoutTimer } from '../../../../../base/common/async.js';
8
import { toDisposable, Disposable } from '../../../../../base/common/lifecycle.js';
9
import { mapObservableArrayCached, derived, IObservable, observableSignal, runOnChange } from '../../../../../base/common/observable.js';
10
import { URI } from '../../../../../base/common/uri.js';
11
import { generateUuid } from '../../../../../base/common/uuid.js';
12
import { TextModelEditSource } from '../../../../../editor/common/textModelEditSource.js';
13
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
14
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
15
import { ISCMRepository, ISCMService } from '../../../scm/common/scm.js';
16
import { AnnotatedDocuments, AnnotatedDocument } from '../helpers/annotatedDocuments.js';
17
import { AiEditTelemetryAdapter, ChatArcTelemetrySender, InlineEditArcTelemetrySender } from './arcTelemetrySender.js';
18
import { createDocWithJustReason, EditSource } from '../helpers/documentWithAnnotatedEdits.js';
19
import { DocumentEditSourceTracker, TrackedEdit } from './editTracker.js';
20
import { sumByCategory } from '../helpers/utils.js';
21
22
export class EditSourceTrackingImpl extends Disposable {
23
public readonly docsState;
24
25
constructor(
26
private readonly _statsEnabled: IObservable<boolean>,
27
private readonly _annotatedDocuments: AnnotatedDocuments,
28
@IInstantiationService private readonly _instantiationService: IInstantiationService,
29
) {
30
super();
31
32
const scmBridge = this._instantiationService.createInstance(ScmBridge);
33
const states = mapObservableArrayCached(this, this._annotatedDocuments.documents, (doc, store) => {
34
return [doc.document, store.add(this._instantiationService.createInstance(TrackedDocumentInfo, doc, scmBridge, this._statsEnabled))] as const;
35
});
36
this.docsState = states.map((entries) => new Map(entries));
37
38
this.docsState.recomputeInitiallyAndOnChange(this._store);
39
}
40
}
41
42
class TrackedDocumentInfo extends Disposable {
43
public readonly longtermTracker: IObservable<DocumentEditSourceTracker<undefined> | undefined>;
44
public readonly windowedTracker: IObservable<DocumentEditSourceTracker<undefined> | undefined>;
45
46
private readonly _repo: Promise<ScmRepoBridge | undefined>;
47
48
constructor(
49
private readonly _doc: AnnotatedDocument,
50
private readonly _scm: ScmBridge,
51
private readonly _statsEnabled: IObservable<boolean>,
52
@IInstantiationService private readonly _instantiationService: IInstantiationService,
53
@ITelemetryService private readonly _telemetryService: ITelemetryService
54
) {
55
super();
56
57
const docWithJustReason = createDocWithJustReason(_doc.documentWithAnnotations, this._store);
58
59
const longtermResetSignal = observableSignal('resetSignal');
60
61
let longtermReason: '10hours' | 'hashChange' | 'branchChange' | 'closed' = 'closed';
62
this.longtermTracker = derived((reader) => {
63
if (!this._statsEnabled.read(reader)) { return undefined; }
64
longtermResetSignal.read(reader);
65
66
const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined));
67
reader.store.add(toDisposable(() => {
68
// send long term document telemetry
69
if (!t.isEmpty()) {
70
this.sendTelemetry('longterm', longtermReason, t);
71
}
72
t.dispose();
73
}));
74
return t;
75
}).recomputeInitiallyAndOnChange(this._store);
76
77
this._store.add(new IntervalTimer()).cancelAndSet(() => {
78
// Reset after 10 hours
79
longtermReason = '10hours';
80
longtermResetSignal.trigger(undefined);
81
longtermReason = 'closed';
82
}, 10 * 60 * 60 * 1000);
83
84
(async () => {
85
const repo = await this._scm.getRepo(_doc.document.uri);
86
if (this._store.isDisposed) {
87
return;
88
}
89
// Reset on branch change or commit
90
if (repo) {
91
this._store.add(runOnChange(repo.headCommitHashObs, () => {
92
longtermReason = 'hashChange';
93
longtermResetSignal.trigger(undefined);
94
longtermReason = 'closed';
95
}));
96
this._store.add(runOnChange(repo.headBranchNameObs, () => {
97
longtermReason = 'branchChange';
98
longtermResetSignal.trigger(undefined);
99
longtermReason = 'closed';
100
}));
101
}
102
103
this._store.add(this._instantiationService.createInstance(InlineEditArcTelemetrySender, _doc.documentWithAnnotations, repo));
104
this._store.add(this._instantiationService.createInstance(ChatArcTelemetrySender, _doc.documentWithAnnotations, repo));
105
this._store.add(this._instantiationService.createInstance(AiEditTelemetryAdapter, _doc.documentWithAnnotations));
106
})();
107
108
const resetSignal = observableSignal('resetSignal');
109
110
this.windowedTracker = derived((reader) => {
111
if (!this._statsEnabled.read(reader)) { return undefined; }
112
113
if (!this._doc.isVisible.read(reader)) {
114
return undefined;
115
}
116
resetSignal.read(reader);
117
118
reader.store.add(new TimeoutTimer(() => {
119
// Reset after 5 minutes
120
resetSignal.trigger(undefined);
121
}, 5 * 60 * 1000));
122
123
const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined));
124
reader.store.add(toDisposable(async () => {
125
// send long term document telemetry
126
this.sendTelemetry('5minWindow', 'time', t);
127
t.dispose();
128
}));
129
130
return t;
131
}).recomputeInitiallyAndOnChange(this._store);
132
133
this._repo = this._scm.getRepo(_doc.document.uri);
134
}
135
136
async sendTelemetry(mode: 'longterm' | '5minWindow', trigger: string, t: DocumentEditSourceTracker) {
137
const ranges = t.getTrackedRanges();
138
if (ranges.length === 0) {
139
return;
140
}
141
142
const data = this.getTelemetryData(ranges);
143
144
145
const statsUuid = generateUuid();
146
147
const sourceKeyToRepresentative = new Map<string, TextModelEditSource>();
148
for (const r of ranges) {
149
sourceKeyToRepresentative.set(r.sourceKey, r.sourceRepresentative);
150
}
151
152
const sums = sumByCategory(ranges, r => r.range.length, r => r.sourceKey);
153
const entries = Object.entries(sums).filter(([key, value]) => value !== undefined);
154
entries.sort(reverseOrder(compareBy(([key, value]) => value!, numberComparator)));
155
entries.length = mode === 'longterm' ? 30 : 10;
156
157
for (const [key, value] of Object.entries(sums)) {
158
if (value === undefined) {
159
continue;
160
}
161
162
const repr = sourceKeyToRepresentative.get(key)!;
163
const m = t.getChangedCharactersCount(key);
164
165
this._telemetryService.publicLog2<{
166
mode: string;
167
sourceKey: string;
168
169
sourceKeyCleaned: string;
170
extensionId: string | undefined;
171
extensionVersion: string | undefined;
172
modelId: string | undefined;
173
174
trigger: string;
175
languageId: string;
176
statsUuid: string;
177
modifiedCount: number;
178
deltaModifiedCount: number;
179
totalModifiedCount: number;
180
}, {
181
owner: 'hediet';
182
comment: 'Reports distribution of various edit sources per session.';
183
184
mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Describes the session mode. Is either longterm or 5minWindow.' };
185
sourceKey: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A description of the source of the edit.' };
186
187
sourceKeyCleaned: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the edit with some properties (such as extensionId, extensionVersion and modelId) removed.' };
188
extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id.' };
189
extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the extension.' };
190
modelId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The LLM id.' };
191
192
languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' };
193
statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier of the session for which stats are reported. The sourceKey is unique in this session.' };
194
195
trigger: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates why the session ended.' };
196
197
modifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The number of characters inserted by the given edit source during the session that are still in the text document at the end of the session.'; isMeasurement: true };
198
deltaModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The number of characters inserted by the given edit source during the session.'; isMeasurement: true };
199
totalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The number of characters inserted by any edit source during the session that are still in the text document at the end of the session.'; isMeasurement: true };
200
201
}>('editTelemetry.editSources.details', {
202
mode,
203
sourceKey: key,
204
205
sourceKeyCleaned: repr.toKey(1, { $extensionId: false, $extensionVersion: false, $modelId: false }),
206
extensionId: repr.props.$extensionId,
207
extensionVersion: repr.props.$extensionVersion,
208
modelId: repr.props.$modelId,
209
210
trigger,
211
languageId: this._doc.document.languageId.get(),
212
statsUuid: statsUuid,
213
modifiedCount: value,
214
deltaModifiedCount: m,
215
totalModifiedCount: data.totalModifiedCharactersInFinalState,
216
});
217
}
218
219
220
const isTrackedByGit = await data.isTrackedByGit;
221
this._telemetryService.publicLog2<{
222
mode: string;
223
languageId: string;
224
statsUuid: string;
225
nesModifiedCount: number;
226
inlineCompletionsCopilotModifiedCount: number;
227
inlineCompletionsNESModifiedCount: number;
228
otherAIModifiedCount: number;
229
unknownModifiedCount: number;
230
userModifiedCount: number;
231
ideModifiedCount: number;
232
totalModifiedCharacters: number;
233
externalModifiedCount: number;
234
isTrackedByGit: number;
235
}, {
236
owner: 'hediet';
237
comment: 'Reports distribution of AI vs user edited characters.';
238
239
mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm or 5minWindow' };
240
languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' };
241
statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' };
242
243
nesModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of nes modified characters'; isMeasurement: true };
244
inlineCompletionsCopilotModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of inline completions copilot modified characters'; isMeasurement: true };
245
inlineCompletionsNESModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of inline completions nes modified characters'; isMeasurement: true };
246
otherAIModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of other AI modified characters'; isMeasurement: true };
247
unknownModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of unknown modified characters'; isMeasurement: true };
248
userModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of user modified characters'; isMeasurement: true };
249
ideModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of IDE modified characters'; isMeasurement: true };
250
totalModifiedCharacters: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total modified characters'; isMeasurement: true };
251
externalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of external modified characters'; isMeasurement: true };
252
isTrackedByGit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates if the document is tracked by git.' };
253
}>('editTelemetry.editSources.stats', {
254
mode,
255
languageId: this._doc.document.languageId.get(),
256
statsUuid: statsUuid,
257
nesModifiedCount: data.nesModifiedCount,
258
inlineCompletionsCopilotModifiedCount: data.inlineCompletionsCopilotModifiedCount,
259
inlineCompletionsNESModifiedCount: data.inlineCompletionsNESModifiedCount,
260
otherAIModifiedCount: data.otherAIModifiedCount,
261
unknownModifiedCount: data.unknownModifiedCount,
262
userModifiedCount: data.userModifiedCount,
263
ideModifiedCount: data.ideModifiedCount,
264
totalModifiedCharacters: data.totalModifiedCharactersInFinalState,
265
externalModifiedCount: data.externalModifiedCount,
266
isTrackedByGit: isTrackedByGit ? 1 : 0,
267
});
268
}
269
270
getTelemetryData(ranges: readonly TrackedEdit[]) {
271
const getEditCategory = (source: EditSource) => {
272
if (source.category === 'ai' && source.kind === 'nes') { return 'nes'; }
273
274
if (source.category === 'ai' && source.kind === 'completion' && source.extensionId === 'github.copilot') { return 'inlineCompletionsCopilot'; }
275
if (source.category === 'ai' && source.kind === 'completion' && source.extensionId === 'github.copilot-chat' && source.providerId === 'completions') { return 'inlineCompletionsCopilot'; }
276
if (source.category === 'ai' && source.kind === 'completion' && source.extensionId === 'github.copilot-chat' && source.providerId === 'nes') { return 'inlineCompletionsNES'; }
277
if (source.category === 'ai' && source.kind === 'completion') { return 'inlineCompletionsOther'; }
278
279
if (source.category === 'ai') { return 'otherAI'; }
280
if (source.category === 'user') { return 'user'; }
281
if (source.category === 'ide') { return 'ide'; }
282
if (source.category === 'external') { return 'external'; }
283
if (source.category === 'unknown') { return 'unknown'; }
284
285
return 'unknown';
286
};
287
288
const sums = sumByCategory(ranges, r => r.range.length, r => getEditCategory(r.source));
289
const totalModifiedCharactersInFinalState = sumBy(ranges, r => r.range.length);
290
291
return {
292
nesModifiedCount: sums.nes ?? 0,
293
inlineCompletionsCopilotModifiedCount: sums.inlineCompletionsCopilot ?? 0,
294
inlineCompletionsNESModifiedCount: sums.inlineCompletionsNES ?? 0,
295
otherAIModifiedCount: sums.otherAI ?? 0,
296
userModifiedCount: sums.user ?? 0,
297
ideModifiedCount: sums.ide ?? 0,
298
unknownModifiedCount: sums.unknown ?? 0,
299
externalModifiedCount: sums.external ?? 0,
300
totalModifiedCharactersInFinalState,
301
languageId: this._doc.document.languageId.get(),
302
isTrackedByGit: this._repo.then(async (repo) => !!repo && !await repo.isIgnored(this._doc.document.uri)),
303
};
304
}
305
}
306
307
class ScmBridge {
308
constructor(
309
@ISCMService private readonly _scmService: ISCMService
310
) { }
311
312
public async getRepo(uri: URI): Promise<ScmRepoBridge | undefined> {
313
const repo = this._scmService.getRepository(uri);
314
if (!repo) {
315
return undefined;
316
}
317
return new ScmRepoBridge(repo);
318
}
319
}
320
321
export class ScmRepoBridge {
322
public readonly headBranchNameObs: IObservable<string | undefined> = derived(reader => this._repo.provider.historyProvider.read(reader)?.historyItemRef.read(reader)?.name);
323
public readonly headCommitHashObs: IObservable<string | undefined> = derived(reader => this._repo.provider.historyProvider.read(reader)?.historyItemRef.read(reader)?.revision);
324
325
constructor(
326
private readonly _repo: ISCMRepository,
327
) {
328
}
329
330
async isIgnored(uri: URI): Promise<boolean> {
331
return false;
332
}
333
}
334
335