Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts
4784 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 assert from 'assert';
7
import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
8
import { constObservable, IObservable, IObservableWithChange, ISettableObservable, ITransaction, observableValue, subtransaction } from '../../../../../base/common/observable.js';
9
import { URI } from '../../../../../base/common/uri.js';
10
import { StringEdit, StringReplacement } from '../../../../../editor/common/core/edits/stringEdit.js';
11
import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';
12
import { StringText } from '../../../../../editor/common/core/text/abstractText.js';
13
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
14
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
15
import { AnnotatedDocument, AnnotatedDocuments, IAnnotatedDocuments, UriVisibilityProvider } from '../../browser/helpers/annotatedDocuments.js';
16
import { IObservableDocument, ObservableWorkspace, StringEditWithReason } from '../../browser/helpers/observableWorkspace.js';
17
import { EditSourceTrackingImpl } from '../../browser/telemetry/editSourceTrackingImpl.js';
18
import { ScmAdapter } from '../../browser/telemetry/scmAdapter.js';
19
import { EditSources } from '../../../../../editor/common/textModelEditSource.js';
20
import { DiffService } from '../../browser/helpers/documentWithAnnotatedEdits.js';
21
import { computeStringDiff } from '../../../../../editor/common/services/editorWebWorker.js';
22
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
23
import { timeout } from '../../../../../base/common/async.js';
24
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
25
import { IAiEditTelemetryService } from '../../browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';
26
import { Random } from '../../../../../editor/test/common/core/random.js';
27
import { AiEditTelemetryServiceImpl } from '../../browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.js';
28
import { IRandomService, RandomService } from '../../browser/randomService.js';
29
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
30
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
31
import { UserAttentionService, UserAttentionServiceEnv } from '../../../../services/userAttention/browser/userAttentionBrowser.js';
32
import { IUserAttentionService } from '../../../../services/userAttention/common/userAttentionService.js';
33
import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';
34
35
suite('Edit Telemetry', () => {
36
ensureNoDisposablesAreLeakedInTestSuite();
37
38
test('1', async () => runWithFakedTimers({}, async () => {
39
const disposables = new DisposableStore();
40
const instantiationService = disposables.add(new TestInstantiationService(new ServiceCollection(
41
[IAiEditTelemetryService, new SyncDescriptor(AiEditTelemetryServiceImpl)],
42
[IUserAttentionService, new SyncDescriptor(UserAttentionService)]
43
), false, undefined, true));
44
45
const sentTelemetry: unknown[] = [];
46
const userActive = observableValue('userActive', true);
47
instantiationService.stubInstance(UserAttentionServiceEnv, {
48
isUserActive: userActive,
49
isVsCodeFocused: constObservable(true),
50
dispose: () => { }
51
});
52
instantiationService.stub(ITelemetryService, {
53
publicLog2(eventName, data) {
54
sentTelemetry.push(`${formatTime(Date.now())} ${eventName}: ${JSON.stringify(data)}`);
55
},
56
});
57
instantiationService.stubInstance(DiffService, { computeDiff: async (original, modified) => computeStringDiff(original, modified, { maxComputationTimeMs: 500 }, 'advanced') });
58
instantiationService.stubInstance(ScmAdapter, { getRepo: (uri, reader) => undefined, });
59
instantiationService.stubInstance(UriVisibilityProvider, { isVisible: (uri, reader) => true, });
60
instantiationService.stub(IRandomService, new DeterministicRandomService());
61
instantiationService.stub(ILogService, new NullLogService());
62
63
const w = new MutableObservableWorkspace();
64
const docs = disposables.add(new AnnotatedDocuments(w, instantiationService));
65
disposables.add(new EditSourceTrackingImpl(constObservable(true), docs, instantiationService));
66
67
const d1 = disposables.add(w.createDocument({
68
uri: URI.parse('file:///a'), initialValue: `
69
function fib(n) {
70
if (n <= 1) return n;
71
return fib(n - 1) + fib(n - 2);
72
}
73
`
74
}, undefined));
75
76
await timeout(10);
77
78
const chatEdit = EditSources.chatApplyEdits({
79
languageId: 'plaintext',
80
modelId: undefined,
81
codeBlockSuggestionId: undefined,
82
extensionId: undefined,
83
mode: undefined,
84
requestId: undefined,
85
sessionId: undefined,
86
});
87
88
d1.applyEdit(StringEditWithReason.replace(d1.findRange('≪≫function fib(n) {'), '// Computes the nth fibonacci number\n', chatEdit));
89
90
await timeout(5000);
91
92
d1.applyEdit(new StringEditWithReason([
93
StringReplacement.replace(d1.findRange('≪//≫ Computes'), '/*'),
94
StringReplacement.replace(d1.findRange('fibonacci number≪≫'), ' */'),
95
], EditSources.cursor({ kind: 'type' })));
96
97
await timeout(5000);
98
99
d1.applyEdit(StringEditWithReason.replace(d1.findRange('Computes the nth fibonacci number'), 'Berechnet die nte Fibonacci Zahl', chatEdit));
100
101
await timeout(3 * 60 * 1000);
102
userActive.set(false, undefined);
103
await timeout(3 * 60 * 1000);
104
userActive.set(true, undefined);
105
await timeout(8 * 60 * 1000);
106
107
assert.deepStrictEqual(sentTelemetry, ([
108
'00:01:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e\",\"didBranchChange\":0,\"timeDelayMs\":0,\"originalCharCount\":37,\"originalLineCount\":1,\"originalDeletedLineCount\":0,\"arc\":37,\"currentLineCount\":1,\"currentDeletedLineCount\":0}',
109
'00:01:010 editTelemetry.codeSuggested: {\"eventId\":\"evt-055ed5f5-c723-4ede-ba79-cccd7685c7ad\",\"suggestionId\":\"sgt-f645627a-cacf-477a-9164-ecd6125616a5\",\"presentation\":\"highlightedEdit\",\"feature\":\"sideBarChat\",\"languageId\":\"plaintext\",\"editCharsInserted\":37,\"editCharsDeleted\":0,\"editLinesInserted\":1,\"editLinesDeleted\":0,\"modelId\":{\"isTrustedTelemetryValue\":true}}',
110
'00:11:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"1eb8a394-2489-41c2-851b-6a79432fc6bc\",\"didBranchChange\":0,\"timeDelayMs\":0,\"originalCharCount\":19,\"originalLineCount\":1,\"originalDeletedLineCount\":1,\"arc\":19,\"currentLineCount\":1,\"currentDeletedLineCount\":1}',
111
'00:11:010 editTelemetry.codeSuggested: {\"eventId\":\"evt-5c9c6fe7-b219-4ff8-aaa7-ab2b355b21c0\",\"suggestionId\":\"sgt-74379122-0452-4e26-9c38-9d62f1e7ae73\",\"presentation\":\"highlightedEdit\",\"feature\":\"sideBarChat\",\"languageId\":\"plaintext\",\"editCharsInserted\":19,\"editCharsDeleted\":20,\"editLinesInserted\":1,\"editLinesDeleted\":1,\"modelId\":{\"isTrustedTelemetryValue\":true}}',
112
'01:01:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e\",\"didBranchChange\":0,\"timeDelayMs\":60000,\"originalCharCount\":37,\"originalLineCount\":1,\"originalDeletedLineCount\":0,\"arc\":16,\"currentLineCount\":1,\"currentDeletedLineCount\":0}',
113
'01:11:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"1eb8a394-2489-41c2-851b-6a79432fc6bc\",\"didBranchChange\":0,\"timeDelayMs\":60000,\"originalCharCount\":19,\"originalLineCount\":1,\"originalDeletedLineCount\":1,\"arc\":19,\"currentLineCount\":1,\"currentDeletedLineCount\":1}',
114
'05:00:000 editTelemetry.editSources.details: {\"mode\":\"5minWindow\",\"sourceKey\":\"source:Chat.applyEdits\",\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"modifiedCount\":35,\"deltaModifiedCount\":56,\"totalModifiedCount\":39}',
115
'05:00:000 editTelemetry.editSources.details: {\"mode\":\"5minWindow\",\"sourceKey\":\"source:cursor-kind:type\",\"sourceKeyCleaned\":\"source:cursor-kind:type\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"modifiedCount\":4,\"deltaModifiedCount\":4,\"totalModifiedCount\":39}',
116
'05:00:000 editTelemetry.editSources.stats: {\"mode\":\"5minWindow\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"nesModifiedCount\":0,\"inlineCompletionsCopilotModifiedCount\":0,\"inlineCompletionsNESModifiedCount\":0,\"otherAIModifiedCount\":35,\"unknownModifiedCount\":0,\"userModifiedCount\":4,\"ideModifiedCount\":0,\"totalModifiedCharacters\":39,\"externalModifiedCount\":0,\"isTrackedByGit\":0,\"focusTime\":250010,\"actualTime\":300000,\"trigger\":\"time\"}',
117
'05:01:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e\",\"didBranchChange\":0,\"timeDelayMs\":300000,\"originalCharCount\":37,\"originalLineCount\":1,\"originalDeletedLineCount\":0,\"arc\":16,\"currentLineCount\":1,\"currentDeletedLineCount\":0}',
118
'05:11:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"1eb8a394-2489-41c2-851b-6a79432fc6bc\",\"didBranchChange\":0,\"timeDelayMs\":300000,\"originalCharCount\":19,\"originalLineCount\":1,\"originalDeletedLineCount\":1,\"arc\":19,\"currentLineCount\":1,\"currentDeletedLineCount\":1}',
119
'12:00:000 editTelemetry.editSources.details: {\"mode\":\"10minFocusWindow\",\"sourceKey\":\"source:Chat.applyEdits\",\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"a794406a-7779-4e9f-a856-1caca85123c7\",\"modifiedCount\":35,\"deltaModifiedCount\":56,\"totalModifiedCount\":39}',
120
'12:00:000 editTelemetry.editSources.details: {\"mode\":\"10minFocusWindow\",\"sourceKey\":\"source:cursor-kind:type\",\"sourceKeyCleaned\":\"source:cursor-kind:type\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"a794406a-7779-4e9f-a856-1caca85123c7\",\"modifiedCount\":4,\"deltaModifiedCount\":4,\"totalModifiedCount\":39}',
121
'12:00:000 editTelemetry.editSources.stats: {\"mode\":\"10minFocusWindow\",\"languageId\":\"plaintext\",\"statsUuid\":\"a794406a-7779-4e9f-a856-1caca85123c7\",\"nesModifiedCount\":0,\"inlineCompletionsCopilotModifiedCount\":0,\"inlineCompletionsNESModifiedCount\":0,\"otherAIModifiedCount\":35,\"unknownModifiedCount\":0,\"userModifiedCount\":4,\"ideModifiedCount\":0,\"totalModifiedCharacters\":39,\"externalModifiedCount\":0,\"isTrackedByGit\":0,\"focusTime\":600000,\"actualTime\":720000,\"trigger\":\"time\"}'
122
]));
123
124
disposables.dispose();
125
}));
126
});
127
128
function formatTime(timeMs: number): string {
129
const totalMs = Math.floor(timeMs);
130
const minutes = Math.floor(totalMs / 60000);
131
const seconds = Math.floor((totalMs % 60000) / 1000);
132
const ms = totalMs % 1000;
133
const str = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}:${ms.toString().padStart(3, '0')}`;
134
return str;
135
}
136
137
class DeterministicRandomService extends RandomService {
138
private readonly _rand = Random.create(0);
139
140
override generateUuid(): string {
141
return this._rand.nextUuid();
142
}
143
}
144
145
export class FakeAnnotatedDocuments extends Disposable implements IAnnotatedDocuments {
146
public readonly documents: IObservable<readonly AnnotatedDocument[]>;
147
148
constructor() {
149
super();
150
151
this.documents = constObservable<readonly AnnotatedDocument[]>([]);
152
}
153
}
154
155
/** Can contain "≪" and "≫" to add context, e.g. e≪l≫ only matches the first l in `hello`. */
156
type SearchString = string;
157
158
function findOffsetRange(str: string, search: SearchString): OffsetRange {
159
const startContextIndex = search.indexOf('≪');
160
const endContextIndex = search.indexOf('≫');
161
162
let searchStr: string;
163
let beforeContext = '';
164
let afterContext = '';
165
166
if (startContextIndex !== -1 && endContextIndex !== -1 && endContextIndex > startContextIndex) {
167
beforeContext = search.substring(0, startContextIndex);
168
afterContext = search.substring(endContextIndex + 1);
169
searchStr = search.substring(startContextIndex + 1, endContextIndex);
170
} else {
171
searchStr = search;
172
}
173
174
const startIndex = str.indexOf(beforeContext + searchStr + afterContext);
175
if (startIndex === -1) {
176
throw new Error(`Could not find context "${beforeContext}" + "${searchStr}" + "${afterContext}" in string "${str}"`);
177
}
178
179
const matchStart = startIndex + beforeContext.length;
180
return new OffsetRange(matchStart, matchStart + searchStr.length);
181
}
182
183
export class MutableObservableWorkspace extends ObservableWorkspace {
184
private readonly _openDocuments = observableValue<readonly IObservableDocument[], { added: readonly IObservableDocument[]; removed: readonly IObservableDocument[] }>(this, []);
185
public readonly documents = this._openDocuments;
186
187
private readonly _documents = new Map</* uri */ string, MutableObservableDocument>();
188
189
constructor() {
190
super();
191
}
192
193
/**
194
* Dispose to remove.
195
*/
196
public createDocument(options: { uri: URI; workspaceRoot?: URI; initialValue?: string; initialVersionId?: number; languageId?: string }, tx: ITransaction | undefined = undefined): MutableObservableDocument {
197
assert(!this._documents.has(options.uri.toString()));
198
199
const document = new MutableObservableDocument(
200
options.uri,
201
new StringText(options.initialValue ?? ''),
202
[],
203
options.languageId ?? 'plaintext',
204
() => {
205
this._documents.delete(options.uri.toString());
206
const docs = this._openDocuments.get();
207
const filteredDocs = docs.filter(d => d.uri.toString() !== document.uri.toString());
208
if (filteredDocs.length !== docs.length) {
209
this._openDocuments.set(filteredDocs, tx, { added: [], removed: [document] });
210
}
211
},
212
options.initialVersionId ?? 0,
213
options.workspaceRoot,
214
);
215
216
this._documents.set(options.uri.toString(), document);
217
this._openDocuments.set([...this._openDocuments.get(), document], tx, { added: [document], removed: [] });
218
219
return document;
220
}
221
222
public override getDocument(id: URI): MutableObservableDocument | undefined {
223
return this._documents.get(id.toString());
224
}
225
226
public clear(): void {
227
this._openDocuments.set([], undefined, { added: [], removed: this._openDocuments.get() });
228
for (const doc of this._documents.values()) {
229
doc.dispose();
230
}
231
this._documents.clear();
232
}
233
}
234
235
export class MutableObservableDocument extends Disposable implements IObservableDocument {
236
private readonly _value: ISettableObservable<StringText, StringEditWithReason>;
237
public get value(): IObservableWithChange<StringText, StringEditWithReason> { return this._value; }
238
239
private readonly _selection: ISettableObservable<readonly OffsetRange[]>;
240
public get selection(): IObservable<readonly OffsetRange[]> { return this._selection; }
241
242
private readonly _visibleRanges: ISettableObservable<readonly OffsetRange[]>;
243
public get visibleRanges(): IObservable<readonly OffsetRange[]> { return this._visibleRanges; }
244
245
private readonly _languageId: ISettableObservable<string>;
246
public get languageId(): IObservable<string> { return this._languageId; }
247
248
private readonly _version: ISettableObservable<number>;
249
public get version(): IObservable<number> { return this._version; }
250
251
constructor(
252
public readonly uri: URI,
253
value: StringText,
254
selection: readonly OffsetRange[],
255
languageId: string,
256
onDispose: () => void,
257
versionId: number,
258
public readonly workspaceRoot: URI | undefined,
259
) {
260
super();
261
262
this._value = observableValue(this, value);
263
this._selection = observableValue(this, selection);
264
this._visibleRanges = observableValue(this, []);
265
this._languageId = observableValue(this, languageId);
266
this._version = observableValue(this, versionId);
267
268
this._register(toDisposable(onDispose));
269
}
270
271
setSelection(selection: readonly OffsetRange[], tx: ITransaction | undefined = undefined): void {
272
this._selection.set(selection, tx);
273
}
274
275
setVisibleRange(visibleRanges: readonly OffsetRange[], tx: ITransaction | undefined = undefined): void {
276
this._visibleRanges.set(visibleRanges, tx);
277
}
278
279
applyEdit(edit: StringEdit | StringEditWithReason, tx: ITransaction | undefined = undefined, newVersion: number | undefined = undefined): void {
280
const newValue = edit.applyOnText(this.value.get());
281
const e = edit instanceof StringEditWithReason ? edit : new StringEditWithReason(edit.replacements, EditSources.unknown({}));
282
subtransaction(tx, tx => {
283
this._value.set(newValue, tx, e);
284
this._version.set(newVersion ?? this._version.get() + 1, tx);
285
});
286
}
287
288
updateSelection(selection: readonly OffsetRange[], tx: ITransaction | undefined = undefined): void {
289
this._selection.set(selection, tx);
290
}
291
292
setValue(value: StringText, tx: ITransaction | undefined = undefined, newVersion: number | undefined = undefined): void {
293
const reason = EditSources.unknown({});
294
const e = new StringEditWithReason([StringReplacement.replace(new OffsetRange(0, this.value.get().value.length), value.value)], reason);
295
subtransaction(tx, tx => {
296
this._value.set(value, tx, e);
297
this._version.set(newVersion ?? this._version.get() + 1, tx);
298
});
299
}
300
301
findRange(search: SearchString): OffsetRange {
302
return findOffsetRange(this.value.get().value, search);
303
}
304
}
305
306