Path: blob/main/extensions/copilot/src/extension/prompt/node/telemetry.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 type { TextDocument } from 'vscode';6import { ChatLocation } from '../../../platform/chat/common/commonTypes';7import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';8import { ITelemetryService, TelemetryProperties } from '../../../platform/telemetry/common/telemetry';9import { TelemetryData } from '../../../platform/telemetry/common/telemetryData';10import { generateUuid } from '../../../util/vs/base/common/uuid';11import { Conversation } from '../common/conversation';1213export type ConversationalBaseTelemetryData = ConversationalTelemetryData<{ messageId: string }, { promptTokenLen: number; messageCharLen: number }>;1415export function createTelemetryWithId(): ConversationalBaseTelemetryData {16const uniqueId = generateUuid();17const baseTelemetry = TelemetryData.createAndMarkAsIssued({ messageId: uniqueId });18return new ConversationalTelemetryData(baseTelemetry);19}2021export class ConversationalTelemetryData<P extends TelemetryProperties, M extends { [key: string]: number }> {2223public get properties(): P { return this.raw.properties as P; }24public get measurements(): M { return this.raw.measurements as M; }2526constructor(27public readonly raw: TelemetryData28) { }2930markAsDisplayed(): void {31this.raw.markAsDisplayed();32}3334extendedBy<P2 extends TelemetryProperties, M2 extends { [key: string]: number }>(properties?: P2, measurements?: M2): ConversationalTelemetryData<P & P2, M & M2> {35const newTelemetryData = this.raw.extendedBy(properties, measurements);36return new ConversationalTelemetryData(newTelemetryData);37}38}3940export function extendUserMessageTelemetryData(41conversation: Conversation,42conversationId: string,43location: ChatLocation,44message: string,45promptTokenLen: number,46suggestion: string | undefined,47baseTelemetry: ConversationalBaseTelemetryData48): ConversationalBaseTelemetryData {4950const properties: TelemetryProperties = {51source: 'user',52turnIndex: (conversation.turns.length - 1).toString(),53conversationId,54uiKind: ChatLocation.toString(location)55};56const measurements = {57promptTokenLen: promptTokenLen,58messageCharLen: message.length,59};60if (suggestion) {61properties.suggestion = suggestion;62}6364baseTelemetry = baseTelemetry.extendedBy(properties, measurements);6566return baseTelemetry;67}6869export function sendUserMessageTelemetry(70telemetryService: ITelemetryService,71location: ChatLocation,72requestId: string,73message: string | undefined,74offTopic: boolean | undefined,75doc: TextDocumentSnapshot | undefined,76baseTelemetry: ConversationalBaseTelemetryData,77modeName: string,78): void {79if (offTopic !== undefined) {80baseTelemetry = baseTelemetry.extendedBy({ offTopic: offTopic.toString() });81}82baseTelemetry = baseTelemetry.extendedBy({ headerRequestId: requestId });83sendConversationalMessageTelemetry(telemetryService, doc, location, message, { mode: modeName }, {}, baseTelemetry);84}8586export function sendModelMessageTelemetry(87telemetryService: ITelemetryService,88conversation: Conversation,89location: ChatLocation,90appliedText: string,91requestId: string,92doc: TextDocumentSnapshot | undefined,93baseTelemetry: ConversationalBaseTelemetryData,94modeName: string,95): void {96// Get the languages of code blocks within the message97const codeBlockLanguages = getCodeBlocks(appliedText);9899sendConversationalMessageTelemetry(100telemetryService,101doc,102location,103appliedText,104{105source: 'model',106turnIndex: conversation.turns.length.toString(),107conversationId: conversation.sessionId,108headerRequestId: requestId,109uiKind: ChatLocation.toString(location),110codeBlockLanguages: JSON.stringify({ ...codeBlockLanguages }),111mode: modeName,112},113{ messageCharLen: appliedText.length, numCodeBlocks: codeBlockLanguages.length },114baseTelemetry115);116}117118export function sendOffTopicMessageTelemetry(119telemetryService: ITelemetryService,120conversation: Conversation,121location: ChatLocation,122appliedText: string,123userMessageId: string,124doc: TextDocumentSnapshot | undefined,125baseTelemetry: ConversationalBaseTelemetryData126): void {127sendConversationalMessageTelemetry(128telemetryService,129doc,130location,131appliedText,132{133source: 'offTopic',134turnIndex: conversation.turns.length.toString(),135conversationId: conversation.sessionId,136userMessageId: userMessageId,137uiKind: ChatLocation.toString(location),138},139{ messageCharLen: appliedText.length },140baseTelemetry141);142}143144/** Create new telemetry data based on baseTelemetryData and send `conversation.message` event */145export function sendConversationalMessageTelemetry(146telemetryService: ITelemetryService,147document: TextDocumentSnapshot | undefined,148location: ChatLocation,149messageText: string | undefined,150properties: TelemetryProperties,151measurements: { [key: string]: number },152baseTelemetry: ConversationalBaseTelemetryData153): TelemetryData {154155const enhancedProperties: { [key: string]: string } = {156...(messageText ? { messageText: messageText } : {}),157...properties,158};159160if (document) {161properties.languageId = document.languageId;162measurements.documentLength = document.getText().length;163}164165const standardTelemetryData = baseTelemetry.extendedBy(properties, measurements);166const enhancedTelemetryLogger = baseTelemetry.extendedBy(enhancedProperties);167168// Telemetrize the message in standard and enhanced telemetry169// Enhanced telemetry will not be sent if the user isn't opted in, same as for ghostText170const prefix = telemetryPrefixForLocation(location);171172telemetryService.sendGHTelemetryEvent(`${prefix}.message`, standardTelemetryData.raw.properties, standardTelemetryData.raw.measurements);173telemetryService.sendEnhancedGHTelemetryEvent(`${prefix}.messageText`, enhancedTelemetryLogger.raw.properties, enhancedTelemetryLogger.raw.measurements);174telemetryService.sendInternalMSFTTelemetryEvent(`${prefix}.messageText`, enhancedTelemetryLogger.raw.properties, enhancedTelemetryLogger.raw.measurements);175176return standardTelemetryData.raw;177}178179export function sendSuggestionShownTelemetryData(180telemetryService: ITelemetryService,181suggestion: string,182messageId: string,183suggestionId: string,184doc: TextDocument | TextDocumentSnapshot | undefined185): TelemetryData {186const telemetryData = sendUserActionTelemetry(187telemetryService,188doc,189{190suggestion: suggestion,191messageId: messageId,192suggestionId: suggestionId,193},194{},195'conversation.suggestionShown'196);197return telemetryData;198}199200/** Create new telemetry data based on baseTelemetryData and send event with name */201export function sendUserActionTelemetry(202telemetryService: ITelemetryService,203document: TextDocument | TextDocumentSnapshot | undefined,204properties: TelemetryProperties,205measurements: { [key: string]: number },206name: string,207baseTelemetry?: TelemetryData208): TelemetryData {209const telemetryData = baseTelemetry ?? TelemetryData.createAndMarkAsIssued();210211if (document) {212properties.languageId = document.languageId;213measurements.documentLength = document.getText().length;214}215216const standardTelemetryData = telemetryData.extendedBy(properties, measurements);217telemetryService.sendGHTelemetryEvent(name, standardTelemetryData.properties, standardTelemetryData.measurements);218219return standardTelemetryData;220}221222function telemetryPrefixForLocation(location: ChatLocation): string {223switch (location) {224case ChatLocation.Editor:225return 'inlineConversation';226case ChatLocation.EditingSession:227return 'editingSession';228case ChatLocation.Panel:229default:230return 'conversation';231}232}233234export interface ICodeblockDetails {235readonly languageId: string;236readonly totalLines: number;237}238239export function getCodeBlocks(text: string): ICodeblockDetails[] {240const lines = text.split('\n');241const codeBlocks: ICodeblockDetails[] = [];242243let codeBlockState: undefined | {244readonly delimiter: string;245readonly languageId: string;246totalLines: number;247};248for (let i = 0; i < lines.length; i++) {249const line = lines[i];250251if (codeBlockState) {252if (new RegExp(`^\\s*${codeBlockState.delimiter}\\s*$`).test(line)) {253codeBlocks.push({254languageId: codeBlockState.languageId,255totalLines: codeBlockState.totalLines256});257codeBlockState = undefined;258} else {259codeBlockState.totalLines++;260}261} else {262const match = line.match(/^(\s*)(`{3,}|~{3,})(\w*)/);263if (match) {264codeBlockState = {265delimiter: match[2],266languageId: match[3],267totalLines: 0268};269}270}271}272return codeBlocks;273}274275276