Skip to content

Commit 8680e75

Browse files
authored
telemetry(amazonq): add metrics to amazonq_interactWithMessage aws#6714
- Send metrics on additional context to the `amazonq_interactWithMessage` event. This enables measuring success metrics as they relate to the new context features. - Improve error handling when collecting context items & metrics. - Add new features to /help message - Improve efficiency by switching from vscode.workspace.findFiles to fs.readDirectory
1 parent f474f14 commit 8680e75

File tree

8 files changed

+119
-60
lines changed

8 files changed

+119
-60
lines changed

package-lock.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"skippedTestReport": "ts-node ./scripts/skippedTestReport.ts ./packages/amazonq/test/e2e/"
4242
},
4343
"devDependencies": {
44-
"@aws-toolkits/telemetry": "^1.0.296",
44+
"@aws-toolkits/telemetry": "^1.0.301",
4545
"@playwright/browser-chromium": "^1.43.1",
4646
"@stylistic/eslint-plugin": "^2.11.0",
4747
"@types/he": "^1.2.3",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Amazon Q chat: Add support for `.md` file rules in workspace-level `.amazonq/rules` directory"
4+
}

packages/core/src/amazonq/webview/ui/texts/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export const helpMessage = `I'm Amazon Q, a generative AI assistant. Learn more
4444
\n\n- Answer questions about AWS
4545
\n\n- Answer questions about general programming concepts
4646
\n\n- Answer questions about your workspace with @workspace
47+
\n\n- Answer questions about your code with @Files and @Folders
48+
\n\n- Create and use saved prompts with @Prompts
4749
\n\n- Explain what a line of code or code function does
4850
\n\n- Write unit tests and code
4951
\n\n- Debug and fix code

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

Lines changed: 75 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ import { DefaultAmazonQAppInitContext } from '../../../amazonq/apps/initContext'
6666
import globals from '../../../shared/extensionGlobals'
6767
import { MynahIconsType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui'
6868
import { LspClient } from '../../../amazonq/lsp/lspClient'
69-
import { ContextCommandItem, ContextCommandItemType } from '../../../amazonq/lsp/types'
69+
import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from '../../../amazonq/lsp/types'
7070
import { workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants'
7171
import fs from '../../../shared/fs/fs'
7272
import { FeatureConfigProvider, Features } from '../../../shared/featureConfig'
@@ -588,8 +588,9 @@ export class ChatController {
588588
return
589589
}
590590

591-
// TODO: Fix for multiple workspace setup
592-
const projectRoot = session.relativePathToWorkspaceRoot.get(message.filePath)
591+
// Check if clicked file is in a different workspace root
592+
const projectRoot =
593+
session.relativePathToWorkspaceRoot.get(message.filePath) || workspace.workspaceFolders?.[0]?.uri.fsPath
593594
if (!projectRoot) {
594595
return
595596
}
@@ -877,6 +878,34 @@ export class ChatController {
877878
this.messenger.sendStaticTextResponse(responseType, triggerID, tabID)
878879
}
879880

881+
/**
882+
* @returns A Uri array of prompt files in each workspace root's .amazonq/rules directory
883+
*/
884+
private async collectWorkspaceRules(): Promise<vscode.Uri[]> {
885+
const rulesFiles: vscode.Uri[] = []
886+
887+
if (!vscode.workspace.workspaceFolders) {
888+
return rulesFiles
889+
}
890+
891+
for (const folder of vscode.workspace.workspaceFolders) {
892+
const rulesPath = vscode.Uri.joinPath(folder.uri, '.amazonq', 'rules')
893+
const folderExists = await fs.exists(rulesPath)
894+
895+
if (folderExists) {
896+
const entries = await fs.readdir(rulesPath)
897+
898+
for (const [name, type] of entries) {
899+
if (type === vscode.FileType.File && name.endsWith(promptFileExtension)) {
900+
rulesFiles.push(vscode.Uri.joinPath(rulesPath, name))
901+
}
902+
}
903+
}
904+
}
905+
906+
return rulesFiles
907+
}
908+
880909
private async resolveContextCommandPayload(
881910
triggerPayload: TriggerPayload,
882911
session: ChatSession
@@ -885,14 +914,13 @@ export class ChatController {
885914
const relativePaths: string[] = []
886915

887916
// Check for workspace rules to add to context
888-
const workspaceRules = await vscode.workspace.findFiles(`.amazonq/rules/*${promptFileExtension}`)
917+
const workspaceRules = await this.collectWorkspaceRules()
889918
if (workspaceRules.length > 0) {
890919
contextCommands.push(
891920
...workspaceRules.map((rule) => {
892921
const workspaceFolderPath = vscode.workspace.getWorkspaceFolder(rule)?.uri?.path || ''
893922
return {
894923
workspaceFolder: workspaceFolderPath,
895-
// todo: add 'prompt' type to LSP model
896924
type: 'file' as ContextCommandItemType,
897925
relativePath: path.relative(workspaceFolderPath, rule.path),
898926
}
@@ -904,7 +932,6 @@ export class ChatController {
904932
// Add context commands added by user to context
905933
if (triggerPayload.context !== undefined && triggerPayload.context.length > 0) {
906934
for (const context of triggerPayload.context) {
907-
// todo: add handling of 'prompt' type (dependent on LSP changes)
908935
if (typeof context !== 'string' && context.route && context.route.length === 2) {
909936
contextCommands.push({
910937
workspaceFolder: context.route[0] || '',
@@ -930,52 +957,59 @@ export class ChatController {
930957
)
931958
session.relativePathToWorkspaceRoot.set(relativePath, contextCommand.workspaceFolder)
932959
}
933-
const prompts = await LspClient.instance.getContextCommandPrompt(contextCommands)
934-
if (prompts.length === 0) {
935-
return []
960+
let prompts: AdditionalContextPrompt[] = []
961+
try {
962+
prompts = await LspClient.instance.getContextCommandPrompt(contextCommands)
963+
} catch (e) {
964+
// todo: handle @workspace used before indexing is ready
965+
getLogger().verbose(`Could not get context command prompts: ${e}`)
936966
}
937967

938968
let currentContextLength = 0
939969
triggerPayload.additionalContents = []
940-
triggerPayload.additionalContextLengths = this.telemetryHelper.getContextLengths(prompts)
941-
triggerPayload.truncatedAdditionalContextLengths = {
970+
const emptyLengths = {
942971
fileContextLength: 0,
943972
promptContextLength: 0,
944973
ruleContextLength: 0,
945974
}
946-
for (const prompt of prompts.slice(0, 20)) {
947-
// Todo: add mechanism for sorting/prioritization of additional context
948-
const entry = {
949-
name: prompt.name.substring(0, aditionalContentNameLimit),
950-
description: prompt.description.substring(0, aditionalContentNameLimit),
951-
innerContext: prompt.content.substring(0, additionalContentInnerContextLimit),
952-
}
953-
// make sure the relevantDocument + additionalContext
954-
// combined does not exceed 40k characters before generating the request payload.
955-
// Do truncation and make sure triggerPayload.documentReferences is up-to-date after truncation
956-
// TODO: Use a context length indicator
957-
if (currentContextLength + entry.innerContext.length > contextMaxLength) {
958-
getLogger().warn(`Selected context exceeds context size limit: ${entry.description} `)
959-
break
960-
}
975+
triggerPayload.additionalContextLengths = emptyLengths
976+
triggerPayload.truncatedAdditionalContextLengths = emptyLengths
977+
978+
if (Array.isArray(prompts) && prompts.length > 0) {
979+
triggerPayload.additionalContextLengths = this.telemetryHelper.getContextLengths(prompts)
980+
for (const prompt of prompts.slice(0, 20)) {
981+
const entry = {
982+
name: prompt.name.substring(0, aditionalContentNameLimit),
983+
description: prompt.description.substring(0, aditionalContentNameLimit),
984+
innerContext: prompt.content.substring(0, additionalContentInnerContextLimit),
985+
}
986+
// make sure the relevantDocument + additionalContext
987+
// combined does not exceed 40k characters before generating the request payload.
988+
// Do truncation and make sure triggerPayload.documentReferences is up-to-date after truncation
989+
// TODO: Use a context length indicator
990+
if (currentContextLength + entry.innerContext.length > contextMaxLength) {
991+
getLogger().warn(`Selected context exceeds context size limit: ${entry.description} `)
992+
break
993+
}
961994

962-
const contextType = this.telemetryHelper.getContextType(prompt)
963-
if (contextType === 'rule') {
964-
triggerPayload.truncatedAdditionalContextLengths.ruleContextLength += entry.innerContext.length
965-
} else if (contextType === 'prompt') {
966-
triggerPayload.truncatedAdditionalContextLengths.promptContextLength += entry.innerContext.length
967-
} else if (contextType === 'file') {
968-
triggerPayload.truncatedAdditionalContextLengths.fileContextLength += entry.innerContext.length
969-
}
995+
const contextType = this.telemetryHelper.getContextType(prompt)
996+
if (contextType === 'rule') {
997+
triggerPayload.truncatedAdditionalContextLengths.ruleContextLength += entry.innerContext.length
998+
} else if (contextType === 'prompt') {
999+
triggerPayload.truncatedAdditionalContextLengths.promptContextLength += entry.innerContext.length
1000+
} else if (contextType === 'file') {
1001+
triggerPayload.truncatedAdditionalContextLengths.fileContextLength += entry.innerContext.length
1002+
}
9701003

971-
triggerPayload.additionalContents.push(entry)
972-
currentContextLength += entry.innerContext.length
973-
let relativePath = path.relative(workspaceFolder, prompt.filePath)
974-
// Handle user prompts outside the workspace
975-
if (prompt.filePath.startsWith(getUserPromptsDirectory())) {
976-
relativePath = path.basename(prompt.filePath)
1004+
triggerPayload.additionalContents.push(entry)
1005+
currentContextLength += entry.innerContext.length
1006+
let relativePath = path.relative(workspaceFolder, prompt.filePath)
1007+
// Handle user prompts outside the workspace
1008+
if (prompt.filePath.startsWith(getUserPromptsDirectory())) {
1009+
relativePath = path.basename(prompt.filePath)
1010+
}
1011+
relativePaths.push(relativePath)
9771012
}
978-
relativePaths.push(relativePath)
9791013
}
9801014
getLogger().info(`Retrieved chunks of additional context count: ${triggerPayload.additionalContents.length} `)
9811015

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,24 @@ export class Messenger {
136136
)
137137
}
138138
this.telemetryHelper.setResponseStreamStartTime(tabID)
139+
140+
let cwsprChatHasProjectContext = false
139141
if (
140142
triggerPayload.relevantTextDocuments &&
141143
triggerPayload.relevantTextDocuments.length > 0 &&
142144
triggerPayload.useRelevantDocuments === true
143145
) {
144-
this.telemetryHelper.setResponseFromProjectContext(messageID)
146+
cwsprChatHasProjectContext = true
145147
}
148+
const additionalCounts = this.telemetryHelper.getAdditionalContextCounts(triggerPayload)
149+
150+
this.telemetryHelper.setResponseFromAdditionalContext(messageID, {
151+
cwsprChatHasProjectContext,
152+
cwsprChatRuleContextCount: triggerPayload.workspaceRulesCount,
153+
cwsprChatFileContextCount: additionalCounts.fileContextCount,
154+
cwsprChatFolderContextCount: additionalCounts.folderContextCount,
155+
cwsprChatPromptContextCount: additionalCounts.promptContextCount,
156+
})
146157

147158
const eventCounts = new Map<string, number>()
148159
waitUntil(

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ export type AdditionalContextLengths = {
205205
ruleContextLength: number
206206
}
207207

208+
export type AdditionalContextInfo = {
209+
cwsprChatFileContextCount?: number
210+
cwsprChatFolderContextCount?: number
211+
cwsprChatPromptContextCount?: number
212+
cwsprChatRuleContextCount?: number
213+
cwsprChatHasProjectContext?: boolean
214+
}
215+
208216
// TODO move this to API definition (or just use this across the codebase)
209217
export type RelevantTextDocumentAddition = RelevantTextDocument & { startLine: number; endLine: number }
210218

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

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
SourceLinkClickMessage,
2929
TriggerPayload,
3030
AdditionalContextLengths,
31+
AdditionalContextInfo,
3132
} from './model'
3233
import { TriggerEvent, TriggerEventsStorage } from '../../storages/triggerEvents'
3334
import globals from '../../../shared/extensionGlobals'
@@ -71,7 +72,7 @@ export class CWCTelemetryHelper {
7172
private conversationStreamStartTime: Map<string, number> = new Map()
7273
private conversationStreamTotalTime: Map<string, number> = new Map()
7374
private responseStreamTimeForChunks: Map<string, number[]> = new Map()
74-
private responseWithProjectContext: Map<string, boolean> = new Map()
75+
private responseWithContextInfo: Map<string, AdditionalContextInfo> = new Map()
7576

7677
// Keeps track of when chunks of data were displayed in a tab
7778
private displayTimeForChunks: Map<string, number[]> = new Map()
@@ -224,6 +225,11 @@ export class CWCTelemetryHelper {
224225
) {
225226
const conversationId = this.getConversationId(message.tabID)
226227
let event: AmazonqInteractWithMessage | undefined
228+
let additionalContextInfo = undefined
229+
const messageId = (message as any).messageId
230+
if (messageId) {
231+
additionalContextInfo = this.responseWithContextInfo.get(messageId)
232+
}
227233
switch (message.command) {
228234
case 'insert_code_at_cursor_position':
229235
message = message as InsertCodeAtCursorPosition
@@ -240,7 +246,6 @@ export class CWCTelemetryHelper {
240246
cwsprChatHasReference: message.codeReference && message.codeReference.length > 0,
241247
cwsprChatCodeBlockIndex: message.codeBlockIndex,
242248
cwsprChatTotalCodeBlocks: message.totalCodeBlocks,
243-
cwsprChatHasProjectContext: this.responseWithProjectContext.get(message.messageId),
244249
cwsprChatProgrammingLanguage: message.codeBlockLanguage,
245250
}
246251
break
@@ -258,7 +263,6 @@ export class CWCTelemetryHelper {
258263
cwsprChatHasReference: message.codeReference && message.codeReference.length > 0,
259264
cwsprChatCodeBlockIndex: message.codeBlockIndex,
260265
cwsprChatTotalCodeBlocks: message.totalCodeBlocks,
261-
cwsprChatHasProjectContext: this.responseWithProjectContext.get(message.messageId),
262266
cwsprChatProgrammingLanguage: message.codeBlockLanguage,
263267
}
264268
break
@@ -275,7 +279,6 @@ export class CWCTelemetryHelper {
275279
message.referenceTrackerInformation && message.referenceTrackerInformation.length > 0,
276280
cwsprChatCodeBlockIndex: message.codeBlockIndex,
277281
cwsprChatTotalCodeBlocks: message.totalCodeBlocks,
278-
cwsprChatHasProjectContext: this.responseWithProjectContext.get(message.messageId),
279282
}
280283
break
281284
case 'view_diff':
@@ -291,7 +294,6 @@ export class CWCTelemetryHelper {
291294
message.referenceTrackerInformation && message.referenceTrackerInformation.length > 0,
292295
cwsprChatCodeBlockIndex: message.codeBlockIndex,
293296
cwsprChatTotalCodeBlocks: message.totalCodeBlocks,
294-
cwsprChatHasProjectContext: this.responseWithProjectContext.get(message.messageId),
295297
}
296298
break
297299
case 'follow-up-was-clicked':
@@ -302,7 +304,6 @@ export class CWCTelemetryHelper {
302304
credentialStartUrl: AuthUtil.instance.startUrl,
303305
cwsprChatMessageId: message.messageId,
304306
cwsprChatInteractionType: 'clickFollowUp',
305-
cwsprChatHasProjectContext: this.responseWithProjectContext.get(message.messageId),
306307
}
307308
break
308309
case 'chat-item-voted':
@@ -313,7 +314,6 @@ export class CWCTelemetryHelper {
313314
cwsprChatConversationId: conversationId ?? '',
314315
credentialStartUrl: AuthUtil.instance.startUrl,
315316
cwsprChatInteractionType: message.vote,
316-
cwsprChatHasProjectContext: this.responseWithProjectContext.get(message.messageId),
317317
}
318318
break
319319
case 'source-link-click':
@@ -325,7 +325,6 @@ export class CWCTelemetryHelper {
325325
credentialStartUrl: AuthUtil.instance.startUrl,
326326
cwsprChatInteractionType: 'clickLink',
327327
cwsprChatInteractionTarget: message.link,
328-
cwsprChatHasProjectContext: this.responseWithProjectContext.get(message.messageId),
329328
}
330329
break
331330
case 'response-body-link-click':
@@ -337,7 +336,6 @@ export class CWCTelemetryHelper {
337336
credentialStartUrl: AuthUtil.instance.startUrl,
338337
cwsprChatInteractionType: 'clickBodyLink',
339338
cwsprChatInteractionTarget: message.link,
340-
cwsprChatHasProjectContext: this.responseWithProjectContext.get(message.messageId),
341339
}
342340
break
343341
case 'footer-info-link-click':
@@ -356,7 +354,7 @@ export class CWCTelemetryHelper {
356354
if (!event) {
357355
return
358356
}
359-
telemetry.amazonq_interactWithMessage.emit(event)
357+
telemetry.amazonq_interactWithMessage.emit({ ...event, ...additionalContextInfo })
360358

361359
codeWhispererClient
362360
.sendTelemetryEvent({
@@ -369,7 +367,8 @@ export class CWCTelemetryHelper {
369367
acceptedCharacterCount: event.cwsprChatAcceptedCharactersLength,
370368
acceptedLineCount: event.cwsprChatAcceptedNumberOfLines,
371369
acceptedSnippetHasReference: false,
372-
hasProjectLevelContext: this.responseWithProjectContext.get(event.cwsprChatMessageId),
370+
hasProjectLevelContext: this.responseWithContextInfo.get(event.cwsprChatMessageId)
371+
?.cwsprChatHasProjectContext,
373372
customizationArn: undefinedIfEmpty(getSelectedCustomization().arn),
374373
},
375374
},
@@ -456,7 +455,7 @@ export class CWCTelemetryHelper {
456455
})
457456
}
458457

459-
private getAdditionalContextCounts(triggerPayload: TriggerPayload) {
458+
public getAdditionalContextCounts(triggerPayload: TriggerPayload) {
460459
const counts = {
461460
fileContextCount: 0,
462461
folderContextCount: 0,
@@ -631,8 +630,8 @@ export class CWCTelemetryHelper {
631630
this.displayTimeForChunks.set(tabID, [...chunkTimes, time])
632631
}
633632

634-
public setResponseFromProjectContext(messageId: string) {
635-
this.responseWithProjectContext.set(messageId, true)
633+
public setResponseFromAdditionalContext(messageId: string, additionalContextInfo: AdditionalContextInfo) {
634+
this.responseWithContextInfo.set(messageId, additionalContextInfo)
636635
}
637636

638637
public setConversationStreamStartTime(tabID: string) {

0 commit comments

Comments
 (0)