packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts (630 lines of code) (raw):
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import * as path from 'path'
import { UserIntent } from '@amzn/codewhisperer-streaming'
import {
AmazonqAddMessage,
AmazonqInteractWithMessage,
CwsprChatCommandType,
CwsprChatInteractionType,
CwsprChatTriggerInteraction,
CwsprChatUserIntent,
Result,
telemetry,
} from '../../../shared/telemetry/telemetry'
import { ChatSessionStorage } from '../../storages/chatSession'
import {
AcceptDiff,
ViewDiff,
ChatItemFeedbackMessage,
ChatItemVotedMessage,
CopyCodeToClipboard,
FooterInfoLinkClick,
InsertCodeAtCursorPosition,
PromptAnswer,
PromptMessage,
ResponseBodyLinkClickMessage,
SourceLinkClickMessage,
TriggerPayload,
AdditionalContextLengths,
AdditionalContextInfo,
} from './model'
import { TriggerEvent, TriggerEventsStorage } from '../../storages/triggerEvents'
import globals from '../../../shared/extensionGlobals'
import { getLogger } from '../../../shared/logger/logger'
import { codeWhispererClient } from '../../../codewhisperer/client/codewhisperer'
import { isAwsError } from '../../../shared/errors'
import { ChatMessageInteractionType } from '../../../codewhisperer/client/codewhispereruserclient'
import { supportedLanguagesList } from '../chat/chatRequest/converter'
import { AuthUtil } from '../../../codewhisperer/util/authUtil'
import { getSelectedCustomization } from '../../../codewhisperer/util/customizationUtil'
import { undefinedIfEmpty } from '../../../shared/utilities/textUtilities'
import { AdditionalContextPrompt } from '../../../amazonq/lsp/types'
import { getUserPromptsDirectory, promptFileExtension } from '../../constants'
import { isInDirectory } from '../../../shared/filesystemUtilities'
export function logSendTelemetryEventFailure(error: any) {
let requestId: string | undefined
if (isAwsError(error)) {
requestId = error.requestId
}
getLogger().debug(
`Failed to sendTelemetryEvent to CodeWhisperer, requestId: ${requestId ?? ''}, message: ${error.message}`
)
}
export function recordTelemetryChatRunCommand(type: CwsprChatCommandType, command?: string) {
telemetry.amazonq_runCommand.emit({
result: 'Succeeded',
cwsprChatCommandType: type,
cwsprChatCommandName: command,
credentialStartUrl: AuthUtil.instance.startUrl,
})
}
export class CWCTelemetryHelper {
static instance: CWCTelemetryHelper
private sessionStorage: ChatSessionStorage
private triggerEventsStorage: TriggerEventsStorage
private responseStreamStartTime: Map<string, number> = new Map()
private responseStreamTotalTime: Map<string, number> = new Map()
private conversationStreamStartTime: Map<string, number> = new Map()
private conversationStreamTotalTime: Map<string, number> = new Map()
private responseStreamTimeForChunks: Map<string, number[]> = new Map()
private responseWithContextInfo: Map<string, AdditionalContextInfo> = new Map()
// Keeps track of when chunks of data were displayed in a tab
private displayTimeForChunks: Map<string, number[]> = new Map()
/**
* Stores payload information about a message response until
* the full round trip time finishes and addMessage telemetry
* is sent
*/
private messageStorage: Map<
string,
{
triggerPayload: TriggerPayload
message: PromptAnswer
}
> = new Map()
constructor(sessionStorage: ChatSessionStorage, triggerEventsStorage: TriggerEventsStorage) {
this.sessionStorage = sessionStorage
this.triggerEventsStorage = triggerEventsStorage
}
public static init(sessionStorage: ChatSessionStorage, triggerEventsStorage: TriggerEventsStorage) {
const lastInstance = CWCTelemetryHelper.instance
if (lastInstance !== undefined) {
return lastInstance
}
getLogger().debug('CWCTelemetryHelper: Initialized new telemetry helper')
const instance = new CWCTelemetryHelper(sessionStorage, triggerEventsStorage)
CWCTelemetryHelper.instance = instance
return instance
}
private getUserIntentForTelemetry(userIntent: UserIntent | undefined): CwsprChatUserIntent | undefined {
switch (userIntent) {
case UserIntent.EXPLAIN_CODE_SELECTION:
return 'explainCodeSelection'
case UserIntent.SUGGEST_ALTERNATE_IMPLEMENTATION:
return 'suggestAlternateImplementation'
case UserIntent.APPLY_COMMON_BEST_PRACTICES:
return 'applyCommonBestPractices'
case UserIntent.IMPROVE_CODE:
return 'improveCode'
case UserIntent.CITE_SOURCES:
return 'citeSources'
case UserIntent.EXPLAIN_LINE_BY_LINE:
return 'explainLineByLine'
case UserIntent.SHOW_EXAMPLES:
return 'showExample'
case UserIntent.GENERATE_UNIT_TESTS:
return 'generateUnitTests'
default:
return undefined
}
}
public recordOpenChat() {
telemetry.amazonq_openChat.emit({ result: 'Succeeded', passive: true })
}
public recordCloseChat() {
telemetry.amazonq_closeChat.emit({ result: 'Succeeded', passive: true })
}
public recordEnterFocusChat() {
telemetry.amazonq_enterFocusChat.emit({ result: 'Succeeded', passive: true })
}
public recordExitFocusChat() {
telemetry.amazonq_exitFocusChat.emit({ result: 'Succeeded', passive: true })
}
public getContextType(prompt: AdditionalContextPrompt): string {
if (prompt.filePath.endsWith(promptFileExtension)) {
if (isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) {
return 'rule'
} else if (isInDirectory(getUserPromptsDirectory(), prompt.filePath)) {
return 'prompt'
}
}
return 'file'
}
public getContextLengths(prompts: AdditionalContextPrompt[]): AdditionalContextLengths {
let fileContextLength = 0
let promptContextLength = 0
let ruleContextLength = 0
for (const prompt of prompts) {
const type = this.getContextType(prompt)
switch (type) {
case 'rule':
ruleContextLength += prompt.content.length
break
case 'file':
fileContextLength += prompt.content.length
break
case 'prompt':
promptContextLength += prompt.content.length
break
}
}
return { fileContextLength, promptContextLength, ruleContextLength }
}
public async recordFeedback(message: ChatItemFeedbackMessage) {
const logger = getLogger()
try {
await globals.telemetry.postFeedback({
comment: JSON.stringify({
type: 'codewhisperer-chat-answer-feedback',
conversationId: this.getConversationId(message.tabID) ?? '',
messageId: message.messageId,
reason: message.selectedOption,
userComment: message.comment,
}),
sentiment: 'Negative',
})
} catch (err) {
const errorMessage = (err as Error).message || 'Failed to submit feedback'
logger.error(`CodeWhispererChat answer feedback failed: "Negative": ${errorMessage}`)
this.recordFeedbackResult('Failed')
return errorMessage
}
logger.info(`CodeWhispererChat answer feedback sent: "Negative"`)
this.recordFeedbackResult('Succeeded')
}
private recordFeedbackResult(feedbackResult: Result) {
telemetry.feedback_result.emit({ result: feedbackResult })
}
public recordInteractWithMessage(
message:
| AcceptDiff
| InsertCodeAtCursorPosition
| CopyCodeToClipboard
| PromptMessage
| ChatItemVotedMessage
| SourceLinkClickMessage
| ResponseBodyLinkClickMessage
| FooterInfoLinkClick
| ViewDiff,
{ result }: { result: Result } = { result: 'Succeeded' }
) {
const conversationId = this.getConversationId(message.tabID)
let event: AmazonqInteractWithMessage | undefined
let additionalContextInfo = undefined
const messageId = (message as any).messageId
if (messageId) {
additionalContextInfo = this.responseWithContextInfo.get(messageId)
}
switch (message.command) {
case 'insert_code_at_cursor_position':
message = message as InsertCodeAtCursorPosition
event = {
result,
cwsprChatConversationId: conversationId ?? '',
credentialStartUrl: AuthUtil.instance.startUrl,
cwsprChatMessageId: message.messageId,
cwsprChatUserIntent: this.getUserIntentForTelemetry(message.userIntent),
cwsprChatInteractionType: 'insertAtCursor',
cwsprChatAcceptedCharactersLength: message.code.length,
cwsprChatAcceptedNumberOfLines: message.code.split('\n').length,
cwsprChatInteractionTarget: message.insertionTargetType,
cwsprChatHasReference: message.codeReference && message.codeReference.length > 0,
cwsprChatCodeBlockIndex: message.codeBlockIndex,
cwsprChatTotalCodeBlocks: message.totalCodeBlocks,
cwsprChatProgrammingLanguage: message.codeBlockLanguage,
}
break
case 'code_was_copied_to_clipboard':
message = message as CopyCodeToClipboard
event = {
result,
cwsprChatConversationId: conversationId ?? '',
credentialStartUrl: AuthUtil.instance.startUrl,
cwsprChatMessageId: message.messageId,
cwsprChatUserIntent: this.getUserIntentForTelemetry(message.userIntent),
cwsprChatInteractionType: 'copySnippet',
cwsprChatAcceptedCharactersLength: message.code.length,
cwsprChatInteractionTarget: message.insertionTargetType,
cwsprChatHasReference: message.codeReference && message.codeReference.length > 0,
cwsprChatCodeBlockIndex: message.codeBlockIndex,
cwsprChatTotalCodeBlocks: message.totalCodeBlocks,
cwsprChatProgrammingLanguage: message.codeBlockLanguage,
}
break
case 'accept_diff':
message = message as AcceptDiff
event = {
result,
cwsprChatConversationId: conversationId ?? '',
cwsprChatMessageId: message.messageId,
cwsprChatInteractionType: 'acceptDiff',
credentialStartUrl: AuthUtil.instance.startUrl,
cwsprChatAcceptedCharactersLength: message.code.length,
cwsprChatHasReference:
message.referenceTrackerInformation && message.referenceTrackerInformation.length > 0,
cwsprChatCodeBlockIndex: message.codeBlockIndex,
cwsprChatTotalCodeBlocks: message.totalCodeBlocks,
}
break
case 'view_diff':
message = message as ViewDiff
event = {
result,
cwsprChatConversationId: conversationId ?? '',
cwsprChatMessageId: message.messageId,
cwsprChatInteractionType: 'viewDiff',
credentialStartUrl: AuthUtil.instance.startUrl,
cwsprChatAcceptedCharactersLength: message.code.length,
cwsprChatHasReference:
message.referenceTrackerInformation && message.referenceTrackerInformation.length > 0,
cwsprChatCodeBlockIndex: message.codeBlockIndex,
cwsprChatTotalCodeBlocks: message.totalCodeBlocks,
}
break
case 'follow-up-was-clicked':
message = message as PromptMessage
event = {
result,
cwsprChatConversationId: conversationId ?? '',
credentialStartUrl: AuthUtil.instance.startUrl,
cwsprChatMessageId: message.messageId,
cwsprChatInteractionType: 'clickFollowUp',
}
break
case 'chat-item-voted':
message = message as ChatItemVotedMessage
event = {
result,
cwsprChatMessageId: message.messageId,
cwsprChatConversationId: conversationId ?? '',
credentialStartUrl: AuthUtil.instance.startUrl,
cwsprChatInteractionType: message.vote,
}
break
case 'source-link-click':
message = message as SourceLinkClickMessage
event = {
result,
cwsprChatMessageId: message.messageId,
cwsprChatConversationId: conversationId ?? '',
credentialStartUrl: AuthUtil.instance.startUrl,
cwsprChatInteractionType: 'clickLink',
cwsprChatInteractionTarget: message.link,
}
break
case 'response-body-link-click':
message = message as ResponseBodyLinkClickMessage
event = {
result,
cwsprChatMessageId: message.messageId,
cwsprChatConversationId: conversationId ?? '',
credentialStartUrl: AuthUtil.instance.startUrl,
cwsprChatInteractionType: 'clickBodyLink',
cwsprChatInteractionTarget: message.link,
}
break
case 'footer-info-link-click':
message = message as FooterInfoLinkClick
event = {
result,
cwsprChatMessageId: 'footer',
cwsprChatConversationId: conversationId ?? '',
credentialStartUrl: AuthUtil.instance.startUrl,
cwsprChatInteractionType: 'clickBodyLink',
cwsprChatInteractionTarget: message.link,
}
break
}
if (!event) {
return
}
telemetry.amazonq_interactWithMessage.emit({ ...event, ...additionalContextInfo })
codeWhispererClient
.sendTelemetryEvent({
telemetryEvent: {
chatInteractWithMessageEvent: {
conversationId: event.cwsprChatConversationId,
messageId: event.cwsprChatMessageId,
interactionType: this.getCWClientTelemetryInteractionType(event.cwsprChatInteractionType),
interactionTarget: event.cwsprChatInteractionTarget,
acceptedCharacterCount: event.cwsprChatAcceptedCharactersLength,
acceptedLineCount: event.cwsprChatAcceptedNumberOfLines,
acceptedSnippetHasReference: false,
hasProjectLevelContext: this.responseWithContextInfo.get(event.cwsprChatMessageId)
?.cwsprChatHasProjectContext,
customizationArn: undefinedIfEmpty(getSelectedCustomization().arn),
},
},
profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn,
})
.then()
.catch(logSendTelemetryEventFailure)
}
private getCWClientTelemetryInteractionType(type: CwsprChatInteractionType): ChatMessageInteractionType {
switch (type) {
case 'copySnippet':
return 'COPY_SNIPPET'
case 'insertAtCursor':
return 'INSERT_AT_CURSOR'
case 'clickFollowUp':
return 'CLICK_FOLLOW_UP'
case 'clickLink':
return 'CLICK_LINK'
case 'clickBodyLink':
return 'CLICK_BODY_LINK'
case 'upvote':
return 'UPVOTE'
case 'downvote':
return 'DOWNVOTE'
case 'acceptDiff':
return 'ACCEPT_DIFF'
case 'viewDiff':
return 'VIEW_DIFF'
default:
return 'UNKNOWN'
}
}
private getTriggerInteractionFromTriggerEvent(triggerEvent: TriggerEvent | undefined): CwsprChatTriggerInteraction {
switch (triggerEvent?.type) {
case 'editor_context_command':
return triggerEvent.command?.triggerType === 'keybinding' ? 'hotkeys' : 'contextMenu'
case 'follow_up':
case 'chat_message':
default:
return 'click'
}
}
public recordStartConversation(
triggerEvent: TriggerEvent,
triggerPayload: TriggerPayload & { projectContextQueryLatencyMs?: number }
) {
if (triggerEvent.tabID === undefined) {
return
}
if (this.triggerEventsStorage.getTriggerEventsByTabID(triggerEvent.tabID).length > 1) {
return
}
const telemetryUserIntent = this.getUserIntentForTelemetry(triggerPayload.userIntent)
telemetry.amazonq_startConversation.emit({
result: 'Succeeded',
cwsprChatConversationId: this.getConversationId(triggerEvent.tabID) ?? '',
cwsprChatTriggerInteraction: this.getTriggerInteractionFromTriggerEvent(triggerEvent),
cwsprChatConversationType: 'Chat',
cwsprChatUserIntent: telemetryUserIntent,
cwsprChatHasCodeSnippet: triggerPayload.codeSelection && !triggerPayload.codeSelection.isEmpty,
cwsprChatProgrammingLanguage: triggerPayload.fileLanguage,
credentialStartUrl: AuthUtil.instance.startUrl,
cwsprChatHasProjectContext: triggerPayload.relevantTextDocuments
? triggerPayload.relevantTextDocuments.length > 0 && triggerPayload.useRelevantDocuments === true
: false,
cwsprChatProjectContextQueryMs: triggerPayload.projectContextQueryLatencyMs,
})
}
/**
* Store the trigger payload and message until the full message round trip finishes
*
* @calls emitAddMessage when the full message round trip finishes
*/
public recordAddMessage(triggerPayload: TriggerPayload, message: PromptAnswer) {
this.messageStorage.set(message.tabID, {
triggerPayload,
message,
})
}
public getAdditionalContextCounts(triggerPayload: TriggerPayload) {
const counts = {
fileContextCount: 0,
folderContextCount: 0,
promptContextCount: 0,
}
if (triggerPayload.context) {
for (const context of triggerPayload.context) {
if (typeof context !== 'string') {
if (context.id === 'file') {
counts.fileContextCount++
} else if (context.id === 'folder') {
counts.folderContextCount++
} else if (context.id === 'prompt') {
counts.promptContextCount++
}
}
}
}
return counts
}
public emitAddMessage(tabID: string, fullDisplayLatency: number, traceId: string, startTime?: number) {
const payload = this.messageStorage.get(tabID)
if (!payload) {
return
}
const { triggerPayload, message } = payload
const triggerEvent = this.triggerEventsStorage.getLastTriggerEventByTabID(message.tabID)
const hasProjectLevelContext =
triggerPayload.relevantTextDocuments &&
triggerPayload.relevantTextDocuments.length > 0 &&
triggerPayload.useRelevantDocuments === true
const contextCounts = this.getAdditionalContextCounts(triggerPayload)
const event: AmazonqAddMessage = {
result: 'Succeeded',
cwsprChatConversationId: this.getConversationId(message.tabID) ?? '',
cwsprChatMessageId: message.messageID,
cwsprChatTriggerInteraction: this.getTriggerInteractionFromTriggerEvent(triggerEvent),
cwsprChatUserIntent: this.getUserIntentForTelemetry(triggerPayload.userIntent),
cwsprChatHasCodeSnippet: triggerPayload.codeSelection && !triggerPayload.codeSelection.isEmpty,
cwsprChatProgrammingLanguage: triggerPayload.fileLanguage,
cwsprChatActiveEditorTotalCharacters: triggerPayload.fileText.length,
cwsprChatActiveEditorImportCount: triggerPayload.codeQuery?.fullyQualifiedNames?.used?.length,
cwsprChatResponseCodeSnippetCount: message.totalNumberOfCodeBlocksInResponse,
cwsprChatResponseCode: message.responseCode,
cwsprChatSourceLinkCount: message.suggestionCount,
cwsprChatReferencesCount: message.codeReferenceCount,
cwsprChatFollowUpCount: message.followUpCount,
cwsprChatTimeToFirstChunk: this.getResponseStreamTimeToFirstChunk(message.tabID),
cwsprChatTimeBetweenChunks: JSON.stringify(
this.getTimeBetweenChunks(message.tabID, this.responseStreamTimeForChunks)
),
cwsprChatFullResponseLatency: this.responseStreamTotalTime.get(message.tabID) ?? 0,
cwsprChatTimeToFirstDisplay: this.getFirstDisplayTime(tabID, startTime),
cwsprChatTimeToFirstUsableChunk: this.getFirstUsableChunkTime(message.tabID) ?? 0,
cwsprChatFullServerResponseLatency: this.conversationStreamTotalTime.get(message.tabID) ?? 0,
cwsprChatTimeBetweenDisplays: JSON.stringify(this.getTimeBetweenChunks(tabID, this.displayTimeForChunks)),
cwsprChatFullDisplayLatency: fullDisplayLatency,
cwsprChatRequestLength: triggerPayload.message.length,
cwsprChatResponseLength: message.messageLength,
cwsprChatConversationType: 'Chat',
credentialStartUrl: AuthUtil.instance.startUrl,
codewhispererCustomizationArn: triggerPayload.customization.arn,
cwsprChatHasProjectContext: hasProjectLevelContext,
cwsprChatHasContextList: triggerPayload.documentReferences.length > 0,
cwsprChatFolderContextCount: contextCounts.folderContextCount,
cwsprChatFileContextCount: contextCounts.fileContextCount,
cwsprChatFileContextLength: triggerPayload.contextLengths.additionalContextLengths.fileContextLength,
cwsprChatFileContextTruncatedLength:
triggerPayload.contextLengths.truncatedAdditionalContextLengths.fileContextLength,
cwsprChatRuleContextCount: triggerPayload.workspaceRulesCount,
cwsprChatRuleContextLength: triggerPayload.contextLengths.additionalContextLengths.ruleContextLength,
cwsprChatRuleContextTruncatedLength:
triggerPayload.contextLengths.truncatedAdditionalContextLengths.ruleContextLength,
cwsprChatPromptContextCount: contextCounts.promptContextCount,
cwsprChatPromptContextLength: triggerPayload.contextLengths.additionalContextLengths.promptContextLength,
cwsprChatPromptContextTruncatedLength:
triggerPayload.contextLengths.truncatedAdditionalContextLengths.promptContextLength,
cwsprChatFocusFileContextLength: triggerPayload.contextLengths.focusFileContextLength,
cwsprChatFocusFileContextTruncatedLength: triggerPayload.contextLengths.truncatedFocusFileContextLength,
cwsprChatUserInputContextLength: triggerPayload.contextLengths.userInputContextLength,
cwsprChatUserInputContextTruncatedLength: triggerPayload.contextLengths.truncatedUserInputContextLength,
cwsprChatWorkspaceContextLength: triggerPayload.contextLengths.workspaceContextLength,
cwsprChatWorkspaceContextTruncatedLength: triggerPayload.contextLengths.truncatedWorkspaceContextLength,
traceId,
}
telemetry.amazonq_addMessage.emit(event)
const language = this.isProgrammingLanguageSupported(triggerPayload.fileLanguage)
? { languageName: triggerPayload.fileLanguage as string }
: undefined
codeWhispererClient
.sendTelemetryEvent({
telemetryEvent: {
chatAddMessageEvent: {
conversationId: event.cwsprChatConversationId,
messageId: event.cwsprChatMessageId,
userIntent: triggerPayload.userIntent,
hasCodeSnippet: event.cwsprChatHasCodeSnippet,
...(language !== undefined ? { programmingLanguage: language } : {}),
activeEditorTotalCharacters: event.cwsprChatActiveEditorTotalCharacters,
timeToFirstChunkMilliseconds: event.cwsprChatTimeToFirstChunk,
timeBetweenChunks: this.getTimeBetweenChunks(message.tabID, this.responseStreamTimeForChunks),
fullResponselatency: event.cwsprChatFullResponseLatency,
requestLength: event.cwsprChatRequestLength,
responseLength: event.cwsprChatResponseLength,
numberOfCodeBlocks: event.cwsprChatResponseCodeSnippetCount,
hasProjectLevelContext: hasProjectLevelContext,
customizationArn: undefinedIfEmpty(getSelectedCustomization().arn),
},
},
profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn,
})
.then()
.catch(logSendTelemetryEventFailure)
this.messageStorage.delete(tabID)
}
public recordMessageResponseError(triggerPayload: TriggerPayload, tabID: string, responseCode: number) {
const triggerEvent = this.triggerEventsStorage.getLastTriggerEventByTabID(tabID)
telemetry.amazonq_messageResponseError.emit({
result: 'Succeeded',
cwsprChatConversationId: this.getConversationId(tabID) ?? '',
cwsprChatTriggerInteraction: this.getTriggerInteractionFromTriggerEvent(triggerEvent),
cwsprChatUserIntent: this.getUserIntentForTelemetry(triggerPayload.userIntent),
cwsprChatHasCodeSnippet: !triggerPayload.codeSelection?.isEmpty,
cwsprChatProgrammingLanguage: triggerPayload.fileLanguage,
cwsprChatActiveEditorTotalCharacters: triggerPayload.fileText?.length,
cwsprChatActiveEditorImportCount: triggerPayload.codeQuery?.fullyQualifiedNames?.used?.length,
cwsprChatResponseCode: responseCode,
cwsprChatRequestLength: triggerPayload.message?.length ?? 0,
cwsprChatConversationType: 'Chat',
credentialStartUrl: AuthUtil.instance.startUrl,
})
}
public recordEnterFocusConversation(tabID: string) {
const conversationId = this.getConversationId(tabID)
if (conversationId) {
telemetry.amazonq_enterFocusConversation.emit({
result: 'Succeeded',
cwsprChatConversationId: conversationId,
})
}
}
public recordExitFocusConversation(tabID: string) {
const conversationId = this.getConversationId(tabID)
if (conversationId) {
telemetry.amazonq_exitFocusConversation.emit({
result: 'Succeeded',
cwsprChatConversationId: conversationId,
})
}
}
public setResponseStreamStartTime(tabID: string) {
this.responseStreamStartTime.set(tabID, performance.now())
this.responseStreamTimeForChunks.set(tabID, [performance.now()])
this.displayTimeForChunks.set(tabID, [])
}
public setResponseStreamTimeForChunks(tabID: string) {
const chunkTimes = this.responseStreamTimeForChunks.get(tabID) ?? []
this.responseStreamTimeForChunks.set(tabID, [...chunkTimes, performance.now()])
}
public setDisplayTimeForChunks(tabID: string, time: number) {
const chunkTimes = this.displayTimeForChunks.get(tabID) ?? []
this.displayTimeForChunks.set(tabID, [...chunkTimes, time])
}
public setResponseFromAdditionalContext(messageId: string, additionalContextInfo: AdditionalContextInfo) {
this.responseWithContextInfo.set(messageId, additionalContextInfo)
}
public setConversationStreamStartTime(tabID: string) {
this.conversationStreamStartTime.set(tabID, performance.now())
}
private getResponseStreamTimeToFirstChunk(tabID: string): number {
const chunkTimes = this.responseStreamTimeForChunks.get(tabID) ?? [0, 0]
if (chunkTimes.length === 1) {
return Math.round(performance.now() - chunkTimes[0])
}
return Math.round(chunkTimes[1] - chunkTimes[0])
}
/**
* Finds the time between when a user pressed enter and the first chunk appears in the UI
*/
private getFirstDisplayTime(tabID: string, startTime?: number) {
if (!startTime) {
return 0
}
const chunkTimes = this.displayTimeForChunks.get(tabID) ?? [0]
return Math.round(chunkTimes[0] - startTime)
}
private getFirstUsableChunkTime(tabID: string) {
const startTime = this.conversationStreamStartTime.get(tabID) ?? 0
const chunkTimes = this.responseStreamTimeForChunks.get(tabID) ?? [0, 0]
// first chunk is the start time, we use the second because thats the first actual usable chunk time
return Math.round(chunkTimes[1] - startTime)
}
private getTimeBetweenChunks(tabID: string, chunks: Map<string, number[]>): number[] {
try {
const chunkDeltaTimes: number[] = []
const chunkTimes = chunks.get(tabID) ?? [0]
for (let idx = 0; idx < chunkTimes.length - 1; idx++) {
chunkDeltaTimes.push(Math.round(chunkTimes[idx + 1] - chunkTimes[idx]))
}
return chunkDeltaTimes.slice(0, 100)
} catch (e) {
getLogger().debug(`Failed to get response time between chunks, message: ${e}`)
return []
}
}
public setResponseStreamTotalTime(tabID: string) {
// time from when the requests started streaming until the end of the stream
const totalStreamingTime = performance.now() - (this.responseStreamStartTime.get(tabID) ?? 0)
this.responseStreamTotalTime.set(tabID, Math.round(totalStreamingTime))
// time from the initial server request, including creating the conversation id, until the end of the stream
const totalConversationTime = performance.now() - (this.conversationStreamStartTime.get(tabID) ?? 0)
this.conversationStreamTotalTime.set(tabID, Math.round(totalConversationTime))
}
public getConversationId(tabID: string): string | undefined {
return this.sessionStorage.getSession(tabID).sessionIdentifier
}
private isProgrammingLanguageSupported(programmingLanguage: string | undefined) {
return (
programmingLanguage !== undefined &&
programmingLanguage !== '' &&
supportedLanguagesList.includes(programmingLanguage)
)
}
}