Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.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 { TimeoutTimer } from '../../../../../base/common/async.js';6import { onUnexpectedError } from '../../../../../base/common/errors.js';7import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';8import { IObservableWithChange, runOnChange } from '../../../../../base/common/observable.js';9import { generateUuid } from '../../../../../base/common/uuid.js';10import { AnnotatedStringEdit, BaseStringEdit } from '../../../../../editor/common/core/edits/stringEdit.js';11import { StringText } from '../../../../../editor/common/core/text/abstractText.js';12import { EditDeltaInfo, EditSuggestionId, ITextModelEditSourceMetadata } from '../../../../../editor/common/textModelEditSource.js';13import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';14import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';15import { EditSourceData, IDocumentWithAnnotatedEdits, createDocWithJustReason } from '../helpers/documentWithAnnotatedEdits.js';16import { IAiEditTelemetryService } from './aiEditTelemetry/aiEditTelemetryService.js';17import { ArcTracker } from '../../common/arcTracker.js';18import type { ScmRepoBridge } from './editSourceTrackingImpl.js';19import { forwardToChannelIf, isCopilotLikeExtension } from './forwardingTelemetryService.js';2021export class InlineEditArcTelemetrySender extends Disposable {22constructor(23docWithAnnotatedEdits: IDocumentWithAnnotatedEdits<EditSourceData>,24scmRepoBridge: ScmRepoBridge | undefined,25@IInstantiationService private readonly _instantiationService: IInstantiationService26) {27super();2829this._register(runOnChange(docWithAnnotatedEdits.value, (_val, _prev, changes) => {30const edit = AnnotatedStringEdit.compose(changes.map(c => c.edit));3132if (!edit.replacements.some(r => r.data.editSource.metadata.source === 'inlineCompletionAccept')) {33return;34}35if (!edit.replacements.every(r => r.data.editSource.metadata.source === 'inlineCompletionAccept')) {36onUnexpectedError(new Error('ArcTelemetrySender: Not all edits are inline completion accept edits!'));37return;38}39if (edit.replacements[0].data.editSource.metadata.source !== 'inlineCompletionAccept') {40return;41}42const data = edit.replacements[0].data.editSource.metadata;4344const docWithJustReason = createDocWithJustReason(docWithAnnotatedEdits, this._store);45const reporter = this._instantiationService.createInstance(ArcTelemetryReporter, [0, 30, 120, 300, 600, 900].map(s => s * 1000), _prev, docWithJustReason, scmRepoBridge, edit, res => {46res.telemetryService.publicLog2<{47extensionId: string;48extensionVersion: string;49opportunityId: string;50languageId: string;51didBranchChange: number;52timeDelayMs: number;5354originalCharCount: number;55originalLineCount: number;56originalDeletedLineCount: number;57arc: number;58currentLineCount: number;59currentDeletedLineCount: number;60}, {61owner: 'hediet';62comment: 'Reports the accepted and retained character count for an inline completion/edit.';6364extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id (copilot or copilot-chat); which provided this inline completion.' };65extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the extension.' };66opportunityId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for an opportunity to show an inline completion or NES.' };67languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' };6869didBranchChange: { 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.' };70timeDelayMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The time delay between the user accepting the edit and measuring the survival rate.' };7172originalCharCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original character count before any edits.' };73originalLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original line count before any edits.' };74originalDeletedLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original deleted line count before any edits.' };75arc: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The accepted and restrained character count.' };76currentLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The current line count after edits.' };77currentDeletedLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The current deleted line count after edits.' };78}>('editTelemetry.reportInlineEditArc', {79extensionId: data.$extensionId ?? '',80extensionVersion: data.$extensionVersion ?? '',81opportunityId: data.$$requestUuid ?? 'unknown',82languageId: data.$$languageId,83didBranchChange: res.didBranchChange ? 1 : 0,84timeDelayMs: res.timeDelayMs,8586originalCharCount: res.originalCharCount,87originalLineCount: res.originalLineCount,88originalDeletedLineCount: res.originalDeletedLineCount,89arc: res.arc,90currentLineCount: res.currentLineCount,91currentDeletedLineCount: res.currentDeletedLineCount,9293...forwardToChannelIf(isCopilotLikeExtension(data.$extensionId)),94});95});9697this._register(toDisposable(() => {98reporter.cancel();99}));100}));101}102}103104export class AiEditTelemetryAdapter extends Disposable {105constructor(106docWithAnnotatedEdits: IDocumentWithAnnotatedEdits<EditSourceData>,107@IAiEditTelemetryService private readonly _aiEditTelemetryService: IAiEditTelemetryService,108) {109super();110111this._register(runOnChange(docWithAnnotatedEdits.value, (_val, _prev, changes) => {112const edit = AnnotatedStringEdit.compose(changes.map(c => c.edit));113114const supportedSource = new Set(['Chat.applyEdits', 'inlineChat.applyEdits'] as ITextModelEditSourceMetadata['source'][]);115116if (!edit.replacements.some(r => supportedSource.has(r.data.editSource.metadata.source))) {117return;118}119if (!edit.replacements.every(r => supportedSource.has(r.data.editSource.metadata.source))) {120onUnexpectedError(new Error(`ArcTelemetrySender: Not all edits are ${edit.replacements[0].data.editSource.metadata.source}!`));121return;122}123let applyCodeBlockSuggestionId: EditSuggestionId | undefined = undefined;124const data = edit.replacements[0].data.editSource;125let feature: 'inlineChat' | 'sideBarChat';126if (data.metadata.source === 'Chat.applyEdits') {127feature = 'sideBarChat';128if (data.metadata.$$mode === 'applyCodeBlock') {129applyCodeBlockSuggestionId = data.metadata.$$codeBlockSuggestionId;130}131} else {132feature = 'inlineChat';133}134135// TODO@hediet tie this suggestion id to hunks, so acceptance can be correlated.136this._aiEditTelemetryService.createSuggestionId({137applyCodeBlockSuggestionId,138languageId: data.props.$$languageId,139presentation: 'highlightedEdit',140feature,141modelId: data.props.$modelId,142modeId: data.props.$$mode as any,143editDeltaInfo: EditDeltaInfo.fromEdit(edit, _prev),144});145}));146}147}148149export class ChatArcTelemetrySender extends Disposable {150constructor(151docWithAnnotatedEdits: IDocumentWithAnnotatedEdits<EditSourceData>,152scmRepoBridge: ScmRepoBridge | undefined,153@IInstantiationService private readonly _instantiationService: IInstantiationService154) {155super();156157this._register(runOnChange(docWithAnnotatedEdits.value, (_val, _prev, changes) => {158const edit = AnnotatedStringEdit.compose(changes.map(c => c.edit));159160const supportedSource = new Set(['Chat.applyEdits', 'inlineChat.applyEdits'] as ITextModelEditSourceMetadata['source'][]);161162if (!edit.replacements.some(r => supportedSource.has(r.data.editSource.metadata.source))) {163return;164}165if (!edit.replacements.every(r => supportedSource.has(r.data.editSource.metadata.source))) {166onUnexpectedError(new Error(`ArcTelemetrySender: Not all edits are ${edit.replacements[0].data.editSource.metadata.source}!`));167return;168}169const data = edit.replacements[0].data.editSource;170171const uniqueEditId = generateUuid();172173const docWithJustReason = createDocWithJustReason(docWithAnnotatedEdits, this._store);174const reporter = this._instantiationService.createInstance(ArcTelemetryReporter, [0, 60, 300].map(s => s * 1000), _prev, docWithJustReason, scmRepoBridge, edit, res => {175res.telemetryService.publicLog2<{176sourceKeyCleaned: string;177extensionId: string | undefined;178extensionVersion: string | undefined;179opportunityId: string | undefined;180editSessionId: string | undefined;181requestId: string | undefined;182modelId: string | undefined;183languageId: string | undefined;184mode: string | undefined;185uniqueEditId: string | undefined;186187didBranchChange: number;188timeDelayMs: number;189190originalCharCount: number;191originalLineCount: number;192originalDeletedLineCount: number;193arc: number;194currentLineCount: number;195currentDeletedLineCount: number;196}, {197owner: 'hediet';198comment: 'Reports the accepted and retained character count for an inline completion/edit.';199200sourceKeyCleaned: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The key of the edit source.' };201extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id (copilot or copilot-chat); which provided this inline completion.' };202extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the extension.' };203opportunityId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for an opportunity to show an inline completion or NES.' };204editSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The session id.' };205requestId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The request id.' };206modelId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The model id.' };207languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' };208mode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The mode chat was in.' };209uniqueEditId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The unique identifier for the edit.' };210211didBranchChange: { 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.' };212timeDelayMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The time delay between the user accepting the edit and measuring the survival rate.' };213214originalCharCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original character count before any edits.' };215originalLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original line count before any edits.' };216originalDeletedLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original deleted line count before any edits.' };217arc: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The accepted and restrained character count.' };218currentLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The current line count after edits.' };219currentDeletedLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The current deleted line count after edits.' };220}>('editTelemetry.reportEditArc', {221sourceKeyCleaned: data.toKey(Number.MAX_SAFE_INTEGER, {222$extensionId: false,223$extensionVersion: false,224$$requestUuid: false,225$$sessionId: false,226$$requestId: false,227$$languageId: false,228$modelId: false,229}),230extensionId: data.props.$extensionId,231extensionVersion: data.props.$extensionVersion,232opportunityId: data.props.$$requestUuid,233editSessionId: data.props.$$sessionId,234requestId: data.props.$$requestId,235modelId: data.props.$modelId,236languageId: data.props.$$languageId,237mode: data.props.$$mode,238uniqueEditId,239240didBranchChange: res.didBranchChange ? 1 : 0,241timeDelayMs: res.timeDelayMs,242243originalCharCount: res.originalCharCount,244originalLineCount: res.originalLineCount,245originalDeletedLineCount: res.originalDeletedLineCount,246arc: res.arc,247currentLineCount: res.currentLineCount,248currentDeletedLineCount: res.currentDeletedLineCount,249250...forwardToChannelIf(isCopilotLikeExtension(data.props.$extensionId)),251});252});253254this._register(toDisposable(() => {255reporter.cancel();256}));257}));258}259}260261262export interface EditTelemetryData {263telemetryService: ITelemetryService;264timeDelayMs: number;265didBranchChange: boolean;266arc: number;267originalCharCount: number;268269currentLineCount: number;270currentDeletedLineCount: number;271originalLineCount: number;272originalDeletedLineCount: number;273}274275export class ArcTelemetryReporter {276private readonly _store = new DisposableStore();277private readonly _arcTracker;278private readonly _initialBranchName: string | undefined;279280private readonly _initialLineCounts;281282constructor(283private readonly _timesMs: number[],284private readonly _documentValueBeforeTrackedEdit: StringText,285private readonly _document: { value: IObservableWithChange<StringText, { edit: BaseStringEdit }> },286// _markedEdits -> document.value287private readonly _gitRepo: ScmRepoBridge | undefined,288private readonly _trackedEdit: BaseStringEdit,289private readonly _sendTelemetryEvent: (res: EditTelemetryData) => void,290291@ITelemetryService private readonly _telemetryService: ITelemetryService292) {293this._arcTracker = new ArcTracker(this._documentValueBeforeTrackedEdit, this._trackedEdit);294295this._store.add(runOnChange(this._document.value, (_val, _prevVal, changes) => {296const edit = BaseStringEdit.composeOrUndefined(changes.map(c => c.edit));297if (edit) {298this._arcTracker.handleEdits(edit);299}300}));301302this._initialLineCounts = this._arcTracker.getLineCountInfo();303304this._initialBranchName = this._gitRepo?.headBranchNameObs.get();305306for (let i = 0; i < this._timesMs.length; i++) {307const timeMs = this._timesMs[i];308309if (timeMs <= 0) {310this._report(timeMs);311} else {312this._reportAfter(timeMs, i === this._timesMs.length - 1 ? () => {313this._store.dispose();314} : undefined);315}316}317}318319private _reportAfter(timeoutMs: number, cb?: () => void) {320const timer = new TimeoutTimer(() => {321this._report(timeoutMs);322timer.dispose();323if (cb) {324cb();325}326}, timeoutMs);327this._store.add(timer);328}329330private _report(timeMs: number): void {331const currentBranch = this._gitRepo?.headBranchNameObs.get();332const didBranchChange = currentBranch !== this._initialBranchName;333const currentLineCounts = this._arcTracker.getLineCountInfo();334335this._sendTelemetryEvent({336telemetryService: this._telemetryService,337timeDelayMs: timeMs,338didBranchChange,339arc: this._arcTracker.getAcceptedRestrainedCharactersCount(),340originalCharCount: this._arcTracker.getOriginalCharacterCount(),341342currentLineCount: currentLineCounts.insertedLineCounts,343currentDeletedLineCount: currentLineCounts.deletedLineCounts,344originalLineCount: this._initialLineCounts.insertedLineCounts,345originalDeletedLineCount: this._initialLineCounts.deletedLineCounts,346});347}348349public cancel(): void {350this._store.dispose();351}352}353354355