Path: blob/main/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.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 { URI } from '../../../../base/common/uri.js';6import { isLocation } from '../../../../editor/common/languages.js';7import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';8import { IChatAgentData } from './chatAgents.js';9import { ChatRequestModel, IChatRequestVariableData } from './chatModel.js';10import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart } from './chatParserTypes.js';11import { ChatAgentVoteDirection, ChatCopyKind, IChatSendRequestOptions, IChatUserActionEvent } from './chatService.js';12import { isImageVariableEntry } from './chatVariableEntries.js';13import { ChatAgentLocation } from './constants.js';14import { ILanguageModelsService } from './languageModels.js';1516type ChatVoteEvent = {17direction: 'up' | 'down';18agentId: string;19command: string | undefined;20reason: string | undefined;21};2223type ChatVoteClassification = {24direction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user voted up or down.' };25agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this vote is for.' };26command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the slash command that this vote is for.' };27reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason selected by the user for voting down.' };28owner: 'roblourens';29comment: 'Provides insight into the performance of Chat agents.';30};3132type ChatCopyEvent = {33copyKind: 'action' | 'toolbar';34agentId: string;35command: string | undefined;36};3738type ChatCopyClassification = {39copyKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the copy was initiated.' };40agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that the copy acted on.' };41command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the slash command the copy acted on.' };42owner: 'roblourens';43comment: 'Provides insight into the usage of Chat features.';44};4546type ChatInsertEvent = {47newFile: boolean;48agentId: string;49command: string | undefined;50};5152type ChatInsertClassification = {53newFile: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the code was inserted into a new untitled file.' };54agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this insertion is for.' };55command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the slash command that this insertion is for.' };56owner: 'roblourens';57comment: 'Provides insight into the usage of Chat features.';58};5960type ChatApplyEvent = {61newFile: boolean;62agentId: string;63command: string | undefined;64codeMapper: string | undefined;65editsProposed: boolean;66};6768type ChatApplyClassification = {69newFile: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the code was inserted into a new untitled file.' };70agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this insertion is for.' };71command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the slash command that this insertion is for.' };72codeMapper: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The code mapper that wa used to compute the edit.' };73editsProposed: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether there was a change proposed to the user.' };74owner: 'aeschli';75comment: 'Provides insight into the usage of Chat features.';76};7778type ChatFollowupEvent = {79agentId: string;80command: string | undefined;81};8283type ChatFollowupClassification = {84agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' };85command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the related slash command.' };86owner: 'roblourens';87comment: 'Provides insight into the usage of Chat features.';88};8990type ChatTerminalEvent = {91languageId: string;92agentId: string;93command: string | undefined;94};9596type ChatTerminalClassification = {97languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language of the code that was run in the terminal.' };98agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' };99command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the related slash command.' };100owner: 'roblourens';101comment: 'Provides insight into the usage of Chat features.';102};103104type ChatFollowupsRetrievedEvent = {105agentId: string;106command: string | undefined;107numFollowups: number;108};109110type ChatFollowupsRetrievedClassification = {111agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' };112command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the related slash command.' };113numFollowups: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of followup prompts returned by the agent.' };114owner: 'roblourens';115comment: 'Provides insight into the usage of Chat features.';116};117118type ChatEditHunkEvent = {119agentId: string;120outcome: 'accepted' | 'rejected';121lineCount: number;122hasRemainingEdits: boolean;123};124125type ChatEditHunkClassification = {126agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' };127outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The outcome of the edit hunk action.' };128lineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of lines in the relevant change.' };129hasRemainingEdits: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether there are remaining edits in the file after this action.' };130owner: 'roblourens';131comment: 'Provides insight into the usage of Chat features.';132};133134export type ChatProviderInvokedEvent = {135timeToFirstProgress: number | undefined;136totalTime: number | undefined;137result: 'success' | 'error' | 'errorWithOutput' | 'cancelled' | 'filtered';138requestType: 'string' | 'followup' | 'slashCommand';139chatSessionId: string;140agent: string;141agentExtensionId: string | undefined;142slashCommand: string | undefined;143location: ChatAgentLocation;144citations: number;145numCodeBlocks: number;146isParticipantDetected: boolean;147enableCommandDetection: boolean;148attachmentKinds: string[];149model: string | undefined;150};151152export type ChatProviderInvokedClassification = {153timeToFirstProgress: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The time in milliseconds from invoking the provider to getting the first data.' };154totalTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The total time it took to run the provider\'s `provideResponseWithProgress`.' };155result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether invoking the ChatProvider resulted in an error.' };156requestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of request that the user made.' };157chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A random ID for the session.' };158agent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of agent used.' };159agentExtensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension that contributed the agent.' };160slashCommand?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of slashCommand used.' };161location: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The location at which chat request was made.' };162citations: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of public code citations that were returned with the response.' };163numCodeBlocks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of code blocks in the response.' };164isParticipantDetected: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the participant was automatically detected.' };165enableCommandDetection: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether participation detection was disabled for this invocation.' };166attachmentKinds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The types of variables/attachments that the user included with their query.' };167model: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The model used to generate the response.' };168owner: 'roblourens';169comment: 'Provides insight into the performance of Chat agents.';170};171172export class ChatServiceTelemetry {173constructor(174@ITelemetryService private readonly telemetryService: ITelemetryService,175) { }176177notifyUserAction(action: IChatUserActionEvent): void {178if (action.action.kind === 'vote') {179this.telemetryService.publicLog2<ChatVoteEvent, ChatVoteClassification>('interactiveSessionVote', {180direction: action.action.direction === ChatAgentVoteDirection.Up ? 'up' : 'down',181agentId: action.agentId ?? '',182command: action.command,183reason: action.action.reason,184});185} else if (action.action.kind === 'copy') {186this.telemetryService.publicLog2<ChatCopyEvent, ChatCopyClassification>('interactiveSessionCopy', {187copyKind: action.action.copyKind === ChatCopyKind.Action ? 'action' : 'toolbar',188agentId: action.agentId ?? '',189command: action.command,190});191} else if (action.action.kind === 'insert') {192this.telemetryService.publicLog2<ChatInsertEvent, ChatInsertClassification>('interactiveSessionInsert', {193newFile: !!action.action.newFile,194agentId: action.agentId ?? '',195command: action.command,196});197} else if (action.action.kind === 'apply') {198this.telemetryService.publicLog2<ChatApplyEvent, ChatApplyClassification>('interactiveSessionApply', {199newFile: !!action.action.newFile,200codeMapper: action.action.codeMapper,201agentId: action.agentId ?? '',202command: action.command,203editsProposed: !!action.action.editsProposed,204});205} else if (action.action.kind === 'runInTerminal') {206this.telemetryService.publicLog2<ChatTerminalEvent, ChatTerminalClassification>('interactiveSessionRunInTerminal', {207languageId: action.action.languageId ?? '',208agentId: action.agentId ?? '',209command: action.command,210});211} else if (action.action.kind === 'followUp') {212this.telemetryService.publicLog2<ChatFollowupEvent, ChatFollowupClassification>('chatFollowupClicked', {213agentId: action.agentId ?? '',214command: action.command,215});216} else if (action.action.kind === 'chatEditingHunkAction') {217this.telemetryService.publicLog2<ChatEditHunkEvent, ChatEditHunkClassification>('chatEditHunk', {218agentId: action.agentId ?? '',219outcome: action.action.outcome,220lineCount: action.action.lineCount,221hasRemainingEdits: action.action.hasRemainingEdits,222});223}224}225226retrievedFollowups(agentId: string, command: string | undefined, numFollowups: number): void {227this.telemetryService.publicLog2<ChatFollowupsRetrievedEvent, ChatFollowupsRetrievedClassification>('chatFollowupsRetrieved', {228agentId,229command,230numFollowups,231});232}233}234235function getCodeBlocks(text: string): string[] {236const lines = text.split('\n');237const codeBlockLanguages: string[] = [];238239let codeBlockState: undefined | { readonly delimiter: string; readonly languageId: string };240for (let i = 0; i < lines.length; i++) {241const line = lines[i];242243if (codeBlockState) {244if (new RegExp(`^\\s*${codeBlockState.delimiter}\\s*$`).test(line)) {245codeBlockLanguages.push(codeBlockState.languageId);246codeBlockState = undefined;247}248} else {249const match = line.match(/^(\s*)(`{3,}|~{3,})(\w*)/);250if (match) {251codeBlockState = { delimiter: match[2], languageId: match[3] };252}253}254}255return codeBlockLanguages;256}257258export class ChatRequestTelemetry {259private isComplete = false;260261constructor(private readonly opts: {262agentPart: ChatRequestAgentPart | undefined;263agentSlashCommandPart: ChatRequestAgentSubcommandPart | undefined;264commandPart: ChatRequestSlashCommandPart | undefined;265sessionId: string;266location: ChatAgentLocation;267options: IChatSendRequestOptions | undefined;268enableCommandDetection: boolean;269},270@ITelemetryService private readonly telemetryService: ITelemetryService,271@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService272) { }273274complete({ timeToFirstProgress, totalTime, result, requestType, request, detectedAgent }: {275timeToFirstProgress: number | undefined;276totalTime: number | undefined;277result: ChatProviderInvokedEvent['result'];278requestType: ChatProviderInvokedEvent['requestType'];279// Should rearrange so these 2 can be in the constructor280request: ChatRequestModel;281detectedAgent: IChatAgentData | undefined;282}) {283if (this.isComplete) {284return;285}286287this.isComplete = true;288this.telemetryService.publicLog2<ChatProviderInvokedEvent, ChatProviderInvokedClassification>('interactiveSessionProviderInvoked', {289timeToFirstProgress,290totalTime,291result,292requestType,293agent: detectedAgent?.id ?? this.opts.agentPart?.agent.id ?? '',294agentExtensionId: detectedAgent?.extensionId.value ?? this.opts.agentPart?.agent.extensionId.value ?? '',295slashCommand: this.opts.agentSlashCommandPart ? this.opts.agentSlashCommandPart.command.name : this.opts.commandPart?.slashCommand.command,296chatSessionId: this.opts.sessionId,297enableCommandDetection: this.opts.enableCommandDetection,298isParticipantDetected: !!detectedAgent,299location: this.opts.location,300citations: request.response?.codeCitations.length ?? 0,301numCodeBlocks: getCodeBlocks(request.response?.response.toString() ?? '').length,302attachmentKinds: this.attachmentKindsForTelemetry(request.variableData),303model: this.resolveModelId(this.opts.options?.userSelectedModelId),304});305}306307private attachmentKindsForTelemetry(variableData: IChatRequestVariableData): string[] {308// this shows why attachments still have to be cleaned up somewhat309return variableData.variables.map(v => {310if (v.kind === 'implicit') {311return 'implicit';312} else if (v.range) {313// 'range' is range within the prompt text314if (v.kind === 'tool') {315return 'toolInPrompt';316} else if (v.kind === 'toolset') {317return 'toolsetInPrompt';318} else {319return 'fileInPrompt';320}321} else if (v.kind === 'command') {322return 'command';323} else if (v.kind === 'symbol') {324return 'symbol';325} else if (isImageVariableEntry(v)) {326return 'image';327} else if (v.kind === 'directory') {328return 'directory';329} else if (v.kind === 'tool') {330return 'tool';331} else if (v.kind === 'toolset') {332return 'toolset';333} else {334if (URI.isUri(v.value)) {335return 'file';336} else if (isLocation(v.value)) {337return 'location';338} else {339return 'otherAttachment';340}341}342});343}344345private resolveModelId(userSelectedModelId: string | undefined): string | undefined {346return userSelectedModelId && this.languageModelsService.lookupLanguageModel(userSelectedModelId)?.id;347}348}349350351