Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts
3296 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 { reverseOrder, compareBy, numberComparator, sumBy } from '../../../../../base/common/arrays.js';6import { IntervalTimer, TimeoutTimer } from '../../../../../base/common/async.js';7import { toDisposable, Disposable } from '../../../../../base/common/lifecycle.js';8import { mapObservableArrayCached, derived, IObservable, observableSignal, runOnChange } from '../../../../../base/common/observable.js';9import { URI } from '../../../../../base/common/uri.js';10import { generateUuid } from '../../../../../base/common/uuid.js';11import { TextModelEditSource } from '../../../../../editor/common/textModelEditSource.js';12import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';13import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';14import { ISCMRepository, ISCMService } from '../../../scm/common/scm.js';15import { AnnotatedDocuments, AnnotatedDocument } from '../helpers/annotatedDocuments.js';16import { AiEditTelemetryAdapter, ChatArcTelemetrySender, InlineEditArcTelemetrySender } from './arcTelemetrySender.js';17import { createDocWithJustReason, EditSource } from '../helpers/documentWithAnnotatedEdits.js';18import { DocumentEditSourceTracker, TrackedEdit } from './editTracker.js';19import { sumByCategory } from '../helpers/utils.js';2021export class EditSourceTrackingImpl extends Disposable {22public readonly docsState;2324constructor(25private readonly _statsEnabled: IObservable<boolean>,26private readonly _annotatedDocuments: AnnotatedDocuments,27@IInstantiationService private readonly _instantiationService: IInstantiationService,28) {29super();3031const scmBridge = this._instantiationService.createInstance(ScmBridge);32const states = mapObservableArrayCached(this, this._annotatedDocuments.documents, (doc, store) => {33return [doc.document, store.add(this._instantiationService.createInstance(TrackedDocumentInfo, doc, scmBridge, this._statsEnabled))] as const;34});35this.docsState = states.map((entries) => new Map(entries));3637this.docsState.recomputeInitiallyAndOnChange(this._store);38}39}4041class TrackedDocumentInfo extends Disposable {42public readonly longtermTracker: IObservable<DocumentEditSourceTracker<undefined> | undefined>;43public readonly windowedTracker: IObservable<DocumentEditSourceTracker<undefined> | undefined>;4445private readonly _repo: Promise<ScmRepoBridge | undefined>;4647constructor(48private readonly _doc: AnnotatedDocument,49private readonly _scm: ScmBridge,50private readonly _statsEnabled: IObservable<boolean>,51@IInstantiationService private readonly _instantiationService: IInstantiationService,52@ITelemetryService private readonly _telemetryService: ITelemetryService53) {54super();5556const docWithJustReason = createDocWithJustReason(_doc.documentWithAnnotations, this._store);5758const longtermResetSignal = observableSignal('resetSignal');5960let longtermReason: '10hours' | 'hashChange' | 'branchChange' | 'closed' = 'closed';61this.longtermTracker = derived((reader) => {62if (!this._statsEnabled.read(reader)) { return undefined; }63longtermResetSignal.read(reader);6465const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined));66reader.store.add(toDisposable(() => {67// send long term document telemetry68if (!t.isEmpty()) {69this.sendTelemetry('longterm', longtermReason, t);70}71t.dispose();72}));73return t;74}).recomputeInitiallyAndOnChange(this._store);7576this._store.add(new IntervalTimer()).cancelAndSet(() => {77// Reset after 10 hours78longtermReason = '10hours';79longtermResetSignal.trigger(undefined);80longtermReason = 'closed';81}, 10 * 60 * 60 * 1000);8283(async () => {84const repo = await this._scm.getRepo(_doc.document.uri);85if (this._store.isDisposed) {86return;87}88// Reset on branch change or commit89if (repo) {90this._store.add(runOnChange(repo.headCommitHashObs, () => {91longtermReason = 'hashChange';92longtermResetSignal.trigger(undefined);93longtermReason = 'closed';94}));95this._store.add(runOnChange(repo.headBranchNameObs, () => {96longtermReason = 'branchChange';97longtermResetSignal.trigger(undefined);98longtermReason = 'closed';99}));100}101102this._store.add(this._instantiationService.createInstance(InlineEditArcTelemetrySender, _doc.documentWithAnnotations, repo));103this._store.add(this._instantiationService.createInstance(ChatArcTelemetrySender, _doc.documentWithAnnotations, repo));104this._store.add(this._instantiationService.createInstance(AiEditTelemetryAdapter, _doc.documentWithAnnotations));105})();106107const resetSignal = observableSignal('resetSignal');108109this.windowedTracker = derived((reader) => {110if (!this._statsEnabled.read(reader)) { return undefined; }111112if (!this._doc.isVisible.read(reader)) {113return undefined;114}115resetSignal.read(reader);116117reader.store.add(new TimeoutTimer(() => {118// Reset after 5 minutes119resetSignal.trigger(undefined);120}, 5 * 60 * 1000));121122const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined));123reader.store.add(toDisposable(async () => {124// send long term document telemetry125this.sendTelemetry('5minWindow', 'time', t);126t.dispose();127}));128129return t;130}).recomputeInitiallyAndOnChange(this._store);131132this._repo = this._scm.getRepo(_doc.document.uri);133}134135async sendTelemetry(mode: 'longterm' | '5minWindow', trigger: string, t: DocumentEditSourceTracker) {136const ranges = t.getTrackedRanges();137if (ranges.length === 0) {138return;139}140141const data = this.getTelemetryData(ranges);142143144const statsUuid = generateUuid();145146const sourceKeyToRepresentative = new Map<string, TextModelEditSource>();147for (const r of ranges) {148sourceKeyToRepresentative.set(r.sourceKey, r.sourceRepresentative);149}150151const sums = sumByCategory(ranges, r => r.range.length, r => r.sourceKey);152const entries = Object.entries(sums).filter(([key, value]) => value !== undefined);153entries.sort(reverseOrder(compareBy(([key, value]) => value!, numberComparator)));154entries.length = mode === 'longterm' ? 30 : 10;155156for (const [key, value] of Object.entries(sums)) {157if (value === undefined) {158continue;159}160161const repr = sourceKeyToRepresentative.get(key)!;162const m = t.getChangedCharactersCount(key);163164this._telemetryService.publicLog2<{165mode: string;166sourceKey: string;167168sourceKeyCleaned: string;169extensionId: string | undefined;170extensionVersion: string | undefined;171modelId: string | undefined;172173trigger: string;174languageId: string;175statsUuid: string;176modifiedCount: number;177deltaModifiedCount: number;178totalModifiedCount: number;179}, {180owner: 'hediet';181comment: 'Reports distribution of various edit sources per session.';182183mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Describes the session mode. Is either longterm or 5minWindow.' };184sourceKey: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A description of the source of the edit.' };185186sourceKeyCleaned: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the edit with some properties (such as extensionId, extensionVersion and modelId) removed.' };187extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id.' };188extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the extension.' };189modelId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The LLM id.' };190191languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' };192statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier of the session for which stats are reported. The sourceKey is unique in this session.' };193194trigger: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates why the session ended.' };195196modifiedCount: { 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 };197deltaModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The number of characters inserted by the given edit source during the session.'; isMeasurement: true };198totalModifiedCount: { 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 };199200}>('editTelemetry.editSources.details', {201mode,202sourceKey: key,203204sourceKeyCleaned: repr.toKey(1, { $extensionId: false, $extensionVersion: false, $modelId: false }),205extensionId: repr.props.$extensionId,206extensionVersion: repr.props.$extensionVersion,207modelId: repr.props.$modelId,208209trigger,210languageId: this._doc.document.languageId.get(),211statsUuid: statsUuid,212modifiedCount: value,213deltaModifiedCount: m,214totalModifiedCount: data.totalModifiedCharactersInFinalState,215});216}217218219const isTrackedByGit = await data.isTrackedByGit;220this._telemetryService.publicLog2<{221mode: string;222languageId: string;223statsUuid: string;224nesModifiedCount: number;225inlineCompletionsCopilotModifiedCount: number;226inlineCompletionsNESModifiedCount: number;227otherAIModifiedCount: number;228unknownModifiedCount: number;229userModifiedCount: number;230ideModifiedCount: number;231totalModifiedCharacters: number;232externalModifiedCount: number;233isTrackedByGit: number;234}, {235owner: 'hediet';236comment: 'Reports distribution of AI vs user edited characters.';237238mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm or 5minWindow' };239languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' };240statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' };241242nesModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of nes modified characters'; isMeasurement: true };243inlineCompletionsCopilotModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of inline completions copilot modified characters'; isMeasurement: true };244inlineCompletionsNESModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of inline completions nes modified characters'; isMeasurement: true };245otherAIModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of other AI modified characters'; isMeasurement: true };246unknownModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of unknown modified characters'; isMeasurement: true };247userModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of user modified characters'; isMeasurement: true };248ideModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of IDE modified characters'; isMeasurement: true };249totalModifiedCharacters: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total modified characters'; isMeasurement: true };250externalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of external modified characters'; isMeasurement: true };251isTrackedByGit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates if the document is tracked by git.' };252}>('editTelemetry.editSources.stats', {253mode,254languageId: this._doc.document.languageId.get(),255statsUuid: statsUuid,256nesModifiedCount: data.nesModifiedCount,257inlineCompletionsCopilotModifiedCount: data.inlineCompletionsCopilotModifiedCount,258inlineCompletionsNESModifiedCount: data.inlineCompletionsNESModifiedCount,259otherAIModifiedCount: data.otherAIModifiedCount,260unknownModifiedCount: data.unknownModifiedCount,261userModifiedCount: data.userModifiedCount,262ideModifiedCount: data.ideModifiedCount,263totalModifiedCharacters: data.totalModifiedCharactersInFinalState,264externalModifiedCount: data.externalModifiedCount,265isTrackedByGit: isTrackedByGit ? 1 : 0,266});267}268269getTelemetryData(ranges: readonly TrackedEdit[]) {270const getEditCategory = (source: EditSource) => {271if (source.category === 'ai' && source.kind === 'nes') { return 'nes'; }272273if (source.category === 'ai' && source.kind === 'completion' && source.extensionId === 'github.copilot') { return 'inlineCompletionsCopilot'; }274if (source.category === 'ai' && source.kind === 'completion' && source.extensionId === 'github.copilot-chat' && source.providerId === 'completions') { return 'inlineCompletionsCopilot'; }275if (source.category === 'ai' && source.kind === 'completion' && source.extensionId === 'github.copilot-chat' && source.providerId === 'nes') { return 'inlineCompletionsNES'; }276if (source.category === 'ai' && source.kind === 'completion') { return 'inlineCompletionsOther'; }277278if (source.category === 'ai') { return 'otherAI'; }279if (source.category === 'user') { return 'user'; }280if (source.category === 'ide') { return 'ide'; }281if (source.category === 'external') { return 'external'; }282if (source.category === 'unknown') { return 'unknown'; }283284return 'unknown';285};286287const sums = sumByCategory(ranges, r => r.range.length, r => getEditCategory(r.source));288const totalModifiedCharactersInFinalState = sumBy(ranges, r => r.range.length);289290return {291nesModifiedCount: sums.nes ?? 0,292inlineCompletionsCopilotModifiedCount: sums.inlineCompletionsCopilot ?? 0,293inlineCompletionsNESModifiedCount: sums.inlineCompletionsNES ?? 0,294otherAIModifiedCount: sums.otherAI ?? 0,295userModifiedCount: sums.user ?? 0,296ideModifiedCount: sums.ide ?? 0,297unknownModifiedCount: sums.unknown ?? 0,298externalModifiedCount: sums.external ?? 0,299totalModifiedCharactersInFinalState,300languageId: this._doc.document.languageId.get(),301isTrackedByGit: this._repo.then(async (repo) => !!repo && !await repo.isIgnored(this._doc.document.uri)),302};303}304}305306class ScmBridge {307constructor(308@ISCMService private readonly _scmService: ISCMService309) { }310311public async getRepo(uri: URI): Promise<ScmRepoBridge | undefined> {312const repo = this._scmService.getRepository(uri);313if (!repo) {314return undefined;315}316return new ScmRepoBridge(repo);317}318}319320export class ScmRepoBridge {321public readonly headBranchNameObs: IObservable<string | undefined> = derived(reader => this._repo.provider.historyProvider.read(reader)?.historyItemRef.read(reader)?.name);322public readonly headCommitHashObs: IObservable<string | undefined> = derived(reader => this._repo.provider.historyProvider.read(reader)?.historyItemRef.read(reader)?.revision);323324constructor(325private readonly _repo: ISCMRepository,326) {327}328329async isIgnored(uri: URI): Promise<boolean> {330return false;331}332}333334335