Skip to content

Commit 359b1d8

Browse files
authored
telemetry(amazonq): measure e2e latency for amazon q requests (#5566)
## Problem: - We can't measure e2e how long a chat request takes ## Solution: - Plumb the trace id through to the webview -> vscode -> webview. This allows us to know exactly what is happening to a message and how long everything took to get there - chat e2e latency can be linked with the other telemetry events via the traceId
1 parent d6336e4 commit 359b1d8

File tree

14 files changed

+273
-12
lines changed

14 files changed

+273
-12
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { globals } from '../../shared'
7+
import { telemetry } from '../../shared/telemetry'
8+
import { Event, uiEventRecorder } from '../util/eventRecorder'
9+
10+
export class AmazonQChatMessageDuration {
11+
/**
12+
* Record the initial requests in the chat message flow
13+
*/
14+
static startChatMessageTelemetry(msg: { traceId: string; startTime: number; trigger?: string }) {
15+
const { traceId, startTime, trigger } = msg
16+
17+
uiEventRecorder.set(traceId, {
18+
events: {
19+
chatMessageSent: startTime,
20+
},
21+
})
22+
uiEventRecorder.set(traceId, {
23+
events: {
24+
editorReceivedMessage: globals.clock.Date.now(),
25+
},
26+
})
27+
if (trigger) {
28+
uiEventRecorder.set(traceId, {
29+
trigger,
30+
})
31+
}
32+
}
33+
34+
/**
35+
* Stop listening to all incoming events and emit what we've found
36+
*/
37+
static stopChatMessageTelemetry(msg: { traceId: string }) {
38+
const { traceId } = msg
39+
40+
// We can't figure out what trace this event was associated with
41+
if (!traceId) {
42+
return
43+
}
44+
45+
uiEventRecorder.set(traceId, {
46+
events: {
47+
messageDisplayed: globals.clock.Date.now(),
48+
},
49+
})
50+
51+
const metrics = uiEventRecorder.get(traceId)
52+
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+
})
62+
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
66+
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]
74+
75+
const timeDifference = currentEvent.duration - previousEvent.duration
76+
77+
timings.set(currentEvent.event as Event, timeDifference)
78+
}
79+
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,
89+
})
90+
91+
uiEventRecorder.delete(traceId)
92+
}
93+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { RecordMap } from '../../shared/utilities/map'
7+
8+
export type Event =
9+
| 'chatMessageSent' // initial on chat prompt event in the ui
10+
| 'editorReceivedMessage' // message gets from the chat prompt to VSCode
11+
| 'featureReceivedMessage' // message gets redirected from VSCode -> Partner team features implementation
12+
| 'messageDisplayed' // message gets received in the UI
13+
14+
/**
15+
* For a given traceID, map an event to a time
16+
*
17+
* This is used to correlated disjoint events that are happening in different
18+
* parts of Q Chat.
19+
*
20+
* It allows us to tracks time intervals between key events:
21+
* - when VSCode received the message
22+
* - when the feature starts processing the message
23+
* - final message rendering
24+
* and emit those as a final result, rather than having to emit each event individually
25+
*/
26+
export const uiEventRecorder = new RecordMap<{
27+
trigger: string
28+
events: Partial<Record<Event, number>>
29+
}>()

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TabType } from '../ui/storages/tabsStorage'
1010
import { getLogger } from '../../../shared/logger'
1111
import { amazonqMark } from '../../../shared/performance/marks'
1212
import { telemetry } from '../../../shared/telemetry'
13+
import { AmazonQChatMessageDuration } from '../../messages/chatMessageDuration'
1314

1415
export function dispatchWebViewMessagesToApps(
1516
webview: Webview,
@@ -33,6 +34,14 @@ export function dispatchWebViewMessagesToApps(
3334
return
3435
}
3536

37+
if (msg.type === 'startChatMessageTelemetry') {
38+
AmazonQChatMessageDuration.startChatMessageTelemetry(msg)
39+
return
40+
} else if (msg.type === 'stopChatMessageTelemetry') {
41+
AmazonQChatMessageDuration.stopChatMessageTelemetry(msg)
42+
return
43+
}
44+
3645
if (msg.type === 'error') {
3746
const event = msg.event === 'webview_load' ? telemetry.webview_load : telemetry.webview_error
3847
event.emit({

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ 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'
1112

1213
interface ChatPayload {
1314
chatMessage: string
15+
traceId?: string
1416
chatCommand?: string
1517
}
1618

@@ -188,6 +190,7 @@ export class Connector {
188190
command: 'chat-prompt',
189191
chatMessage: payload.chatMessage,
190192
chatCommand: payload.chatCommand,
193+
traceId: payload.traceId,
191194
tabType: 'cwc',
192195
})
193196
})
@@ -258,13 +261,14 @@ export class Connector {
258261
}
259262
: undefined
260263

261-
const answer: ChatItem = {
264+
const answer: TracedChatItem = {
262265
type: messageData.messageType,
263266
messageId: messageData.messageID ?? messageData.triggerID,
264267
body: messageData.message,
265268
followUp: followUps,
266269
canBeVoted: true,
267270
codeReference: messageData.codeReference,
271+
traceId: messageData.traceId,
268272
}
269273

270274
// If it is not there we will not set it
@@ -291,7 +295,7 @@ export class Connector {
291295
return
292296
}
293297
if (messageData.messageType === ChatItemType.ANSWER) {
294-
const answer: ChatItem = {
298+
const answer: TracedChatItem = {
295299
type: messageData.messageType,
296300
body: undefined,
297301
relatedContent: undefined,
@@ -304,6 +308,7 @@ export class Connector {
304308
options: messageData.followUps,
305309
}
306310
: undefined,
311+
traceId: messageData.traceId,
307312
}
308313
this.onChatAnswerReceived(messageData.tabID, answer)
309314

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,23 @@ 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
2930
chatCommand?: string
3031
}
3132

33+
export interface TracedChatItem extends ChatItem {
34+
traceId?: string
35+
}
36+
3237
export interface ConnectorProps {
3338
sendMessageToExtension: (message: ExtensionMessage) => void
3439
onMessageReceived?: (tabID: string, messageData: any, needToShowAPIDocsTab: boolean) => void
3540
onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void
36-
onChatAnswerReceived?: (tabID: string, message: ChatItem) => void
41+
onChatAnswerReceived?: (tabID: string, message: TracedChatItem) => void
3742
onWelcomeFollowUpClicked: (tabID: string, welcomeFollowUpType: WelcomeFollowupType) => void
3843
onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string | undefined) => void
3944
onQuickHandlerCommand: (tabID: string, command?: string, eventId?: string) => void
40-
onCWCContextCommandMessage: (message: ChatItem, command?: string) => string | undefined
45+
onCWCContextCommandMessage: (message: TracedChatItem, command?: string) => string | undefined
4146
onOpenSettingsMessage: (tabID: string) => void
4247
onError: (tabID: string, message: string, title: string) => void
4348
onWarning: (tabID: string, message: string, title: string) => void

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

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5-
import { Connector } from './connector'
5+
import { Connector, TracedChatItem } from './connector'
66
import { ChatItem, ChatItemType, MynahIcons, MynahUI, MynahUIDataModel, NotificationType } from '@aws/mynah-ui'
77
import { ChatPrompt } from '@aws/mynah-ui/dist/static'
88
import { TabsStorage, TabType } from './storages/tabsStorage'
@@ -189,7 +189,7 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
189189
} as ChatItem)
190190
}
191191
},
192-
onChatAnswerReceived: (tabID: string, item: ChatItem) => {
192+
onChatAnswerReceived: (tabID: string, item: TracedChatItem) => {
193193
if (item.type === ChatItemType.ANSWER_PART || item.type === ChatItemType.CODE_RESULT) {
194194
mynahUI.updateLastChatAnswer(tabID, {
195195
...(item.messageId !== undefined ? { messageId: item.messageId } : {}),
@@ -234,6 +234,20 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
234234
promptInputDisabledState: tabsStorage.isTabDead(tabID),
235235
})
236236
tabsStorage.updateTabStatus(tabID, 'free')
237+
238+
if (item.traceId) {
239+
/**
240+
* We've received an answer for a traceId and this message has
241+
* completed its round trip. Send that information back to
242+
* VSCode so we can emit a round trip event
243+
**/
244+
ideApi.postMessage({
245+
type: 'stopChatMessageTelemetry',
246+
tabID,
247+
traceId: item.traceId,
248+
tabType: tabsStorage.getTab(tabID)?.type,
249+
})
250+
}
237251
}
238252
},
239253
onMessageReceived: (tabID: string, messageData: MynahUIDataModel) => {
@@ -362,6 +376,7 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
362376
},
363377
onTabRemove: connector.onTabRemove,
364378
onTabChange: connector.onTabChange,
379+
// TODO: update mynah-ui this type doesn't seem correct https://github.com/aws/mynah-ui/blob/3777a39eb534a91fd6b99d6cf421ce78ee5c7526/src/main.ts#L372
365380
onChatPrompt: (tabID: string, prompt: ChatPrompt, eventId: string | undefined) => {
366381
if ((prompt.prompt ?? '') === '' && (prompt.command ?? '') === '') {
367382
return
@@ -374,6 +389,7 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
374389
} else if (tabsStorage.getTab(tabID)?.type === 'gumby') {
375390
connector.requestAnswer(tabID, {
376391
chatMessage: prompt.prompt ?? '',
392+
traceId: eventId as string,
377393
})
378394
return
379395
}
@@ -383,7 +399,20 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
383399
return
384400
}
385401

386-
textMessageHandler.handle(prompt, tabID)
402+
/**
403+
* When a user presses "enter" send an event that indicates
404+
* we should start tracking the round trip time for this message
405+
**/
406+
ideApi.postMessage({
407+
type: 'startChatMessageTelemetry',
408+
trigger: 'onChatPrompt',
409+
tabID,
410+
traceId: eventId,
411+
tabType: tabsStorage.getTab(tabID)?.type,
412+
startTime: Date.now(),
413+
})
414+
415+
textMessageHandler.handle(prompt, tabID, eventId as string)
387416
},
388417
onVote: connector.onChatItemVoted,
389418
onInBodyButtonClicked: (tabId, messageId, action, eventId) => {

packages/core/src/amazonq/webview/ui/messages/handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class TextMessageHandler {
2424
this.tabsStorage = props.tabsStorage
2525
}
2626

27-
public handle(chatPrompt: ChatPrompt, tabID: string) {
27+
public handle(chatPrompt: ChatPrompt, tabID: string, eventID: string) {
2828
this.tabsStorage.updateTabTypeFromUnknown(tabID, 'cwc')
2929
this.tabsStorage.resetTabTimer(tabID)
3030
this.connector.onUpdateTabType(tabID)
@@ -44,6 +44,7 @@ export class TextMessageHandler {
4444
.requestGenerativeAIAnswer(tabID, {
4545
chatMessage: chatPrompt.prompt ?? '',
4646
chatCommand: chatPrompt.command,
47+
traceId: eventID,
4748
})
4849
.then(() => {})
4950
}

packages/core/src/codewhispererChat/controllers/chat/controller.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhisperer
4949
import { getSelectedCustomization } from '../../../codewhisperer/util/customizationUtil'
5050
import { FeatureConfigProvider } from '../../../shared/featureConfig'
5151
import { getHttpStatusCode, AwsClientResponseError } from '../../../shared/errors'
52+
import { uiEventRecorder } from '../../../amazonq/util/eventRecorder'
53+
import { globals } from '../../../shared'
54+
import { telemetry } from '../../../shared/telemetry'
5255

5356
export interface ChatControllerMessagePublishers {
5457
readonly processPromptChatMessage: MessagePublisher<PromptMessage>
@@ -122,7 +125,21 @@ export class ChatController {
122125
})
123126

124127
this.chatControllerMessageListeners.processPromptChatMessage.onMessage((data) => {
125-
return this.processPromptChatMessage(data)
128+
if (data.traceId) {
129+
uiEventRecorder.set(data.traceId, {
130+
events: {
131+
featureReceivedMessage: globals.clock.Date.now(),
132+
},
133+
})
134+
}
135+
/**
136+
* traceId is only instrumented for chat-prompt but not for things
137+
* like follow-up-was-clicked. In those cases we fallback to a different
138+
* uuid
139+
**/
140+
return telemetry.withTraceId(() => {
141+
return this.processPromptChatMessage(data)
142+
}, data.traceId ?? randomUUID())
126143
})
127144

128145
this.chatControllerMessageListeners.processTabCreatedMessage.onMessage((data) => {
@@ -486,6 +503,7 @@ export class ChatController {
486503
codeQuery: context?.focusAreaContext?.names,
487504
userIntent: this.userIntentRecognizer.getFromPromptChatMessage(message),
488505
customization: getSelectedCustomization(),
506+
traceId: message.traceId,
489507
},
490508
triggerID
491509
)

packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ export class Messenger {
293293
relatedSuggestions: undefined,
294294
triggerID,
295295
messageID,
296+
traceId: triggerPayload.traceId,
296297
},
297298
tabID
298299
)

packages/core/src/codewhispererChat/controllers/chat/model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export type ChatPromptCommandType =
7272
export interface PromptMessage {
7373
message: string | undefined
7474
messageId: string
75+
traceId?: string
7576
command: ChatPromptCommandType | undefined
7677
userIntent: UserIntent | undefined
7778
tabID: string
@@ -145,6 +146,7 @@ export interface TriggerPayload {
145146
readonly customization: Customization
146147
relevantTextDocuments?: RelevantTextDocument[]
147148
useRelevantDocuments?: boolean
149+
traceId?: string
148150
}
149151

150152
export interface InsertedCode {

0 commit comments

Comments
 (0)