Skip to content

Commit

Permalink
fixup
Browse files Browse the repository at this point in the history
  • Loading branch information
jpinkney-aws committed Sep 11, 2024
1 parent a52e352 commit 6ea7da1
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 163 deletions.
132 changes: 132 additions & 0 deletions packages/core/src/amazonq/messages/chatMessageDuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { globals } from '../../shared'
import { telemetry } from '../../shared/telemetry'
import { MetadataEntry, MetricDatum } from '../../shared/telemetry/clienttelemetry'
import { TelemetrySpan } from '../../shared/telemetry/spans'
import { uiEventRecorder } from '../util/eventRecorder'
import { ChatSessionStorageFacade } from '../util/storageFacade'
import { TabType } from '../webview/ui/storages/tabsStorage'

export class AmazonQChatMessageDuration {
/**
* Listen to all incoming telemetry events. If one of them has cwsprChatConversationId or amazonQConversationId
* then it is considered an Amazon Q event that we want to track.
*
* We build up all the Amazon Q events we want to track by conversation ID
*
* This allows us to eventually connect metadata coming from a conversation ID with metadata coming from a tabID. Tabs know nothing
* of conversation IDs and conversation IDs have no knowledge of tabIDs except for in chat session storages.
*
* In the flow Webview -> Message Dispatcher -> Amazon Q Feature -> Amazon Q Feature Implementation -> Message Dispatcher -> Webview
* only "Amazon Q Feature Implementation" has knowledge about conversation IDs
* everything else has knowledge of tabIDs
*/
static initializeListener() {
telemetry.chat_roundTrip.listen(
(metric: MetricDatum) => {
if (!metric.Metadata) {
return false
}

return metric.Metadata.some((k: MetadataEntry) => k.Key === 'cwsprChatConversationId')
},
(_: TelemetrySpan, metric: MetricDatum) => {
if (!metric.Metadata) {
return
}
const conversationId = metric.Metadata.find((x: any) => x.Key === 'cwsprChatConversationId')?.Value
if (!conversationId) {
return
}
uiEventRecorder.set(conversationId, {
events: {
[metric.MetricName]: metric.EpochTimestamp,
},
})
}
)
}

/**
* Record the initial requests in the chat message flow
*/
static startListening(tabID: string, startTime: number, trigger?: string) {
// listen to all events for this tab
uiEventRecorder.set(tabID, {
events: {
initialRequestTime: startTime,
},
})
uiEventRecorder.set(tabID, {
events: {
webviewToDispatcher: globals.clock.Date.now(),
},
})
if (trigger) {
uiEventRecorder.set(tabID, {
trigger,
})
}
}

/**
* Stop listening to all incoming events and emit what we've found
*/
static stopListening(msg: { tabID: string; tabType: TabType }, chatSessionFacade: ChatSessionStorageFacade) {
const { tabID, tabType } = msg

uiEventRecorder.set(tabID, {
events: {
finalRequestTime: globals.clock.Date.now(),
},
})

const conversationId = chatSessionFacade.getConversationId(tabType, tabID)
if (!conversationId) {
return
}

/**
* Consolidate all the events we know about tabID with all the telemetry events that happened inside of that
* conversation in the tab
*/
const tabUIMetrics = uiEventRecorder.get(tabID)
const conversationMetrics = uiEventRecorder.get(conversationId)

// get metrics sorted by the time they were created
const allMetrics = Object.entries({ ...tabUIMetrics.events, ...conversationMetrics.events }).sort((a, b) => {
return a[1] - b[1]
})

// Get the total duration by subtracting the last (finalRequestTime) and the first duration (initialRequestTime)
const totalDuration = allMetrics[allMetrics.length - 1][1] - allMetrics[0][1]

// Find the time difference between current metric and current metric - 1
const differences = new Map<string, number>()
for (let i = 1; i < allMetrics.length; i++) {
const currentEvent = allMetrics[i]
const previousEvent = allMetrics[i - 1]

const timeDifference = currentEvent[1] - previousEvent[1]

differences.set(currentEvent[0], timeDifference)
}

const flowName = tabUIMetrics.trigger ?? conversationMetrics.trigger ?? 'unknown'

telemetry.chat_roundTrip.emit({
duration: totalDuration,
name: flowName,
child_metrics: allMetrics.map((x) => x[0]),
child_metric_durations: JSON.stringify(Object.fromEntries(differences)),
result: 'Succeeded',
} as any)

uiEventRecorder.delete(tabID)
uiEventRecorder.delete(conversationId)
}
}
22 changes: 22 additions & 0 deletions packages/core/src/amazonq/util/eventRecorder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { MetricName } from '../../shared/telemetry/clienttelemetry'
import { RecordMap } from '../../shared/utilities/map'

type Event =
| 'initialRequestTime' // initial on chat prompt event in the ui
| 'webviewToDispatcher' // message gets from the chat prompt to VSCode
| 'dispatcherToFeature' // message gets redirected from VSCode -> Partner team features implementation
| MetricName // any telemetry event emitted between when the partner teams code gets the original message to when its sent to the UI
| 'finalRequestTime' // message gets received in the UI

/**
* For a given conversation ID or tabID, map an event to a time
*/
export const uiEventRecorder = new RecordMap<{
trigger: string
events: Record<Event, number>
}>()
9 changes: 6 additions & 3 deletions packages/core/src/amazonq/util/storageFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import { ChatSessionStorage } from '../../codewhispererChat/storages/chatSession'
import { TabType } from '../webview/ui/storages/tabsStorage'

// A common interface over existing chat session storages
// TODO align all the chatsessionstorages so we don't have to do this :/
/**
* A common interface over existing chat session storages
* TODO align all the chatsessionstorages so we don't have to do this :/
*/
export class ChatSessionStorageFacade {
constructor(private readonly cwcStorage: ChatSessionStorage) {}

Expand All @@ -16,7 +18,8 @@ export class ChatSessionStorageFacade {
case 'cwc':
return this.cwcStorage.getSession(tabID)?.sessionIdentifier
default:
throw new Error(`Unknown tab type: ${tabType}`)
// We are only handling cwc for now, since each amazon q product has to be added to this facade
return undefined
}
}
}
67 changes: 0 additions & 67 deletions packages/core/src/amazonq/util/telemetryHelper.ts

This file was deleted.

90 changes: 4 additions & 86 deletions packages/core/src/amazonq/webview/messages/messageDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,53 +10,15 @@ import { TabType } from '../ui/storages/tabsStorage'
import { getLogger } from '../../../shared/logger'
import { amazonqMark } from '../../../shared/performance/marks'
import { telemetry } from '../../../shared/telemetry'
import { TelemetrySpan } from '../../../shared/telemetry/spans'
import { ChatSessionStorageFacade } from '../../util/storageFacade'
import { AmazonQUITelemetryHelper } from '../../util/telemetryHelper'
import { globals } from '../../../shared'
import { MetadataEntry, MetricDatum } from '../../../shared/telemetry/clienttelemetry'
import { AmazonQChatMessageDuration } from '../../messages/chatMessageDuration'

export function dispatchWebViewMessagesToApps(
webview: Webview,
webViewToAppsMessagePublishers: Map<TabType, MessagePublisher<any>>,
chatSessionFacade: ChatSessionStorageFacade
) {
/**
* Listen to all incoming telemetry events. If one of them has cwsprChatConversationId or amazonQConversationId
* then it is considered an Amazon Q event that we want to track.
*
* We build up all the Amazon Q events we want to track by conversation ID
*
* This allows us to eventually connect metadata coming from a conversation ID with metadata coming from a tabID. Tabs know nothing
* of conversation IDs and conversation IDs have no knowledge of tabIDs except for in chat session storages.
*
* In the flow Webview -> Message Dispatcher -> Amazon Q Feature -> Amazon Q Feature Implementation -> Message Dispatcher -> Webview
* only "Amazon Q Feature Implementation" has knowledge about conversation IDs
* everything else has knowledge of tabIDs
*/
telemetry.chat_roundTrip.listen(
(metric: MetricDatum) => {
if (!metric.Metadata) {
return false
}

// TODO figure out how we can listen to tab ids or can we wait for the request to finish?
// we can't wait for the request to finish because its fire and forget and conversation IDs never enter the webview
// we wan't listen to tab ids because theres no reason to emit them in telemetry
return metric.Metadata.some((k: MetadataEntry) => k.Key === 'cwsprChatConversationId')
},
(_: TelemetrySpan, metric: MetricDatum) => {
if (!metric.Metadata) {
return
}
const conversationId = metric.Metadata.find((x: any) => x.Key === 'cwsprChatConversationId')?.Value
if (!conversationId) {
return
}
AmazonQUITelemetryHelper.instance.record(conversationId, metric.MetricName, metric.EpochTimestamp)
}
)

AmazonQChatMessageDuration.initializeListener()
webview.onDidReceiveMessage((msg) => {
if (msg.command === 'ui-is-ready') {
/**
Expand All @@ -77,54 +39,10 @@ export function dispatchWebViewMessagesToApps(

const tabID = msg.tabID
if (msg.type === 'startListening') {
// listen to all events for this tab
AmazonQUITelemetryHelper.instance.record(tabID, 'initialRequestTime', msg.startTime)
AmazonQUITelemetryHelper.instance.record(tabID, 'webviewToDispatcher', globals.clock.Date.now())

AmazonQChatMessageDuration.startListening(tabID, msg.startTime, msg.trigger)
return
} else if (msg.type === 'stopListening') {
AmazonQUITelemetryHelper.instance.record(tabID, 'finalRequestTime', globals.clock.Date.now())
// stop listening to all events for this tab
const conversationId = chatSessionFacade.getConversationId(msg.tabType, tabID)
if (!conversationId) {
return
}

/**
* Right now Tab UI Metrics contains all the performance information related to the tab
* We got the conversationID for the current tab back from the chatSessionFacade
* so now we know that the conversationUIMetrics are the same ones that were emitted from
* that tab
*/
const tabUIMetrics = AmazonQUITelemetryHelper.instance.getMetrics(tabID)
const conversationMetrics = AmazonQUITelemetryHelper.instance.getMetrics(conversationId)

// get metrics sorted by the time they were created
const allMetrics = [...tabUIMetrics, ...conversationMetrics].sort((a, b) => a[1] - b[1])

// Get the total duration by subtracting the last (finalRequestTime) and the first duration (initialRequestTime)
const totalDuration = allMetrics[allMetrics.length - 1][1] - allMetrics[0][1]

// Find the time difference between current metric and current metric - 1
const differences = new Map<string, number>()
for (let i = 1; i < allMetrics.length; i++) {
const currentEvent = allMetrics[i]
const previousEvent = allMetrics[i - 1]

const timeDifference = currentEvent[1] - previousEvent[1]

differences.set(currentEvent[0], timeDifference)
}

telemetry.chat_roundTrip.emit({
duration: totalDuration,
child_metrics: allMetrics.map((x) => x[0]),
child_metric_durations: JSON.stringify(Object.fromEntries(differences)),
} as any)

AmazonQUITelemetryHelper.instance.delete(tabID)
AmazonQUITelemetryHelper.instance.delete(conversationId)

AmazonQChatMessageDuration.stopListening(msg, chatSessionFacade)
return
}

Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/amazonq/webview/ui/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
if (command === 'aws.amazonq.sendToPrompt') {
return messageController.sendSelectedCodeToTab(message)
} else {
return messageController.sendMessageToTab(message, 'cwc')
return messageController.sendMessageToTab(message, 'cwc', command)
}
},
onWelcomeFollowUpClicked: (tabID: string, welcomeFollowUpType: WelcomeFollowupType) => {
Expand Down Expand Up @@ -389,8 +389,10 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
return
}

// When a user presses "enter"
ideApi.postMessage({
type: 'startListening',
trigger: 'onChatPrompt',
tabID,
tabType: tabsStorage.getTab(tabID)?.type,
startTime: Date.now(),
Expand Down Expand Up @@ -504,6 +506,7 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
tabsStorage,
isFeatureDevEnabled,
isGumbyEnabled,
ideApi,
})

return {
Expand Down
Loading

0 comments on commit 6ea7da1

Please sign in to comment.