Path: blob/main/extensions/copilot/src/platform/multiFileEdit/common/multiFileEditQualityTelemetry.ts
13401 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 type { NotebookDocument, TextDocument, Uri } from 'vscode';6import { createServiceIdentifier } from '../../../util/common/services';7import { Disposable } from '../../../util/vs/base/common/lifecycle';8import { ResourceMap } from '../../../util/vs/base/common/map';9import { IChatSessionService } from '../../chat/common/chatSessionService';10import { IGitService } from '../../git/common/gitService';11import { ILogService } from '../../log/common/logService';12import { IAlternativeNotebookContentService } from '../../notebook/common/alternativeContent';13import { INotebookService } from '../../notebook/common/notebookService';14import { resolveWorkspaceOTelMetadata } from '../../otel/common/workspaceOTelMetadata';15import { ITelemetryService, multiplexProperties } from '../../telemetry/common/telemetry';16import { IWorkspaceService } from '../../workspace/common/workspaceService';1718export interface IMultiFileEdit {19readonly isAgent?: boolean;20readonly uri: Uri;21readonly prompt: string;22readonly document?: TextDocument | NotebookDocument;23}2425export interface IMultiFileEditRequestInfo {26readonly chatRequestId: string;27}2829export interface IMultiFileEditTelemetry {30readonly mapper: string;31readonly chatSessionId?: string;32readonly chatRequestId: string;33readonly speculationRequestId: string;34}3536export const IMultiFileEditInternalTelemetryService = createServiceIdentifier<IMultiFileEditInternalTelemetryService>('IMultiFileEditInternalTelemetryService');37export interface IMultiFileEditInternalTelemetryService {38_serviceBrand: undefined;39/**40* Store telemetry info for a multi-file edit41*/42storeEditPrompt(edit: IMultiFileEdit, telemetryOptions: IMultiFileEditTelemetry): void;43/**44* Send a telemetry event with the outcome of a multi-file edit45* @param chatRequestId The chat request id of the multi-file edit46* @param uri The uri of the file that was accepted47* Note: we do NOT track partial accepts and rejects48*/49sendEditPromptAndResult(telemetry: IMultiFileEditRequestInfo, uri: Uri, outcome: 'accept' | 'reject'): Promise<void>;50}5152export class MultiFileEditInternalTelemetryService extends Disposable implements IMultiFileEditInternalTelemetryService {5354declare _serviceBrand: undefined;5556// URI -> chatResponseId -> edits57private readonly editedFiles = new ResourceMap<Map<string, (IMultiFileEdit & IMultiFileEditTelemetry)[]>>();58// sessionId -> (URI -> TextDocument | NotebookDocument)59private readonly editedDocuments = new Map<string, ResourceMap<TextDocument | NotebookDocument>>();6061constructor(62@ITelemetryService private readonly telemetryService: ITelemetryService,63@IWorkspaceService private readonly workspaceService: IWorkspaceService,64@INotebookService private readonly notebookService: INotebookService,65@ILogService private readonly logService: ILogService,66@IAlternativeNotebookContentService private readonly alternativeNotebookContent: IAlternativeNotebookContentService,67@IChatSessionService private readonly chatSessionService: IChatSessionService,68@IGitService private readonly gitService: IGitService,69) {70super();71this._register(this.chatSessionService.onDidDisposeChatSession(sessionId => {72this.editedDocuments.delete(sessionId);73}));74}7576storeEditPrompt(edit: IMultiFileEdit, telemetryOptions: IMultiFileEditTelemetry): void {77this.logService.debug(`Storing edit prompt for ${edit.uri.toString()} with request ID ${telemetryOptions.chatRequestId}`);7879const existingEditsForUri = this.editedFiles.get(edit.uri) ?? new Map();80const existingEditsForUriInRequest = existingEditsForUri.get(telemetryOptions.chatRequestId) ?? [];81existingEditsForUriInRequest.push({ ...edit, ...telemetryOptions });82existingEditsForUri.set(telemetryOptions.chatRequestId, existingEditsForUriInRequest);83this.editedFiles.set(edit.uri, existingEditsForUri);84if (edit.document && telemetryOptions.chatSessionId) {85let sessionMap = this.editedDocuments.get(telemetryOptions.chatSessionId);86if (!sessionMap) {87sessionMap = new ResourceMap<TextDocument | NotebookDocument>();88this.editedDocuments.set(telemetryOptions.chatSessionId, sessionMap);89}90sessionMap.set(edit.uri, edit.document);91}92}9394async sendEditPromptAndResult(telemetry: IMultiFileEditRequestInfo, uri: Uri, outcome: 'accept' | 'reject'): Promise<void> {95const editsForUri = this.editedFiles.get(uri);96if (!editsForUri) {97return;98}99if (editsForUri.size > 1) {100// Multiple edit turns have affected this file101// i.e. edit -> edit -> accept/reject102// Skip sending telemetry for files which originated from multiple SD prompts103// and reset our tracking104this.logService.debug(`Skipping telemetry for ${uri.toString()} with request ID ${telemetry.chatRequestId} due to multiple edit turns`);105this.editedFiles.delete(uri);106return;107}108109const editsForUriInChatRequest = editsForUri.get(telemetry.chatRequestId);110if (!editsForUriInChatRequest) {111return;112}113114if (editsForUriInChatRequest.length > 1) {115// This file has been edited twice in one edit turn,116// which can happen if the LLM iterates on a file in agentic edit mode117// and can also happen when the LLM ignores instructions in non-agentic edits.118// Again, skip sending telemetry for files which originated from multiple SD prompts119// and reset our tracking120this.logService.debug(`Skipping telemetry for ${uri.toString()} with request ID ${telemetry.chatRequestId} due to multiple edits in one turn`);121this.editedFiles.delete(uri);122return;123}124125try {126const edit = editsForUriInChatRequest[0];127128// NOTE: this may not be what's on disk, but should reflect the outcome of accepting/rejecting129// regardless of whether the user is an autosave user / has saved the edits by now130let languageId: string | undefined = undefined;131let documentText: string | undefined = undefined;132if (edit.chatSessionId) {133const editedDocument = this.editedDocuments.get(edit.chatSessionId)?.get(uri);134if (editedDocument && 'getText' in editedDocument) {135languageId = editedDocument.languageId;136documentText = editedDocument.getText();137}138}139if (!documentText && !languageId) {140if (this.notebookService.hasSupportedNotebooks(uri)) {141const snapshot = await this.workspaceService.openNotebookDocumentAndSnapshot(uri, this.alternativeNotebookContent.getFormat(undefined));142languageId ??= snapshot.languageId;143documentText ??= snapshot.getText();144}145else {146const textDocument = await this.workspaceService.openTextDocument(uri);147languageId = textDocument.languageId;148documentText = textDocument.getText();149}150}151152this.telemetryService.sendInternalMSFTTelemetryEvent('multiFileEditQuality',153{154requestId: telemetry.chatRequestId,155speculationRequestId: edit.speculationRequestId,156// NOTE: for now this will always be false because in agent mode the edits are invoked via the MappedEditsProvider, so we lose the turn ID157isAgent: String(edit.isAgent),158outcome,159prompt: edit.prompt,160languageId,161file: documentText, // Note that this is not necessarily the same as the model output because the user may have made manual edits162mapper: edit.mapper163},164{165isNotebook: this.notebookService.hasSupportedNotebooks(uri) ? 1 : 0166}167);168169const workspace = resolveWorkspaceOTelMetadata(this.gitService, uri);170const gitHubEnhancedTelemetryProperties = multiplexProperties({171headerRequestId: edit.speculationRequestId,172providerId: edit.mapper,173languageId: languageId,174messageText: edit.prompt,175suggestion: outcome,176completionTextJson: documentText, // Note that this is not necessarily the same as the model output because the user may have made manual edits177conversationId: edit.chatSessionId,178messageId: edit.chatRequestId,179headBranchName: workspace.headBranchName,180headCommitHash: workspace.headCommitHash,181remoteUrl: workspace.remoteUrl,182fileRelativePath: workspace.fileRelativePath,183});184this.telemetryService.sendEnhancedGHTelemetryEvent('fastApply/editOutcome', gitHubEnhancedTelemetryProperties);185this.logService.debug(`Sent telemetry for ${uri.toString()} with request ID ${edit.chatRequestId}, SD request ID ${edit.speculationRequestId}, and outcome ${outcome}`);186} catch (e) {187this.logService.error('Error sending multi-file edit telemetry', JSON.stringify(e));188} finally {189this.editedFiles.delete(uri);190}191}192}193194195