Skip to content

Commit de9d7cf

Browse files
authored
telemetry(amazonq): Add client latency metrics for amazon q chat (#5705)
## Problem: - It's not clear what's meant by cwsprChatTimeToFirstChunk, cwsprChatTimeBetweenChunks, cwsprChatFullResponseLatency - We don't have any client latency metrics ## Solution: - Clarify the descriptions of cwsprChatTimeToFirstChunk, cwsprChatTimeBetweenChunks, cwsprChatFullResponseLatency - Implement cwsprTimeToFirstDisplay, cwsprChatTimeBetweenDisplay, cwsprChatFullDisplayLatency metrics ## Other changes: - Instead of instrumenting everything via a traceId instead use the tabId as the unique identifier and store a traceId when we get it inside of the uiEventRecorder. That way we can associate events from a tab to the trace without needing to pass the trace around the entire application. - Lock stopChatMessageTelemetry on the tabId just in case something calls that twice for the same event ## Risks: - All flows from start of chat message -> end of chat message need to be instrumented otherwise an addChatMessage event wont be sent. - RIght now: - Right click context menu works - Normal chat message work - Follow up button works - Are there any other flows that we need to know about?
1 parent c529794 commit de9d7cf

File tree

17 files changed

+281
-144
lines changed

17 files changed

+281
-144
lines changed

packages/core/src/amazonq/messages/chatMessageDuration.ts

Lines changed: 69 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,91 +3,111 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import AsyncLock from 'async-lock'
67
import { globals } from '../../shared'
78
import { telemetry } from '../../shared/telemetry'
89
import { Event, uiEventRecorder } from '../util/eventRecorder'
10+
import { CWCTelemetryHelper } from '../../codewhispererChat/controllers/chat/telemetryHelper'
11+
import { TabType } from '../webview/ui/storages/tabsStorage'
912

1013
export class AmazonQChatMessageDuration {
14+
private static _asyncLock = new AsyncLock()
15+
private static getAsyncLock() {
16+
if (!AmazonQChatMessageDuration._asyncLock) {
17+
AmazonQChatMessageDuration._asyncLock = new AsyncLock()
18+
}
19+
return AmazonQChatMessageDuration._asyncLock
20+
}
21+
1122
/**
1223
* Record the initial requests in the chat message flow
1324
*/
14-
static startChatMessageTelemetry(msg: { traceId: string; startTime: number; trigger?: string }) {
15-
const { traceId, startTime, trigger } = msg
25+
static startChatMessageTelemetry(msg: { traceId: string; startTime: number; tabID: string; trigger?: string }) {
26+
const { traceId, startTime, tabID, trigger } = msg
1627

17-
uiEventRecorder.set(traceId, {
28+
uiEventRecorder.set(tabID, {
29+
traceId,
1830
events: {
1931
chatMessageSent: startTime,
20-
},
21-
})
22-
uiEventRecorder.set(traceId, {
23-
events: {
2432
editorReceivedMessage: globals.clock.Date.now(),
2533
},
2634
})
2735
if (trigger) {
28-
uiEventRecorder.set(traceId, {
36+
uiEventRecorder.set(tabID, {
2937
trigger,
3038
})
3139
}
40+
CWCTelemetryHelper.instance.setDisplayTimeForChunks(tabID, startTime)
3241
}
3342

3443
/**
3544
* Stop listening to all incoming events and emit what we've found
3645
*/
37-
static stopChatMessageTelemetry(msg: { traceId: string }) {
38-
const { traceId } = msg
46+
static stopChatMessageTelemetry(msg: { tabID: string; time: number; tabType: TabType }) {
47+
const { tabID, time, tabType } = msg
3948

4049
// We can't figure out what trace this event was associated with
41-
if (!traceId) {
50+
if (!tabID || tabType !== 'cwc') {
4251
return
4352
}
4453

45-
uiEventRecorder.set(traceId, {
46-
events: {
47-
messageDisplayed: globals.clock.Date.now(),
48-
},
49-
})
50-
51-
const metrics = uiEventRecorder.get(traceId)
54+
// Lock the tab id just in case another event tries to trigger this
55+
void AmazonQChatMessageDuration.getAsyncLock().acquire(tabID, () => {
56+
const metrics = uiEventRecorder.get(tabID)
57+
if (!metrics) {
58+
return
59+
}
5260

53-
// get events sorted by the time they were created
54-
const events = Object.entries(metrics.events)
55-
.map((x) => ({
56-
event: x[0],
57-
duration: x[1],
58-
}))
59-
.sort((a, b) => {
60-
return a.duration - b.duration
61+
uiEventRecorder.set(tabID, {
62+
events: {
63+
messageDisplayed: time,
64+
},
6165
})
6266

63-
const chatMessageSentTime = events[events.length - 1].duration
64-
// Get the total duration by subtracting when the message was displayed and when the chat message was first sent
65-
const totalDuration = events[events.length - 1].duration - events[0].duration
67+
const displayTime = metrics.events.messageDisplayed
68+
const sentTime = metrics.events.chatMessageSent
69+
if (!displayTime || !sentTime) {
70+
return
71+
}
6672

67-
/**
68-
* Find the time it took to get between two metric events
69-
*/
70-
const timings = new Map<Event, number>()
71-
for (let i = 1; i < events.length; i++) {
72-
const currentEvent = events[i]
73-
const previousEvent = events[i - 1]
73+
const totalDuration = displayTime - sentTime
7474

75-
const timeDifference = currentEvent.duration - previousEvent.duration
75+
function durationFrom(start: Event, end: Event) {
76+
const startEvent = metrics.events[start]
77+
const endEvent = metrics.events[end]
78+
if (!startEvent || !endEvent) {
79+
return -1
80+
}
81+
return endEvent - startEvent
82+
}
7683

77-
timings.set(currentEvent.event as Event, timeDifference)
78-
}
84+
// TODO: handle onContextCommand round trip time
85+
if (metrics.trigger !== 'onContextCommand') {
86+
telemetry.amazonq_chatRoundTrip.emit({
87+
amazonqChatMessageSentTime: metrics.events.chatMessageSent ?? -1,
88+
amazonqEditorReceivedMessageMs: durationFrom('chatMessageSent', 'editorReceivedMessage') ?? -1,
89+
amazonqFeatureReceivedMessageMs:
90+
durationFrom('editorReceivedMessage', 'featureReceivedMessage') ?? -1,
91+
amazonqMessageDisplayedMs: durationFrom('featureReceivedMessage', 'messageDisplayed') ?? -1,
92+
source: metrics.trigger,
93+
duration: totalDuration,
94+
result: 'Succeeded',
95+
traceId: metrics.traceId,
96+
})
97+
}
7998

80-
telemetry.amazonq_chatRoundTrip.emit({
81-
amazonqChatMessageSentTime: chatMessageSentTime,
82-
amazonqEditorReceivedMessageMs: timings.get('editorReceivedMessage') ?? -1,
83-
amazonqFeatureReceivedMessageMs: timings.get('featureReceivedMessage') ?? -1,
84-
amazonqMessageDisplayedMs: timings.get('messageDisplayed') ?? -1,
85-
source: metrics.trigger,
86-
duration: totalDuration,
87-
result: 'Succeeded',
88-
traceId,
99+
CWCTelemetryHelper.instance.emitAddMessage(tabID, totalDuration, metrics.events.chatMessageSent)
100+
101+
uiEventRecorder.delete(tabID)
89102
})
103+
}
104+
105+
static updateChatMessageTelemetry(msg: { tabID: string; time: number; tabType: TabType }) {
106+
const { tabID, time, tabType } = msg
107+
if (!tabID || tabType !== 'cwc') {
108+
return
109+
}
90110

91-
uiEventRecorder.delete(traceId)
111+
CWCTelemetryHelper.instance.setDisplayTimeForChunks(tabID, time)
92112
}
93113
}

packages/core/src/amazonq/util/eventRecorder.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ export type Event =
99
| 'chatMessageSent' // initial on chat prompt event in the ui
1010
| 'editorReceivedMessage' // message gets from the chat prompt to VSCode
1111
| 'featureReceivedMessage' // message gets redirected from VSCode -> Partner team features implementation
12-
| 'messageDisplayed' // message gets received in the UI
12+
| 'messageDisplayed' // message gets shown in the UI
1313

1414
/**
15-
* For a given traceID, map an event to a time
15+
* For a given tabId, map an event to a time
1616
*
1717
* This is used to correlated disjoint events that are happening in different
1818
* parts of Q Chat.
@@ -22,8 +22,14 @@ export type Event =
2222
* - when the feature starts processing the message
2323
* - final message rendering
2424
* and emit those as a final result, rather than having to emit each event individually
25+
*
26+
* Event timings are generated using Date.now() instead of performance.now() for cross-context consistency.
27+
* performance.now() provides timestamps relative to the context's time origin (when the webview or VS Code was opened),
28+
* which can lead to inconsistent measurements between the webview and vscode.
29+
* Date.now() is more consistent across both contexts
2530
*/
2631
export const uiEventRecorder = new RecordMap<{
2732
trigger: string
33+
traceId: string
2834
events: Partial<Record<Event, number>>
2935
}>()

packages/core/src/amazonq/webview/messages/messageDispatcher.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,35 @@ export function dispatchWebViewMessagesToApps(
1717
webViewToAppsMessagePublishers: Map<TabType, MessagePublisher<any>>
1818
) {
1919
webview.onDidReceiveMessage((msg) => {
20-
if (msg.command === 'ui-is-ready') {
21-
/**
22-
* ui-is-ready isn't associated to any tab so just record the telemetry event and continue.
23-
* This would be equivalent of the duration between "user clicked open q" and "ui has become available"
24-
* NOTE: Amazon Q UI is only loaded ONCE. The state is saved between each hide/show of the webview.
25-
*/
20+
switch (msg.command) {
21+
case 'ui-is-ready': {
22+
/**
23+
* ui-is-ready isn't associated to any tab so just record the telemetry event and continue.
24+
* This would be equivalent of the duration between "user clicked open q" and "ui has become available"
25+
* NOTE: Amazon Q UI is only loaded ONCE. The state is saved between each hide/show of the webview.
26+
*/
2627

27-
telemetry.webview_load.emit({
28-
webviewName: 'amazonq',
29-
duration: performance.measure(amazonqMark.uiReady, amazonqMark.open).duration,
30-
result: 'Succeeded',
31-
})
32-
performance.clearMarks(amazonqMark.uiReady)
33-
performance.clearMarks(amazonqMark.open)
34-
return
35-
}
36-
37-
if (msg.type === 'startChatMessageTelemetry') {
38-
AmazonQChatMessageDuration.startChatMessageTelemetry(msg)
39-
return
40-
} else if (msg.type === 'stopChatMessageTelemetry') {
41-
AmazonQChatMessageDuration.stopChatMessageTelemetry(msg)
42-
return
28+
telemetry.webview_load.emit({
29+
webviewName: 'amazonq',
30+
duration: performance.measure(amazonqMark.uiReady, amazonqMark.open).duration,
31+
result: 'Succeeded',
32+
})
33+
performance.clearMarks(amazonqMark.uiReady)
34+
performance.clearMarks(amazonqMark.open)
35+
return
36+
}
37+
case 'start-chat-message-telemetry': {
38+
AmazonQChatMessageDuration.startChatMessageTelemetry(msg)
39+
return
40+
}
41+
case 'update-chat-message-telemetry': {
42+
AmazonQChatMessageDuration.updateChatMessageTelemetry(msg)
43+
return
44+
}
45+
case 'stop-chat-message-telemetry': {
46+
AmazonQChatMessageDuration.stopChatMessageTelemetry(msg)
47+
return
48+
}
4349
}
4450

4551
if (msg.type === 'error') {

packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ import { ExtensionMessage } from '../commands'
88
import { CodeReference } from './amazonqCommonsConnector'
99
import { TabOpenType, TabsStorage } from '../storages/tabsStorage'
1010
import { FollowUpGenerator } from '../followUps/generator'
11-
import { TracedChatItem } from '../connector'
1211

1312
interface ChatPayload {
1413
chatMessage: string
15-
traceId?: string
1614
chatCommand?: string
1715
}
1816

@@ -74,6 +72,17 @@ export class Connector {
7472
}
7573

7674
followUpClicked = (tabID: string, messageId: string, followUp: ChatItemAction): void => {
75+
/**
76+
* We've pressed on a followup button and should start watching that round trip telemetry
77+
*/
78+
this.sendMessageToExtension({
79+
command: 'start-chat-message-telemetry',
80+
trigger: 'followUpClicked',
81+
tabID,
82+
traceId: messageId,
83+
tabType: 'cwc',
84+
startTime: Date.now(),
85+
})
7786
this.sendMessageToExtension({
7887
command: 'follow-up-was-clicked',
7988
followUp,
@@ -183,17 +192,29 @@ export class Connector {
183192
})
184193
}
185194

186-
requestGenerativeAIAnswer = (tabID: string, payload: ChatPayload): Promise<any> =>
187-
new Promise((resolve, reject) => {
195+
requestGenerativeAIAnswer = (tabID: string, messageId: string, payload: ChatPayload): Promise<any> => {
196+
/**
197+
* When a user presses "enter" send an event that indicates
198+
* we should start tracking the round trip time for this message
199+
**/
200+
this.sendMessageToExtension({
201+
command: 'start-chat-message-telemetry',
202+
trigger: 'onChatPrompt',
203+
tabID,
204+
traceId: messageId,
205+
tabType: 'cwc',
206+
startTime: Date.now(),
207+
})
208+
return new Promise((resolve, reject) => {
188209
this.sendMessageToExtension({
189210
tabID: tabID,
190211
command: 'chat-prompt',
191212
chatMessage: payload.chatMessage,
192213
chatCommand: payload.chatCommand,
193-
traceId: payload.traceId,
194214
tabType: 'cwc',
195215
})
196216
})
217+
}
197218

198219
clearChat = (tabID: string): void => {
199220
this.sendMessageToExtension({
@@ -261,14 +282,13 @@ export class Connector {
261282
}
262283
: undefined
263284

264-
const answer: TracedChatItem = {
285+
const answer: ChatItem = {
265286
type: messageData.messageType,
266287
messageId: messageData.messageID ?? messageData.triggerID,
267288
body: messageData.message,
268289
followUp: followUps,
269290
canBeVoted: true,
270291
codeReference: messageData.codeReference,
271-
traceId: messageData.traceId,
272292
}
273293

274294
// If it is not there we will not set it
@@ -295,7 +315,7 @@ export class Connector {
295315
return
296316
}
297317
if (messageData.messageType === ChatItemType.ANSWER) {
298-
const answer: TracedChatItem = {
318+
const answer: ChatItem = {
299319
type: messageData.messageType,
300320
body: undefined,
301321
relatedContent: undefined,
@@ -308,7 +328,6 @@ export class Connector {
308328
options: messageData.followUps,
309329
}
310330
: undefined,
311-
traceId: messageData.traceId,
312331
}
313332
this.onChatAnswerReceived(messageData.tabID, answer)
314333

packages/core/src/amazonq/webview/ui/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,7 @@ type MessageCommand =
3131
| 'file-click'
3232
| 'form-action-click'
3333
| 'open-settings'
34+
| 'start-chat-message-telemetry'
35+
| 'stop-chat-message-telemetry'
3436

3537
export type ExtensionMessage = Record<string, any> & { command: MessageCommand }

packages/core/src/amazonq/webview/ui/connector.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,18 @@ export interface CodeReference {
2626

2727
export interface ChatPayload {
2828
chatMessage: string
29-
traceId?: string // TODO: instrumented for cwc, not for gumby/featuredev. Remove the ? once we support all features
3029
chatCommand?: string
3130
}
3231

33-
export interface TracedChatItem extends ChatItem {
34-
traceId?: string
35-
}
36-
3732
export interface ConnectorProps {
3833
sendMessageToExtension: (message: ExtensionMessage) => void
3934
onMessageReceived?: (tabID: string, messageData: any, needToShowAPIDocsTab: boolean) => void
4035
onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void
41-
onChatAnswerReceived?: (tabID: string, message: TracedChatItem) => void
36+
onChatAnswerReceived?: (tabID: string, message: ChatItem) => void
4237
onWelcomeFollowUpClicked: (tabID: string, welcomeFollowUpType: WelcomeFollowupType) => void
4338
onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string | undefined) => void
4439
onQuickHandlerCommand: (tabID: string, command?: string, eventId?: string) => void
45-
onCWCContextCommandMessage: (message: TracedChatItem, command?: string) => string | undefined
40+
onCWCContextCommandMessage: (message: ChatItem, command?: string) => string | undefined
4641
onOpenSettingsMessage: (tabID: string) => void
4742
onError: (tabID: string, message: string, title: string) => void
4843
onWarning: (tabID: string, message: string, title: string) => void
@@ -120,18 +115,18 @@ export class Connector {
120115
}
121116
}
122117

123-
requestGenerativeAIAnswer = (tabID: string, payload: ChatPayload): Promise<any> =>
118+
requestGenerativeAIAnswer = (tabID: string, messageId: string, payload: ChatPayload): Promise<any> =>
124119
new Promise((resolve, reject) => {
125120
if (this.isUIReady) {
126121
switch (this.tabsStorage.getTab(tabID)?.type) {
127122
case 'featuredev':
128123
return this.featureDevChatConnector.requestGenerativeAIAnswer(tabID, payload)
129124
default:
130-
return this.cwChatConnector.requestGenerativeAIAnswer(tabID, payload)
125+
return this.cwChatConnector.requestGenerativeAIAnswer(tabID, messageId, payload)
131126
}
132127
} else {
133128
return setTimeout(() => {
134-
return this.requestGenerativeAIAnswer(tabID, payload)
129+
return this.requestGenerativeAIAnswer(tabID, messageId, payload)
135130
}, 2000)
136131
}
137132
})

0 commit comments

Comments
 (0)