Path: blob/main/extensions/copilot/src/extension/inlineEdits/node/nextEditProviderTelemetry.ts
13399 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 { ChatFetchResponseType } from '../../../platform/chat/common/commonTypes';6import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService';7import { DebugRecorderBookmark } from '../../../platform/inlineEdits/common/debugRecorderBookmark';8import { IObservableDocument, ObservableWorkspace } from '../../../platform/inlineEdits/common/observableWorkspace';9import { IStatelessNextEditTelemetry, StatelessNextEditRequest } from '../../../platform/inlineEdits/common/statelessNextEditProvider';10import { autorunWithChanges } from '../../../platform/inlineEdits/common/utils/observable';11import { APIUsage } from '../../../platform/networking/common/openai';12import { INotebookService } from '../../../platform/notebook/common/notebookService';13import { ITelemetryService, multiplexProperties, TelemetryEventMeasurements, TelemetryEventProperties } from '../../../platform/telemetry/common/telemetry';14import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';15import { LogEntry } from '../../../platform/workspaceRecorder/common/workspaceLog';16import { findNotebook } from '../../../util/common/notebooks';17import { RunOnceScheduler } from '../../../util/vs/base/common/async';18import { Disposable, DisposableStore, IDisposable, RefCountedDisposable } from '../../../util/vs/base/common/lifecycle';19import { Schemas } from '../../../util/vs/base/common/network';20import { autorun, autorunHandleChanges } from '../../../util/vs/base/common/observableInternal';21import { StringEdit, StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit';22import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';23import { StringText } from '../../../util/vs/editor/common/core/text/abstractText';24import { Uri } from '../../../vscodeTypes';25import { DebugRecorder } from './debugRecorder';26import { INesConfigs } from './nesConfigs';27import { INextEditDisplayLocation, INextEditResult } from './nextEditResult';2829export type NextEditTelemetryStatus = 'new' | 'requested' | `noEdit:${string}` | 'docChanged' | 'emptyEdits' | 'emptyEditsButHasNextCursorPosition' | 'previouslyRejected' | 'previouslyRejectedCache' | 'accepted' | 'notAccepted' | 'rejected';3031export type NesAcceptance = 'accepted' | 'notAccepted' | 'rejected';3233export type EnhancedTelemetrySendingReasonKind = 'idle' | 'hard_cap' | 'user_jump';3435export interface IEnhancedTelemetrySendingReason {36readonly reason: EnhancedTelemetrySendingReasonKind;37readonly details: {38readonly idleTimeoutMs?: number;39readonly hardCapTimeoutMs?: number;40readonly from?: { readonly file: string; readonly line: number };41readonly to?: { readonly file: string; readonly line: number | undefined };42};43}4445export interface IAlternativeAction {46readonly text: string | undefined; // undefined if the text is too long47readonly textLength: number;48readonly selection: ITelemetryRange[];49readonly edits: ITelemetryEdit[];50readonly tags: string[];51readonly recording: ITelemetryRecording | undefined;52}5354export interface ITelemetryEdit {55readonly time: string;56readonly start: number;57readonly endExclusive: number;58readonly newText: string;59}6061export interface ITelemetryRange {62readonly start: number;63readonly endExclusive: number;64}6566export interface ITelemetryRecording {67readonly entries: LogEntry[] | undefined;68readonly entriesSize: number;69readonly requestTime: number;70}7172export const enum ReusedRequestKind {73Speculative = 'speculative',74Async = 'async',75}7677export interface ILlmNESTelemetry extends Partial<IStatelessNextEditTelemetry> { // it's partial because the next edit can be pulled from cache resulting in no stateless provider telemetry78readonly providerId: string;79readonly headerRequestId: string | undefined;80readonly nextEditProviderDuration: number | undefined;81readonly fetchStartedAfterMs: number | undefined;82readonly isFromCache: boolean;83readonly reusedRequest: ReusedRequestKind | undefined;84readonly subsequentEditOrder: number | undefined;85readonly activeDocumentOriginalLineCount: number | undefined;86readonly activeDocumentEditsCount: number | undefined;87readonly activeDocumentLanguageId: string | undefined;88readonly activeDocumentRepository: string | undefined;89readonly hasNextEdit: boolean;90readonly wasPreviouslyRejected: boolean;91readonly status: NextEditTelemetryStatus;92readonly nextEditProviderError: string | undefined;93readonly nesConfigs: INesConfigs | undefined;94readonly repositoryUrls: string[] | undefined;95readonly documentsCount: number | undefined;96readonly editsCount: number | undefined;97readonly isNotebook: boolean;98readonly notebookType: string | undefined;99readonly alternativeAction: IAlternativeAction | undefined;100}101102export interface IDiagnosticsTelemetry {103readonly diagnosticType: string | undefined;104readonly diagnosticDroppedReasons: string | undefined;105readonly diagnosticDistanceToUnknownDiagnostic: number | undefined;106readonly diagnosticDistanceToAlternativeDiagnostic: number | undefined;107readonly diagnosticHasAlternativeDiagnosticForSameRange: boolean | undefined;108109// imports110readonly diagnosticHasExistingSameFileImport: boolean | undefined;111readonly diagnosticIsLocalImport: boolean | undefined;112readonly diagnosticAlternativeImportsCount: number | undefined;113}114115export interface INextEditProviderTelemetry extends ILlmNESTelemetry, IDiagnosticsTelemetry {116readonly opportunityId: string;117readonly requestN: number;118readonly isShown: boolean;119readonly acceptance: NesAcceptance;120readonly disposalReason: string | undefined;121readonly supersededByOpportunityId: string | undefined;122readonly status: NextEditTelemetryStatus;123readonly nextEditProviderError: string | undefined;124readonly activeDocumentRepository: string | undefined;125readonly repositoryUrls: string[] | undefined;126readonly alternativeAction: IAlternativeAction | undefined;127readonly postProcessingOutcome: string | undefined;128readonly isNESForAnotherDoc: boolean;129readonly notebookCellMarkerCount: number;130readonly notebookCellMarkerIndex: number;131readonly notebookId: string | undefined;132readonly notebookCellLines: string | undefined;133readonly isActiveDocument?: boolean;134readonly isMultilineEdit?: boolean;135readonly isEolDifferent?: boolean;136readonly isNextEditorVisible?: boolean;137readonly isNextEditorRangeVisible?: boolean;138readonly isNaturalLanguageDominated: boolean;139140readonly hadLlmNES: boolean;141readonly hadDiagnosticsNES: boolean;142readonly pickedNES: 'llm' | 'diagnostics' | undefined;143readonly configIsDiagnosticsNESEnabled: boolean;144145readonly userTypingDisagreed: boolean | undefined;146}147148export class LlmNESTelemetryBuilder extends Disposable {149150public build(includeAlternativeAction: boolean): ILlmNESTelemetry {151let documentsCount: number | undefined = undefined;152let editsCount: number | undefined = undefined;153let activeDocumentEditsCount: number | undefined = undefined;154let activeDocumentLanguageId: string | undefined = undefined;155let activeDocumentOriginalLineCount: number | undefined = undefined;156let isNotebook: boolean = false;157let notebookType: string | undefined = undefined;158let activeDocumentRepository: string | undefined = undefined;159let repositoryUrls: string[] | undefined = undefined;160161if (this._request) {162const activeDoc = this._request.getActiveDocument();163documentsCount = this._request.documents.length;164editsCount = this._request.documents.reduce((acc, doc) => acc + doc.recentEdits.edits.length, 0);165activeDocumentEditsCount = activeDoc.recentEdits.edits.length;166activeDocumentLanguageId = activeDoc.languageId;167activeDocumentOriginalLineCount = activeDoc.documentAfterEditsLines.length;168isNotebook = activeDoc.id.toUri().scheme === Schemas.vscodeNotebookCell || this._notebookService?.hasSupportedNotebooks(activeDoc.id.toUri()) || false;169notebookType = this._workspaceService === undefined ? undefined : findNotebook(activeDoc.id.toUri(), this._workspaceService.notebookDocuments)?.notebookType;170const git = this._gitExtensionService?.getExtensionApi();171if (git) {172const activeDocRepository = git.getRepository(Uri.parse(activeDoc.id.uri));173if (activeDocRepository) {174const remoteName = activeDocRepository.state.HEAD?.upstream?.remote;175const remote = activeDocRepository.state.remotes.find(r => r.name === remoteName);176if (remote?.fetchUrl) {177activeDocumentRepository = remote.pushUrl || remote.fetchUrl;178}179}180181const remoteUrlSet = new Set<string>();182const repositories = [...new Set(this._request.documents.map(doc => git.getRepository(Uri.parse(doc.id.uri))).filter(Boolean))];183for (const repository of repositories) {184const remoteName = repository?.state.HEAD?.upstream?.remote;185const remote = repository?.state.remotes.find(r => r.name === remoteName);186if (remote?.fetchUrl) {187remoteUrlSet.add(remote.fetchUrl);188}189if (remote?.pushUrl) {190remoteUrlSet.add(remote.pushUrl);191}192}193repositoryUrls = [...remoteUrlSet];194}195}196197let alternativeAction: IAlternativeAction | undefined;198if (includeAlternativeAction && this.editCollectingInfo !== undefined) {199const originalText = this.editCollectingInfo.originalDoc.value;200let recording: ITelemetryRecording | undefined;201if (this._debugRecorder && this._requestBookmark) {202const entries = this._debugRecorder.getRecentLog();203const entriesSize = JSON.stringify(entries)?.length || 0;204recording = {205entries: entriesSize > 200 * 1024 ? undefined : entries,206entriesSize: entriesSize,207requestTime: this._requestBookmark.timeMs,208};209}210alternativeAction = {211text: originalText.length > 200 * 1024 ? undefined : originalText,212textLength: originalText.length,213selection: this.editCollectingInfo.originalSelection.map(range => ({214start: range.start,215endExclusive: range.endExclusive,216})),217edits: this.editCollectingInfo.edits.map(edit => edit.edit.replacements.map(e => ({218time: edit.time.toISOString(),219start: e.replaceRange.start,220endExclusive: e.replaceRange.endExclusive,221newText: e.newText,222}))).flat(),223tags: [],224recording,225};226}227228const fetchStartedAfterMs = this._statelessNextEditTelemetry?.fetchStartedAt === undefined ? undefined : this._statelessNextEditTelemetry.fetchStartedAt - this._startTime;229230return {231providerId: this._providerId,232headerRequestId: this._headerRequestId,233nextEditProviderDuration: this._duration,234isFromCache: this._isFromCache,235reusedRequest: this._reusedRequest,236subsequentEditOrder: this._subsequentEditOrder,237documentsCount,238editsCount,239activeDocumentEditsCount,240activeDocumentLanguageId,241activeDocumentOriginalLineCount,242fetchStartedAfterMs,243hasNextEdit: this._hasNextEdit,244wasPreviouslyRejected: this._wasPreviouslyRejected,245isNotebook,246notebookType,247status: this._status,248nextEditProviderError: this._nextEditProviderError,249alternativeAction,250251...this._statelessNextEditTelemetry,252253activeDocumentRepository,254repositoryUrls,255256nesConfigs: this._nesConfigs,257};258}259260private _startTime: number;261262/** Dependent on the observable document to track edits and selections */263private editCollectingInfo: undefined | {264originalDoc: StringText;265originalSelection: readonly OffsetRange[];266originalSelectionLine: number | undefined;267edits: { time: Date; edit: StringEdit }[];268};269270public get originalSelectionLine(): number | undefined {271return this.editCollectingInfo?.originalSelectionLine;272}273274/**275* @param _doc passing an observable document allows to track edits and selections276*/277constructor(278private readonly _gitExtensionService: IGitExtensionService | undefined,279private readonly _notebookService: INotebookService | undefined,280private readonly _workspaceService: IWorkspaceService | undefined,281private readonly _providerId: string,282private readonly _doc: IObservableDocument | undefined,283private readonly _debugRecorder?: DebugRecorder,284private readonly _requestBookmark?: DebugRecorderBookmark,285) {286super();287this._startTime = Date.now();288289if (this._doc) {290this.editCollectingInfo = {291originalDoc: this._doc.value.get(),292originalSelection: this._doc.selection.get(),293originalSelectionLine: this._doc.primarySelectionLine.get(),294edits: [],295};296297this._store.add(autorunWithChanges(this, {298value: this._doc.value,299}, (data) => {300const time = new Date();301data.value.changes.forEach(change => {302this.editCollectingInfo?.edits.push({303time,304edit: change,305});306});307}));308}309}310311private _nesConfigs: INesConfigs | undefined;312public setNESConfigs(nesConfigs: INesConfigs): this {313this._nesConfigs = nesConfigs;314return this;315}316317private _headerRequestId: string | undefined;318public setHeaderRequestId(uuid: string): this {319this._headerRequestId = uuid;320return this;321}322323private _isFromCache: boolean = false;324public setIsFromCache(): this {325this._isFromCache = true;326return this;327}328329private _reusedRequest: ReusedRequestKind | undefined;330public setReusedRequest(kind: ReusedRequestKind): this {331this._reusedRequest = kind;332return this;333}334335private _subsequentEditOrder: number | undefined;336public setSubsequentEditOrder(subsequentEditOrder: number | undefined): this {337this._subsequentEditOrder = subsequentEditOrder;338return this;339}340341private _request: StatelessNextEditRequest | undefined;342public setRequest(request: StatelessNextEditRequest): this {343this._request = request;344return this;345}346347private _statelessNextEditTelemetry: IStatelessNextEditTelemetry | undefined;348public setStatelessNextEditTelemetry(statelessNextEditTelemetry: IStatelessNextEditTelemetry): this {349this._statelessNextEditTelemetry = statelessNextEditTelemetry;350return this;351}352353private _hasNextEdit: boolean = false;354public setHasNextEdit(hasNextEdit: boolean): this {355this._hasNextEdit = hasNextEdit;356return this;357}358359private _wasPreviouslyRejected: boolean = false;360public setWasPreviouslyRejected(): this {361this._wasPreviouslyRejected = true;362return this;363}364365private _duration: number | undefined;366public markEndTime(): this {367this._duration = Date.now() - this._startTime;368return this;369}370371private _status: NextEditTelemetryStatus = 'new';372public setStatus(status: NextEditTelemetryStatus): this {373this._status = status;374return this;375}376377private _nextEditProviderError: string | undefined;378public setNextEditProviderError(nextEditProviderError: string | undefined): this {379this._nextEditProviderError = nextEditProviderError;380return this;381}382}383384interface IDiagnosticTelemetryRun {385alternativeImportsCount?: number;386hasExistingSameFileImport?: boolean;387isLocalImport?: boolean;388distanceToUnknownDiagnostic?: number;389distanceToAlternativeDiagnostic?: number;390hasAlternativeDiagnosticForSameRange?: boolean;391}392393export class DiagnosticsTelemetryBuilder {394395public build(): IDiagnosticsTelemetry {396const diagnosticDroppedReasons = this._droppedReasons.length > 0 ? JSON.stringify(this._droppedReasons) : undefined;397return {398diagnosticType: this._type,399diagnosticDroppedReasons,400diagnosticAlternativeImportsCount: this._diagnosticRunTelemetry?.alternativeImportsCount,401diagnosticHasExistingSameFileImport: this._diagnosticRunTelemetry?.hasExistingSameFileImport,402diagnosticIsLocalImport: this._diagnosticRunTelemetry?.isLocalImport,403diagnosticDistanceToUnknownDiagnostic: this._diagnosticRunTelemetry?.distanceToUnknownDiagnostic,404diagnosticDistanceToAlternativeDiagnostic: this._diagnosticRunTelemetry?.distanceToAlternativeDiagnostic,405diagnosticHasAlternativeDiagnosticForSameRange: this._diagnosticRunTelemetry?.hasAlternativeDiagnosticForSameRange406};407}408409public populate(telemetry: DiagnosticsTelemetryBuilder) {410this._droppedReasons.forEach(reason => telemetry.addDroppedReason(reason));411if (this._type) {412telemetry.setType(this._type);413}414if (this._diagnosticRunTelemetry) {415telemetry.setDiagnosticRunTelemetry(this._diagnosticRunTelemetry);416}417}418419private _type: string | undefined;420setType(type: string): this {421this._type = type;422return this;423}424425private _droppedReasons: string[] = [];426addDroppedReason(reason: string): this {427this._droppedReasons.push(reason);428return this;429}430431private _diagnosticRunTelemetry: IDiagnosticTelemetryRun | undefined;432setDiagnosticRunTelemetry(diagnosticRun: IDiagnosticTelemetryRun): this {433this._diagnosticRunTelemetry = diagnosticRun;434return this;435}436}437438export class NextEditProviderTelemetryBuilder extends Disposable {439440private static providerIdToReqN = new Map<string /* providerId */, number>();441442/**443* Whether telemetry for this builder has been sent -- only for ordinary telemetry, not enhanced telemetry444*/445private _isSent: boolean = false;446public get isSent(): boolean {447return this._isSent;448}449public markAsSent(): void {450this._isSent = true;451}452453public build(includeAlternativeAction: boolean): INextEditProviderTelemetry {454455const nesTelemetry = this._nesBuilder.build(includeAlternativeAction);456const diagnosticsTelemetry = this._diagnosticsBuilder.build();457458return {459...nesTelemetry,460...diagnosticsTelemetry,461462opportunityId: this._opportunityId || '',463requestN: this._requestN,464isShown: this._isShown,465acceptance: this._acceptance,466disposalReason: this._disposalReason,467supersededByOpportunityId: this._supersededByOpportunityId,468pickedNES: this._nesTypePicked,469hadLlmNES: this._hadLlmNES,470isMultilineEdit: this._isMultilineEdit,471isEolDifferent: this._isEolDifferent,472isActiveDocument: this._isActiveDocument,473isNextEditorVisible: this._isNextEditorVisible,474isNextEditorRangeVisible: this._isNextEditorRangeVisible,475isNESForAnotherDoc: this._isNESForAnotherDoc,476notebookId: this._notebookId,477notebookCellLines: this._notebookCellLines,478notebookCellMarkerCount: this._notebookCellMarkerCount,479notebookCellMarkerIndex: this._notebookCellMarkerIndex,480hadDiagnosticsNES: this._hadDiagnosticsNES,481configIsDiagnosticsNESEnabled: this._configIsDiagnosticsNESEnabled,482isNaturalLanguageDominated: this._isNaturalLanguageDominated,483postProcessingOutcome: this._postProcessingOutcome,484userTypingDisagreed: this._userTypingDisagreed,485};486}487488private _requestN: number;489490private readonly _nesBuilder: LlmNESTelemetryBuilder;491public get nesBuilder(): LlmNESTelemetryBuilder {492return this._nesBuilder;493}494private readonly _diagnosticsBuilder: DiagnosticsTelemetryBuilder;495public get diagnosticsBuilder(): DiagnosticsTelemetryBuilder {496return this._diagnosticsBuilder;497}498499/**500* @param _doc passing an observable document allows to track edits and selections501*/502constructor(503gitExtensionService: IGitExtensionService | undefined,504notebookService: INotebookService | undefined,505workspaceService: IWorkspaceService | undefined,506providerId: string,507public readonly doc: IObservableDocument | undefined,508debugRecorder?: DebugRecorder,509requestBookmark?: DebugRecorderBookmark,510) {511super();512513let requestN = NextEditProviderTelemetryBuilder.providerIdToReqN.get(providerId) || 0;514this._requestN = ++requestN;515NextEditProviderTelemetryBuilder.providerIdToReqN.set(providerId, requestN);516517this._nesBuilder = this._register(new LlmNESTelemetryBuilder(gitExtensionService, notebookService, workspaceService, providerId, doc, debugRecorder, requestBookmark));518this._diagnosticsBuilder = new DiagnosticsTelemetryBuilder();519}520521private _opportunityId: string | undefined;522public setOpportunityId(uuid: string): this {523this._opportunityId = uuid;524return this;525}526527private _isShown: boolean = false;528public setAsShown(): this {529this._isShown = true;530return this;531}532533private _acceptance: NesAcceptance = 'notAccepted';534public setAcceptance(acceptance: NesAcceptance): this {535this._acceptance = acceptance;536return this;537}538539private _disposalReason: string | undefined = undefined;540public setDisposalReason(disposalReason: string | undefined): this {541this._disposalReason = disposalReason;542return this;543}544545private _supersededByOpportunityId: string | undefined = undefined;546public setSupersededBy(opportunityId: string | undefined): this {547this._supersededByOpportunityId = opportunityId;548return this;549}550551private _userTypingDisagreed: boolean | undefined = undefined;552public setUserTypingDisagreed(userTypingDisagreed: boolean): this {553this._userTypingDisagreed = userTypingDisagreed;554return this;555}556557private _nesTypePicked: 'llm' | 'diagnostics' | undefined;558public setPickedNESType(nesTypePicked: 'llm' | 'diagnostics'): this {559this._nesTypePicked = nesTypePicked;560return this;561}562563private _isActiveDocument?: boolean;564public setIsActiveDocument(isActive: boolean): this {565this._isActiveDocument = isActive;566return this;567}568569private _notebookCellMarkerCount: number = 0;570public setNotebookCellMarkerCount(count: number): this {571this._notebookCellMarkerCount = count;572return this;573}574575private _isMultilineEdit?: boolean;576public setIsMultilineEdit(isMultiLine: boolean): this {577this._isMultilineEdit = isMultiLine;578return this;579}580581private _isEolDifferent?: boolean;582public setIsEolDifferent(isEolDifferent: boolean): this {583this._isEolDifferent = isEolDifferent;584return this;585}586587private _isNextEditorVisible?: boolean;588public setIsNextEditorVisible(isVisible: boolean): this {589this._isNextEditorVisible = isVisible;590return this;591}592593private _isNextEditorRangeVisible?: boolean;594public setIsNextEditorRangeVisible(isVisible: boolean): this {595this._isNextEditorRangeVisible = isVisible;596return this;597}598599private _notebookId?: string;600public setNotebookId(notebookId: string): this {601this._notebookId = notebookId;602return this;603}604605private _notebookCellLines?: string;606public setNotebookCellLines(notebookCellLines: string): this {607this._notebookCellLines = notebookCellLines;608return this;609}610611private _notebookCellMarkerIndex: number = -1;612public setNotebookCellMarkerIndex(index: number): this {613this._notebookCellMarkerIndex = index;614return this;615}616617private _isNESForAnotherDoc: boolean = false;618public setIsNESForOtherEditor(isForAnotherDoc: boolean): this {619this._isNESForAnotherDoc = isForAnotherDoc;620return this;621}622623private _hadLlmNES: boolean = false;624public setHadLlmNES(boolean: boolean): this {625this._hadLlmNES = boolean;626return this;627}628629private _hadDiagnosticsNES: boolean = false;630public setHadDiagnosticsNES(boolean: boolean): this {631this._hadDiagnosticsNES = boolean;632return this;633}634635public setStatus(status: NextEditTelemetryStatus): this {636this._nesBuilder.setStatus(status);637return this;638}639640private _configIsDiagnosticsNESEnabled: boolean = false;641public setConfigIsDiagnosticsNESEnabled(boolean: boolean): this {642this._configIsDiagnosticsNESEnabled = boolean;643return this;644}645646private _isNaturalLanguageDominated: boolean = false;647public setIsNaturalLanguageDominated(isNaturalLanguageDominated: boolean): this {648this._isNaturalLanguageDominated = isNaturalLanguageDominated;649return this;650}651652private _postProcessingOutcome: string | undefined;653public setPostProcessingOutcome(suggestion: {654edit: StringReplacement;655isInlineCompletion: boolean;656displayLocation?: INextEditDisplayLocation;657}): this {658const displayLocation = suggestion.displayLocation ? {659label: suggestion.displayLocation.label,660range: suggestion.displayLocation.range.toString()661} : undefined;662663this._postProcessingOutcome = JSON.stringify({664suggestedEdit: suggestion.edit.toString(),665isInlineCompletion: suggestion.isInlineCompletion,666displayLocation667});668669return this;670}671}672673/**674* Watches all documents in the {@link ObservableWorkspace} for idle periods and cursor jumps.675*676* Only documents tracked by the workspace are monitored. Documents in languages where677* Copilot completions are disabled (e.g. markdown, plaintext), non-file URI schemes,678* and copilot-ignored files are excluded. This matches the scope of {@link DebugRecorder}.679*680* Fires `onIdle` after 5 seconds of no document edits across the workspace,681* and `onUserJump` when the user moves their cursor to a different line or file682* (ignoring selection changes within 200ms of an edit, which are likely side-effects of typing).683*684* Ref-counted via {@link RefCountedDisposable}: call {@link acquire} when a telemetry entry685* starts using this detector, {@link release} when it's done. Auto-disposes when all686* references are released. Use {@link forceDispose} on owner shutdown.687*/688class IdleDetector {689private readonly _store = new DisposableStore();690private readonly _disposalTracker = new RefCountedDisposable(this._store);691692/** Snapshot of each document's primarySelectionLine to detect which doc's cursor actually moved. */693private readonly _selectionSnapshots = new Map<string, number | undefined>();694695/** Timestamp of the last document edit, used to suppress selection changes caused by typing. */696private _lastEditTime = 0;697698get isDisposed(): boolean { return this._store.isDisposed; }699700constructor(701workspace: ObservableWorkspace,702private readonly _onIdle: (idleTimeoutMs: number) => void,703private readonly _onUserJump: (toDocId: string, toLine: number | undefined) => void,704) {705const idleTimeMs = 5_000;706707// Idle timer: resets each time any tracked document changes, fires after 5s of inactivity708const idleScheduler = this._store.add(new RunOnceScheduler(() => {709this._onIdle(idleTimeMs);710}, idleTimeMs));711this._idleScheduler = idleScheduler;712713// Watch for document content changes across the workspace.714// Skip scheduling on the first (initialization) run — the idle timer is started715// explicitly via scheduleIdleTimer() when the first entry acquires the detector.716let isFirstDocRun = true;717this._store.add(autorun(reader => {718workspace.onDidOpenDocumentChange.read(reader);719if (isFirstDocRun) {720isFirstDocRun = false;721return;722}723this._lastEditTime = Date.now();724idleScheduler.schedule();725}));726727// Watch for selection (cursor) changes across all documents to detect user jumps.728// Uses autorunHandleChanges to get the `removed` list from openDocuments change data729// so we can clean up stale selection snapshots when documents are closed.730let isFirstSelectionRun = true;731this._store.add(autorunHandleChanges({732owner: this,733changeTracker: {734createChangeSummary: () => ({ removed: [] as readonly IObservableDocument[] }),735handleChange: (ctx, summary) => {736if (ctx.didChange(workspace.openDocuments)) {737summary.removed = ctx.change.removed;738}739return true;740}741}742}, (reader, changeSummary) => {743if (this._store.isDisposed) { return; }744745// Subscribe to all document primarySelectionLine observables to detect line changes746const docs = workspace.openDocuments.read(reader);747for (const doc of docs) {748doc.primarySelectionLine.read(reader);749}750751// On the first run, snapshot all current selection lines as baseline752if (isFirstSelectionRun) {753isFirstSelectionRun = false;754for (const doc of docs) {755// eslint-disable-next-line local/code-no-observable-get-in-reactive-context756this._selectionSnapshots.set(doc.id.uri, doc.primarySelectionLine.get());757}758return;759}760761// Clean up snapshots for closed documents762for (const removed of changeSummary.removed) {763this._selectionSnapshots.delete(removed.id.uri);764}765766// If a document was edited very recently (within 200ms), this selection change767// is likely a side-effect of the edit (e.g. cursor moves when typing) — not a deliberate jump768if (Date.now() - this._lastEditTime < 200) { return; }769770// Find the doc whose selection line actually changed from what we last saw771for (const doc of docs) {772const currentDocId = doc.id.uri;773// eslint-disable-next-line local/code-no-observable-get-in-reactive-context774const currentLine = doc.primarySelectionLine.get();775const previousLine = this._selectionSnapshots.get(currentDocId);776777if (previousLine === currentLine) { continue; }778779this._selectionSnapshots.set(currentDocId, currentLine);780this._onUserJump(currentDocId, currentLine);781return;782}783}));784}785786private _idleScheduler: RunOnceScheduler | undefined;787788/** Start the idle timer. Called when an entry first acquires this detector. */789scheduleIdleTimer(): void { this._idleScheduler?.schedule(); }790791acquire(): void { this._disposalTracker.acquire(); }792release(): void { this._disposalTracker.release(); }793forceDispose(): void { this._store.dispose(); }794}795796export class TelemetrySender implements IDisposable {797798private readonly _map = new Map<INextEditResult, { builder: NextEditProviderTelemetryBuilder; timeout: TimeoutHandle; hardCapTimeout?: TimeoutHandle }>();799private _idleDetector: IdleDetector | undefined;800801constructor(802private readonly _workspace: ObservableWorkspace | undefined,803@ITelemetryService private readonly _telemetryService: ITelemetryService,804) {805}806807/**808* Schedule sending enhanced telemetry for a NES suggestion.809*810* After a 2-minute initial delay, enters an idle-detection phase that monitors all workspace documents811* and finally sends the telemetry event when one of these conditions is met:812*813* - **idle** (5s): No document edits across the entire workspace for 5 seconds.814* - **user_jump**: User moves cursor to a different line or different file (detected via815* {@link IObservableDocument.primarySelectionLine} snapshot diffs.816* - **hard_cap** (30s): Forced send after 30 seconds regardless of activity.817*818* Note: only documents tracked by the {@link ObservableWorkspace} are monitored. Documents in819* languages where Copilot completions are disabled (e.g. markdown) and copilot-ignored files are excluded,820* so activity in those files won't reset the idle timer. This matches the scope of {@link DebugRecorder}.821*/822public scheduleSendingEnhancedTelemetry(nextEditResult: INextEditResult, builder: NextEditProviderTelemetryBuilder): void {823const existing = this._map.get(nextEditResult);824if (existing) {825if (existing.builder !== builder) {826existing.builder.dispose();827}828this._removeEntry(nextEditResult, existing);829}830831const timeout = setTimeout(() => {832this._enterIdleDetection(nextEditResult, builder);833}, /* 2 minutes */ 2 * 60 * 1000);834this._map.set(nextEditResult, { builder, timeout });835}836837private _enterIdleDetection(nextEditResult: INextEditResult, builder: NextEditProviderTelemetryBuilder): void {838const workspace = this._workspace;839if (!workspace) {840this._buildAndSendEnhancedTelemetry(nextEditResult, builder, { reason: 'idle', details: { idleTimeoutMs: 0 } });841return;842}843844if (!this._idleDetector) {845this._idleDetector = new IdleDetector(846workspace,847idleTimeoutMs => this._sendAllPendingInIdlePhase({ reason: 'idle', details: { idleTimeoutMs } }),848(toDocId, toLine) => this._sendAllPendingInIdlePhaseWithJump(toDocId, toLine),849);850// RefCountedDisposable starts at count=1, which covers this first entry.851// Only subsequent entries need acquire().852} else {853this._idleDetector.acquire();854}855// Start/restart the idle timer so this entry gets a fresh 5s window856this._idleDetector.scheduleIdleTimer();857858const hardCapMs = 30_000;859const hardCapTimeout = setTimeout(() => {860this._sendForEntry(nextEditResult, { reason: 'hard_cap', details: { hardCapTimeoutMs: hardCapMs } });861}, hardCapMs);862863const entry = this._map.get(nextEditResult);864if (entry) {865entry.hardCapTimeout = hardCapTimeout;866}867}868869private _releaseIdleDetector(): void {870this._idleDetector?.release();871if (this._idleDetector?.isDisposed) {872this._idleDetector = undefined;873}874}875876/** Send all entries that are in the idle-detection phase (have no initial timeout pending) with a shared reason. */877private _sendAllPendingInIdlePhase(reason: IEnhancedTelemetrySendingReason): void {878const entriesToSend: INextEditResult[] = [];879for (const [result, data] of this._map) {880if (data.hardCapTimeout !== undefined) {881entriesToSend.push(result);882}883}884for (const result of entriesToSend) {885this._sendForEntry(result, reason);886}887}888889/** Send all entries in idle-detection phase with user_jump, using per-entry `from` positions. */890private _sendAllPendingInIdlePhaseWithJump(toDocId: string, toLine: number | undefined): void {891const entriesToSend: [INextEditResult, NextEditProviderTelemetryBuilder][] = [];892for (const [result, data] of this._map) {893if (data.hardCapTimeout !== undefined) {894entriesToSend.push([result, data.builder]);895}896}897for (const [result, builder] of entriesToSend) {898const nesDocId: string | undefined = builder.doc?.id.uri;899const nesDocLine: number | undefined = builder.nesBuilder.originalSelectionLine;900const from = nesDocId !== undefined && nesDocLine !== undefined901? { file: nesDocId, line: nesDocLine }902: undefined;903this._sendForEntry(result, {904reason: 'user_jump',905details: {906from,907to: { file: toDocId, line: toLine },908},909});910}911}912913/** Send enhanced telemetry for a single entry that's in the idle-detection phase. */914private _sendForEntry(nextEditResult: INextEditResult, reason: IEnhancedTelemetrySendingReason): void {915const data = this._map.get(nextEditResult);916if (!data) { return; }917918if (data.hardCapTimeout !== undefined) {919clearTimeout(data.hardCapTimeout);920this._releaseIdleDetector();921}922this._map.delete(nextEditResult);923924let telemetry: INextEditProviderTelemetry;925try {926telemetry = data.builder.build(true);927} finally {928data.builder.dispose();929}930this._doSendEnhancedTelemetry(telemetry, reason);931}932933private _removeEntry(nextEditResult: INextEditResult, data: { builder: NextEditProviderTelemetryBuilder; timeout: TimeoutHandle; hardCapTimeout?: TimeoutHandle }): void {934clearTimeout(data.timeout);935if (data.hardCapTimeout !== undefined) {936clearTimeout(data.hardCapTimeout);937this._releaseIdleDetector();938}939this._map.delete(nextEditResult);940}941942private _buildAndSendEnhancedTelemetry(nextEditResult: INextEditResult, builder: NextEditProviderTelemetryBuilder, sendingReason: IEnhancedTelemetrySendingReason): void {943let telemetry: INextEditProviderTelemetry;944this._map.delete(nextEditResult);945try {946telemetry = builder.build(true);947} finally {948builder.dispose();949}950this._doSendEnhancedTelemetry(telemetry, sendingReason);951}952953/**954* Send telemetry for the next edit result in case it has already been rejected or contains no edits to be shown.955*/956public sendTelemetry(nextEditResult: INextEditResult | undefined, builder: NextEditProviderTelemetryBuilder): void {957if (nextEditResult) {958const data = this._map.get(nextEditResult);959if (data) {960this._removeEntry(nextEditResult, data);961}962}963const telemetry = builder.build(true);964if (!builder.isSent) {965this._doSendTelemetry(telemetry);966builder.markAsSent();967}968this._doSendEnhancedTelemetry(telemetry, undefined);969}970971public sendTelemetryForBuilder(builder: NextEditProviderTelemetryBuilder): void {972if (builder.isSent) {973return;974}975const telemetry = builder.build(false); // disposal is done by enhanced telemetry sending in a setTimeout callback976this._doSendTelemetry(telemetry);977builder.markAsSent();978}979980private async _doSendTelemetry(telemetry: INextEditProviderTelemetry): Promise<void> {981const {982opportunityId,983headerRequestId,984requestN,985providerId,986modelName,987hadStatelessNextEditProviderCall,988statelessNextEditProviderDuration,989nextEditProviderDuration,990isFromCache,991reusedRequest,992subsequentEditOrder,993activeDocumentLanguageId,994activeDocumentOriginalLineCount,995nLinesOfCurrentFileInPrompt,996wasPreviouslyRejected,997isShown,998isNotebook,999notebookType,1000isNESForAnotherDoc,1001isActiveDocument,1002isEolDifferent,1003isMultilineEdit,1004isNextEditorRangeVisible,1005isNextEditorVisible,1006acceptance,1007disposalReason,1008logProbThreshold,1009documentsCount,1010editsCount,1011activeDocumentEditsCount,1012promptLineCount,1013promptCharCount,1014hadLowLogProbSuggestion,1015nEditsSuggested,1016lineDistanceToMostRecentEdit,1017isCursorAtEndOfLine,1018isInlineSuggestion,1019debounceTime,1020artificialDelay,1021hasNextEdit,1022notebookCellMarkerCount,1023notebookCellMarkerIndex,1024notebookId,1025notebookCellLines,1026nextEditLogprob,1027supersededByOpportunityId,1028noNextEditReasonKind,1029noNextEditReasonMessage,1030fetchStartedAfterMs,1031response: responseWithStats,1032configIsDiagnosticsNESEnabled,1033isNaturalLanguageDominated,1034diagnosticType,1035diagnosticDroppedReasons,1036diagnosticHasExistingSameFileImport,1037diagnosticIsLocalImport,1038diagnosticAlternativeImportsCount,1039diagnosticDistanceToUnknownDiagnostic,1040diagnosticDistanceToAlternativeDiagnostic,1041diagnosticHasAlternativeDiagnosticForSameRange,1042hadDiagnosticsNES,1043hadLlmNES,1044pickedNES,1045xtabAggressivenessLevel,1046xtabUserHappinessScore,1047userAggressivenessSetting,1048modelConfig,1049} = telemetry;10501051let usage: APIUsage | undefined;1052let ttft_: number | undefined;1053let fetchResult_: ChatFetchResponseType | undefined;1054let fetchTime_: number | undefined;1055if (responseWithStats !== undefined) {1056const { response, ttft, fetchResult, fetchTime } = await responseWithStats;1057if (response.type === ChatFetchResponseType.Success) {1058usage = response.usage;1059}1060ttft_ = ttft;1061fetchResult_ = fetchResult;1062fetchTime_ = fetchTime;1063}10641065/* __GDPR__1066"provideInlineEdit" : {1067"owner": "ulugbekna",1068"comment": "Telemetry for inline edit (NES) provided",1069"opportunityId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Unique identifier for an opportunity to show an NES." },1070"headerRequestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Unique identifier of the network request which is also included in the fetch request header." },1071"providerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "NES provider identifier (StatelessNextEditProvider)" },1072"modelName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Name of the model used to provide the NES" },1073"activeDocumentLanguageId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "LanguageId of the active document" },1074"mergeConflictExpanded": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "If and how edit window expanded to include merge conflict lines ('normal' or 'only' or undefined if not expanded)" },1075"acceptance": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "User acceptance of the edit" },1076"disposalReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Reason for disposal of NES" },1077"supersededByOpportunityId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "UUID of the opportunity that superseded this edit" },1078"endpoint": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Endpoint for the request" },1079"noNextEditReasonKind": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Reason kind for no next edit" },1080"noNextEditReasonMessage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Reason message for no next edit" },1081"fetchResult": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Fetch result" },1082"fetchError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Fetch error message" },1083"pickedNES": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request had picked NES" },1084"nextEditProviderError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Error message from next edit provider" },1085"diagnosticType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Type of diagnostics" },1086"diagnosticDroppedReasons": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Reasons for dropping diagnostics NES suggestions" },1087"requestN": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Request number", "isMeasurement": true },1088"hadStatelessNextEditProviderCall": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request had a stateless next edit provider call", "isMeasurement": true },1089"statelessNextEditProviderDuration": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Duration of stateless next edit provider", "isMeasurement": true },1090"nextEditProviderDuration": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Duration of next edit provider", "isMeasurement": true },1091"isFromCache": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the edit was provided from cache", "isMeasurement": true },1092"reusedRequest": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the result was obtained by joining a pending request ('speculative' or 'async'), undefined for fresh requests and cache hits" },1093"subsequentEditOrder": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Order of the subsequent edit", "isMeasurement": true },1094"activeDocumentOriginalLineCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of lines in the active document before shortening", "isMeasurement": true },1095"activeDocumentNLinesInPrompt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of lines in the active document included in prompt", "isMeasurement": true },1096"wasPreviouslyRejected": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the edit was previously rejected", "isMeasurement": true },1097"isShown": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the edit was shown", "isMeasurement": true },1098"isNotebook": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the document is a notebook", "isMeasurement": true },1099"isNESForAnotherDoc": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the NES if for another document", "isMeasurement": true },1100"isMultilineEdit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the NES is for a multiline edit", "isMeasurement": true },1101"isEolDifferent": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the NES edit and original text have different end of lines", "isMeasurement": true },1102"isNextEditorVisible": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the next editor is visible", "isMeasurement": true },1103"isNextEditorRangeVisible": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the next editor range is visible", "isMeasurement": true },1104"notebookCellMarkerIndex": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Index of the notebook cell marker in the edit", "isMeasurement": true },1105"isActiveDocument": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the document is the active document", "isMeasurement": true },1106"hasNotebookCellMarker": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the edit has a notebook cell marker", "isMeasurement": true },1107"notebookCellMarkerCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Count of notebook cell markers in the edit", "isMeasurement": true },1108"notebookId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Id of notebook" },1109"notebookCellLines": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Line counts of notebook cells" },1110"notebookType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Type of notebook, if any" },1111"logProbThreshold": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Log probability threshold for the edit", "isMeasurement": true },1112"documentsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of documents", "isMeasurement": true },1113"editsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of edits", "isMeasurement": true },1114"activeDocumentEditsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of edits in the active document", "isMeasurement": true },1115"promptLineCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of lines in the prompt", "isMeasurement": true },1116"promptCharCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of characters in the prompt", "isMeasurement": true },1117"nDiffsInPrompt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of diffs included in the prompt", "isMeasurement": true },1118"diffTokensInPrompt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens consumed by diffs in the prompt", "isMeasurement": true },1119"nNeighborSnippetsComputed": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Total number of neighbor (similar files) snippets computed before budget filtering", "isMeasurement": true },1120"nNeighborSnippetsInPrompt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of neighbor (similar files) snippets actually included in the prompt", "isMeasurement": true },1121"neighborSnippetIndicesInPrompt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "JSON-encoded array of original input indices (ascending) of neighbor snippets included in the prompt" },1122"hadLowLogProbSuggestion": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the suggestion had low log probability", "isMeasurement": true },1123"nEditsSuggested": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of edits suggested", "isMeasurement": true },1124"hasNextEdit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether next edit provider returned an edit (if an edit was previously rejected, this field is false)", "isMeasurement": true },1125"nextEditLogprob": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Log probability of the next edit", "isMeasurement": true },1126"lineDistanceToMostRecentEdit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Line distance to most recent edit", "isMeasurement": true },1127"isCursorAtEndOfLine": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the cursor is at the end of the line", "isMeasurement": true },1128"isInlineSuggestion": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the cursor is at a valid inline suggestion position (middle of line with valid trailing characters)", "isMeasurement": true },1129"debounceTime": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Debounce time", "isMeasurement": true },1130"artificialDelay": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Artificial delay (aka backoff) on the response based on previous user acceptance/rejection in milliseconds", "isMeasurement": true },1131"fetchStartedAfterMs": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Time from inline edit provider invocation to fetch init", "isMeasurement": true },1132"ttft": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Time to first token", "isMeasurement": true },1133"fetchTime": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Time from fetch init to end of stream", "isMeasurement": true },1134"promptTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the prompt", "isMeasurement": true },1135"responseTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the response", "isMeasurement": true },1136"cachedTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of cached tokens in the response", "isMeasurement": true },1137"acceptedPredictionTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the prediction that appeared in the completion", "isMeasurement": true },1138"rejectedPredictionTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the prediction that appeared in the completion", "isMeasurement": true },1139"hadDiagnosticsNES": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request had diagnostics NES", "isMeasurement": true },1140"hadLlmNES": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request had LLM NES", "isMeasurement": true },1141"configIsDiagnosticsNESEnabled": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether diagnostics NES is enabled", "isMeasurement": true },1142"isNaturalLanguageDominated": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the context is dominated by natural language", "isMeasurement": true },1143"diagnosticHasExistingSameFileImport": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the diagnostic has an existing same file import", "isMeasurement": true },1144"diagnosticIsLocalImport": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the diagnostic is a local import", "isMeasurement": true },1145"diagnosticAlternativeImportsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of alternative imports for the diagnostic", "isMeasurement": true },1146"diagnosticDistanceToUnknownDiagnostic": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Distance to the unknown diagnostic", "isMeasurement": true },1147"diagnosticDistanceToAlternativeDiagnostic": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Distance to the alternative diagnostic", "isMeasurement": true },1148"diagnosticHasAlternativeDiagnosticForSameRange": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether there is an alternative diagnostic for the same range", "isMeasurement": true },1149"nextCursorLineDistance": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Distance from next cursor line to current cursor line: newCursorLineNumber - currentCursorLineNumber", "isMeasurement": true },1150"nextCursorLineError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Error in the predicted next cursor line" },1151"xtabAggressivenessLevel": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The aggressiveness level used for xtabAggressiveness prompting strategy (low, medium, high)" },1152"userAggressivenessSetting": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The raw user-facing aggressiveness setting value (only set when user changed from default)" },1153"xtabUserHappinessScore": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "User happiness score (0-1) when using xtabAggressiveness prompting strategy", "isMeasurement": true },1154"userTypingDisagreed": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the user typing disagreed with the suggestion", "isMeasurement": true },1155"modelConfig": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "JSON-encoded model configuration from the model service" }1156}1157*/1158this._sendTelemetryToBoth(1159{1160opportunityId,1161headerRequestId,1162providerId,1163modelName,1164activeDocumentLanguageId,1165mergeConflictExpanded: telemetry.mergeConflictExpanded,1166acceptance,1167disposalReason,1168supersededByOpportunityId,1169noNextEditReasonKind,1170noNextEditReasonMessage,1171fetchResult: fetchResult_,1172nextEditProviderError: telemetry.nextEditProviderError,1173reusedRequest,1174diagnosticType,1175diagnosticDroppedReasons,1176pickedNES,1177notebookType,1178notebookId,1179notebookCellLines,1180nextCursorLineError: telemetry.nextCursorPrediction?.nextCursorLineError,1181xtabAggressivenessLevel,1182userAggressivenessSetting,1183modelConfig,1184neighborSnippetIndicesInPrompt: telemetry.neighborSnippetIndicesInPrompt,1185},1186{1187requestN,1188hadStatelessNextEditProviderCall: this._boolToNum(hadStatelessNextEditProviderCall),1189statelessNextEditProviderDuration,1190nextEditProviderDuration,1191isFromCache: this._boolToNum(isFromCache),1192subsequentEditOrder,1193activeDocumentOriginalLineCount,1194activeDocumentNLinesInPrompt: nLinesOfCurrentFileInPrompt,1195wasPreviouslyRejected: this._boolToNum(wasPreviouslyRejected),1196isShown: this._boolToNum(isShown),1197isNotebook: this._boolToNum(isNotebook),1198isNESForAnotherDoc: this._boolToNum(isNESForAnotherDoc),1199isActiveDocument: this._boolToNum(isActiveDocument),1200isEolDifferent: this._boolToNum(isEolDifferent),1201isMultilineEdit: this._boolToNum(isMultilineEdit),1202isNextEditorRangeVisible: this._boolToNum(isNextEditorRangeVisible),1203isNextEditorVisible: this._boolToNum(isNextEditorVisible),1204hasNotebookCellMarker: notebookCellMarkerCount > 0 ? 1 : 0,1205notebookCellMarkerCount,1206notebookCellMarkerIndex,1207logProbThreshold,1208documentsCount,1209editsCount,1210activeDocumentEditsCount,1211promptLineCount,1212promptCharCount,1213hadLowLogProbSuggestion: this._boolToNum(hadLowLogProbSuggestion),1214nEditsSuggested,1215lineDistanceToMostRecentEdit,1216isCursorAtEndOfLine: this._boolToNum(isCursorAtEndOfLine),1217isInlineSuggestion: this._boolToNum(isInlineSuggestion),1218debounceTime,1219artificialDelay,1220fetchStartedAfterMs,1221ttft: ttft_,1222fetchTime: fetchTime_,1223promptTokens: usage?.prompt_tokens,1224responseTokens: usage?.completion_tokens,1225cachedTokens: usage?.prompt_tokens_details?.cached_tokens,1226acceptedPredictionTokens: usage?.completion_tokens_details?.accepted_prediction_tokens,1227rejectedPredictionTokens: usage?.completion_tokens_details?.rejected_prediction_tokens,1228hasNextEdit: this._boolToNum(hasNextEdit),1229userTypingDisagreed: this._boolToNum(telemetry.userTypingDisagreed),1230nextEditLogprob,1231hadDiagnosticsNES: this._boolToNum(hadDiagnosticsNES),1232hadLlmNES: this._boolToNum(hadLlmNES),1233configIsDiagnosticsNESEnabled: this._boolToNum(configIsDiagnosticsNESEnabled),1234isNaturalLanguageDominated: this._boolToNum(isNaturalLanguageDominated),1235diagnosticHasExistingSameFileImport: this._boolToNum(diagnosticHasExistingSameFileImport),1236diagnosticIsLocalImport: this._boolToNum(diagnosticIsLocalImport),1237diagnosticAlternativeImportsCount: diagnosticAlternativeImportsCount,1238diagnosticDistanceToUnknownDiagnostic: diagnosticDistanceToUnknownDiagnostic,1239diagnosticDistanceToAlternativeDiagnostic: diagnosticDistanceToAlternativeDiagnostic,1240diagnosticHasAlternativeDiagnosticForSameRange: this._boolToNum(diagnosticHasAlternativeDiagnosticForSameRange),1241nextCursorLineDistance: telemetry.nextCursorPrediction?.nextCursorLineDistance,1242xtabUserHappinessScore,1243nDiffsInPrompt: telemetry.nDiffsInPrompt,1244diffTokensInPrompt: telemetry.diffTokensInPrompt,1245nNeighborSnippetsComputed: telemetry.nNeighborSnippetsComputed,1246nNeighborSnippetsInPrompt: telemetry.nNeighborSnippetsInPrompt,1247}1248);1249}12501251private _sendTelemetryToBoth(properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements): void {1252this._telemetryService.sendMSFTTelemetryEvent('provideInlineEdit', properties, measurements);1253this._telemetryService.sendGHTelemetryEvent('copilot-nes/provideInlineEdit', properties, measurements);1254}12551256private async _doSendEnhancedTelemetry(telemetry: INextEditProviderTelemetry, sendingReason: IEnhancedTelemetrySendingReason | undefined): Promise<void> {12571258const {1259opportunityId,1260headerRequestId,1261providerId,1262activeDocumentLanguageId,1263status: suggestionStatus,1264modelName,1265prompt,1266response,1267alternativeAction,1268postProcessingOutcome,1269activeDocumentRepository,1270repositoryUrls,1271cursorJumpModelName,1272cursorJumpPrompt,1273cursorJumpResponse,1274lintErrors,1275terminalOutput,1276similarFilesContext,1277modelConfig,1278isFromCache,1279} = telemetry;12801281const modelResponse = response === undefined ? response : await response;1282const resolvedSimilarFilesContext = await similarFilesContext?.catch(() => undefined);12831284this._telemetryService.sendEnhancedGHTelemetryEvent('copilot-nes/provideInlineEdit',1285multiplexProperties({1286opportunityId,1287headerRequestId,1288providerId,1289activeDocumentLanguageId,1290suggestionStatus,1291modelName,1292prompt,1293modelResponse: modelResponse === undefined || modelResponse.response.type !== ChatFetchResponseType.Success ? undefined : modelResponse.response.value,1294alternativeAction: alternativeAction ? JSON.stringify({ ...alternativeAction, enhancedTelemetrySendingReason: sendingReason }) : undefined,1295enhancedTelemetrySendingReason: !alternativeAction && sendingReason ? JSON.stringify(sendingReason) : undefined,1296postProcessingOutcome,1297activeDocumentRepository,1298repositories: JSON.stringify(repositoryUrls),1299cursorJumpModelName,1300cursorJumpPrompt,1301cursorJumpResponse,1302lintErrors,1303terminalOutput,1304similarFilesContext: resolvedSimilarFilesContext,1305modelConfig,1306}),1307{1308isFromCache: this._boolToNum(isFromCache),1309}1310);1311}13121313/**1314* If `value` is undefined, return undefined, otherwise return 1 if `value` is true, 0 otherwise.1315*/1316private _boolToNum(value: boolean | undefined): number | undefined {1317return value === undefined ? undefined : (value ? 1 : 0);1318}13191320dispose(): void {1321for (const data of this._map.values()) {1322clearTimeout(data.timeout);1323if (data.hardCapTimeout !== undefined) {1324clearTimeout(data.hardCapTimeout);1325}1326data.builder.dispose();1327}1328this._map.clear();13291330if (this._idleDetector) {1331this._idleDetector.forceDispose();1332this._idleDetector = undefined;1333}1334}1335}133613371338