Skip to content

Commit f9023a6

Browse files
committed
merge master
2 parents 8a9a620 + 7d6a975 commit f9023a6

File tree

24 files changed

+341
-201
lines changed

24 files changed

+341
-201
lines changed

packages/core/scripts/build/generateServiceClient.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import * as proc from 'child_process'
7-
import * as fs from 'fs-extra' // eslint-disable-line no-restricted-imports
7+
import * as nodefs from 'fs' // eslint-disable-line no-restricted-imports
88
import * as path from 'path'
99

1010
const repoRoot = path.join(process.cwd(), '../../') // root/packages/toolkit -> root/
@@ -33,7 +33,7 @@ async function generateServiceClients(serviceClientDefinitions: ServiceClientDef
3333

3434
/** When cloning aws-sdk-js, we want to pull the version actually used in package-lock.json. */
3535
function getJsSdkVersion(): string {
36-
const json = fs.readFileSync(path.resolve(repoRoot, 'package-lock.json')).toString()
36+
const json = nodefs.readFileSync(path.resolve(repoRoot, 'package-lock.json')).toString()
3737
const packageLock = JSON.parse(json)
3838

3939
return packageLock['packages']['node_modules/aws-sdk']['version']
@@ -115,7 +115,7 @@ async function insertServiceClientsIntoJsSdk(
115115
'apis',
116116
`${serviceClientDefinition.serviceName.toLowerCase()}-${apiVersion}.normal.json`
117117
)
118-
fs.copyFileSync(serviceClientDefinition.serviceJsonPath, jsSdkServiceJsonPath)
118+
nodefs.copyFileSync(serviceClientDefinition.serviceJsonPath, jsSdkServiceJsonPath)
119119
})
120120

121121
const apiMetadataPath = path.join(jsSdkPath, 'apis', 'metadata.json')
@@ -132,7 +132,7 @@ interface ServiceJsonSchema {
132132
}
133133

134134
function getApiVersion(serviceJsonPath: string): string {
135-
const json = fs.readFileSync(serviceJsonPath).toString()
135+
const json = nodefs.readFileSync(serviceJsonPath).toString()
136136
const serviceJson = JSON.parse(json) as ServiceJsonSchema
137137

138138
return serviceJson.metadata.apiVersion
@@ -148,14 +148,14 @@ interface ApiMetadata {
148148
async function patchServicesIntoApiMetadata(apiMetadataPath: string, serviceNames: string[]): Promise<void> {
149149
console.log(`Patching services (${serviceNames.join(', ')}) into API Metadata...`)
150150

151-
const apiMetadataJson = fs.readFileSync(apiMetadataPath).toString()
151+
const apiMetadataJson = nodefs.readFileSync(apiMetadataPath).toString()
152152
const apiMetadata = JSON.parse(apiMetadataJson) as ApiMetadata
153153

154154
serviceNames.forEach((serviceName) => {
155155
apiMetadata[serviceName.toLowerCase()] = { name: serviceName }
156156
})
157157

158-
fs.writeFileSync(apiMetadataPath, JSON.stringify(apiMetadata, undefined, 4))
158+
nodefs.writeFileSync(apiMetadataPath, JSON.stringify(apiMetadata, undefined, 4))
159159
}
160160

161161
/**
@@ -197,7 +197,7 @@ async function integrateServiceClient(repoPath: string, serviceJsonPath: string,
197197

198198
console.log(`Integrating ${typingsFilename} ...`)
199199

200-
fs.copyFileSync(sourceClientPath, destinationClientPath)
200+
nodefs.copyFileSync(sourceClientPath, destinationClientPath)
201201

202202
await sanitizeServiceClient(destinationClientPath)
203203
}
@@ -208,7 +208,7 @@ async function integrateServiceClient(repoPath: string, serviceJsonPath: string,
208208
async function sanitizeServiceClient(generatedClientPath: string): Promise<void> {
209209
console.log('Altering Service Client to fit the codebase...')
210210

211-
let fileContents = fs.readFileSync(generatedClientPath).toString()
211+
let fileContents = nodefs.readFileSync(generatedClientPath).toString()
212212

213213
// Add a header stating the file is autogenerated
214214
fileContents = `
@@ -222,7 +222,7 @@ ${fileContents}
222222

223223
fileContents = fileContents.replace(/(import .* from.*)\.\.(.*)/g, '$1aws-sdk$2')
224224

225-
fs.writeFileSync(generatedClientPath, fileContents)
225+
nodefs.writeFileSync(generatedClientPath, fileContents)
226226
}
227227

228228
// ---------------------------------------------------------------------------------------------------------------------

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') {

0 commit comments

Comments
 (0)