Path: blob/main/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts
4784 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';7import { constObservable, IObservable, IObservableWithChange, ISettableObservable, ITransaction, observableValue, subtransaction } from '../../../../../base/common/observable.js';8import { URI } from '../../../../../base/common/uri.js';9import { StringEdit, StringReplacement } from '../../../../../editor/common/core/edits/stringEdit.js';10import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';11import { StringText } from '../../../../../editor/common/core/text/abstractText.js';12import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';13import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';14import { AnnotatedDocument, AnnotatedDocuments, IAnnotatedDocuments, UriVisibilityProvider } from '../../browser/helpers/annotatedDocuments.js';15import { IObservableDocument, ObservableWorkspace, StringEditWithReason } from '../../browser/helpers/observableWorkspace.js';16import { EditSourceTrackingImpl } from '../../browser/telemetry/editSourceTrackingImpl.js';17import { ScmAdapter } from '../../browser/telemetry/scmAdapter.js';18import { EditSources } from '../../../../../editor/common/textModelEditSource.js';19import { DiffService } from '../../browser/helpers/documentWithAnnotatedEdits.js';20import { computeStringDiff } from '../../../../../editor/common/services/editorWebWorker.js';21import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';22import { timeout } from '../../../../../base/common/async.js';23import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';24import { IAiEditTelemetryService } from '../../browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';25import { Random } from '../../../../../editor/test/common/core/random.js';26import { AiEditTelemetryServiceImpl } from '../../browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.js';27import { IRandomService, RandomService } from '../../browser/randomService.js';28import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';29import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';30import { UserAttentionService, UserAttentionServiceEnv } from '../../../../services/userAttention/browser/userAttentionBrowser.js';31import { IUserAttentionService } from '../../../../services/userAttention/common/userAttentionService.js';32import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';3334suite('Edit Telemetry', () => {35ensureNoDisposablesAreLeakedInTestSuite();3637test('1', async () => runWithFakedTimers({}, async () => {38const disposables = new DisposableStore();39const instantiationService = disposables.add(new TestInstantiationService(new ServiceCollection(40[IAiEditTelemetryService, new SyncDescriptor(AiEditTelemetryServiceImpl)],41[IUserAttentionService, new SyncDescriptor(UserAttentionService)]42), false, undefined, true));4344const sentTelemetry: unknown[] = [];45const userActive = observableValue('userActive', true);46instantiationService.stubInstance(UserAttentionServiceEnv, {47isUserActive: userActive,48isVsCodeFocused: constObservable(true),49dispose: () => { }50});51instantiationService.stub(ITelemetryService, {52publicLog2(eventName, data) {53sentTelemetry.push(`${formatTime(Date.now())} ${eventName}: ${JSON.stringify(data)}`);54},55});56instantiationService.stubInstance(DiffService, { computeDiff: async (original, modified) => computeStringDiff(original, modified, { maxComputationTimeMs: 500 }, 'advanced') });57instantiationService.stubInstance(ScmAdapter, { getRepo: (uri, reader) => undefined, });58instantiationService.stubInstance(UriVisibilityProvider, { isVisible: (uri, reader) => true, });59instantiationService.stub(IRandomService, new DeterministicRandomService());60instantiationService.stub(ILogService, new NullLogService());6162const w = new MutableObservableWorkspace();63const docs = disposables.add(new AnnotatedDocuments(w, instantiationService));64disposables.add(new EditSourceTrackingImpl(constObservable(true), docs, instantiationService));6566const d1 = disposables.add(w.createDocument({67uri: URI.parse('file:///a'), initialValue: `68function fib(n) {69if (n <= 1) return n;70return fib(n - 1) + fib(n - 2);71}72`73}, undefined));7475await timeout(10);7677const chatEdit = EditSources.chatApplyEdits({78languageId: 'plaintext',79modelId: undefined,80codeBlockSuggestionId: undefined,81extensionId: undefined,82mode: undefined,83requestId: undefined,84sessionId: undefined,85});8687d1.applyEdit(StringEditWithReason.replace(d1.findRange('≪≫function fib(n) {'), '// Computes the nth fibonacci number\n', chatEdit));8889await timeout(5000);9091d1.applyEdit(new StringEditWithReason([92StringReplacement.replace(d1.findRange('≪//≫ Computes'), '/*'),93StringReplacement.replace(d1.findRange('fibonacci number≪≫'), ' */'),94], EditSources.cursor({ kind: 'type' })));9596await timeout(5000);9798d1.applyEdit(StringEditWithReason.replace(d1.findRange('Computes the nth fibonacci number'), 'Berechnet die nte Fibonacci Zahl', chatEdit));99100await timeout(3 * 60 * 1000);101userActive.set(false, undefined);102await timeout(3 * 60 * 1000);103userActive.set(true, undefined);104await timeout(8 * 60 * 1000);105106assert.deepStrictEqual(sentTelemetry, ([107'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}',108'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}}',109'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}',110'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}}',111'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}',112'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}',113'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}',114'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}',115'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\"}',116'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}',117'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}',118'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}',119'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}',120'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\"}'121]));122123disposables.dispose();124}));125});126127function formatTime(timeMs: number): string {128const totalMs = Math.floor(timeMs);129const minutes = Math.floor(totalMs / 60000);130const seconds = Math.floor((totalMs % 60000) / 1000);131const ms = totalMs % 1000;132const str = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}:${ms.toString().padStart(3, '0')}`;133return str;134}135136class DeterministicRandomService extends RandomService {137private readonly _rand = Random.create(0);138139override generateUuid(): string {140return this._rand.nextUuid();141}142}143144export class FakeAnnotatedDocuments extends Disposable implements IAnnotatedDocuments {145public readonly documents: IObservable<readonly AnnotatedDocument[]>;146147constructor() {148super();149150this.documents = constObservable<readonly AnnotatedDocument[]>([]);151}152}153154/** Can contain "≪" and "≫" to add context, e.g. e≪l≫ only matches the first l in `hello`. */155type SearchString = string;156157function findOffsetRange(str: string, search: SearchString): OffsetRange {158const startContextIndex = search.indexOf('≪');159const endContextIndex = search.indexOf('≫');160161let searchStr: string;162let beforeContext = '';163let afterContext = '';164165if (startContextIndex !== -1 && endContextIndex !== -1 && endContextIndex > startContextIndex) {166beforeContext = search.substring(0, startContextIndex);167afterContext = search.substring(endContextIndex + 1);168searchStr = search.substring(startContextIndex + 1, endContextIndex);169} else {170searchStr = search;171}172173const startIndex = str.indexOf(beforeContext + searchStr + afterContext);174if (startIndex === -1) {175throw new Error(`Could not find context "${beforeContext}" + "${searchStr}" + "${afterContext}" in string "${str}"`);176}177178const matchStart = startIndex + beforeContext.length;179return new OffsetRange(matchStart, matchStart + searchStr.length);180}181182export class MutableObservableWorkspace extends ObservableWorkspace {183private readonly _openDocuments = observableValue<readonly IObservableDocument[], { added: readonly IObservableDocument[]; removed: readonly IObservableDocument[] }>(this, []);184public readonly documents = this._openDocuments;185186private readonly _documents = new Map</* uri */ string, MutableObservableDocument>();187188constructor() {189super();190}191192/**193* Dispose to remove.194*/195public createDocument(options: { uri: URI; workspaceRoot?: URI; initialValue?: string; initialVersionId?: number; languageId?: string }, tx: ITransaction | undefined = undefined): MutableObservableDocument {196assert(!this._documents.has(options.uri.toString()));197198const document = new MutableObservableDocument(199options.uri,200new StringText(options.initialValue ?? ''),201[],202options.languageId ?? 'plaintext',203() => {204this._documents.delete(options.uri.toString());205const docs = this._openDocuments.get();206const filteredDocs = docs.filter(d => d.uri.toString() !== document.uri.toString());207if (filteredDocs.length !== docs.length) {208this._openDocuments.set(filteredDocs, tx, { added: [], removed: [document] });209}210},211options.initialVersionId ?? 0,212options.workspaceRoot,213);214215this._documents.set(options.uri.toString(), document);216this._openDocuments.set([...this._openDocuments.get(), document], tx, { added: [document], removed: [] });217218return document;219}220221public override getDocument(id: URI): MutableObservableDocument | undefined {222return this._documents.get(id.toString());223}224225public clear(): void {226this._openDocuments.set([], undefined, { added: [], removed: this._openDocuments.get() });227for (const doc of this._documents.values()) {228doc.dispose();229}230this._documents.clear();231}232}233234export class MutableObservableDocument extends Disposable implements IObservableDocument {235private readonly _value: ISettableObservable<StringText, StringEditWithReason>;236public get value(): IObservableWithChange<StringText, StringEditWithReason> { return this._value; }237238private readonly _selection: ISettableObservable<readonly OffsetRange[]>;239public get selection(): IObservable<readonly OffsetRange[]> { return this._selection; }240241private readonly _visibleRanges: ISettableObservable<readonly OffsetRange[]>;242public get visibleRanges(): IObservable<readonly OffsetRange[]> { return this._visibleRanges; }243244private readonly _languageId: ISettableObservable<string>;245public get languageId(): IObservable<string> { return this._languageId; }246247private readonly _version: ISettableObservable<number>;248public get version(): IObservable<number> { return this._version; }249250constructor(251public readonly uri: URI,252value: StringText,253selection: readonly OffsetRange[],254languageId: string,255onDispose: () => void,256versionId: number,257public readonly workspaceRoot: URI | undefined,258) {259super();260261this._value = observableValue(this, value);262this._selection = observableValue(this, selection);263this._visibleRanges = observableValue(this, []);264this._languageId = observableValue(this, languageId);265this._version = observableValue(this, versionId);266267this._register(toDisposable(onDispose));268}269270setSelection(selection: readonly OffsetRange[], tx: ITransaction | undefined = undefined): void {271this._selection.set(selection, tx);272}273274setVisibleRange(visibleRanges: readonly OffsetRange[], tx: ITransaction | undefined = undefined): void {275this._visibleRanges.set(visibleRanges, tx);276}277278applyEdit(edit: StringEdit | StringEditWithReason, tx: ITransaction | undefined = undefined, newVersion: number | undefined = undefined): void {279const newValue = edit.applyOnText(this.value.get());280const e = edit instanceof StringEditWithReason ? edit : new StringEditWithReason(edit.replacements, EditSources.unknown({}));281subtransaction(tx, tx => {282this._value.set(newValue, tx, e);283this._version.set(newVersion ?? this._version.get() + 1, tx);284});285}286287updateSelection(selection: readonly OffsetRange[], tx: ITransaction | undefined = undefined): void {288this._selection.set(selection, tx);289}290291setValue(value: StringText, tx: ITransaction | undefined = undefined, newVersion: number | undefined = undefined): void {292const reason = EditSources.unknown({});293const e = new StringEditWithReason([StringReplacement.replace(new OffsetRange(0, this.value.get().value.length), value.value)], reason);294subtransaction(tx, tx => {295this._value.set(value, tx, e);296this._version.set(newVersion ?? this._version.get() + 1, tx);297});298}299300findRange(search: SearchString): OffsetRange {301return findOffsetRange(this.value.get().value, search);302}303}304305306