From 2f76fdf69d180b3f3f82638107da8f00b814266b Mon Sep 17 00:00:00 2001 From: Avi Alpert Date: Mon, 12 May 2025 14:39:45 -0400 Subject: [PATCH] feat(amazonq): telemetry for chat history and export --- chat-client/src/client/messager.ts | 6 +- chat-client/src/client/mynahUi.ts | 2 +- chat-client/src/contracts/telemetry.ts | 1 + .../agenticChat/agenticChatController.test.ts | 1 + .../agenticChat/agenticChatController.ts | 2 +- .../agenticChat/qAgenticChatServer.test.ts | 1 + .../agenticChat/tabBarController.test.ts | 73 ++++++++++++++++++- .../agenticChat/tabBarController.ts | 55 +++++++++++++- .../agenticChat/tools/chatDb/chatDb.ts | 41 +++++++++-- .../chat/telemetry/chatTelemetryController.ts | 3 + .../chat/telemetry/clientTelemetry.ts | 4 + .../src/shared/telemetry/telemetryService.ts | 40 ++++++++++ .../src/shared/telemetry/types.ts | 42 +++++++++++ 13 files changed, 254 insertions(+), 17 deletions(-) diff --git a/chat-client/src/client/messager.ts b/chat-client/src/client/messager.ts index b884fc557f..c089c644ed 100644 --- a/chat-client/src/client/messager.ts +++ b/chat-client/src/client/messager.ts @@ -51,6 +51,7 @@ import { ENTER_FOCUS, ERROR_MESSAGE_TELEMETRY_EVENT, EXIT_FOCUS, + HISTORY_BUTTON_CLICK_TELEMETRY_EVENT, INFO_LINK_CLICK_TELEMETRY_EVENT, INSERT_TO_CURSOR_POSITION_TELEMETRY_EVENT, LINK_CLICK_TELEMETRY_EVENT, @@ -205,8 +206,11 @@ export class Messager { this.chatApi.fileClick(params) } - onListConversations = (filter?: Record): void => { + onListConversations = (filter?: Record, tabButtonClicked?: boolean): void => { this.chatApi.listConversations({ filter }) + if (tabButtonClicked) { + this.chatApi.telemetry({ triggerType: 'click', name: HISTORY_BUTTON_CLICK_TELEMETRY_EVENT }) + } } onConversationClick = (conversationId: string, action?: ConversationAction): void => { diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index 5cb7723ade..5d40f8ca36 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -444,7 +444,7 @@ export const createMynahUi = ( }, onTabBarButtonClick: (tabId: string, buttonId: string) => { if (buttonId === ChatHistory.TabBarButtonId) { - messager.onListConversations() + messager.onListConversations(undefined, true) return } diff --git a/chat-client/src/contracts/telemetry.ts b/chat-client/src/contracts/telemetry.ts index 274d05bab2..4b71b1494e 100644 --- a/chat-client/src/contracts/telemetry.ts +++ b/chat-client/src/contracts/telemetry.ts @@ -12,6 +12,7 @@ export const LINK_CLICK_TELEMETRY_EVENT = 'linkClick' export const INFO_LINK_CLICK_TELEMETRY_EVENT = 'infoLinkClick' export const SOURCE_LINK_CLICK_TELEMETRY_EVENT = 'sourceLinkClick' export const AUTH_FOLLOW_UP_CLICKED_TELEMETRY_EVENT = 'authFollowupClicked' +export const HISTORY_BUTTON_CLICK_TELEMETRY_EVENT = 'historyButtonClick' export enum RelevancyVoteType { UP = 'upvote', diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts index 2b570bf46a..bc03477ea0 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts @@ -176,6 +176,7 @@ describe('AgenticChatController', () => { readFile: sinon.stub().resolves(), writeFile: fsWriteFileStub.resolves(), rm: sinon.stub().resolves(), + getFileSize: sinon.stub().resolves(), } // Add agent with runTool method to testFeatures diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts index 2ec71eddde..0753c04930 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -174,7 +174,7 @@ export class AgenticChatController implements ChatHandlers { this.#telemetryService = telemetryService this.#serviceManager = serviceManager this.#chatHistoryDb = new ChatDatabase(features) - this.#tabBarController = new TabBarController(features, this.#chatHistoryDb) + this.#tabBarController = new TabBarController(features, this.#chatHistoryDb, telemetryService) this.#additionalContextProvider = new AdditionalContextProvider(features.workspace, features.lsp) this.#contextCommandsProvider = new ContextCommandsProvider( this.#features.logging, diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.test.ts index 792bfaec76..538e349835 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.test.ts @@ -31,6 +31,7 @@ describe('QAgenticChatServer', () => { readFile: sinon.stub().resolves(), writeFile: sinon.stub().resolves(), rm: sinon.stub().resolves(), + getFileSize: sinon.stub().resolves(), } // @ts-ignore diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.test.ts index 85d29a004b..7b175f9dd5 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.test.ts @@ -11,12 +11,15 @@ import { ChatDatabase, EMPTY_CONVERSATION_LIST_ID } from './tools/chatDb/chatDb' import { Tab } from './tools/chatDb/util' import { ConversationItemGroup, OpenTabParams, OpenTabResult } from '@aws/language-server-runtimes-types' import { InitializeParams } from '@aws/language-server-runtimes/protocol' +import { ChatHistoryActionType } from '../../shared/telemetry/types' +import { TelemetryService } from '../../shared/telemetry/telemetryService' describe('TabBarController', () => { let testFeatures: TestFeatures let chatHistoryDb: ChatDatabase let tabBarController: TabBarController let clock: sinon.SinonFakeTimers + let telemetryService: TelemetryService beforeEach(() => { testFeatures = new TestFeatures() @@ -29,9 +32,17 @@ describe('TabBarController', () => { setHistoryIdMapping: sinon.stub(), getOpenTabs: sinon.stub().returns([]), updateTabOpenState: sinon.stub(), + getDatabaseFileSize: sinon.stub(), + getLoadTime: sinon.stub(), } as unknown as ChatDatabase - tabBarController = new TabBarController(testFeatures, chatHistoryDb) + telemetryService = { + emitChatHistoryAction: sinon.stub(), + emitExportTab: sinon.stub(), + emitLoadHistory: sinon.stub(), + } as any + + tabBarController = new TabBarController(testFeatures, chatHistoryDb, telemetryService) clock = sinon.useFakeTimers() }) @@ -56,7 +67,7 @@ describe('TabBarController', () => { it('should perform debounced search when search filter is provided', async () => { const mockSearchResults = [{ id: 'result1' }] - ;(chatHistoryDb.searchMessages as sinon.SinonStub).returns(mockSearchResults) + ;(chatHistoryDb.searchMessages as sinon.SinonStub).returns({ results: mockSearchResults, searchTime: 100 }) const promise = tabBarController.onListConversations({ filter: { search: 'test query' } }) @@ -67,11 +78,27 @@ describe('TabBarController', () => { assert.deepStrictEqual(result.list, mockSearchResults) sinon.assert.calledWith(chatHistoryDb.searchMessages as sinon.SinonStub, 'test query') + sinon.assert.calledWith(telemetryService.emitChatHistoryAction as sinon.SinonStub, { + action: ChatHistoryActionType.Search, + languageServerVersion: testFeatures.runtime.serverInfo.version, + amazonqHistoryFileSize: undefined, + amazonqTimeToSearchHistory: 100, + result: 'Succeeded', + }) }) it('should clear previous timeout when multiple search requests are made', async () => { const clearTimeoutSpy = sinon.spy(global, 'clearTimeout') + // Setup mock return values for searchMessages + const mockSearchResults1 = [{ id: 'result1' }] + const mockSearchResults2 = [{ id: 'result2' }] + ;(chatHistoryDb.searchMessages as sinon.SinonStub) + .onFirstCall() + .returns({ results: mockSearchResults1, searchTime: 100 }) + .onSecondCall() + .returns({ results: mockSearchResults2, searchTime: 100 }) + // First search request const promise1 = tabBarController.onListConversations({ filter: { search: 'first query' } }) @@ -158,7 +185,7 @@ describe('TabBarController', () => { items: [{ id: 'history1' }, { id: 'history2' }], }, ] - ;(chatHistoryDb.searchMessages as sinon.SinonStub).returns(mockSearchResults) + ;(chatHistoryDb.searchMessages as sinon.SinonStub).returns({ results: mockSearchResults, searchTime: 100 }) const promise = tabBarController.onListConversations({ filter: { @@ -186,7 +213,7 @@ describe('TabBarController', () => { const mockSearchResults: ConversationItemGroup[] = [ { items: [{ id: 'empty', description: 'No matches found' }] }, ] - ;(chatHistoryDb.searchMessages as sinon.SinonStub).returns(mockSearchResults) + ;(chatHistoryDb.searchMessages as sinon.SinonStub).returns({ results: mockSearchResults, searchTime: 100 }) const promise = tabBarController.onListConversations({ filter: { @@ -218,6 +245,11 @@ describe('TabBarController', () => { await tabBarController.onConversationClick({ id: historyId }) sinon.assert.calledWith(openTabStub, { tabId: openTabId }) + sinon.assert.calledWith(telemetryService.emitChatHistoryAction as sinon.SinonStub, { + action: ChatHistoryActionType.Open, + languageServerVersion: testFeatures.runtime.serverInfo.version, + result: 'Succeeded', + }) }) it('should restore tab when conversation is not already open', async () => { @@ -242,6 +274,11 @@ describe('TabBarController', () => { const result = await tabBarController.onConversationClick({ id: historyId, action: 'delete' }) sinon.assert.calledWith(chatHistoryDb.deleteHistory as sinon.SinonStub, historyId) + sinon.assert.calledWith(telemetryService.emitChatHistoryAction as sinon.SinonStub, { + action: ChatHistoryActionType.Delete, + languageServerVersion: testFeatures.runtime.serverInfo.version, + result: 'Succeeded', + }) assert.strictEqual(result.success, true) }) @@ -313,6 +350,13 @@ describe('TabBarController', () => { // Write serialized content to file sinon.assert.calledWith(fsWriteFileStub, '/testworkspace/test.md', 'Test Serialized Content') + sinon.assert.calledWith(telemetryService.emitChatHistoryAction as sinon.SinonStub, { + action: ChatHistoryActionType.Export, + languageServerVersion: testFeatures.runtime.serverInfo.version, + filenameExt: 'markdown', + result: 'Succeeded', + }) + assert.strictEqual(result.success, true) }) @@ -381,6 +425,12 @@ describe('TabBarController', () => { // Write serialized content to file sinon.assert.calledWith(fsWriteFileStub, '/testworkspace/test.md', 'Test Serialized Content') + sinon.assert.calledWith(telemetryService.emitExportTab as sinon.SinonStub, { + filenameExt: 'markdown', + languageServerVersion: testFeatures.runtime.serverInfo.version, + result: 'Succeeded', + }) + assert.strictEqual(result.success, true) }) }) @@ -436,6 +486,13 @@ describe('TabBarController', () => { sinon.assert.calledTwice(restoreTabStub) sinon.assert.calledWith(restoreTabStub.firstCall, mockTabs[0]) sinon.assert.calledWith(restoreTabStub.secondCall, mockTabs[1]) + sinon.assert.calledWith(telemetryService.emitLoadHistory as sinon.SinonStub, { + openTabCount: 2, + amazonqTimeToLoadHistory: -1, + amazonqHistoryFileSize: -1, + languageServerVersion: testFeatures.runtime.serverInfo.version, + result: 'Succeeded', + }) }) it('should only load chats once', async () => { @@ -448,6 +505,14 @@ describe('TabBarController', () => { await tabBarController.loadChats() // Second call should be ignored sinon.assert.calledOnce(restoreTabStub) + sinon.assert.calledOnce(telemetryService.emitLoadHistory as sinon.SinonStub) + sinon.assert.calledWith(telemetryService.emitLoadHistory as sinon.SinonStub, { + openTabCount: 1, + amazonqTimeToLoadHistory: -1, + amazonqHistoryFileSize: -1, + languageServerVersion: testFeatures.runtime.serverInfo.version, + result: 'Succeeded', + }) }) it('should not restore tabs with empty conversations', async () => { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.ts index 85d3859bb1..860337a6bd 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.ts @@ -17,6 +17,8 @@ import { } from '@aws/language-server-runtimes-types' import { URI, Utils } from 'vscode-uri' import { InitializeParams } from '@aws/language-server-runtimes/server-interface' +import { TelemetryService } from '../../shared/telemetry/telemetryService' +import { ChatHistoryActionType } from '../../shared/telemetry/types' /** * Controller for managing chat history and export functionality. @@ -35,10 +37,12 @@ export class TabBarController { readonly #DebounceTime = 300 // milliseconds #features: Features #chatHistoryDb: ChatDatabase + #telemetryService: TelemetryService - constructor(features: Features, chatHistoryDb: ChatDatabase) { + constructor(features: Features, chatHistoryDb: ChatDatabase, telemetryService: TelemetryService) { this.#features = features this.#chatHistoryDb = chatHistoryDb + this.#telemetryService = telemetryService } /** @@ -71,9 +75,20 @@ export class TabBarController { } if (searchFilter) { + const dbSize = this.#chatHistoryDb.getDatabaseFileSize() + let list: ConversationItemGroup[] = await new Promise(resolve => { this.#searchTimeout = setTimeout(() => { - const results = this.#chatHistoryDb.searchMessages(searchFilter) + const { results, searchTime } = this.#chatHistoryDb.searchMessages(searchFilter) + + this.#telemetryService.emitChatHistoryAction({ + action: ChatHistoryActionType.Search, + languageServerVersion: this.#features.runtime.serverInfo.version, + amazonqHistoryFileSize: dbSize, + amazonqTimeToSearchHistory: searchTime, + result: 'Succeeded', + }) + resolve(results) }, this.#DebounceTime) }) @@ -145,8 +160,18 @@ export class TabBarController { const selectedTab = this.#chatHistoryDb.getTab(historyID) await this.restoreTab(selectedTab) } + this.#telemetryService.emitChatHistoryAction({ + action: ChatHistoryActionType.Open, + languageServerVersion: this.#features.runtime.serverInfo.version, + result: 'Succeeded', + }) } else if (params.action === 'delete') { this.#chatHistoryDb.deleteHistory(historyID) + this.#telemetryService.emitChatHistoryAction({ + action: ChatHistoryActionType.Delete, + languageServerVersion: this.#features.runtime.serverInfo.version, + result: 'Succeeded', + }) } else if (params.action === 'export') { let openTabID = this.#chatHistoryDb.getOpenTabId(historyID) @@ -164,7 +189,14 @@ export class TabBarController { return { ...params, success: false } } - await this.onExportTab(openTabID) + const format = await this.onExportTab(openTabID) + + this.#telemetryService.emitChatHistoryAction({ + action: ChatHistoryActionType.Export, + languageServerVersion: this.#features.runtime.serverInfo.version, + filenameExt: format, + result: 'Succeeded', + }) } else { this.#features.logging.error(`Unsupported action: ${params.action}`) return { ...params, success: false } @@ -175,7 +207,13 @@ export class TabBarController { async onTabBarAction(params: TabBarActionParams) { if (params.action === 'export' && params.tabId) { - await this.onExportTab(params.tabId) + const format = await this.onExportTab(params.tabId) + + this.#telemetryService.emitExportTab({ + filenameExt: format, + languageServerVersion: this.#features.runtime.serverInfo.version, + result: 'Succeeded', + }) return { ...params, success: true } } @@ -210,6 +248,8 @@ export class TabBarController { }) await this.#features.workspace.fs.writeFile(targetPath.path, content) + + return format } /** @@ -242,6 +282,13 @@ export class TabBarController { await this.restoreTab(conversation) } } + this.#telemetryService.emitLoadHistory({ + amazonqTimeToLoadHistory: this.#chatHistoryDb.getLoadTime() ?? -1, + amazonqHistoryFileSize: this.#chatHistoryDb.getDatabaseFileSize() ?? -1, + openTabCount: openConversations.length, + languageServerVersion: this.#features.runtime.serverInfo.version, + result: 'Succeeded', + }) } } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts index 3cdd07f29e..0d2f0903bf 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts @@ -50,6 +50,8 @@ export class ChatDatabase { #dbDirectory: string #features: Features #initialized: boolean = false + #loadTimeMs?: number + #dbFileSize?: number constructor(features: Features) { this.#features = features @@ -61,14 +63,26 @@ export class ChatDatabase { ) const workspaceId = this.getWorkspaceIdentifier() const dbName = `chat-history-${workspaceId}.json` + const dbPath = path.join(this.#dbDirectory, dbName) - this.#features.logging.log(`Initializing database at ${this.#dbDirectory}/${dbName}`) + this.#features.logging.log(`Initializing database at ${dbPath}`) + + this.#features.workspace.fs + .getFileSize(dbPath) + .then(({ size }) => { + this.#dbFileSize = size + }) + .catch(err => { + this.#features.logging.log(`Error getting db file size: ${err}`) + }) + + const startTime = Date.now() this.#db = new Loki(dbName, { adapter: new FileSystemAdapter(features.workspace, this.#dbDirectory), autosave: true, autoload: true, - autoloadCallback: () => this.databaseInitialize(), + autoloadCallback: () => this.databaseInitialize(startTime), autosaveInterval: 1000, persistenceMethod: 'fs', }) @@ -115,7 +129,15 @@ export class ChatDatabase { return 'no-workspace' } - async databaseInitialize() { + /** + * Gets the current size of the database file in bytes. + * @returns Promise that resolves to the file size in bytes, or undefined if the file doesn't exist + */ + getDatabaseFileSize(): number | undefined { + return this.#dbFileSize + } + + async databaseInitialize(startTime: number) { let entries = this.#db.getCollection(TabCollection) if (entries === null) { this.#features.logging.log(`Creating new collection`) @@ -125,6 +147,7 @@ export class ChatDatabase { }) } this.#initialized = true + this.#loadTimeMs = Date.now() - startTime } getOpenTabs() { @@ -134,6 +157,10 @@ export class ChatDatabase { } } + getLoadTime() { + return this.#loadTimeMs + } + getTab(historyId: string) { if (this.#initialized) { const collection = this.#db.getCollection(TabCollection) @@ -197,12 +224,14 @@ export class ChatDatabase { * - Groups the filtered results by date * - If no results are found, returns a single group with a "No matches found" message **/ - searchMessages(filter: string): ConversationItemGroup[] { + searchMessages(filter: string): { results: ConversationItemGroup[]; searchTime: number } { let searchResults: ConversationItemGroup[] = [] + const startTime = Date.now() + if (this.#initialized) { if (!filter) { this.#features.logging.log(`Empty search filter, returning all history`) - return this.getHistory() + return { results: this.getHistory(), searchTime: Date.now() - startTime } } this.#features.logging.log(`Searching for ${filter}`) @@ -223,7 +252,7 @@ export class ChatDatabase { this.#features.logging.log(`No matches found`) searchResults = [{ items: [{ id: EMPTY_CONVERSATION_LIST_ID, description: 'No matches found' }] }] } - return searchResults + return { results: searchResults, searchTime: Date.now() - startTime } } /** diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts index d33e1a460e..4396a026d4 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts @@ -431,6 +431,9 @@ export class ChatTelemetryController { } await this.emitInteractWithMessageMetric(params.tabId, clickLinkData) break + case ChatUIEventName.HistoryButtonClick: + this.#telemetryService.emitUiClick({ elementId: 'amazonq_historyTabButton' }) + break } } } catch (err) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/clientTelemetry.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/clientTelemetry.ts index 1d0291805d..38c4c10a05 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/clientTelemetry.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/clientTelemetry.ts @@ -14,6 +14,7 @@ export enum ChatUIEventName { LinkClick = 'linkClick', InfoLinkClick = 'infoLinkClick', SourceLinkClick = 'sourceLinkClick', + HistoryButtonClick = 'historyButtonClick', } /* Chat client only telemetry - we should import these in the future */ @@ -77,6 +78,8 @@ export type InsertToCursorPositionParams = ServerInterface.InsertToCursorPositio cursorState?: ServerInterface.CursorState[] } +export type HistoryButtonClickParams = { name: ChatUIEventName.HistoryButtonClick } + export type ClientTelemetryEvent = | BaseClientTelemetryParams | BaseClientTelemetryParams @@ -89,6 +92,7 @@ export type ClientTelemetryEvent = | LinkClickParams | SourceLinkClickParams | InsertToCursorPositionParams + | HistoryButtonClickParams const chatUIEventNameSet = new Set(Object.values(ChatUIEventName)) diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts index dc4ddcc082..efc909fa18 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts @@ -22,10 +22,14 @@ import { import { getCompletionType, getSsoConnectionType, isAwsError } from '../utils' import { ChatConversationType, + ChatHistoryActionEvent, ChatInteractionType, ChatTelemetryEventName, CodeWhispererUserTriggerDecisionEvent, + ExportTabEvent, InteractWithMessageEvent, + LoadHistoryEvent, + UiClickEvent, } from './types' import { CodewhispererLanguage, getRuntimeLanguage } from '../languageDetection' import { CONVERSATION_ID_METRIC_KEY } from '../../language-server/chat/telemetry/chatTelemetryController' @@ -374,6 +378,42 @@ export class TelemetryService { }) } + public emitExportTab(event: ExportTabEvent) { + if (this.enableTelemetryEventsToDestination) { + this.telemetry.emitMetric({ + name: ChatTelemetryEventName.ExportTab, + data: event, + }) + } + } + + public emitLoadHistory(event: LoadHistoryEvent) { + if (this.enableTelemetryEventsToDestination) { + this.telemetry.emitMetric({ + name: ChatTelemetryEventName.LoadHistory, + data: event, + }) + } + } + + public emitChatHistoryAction(event: ChatHistoryActionEvent) { + if (this.enableTelemetryEventsToDestination) { + this.telemetry.emitMetric({ + name: ChatTelemetryEventName.ChatHistoryAction, + data: event, + }) + } + } + + public emitUiClick(event: UiClickEvent) { + if (this.enableTelemetryEventsToDestination) { + this.telemetry.emitMetric({ + name: ChatTelemetryEventName.UiClick, + data: { elementId: event.elementId }, + }) + } + } + public emitChatAddMessage( params: { conversationId?: string diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts index 15fae9756c..fa2bb501ed 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts @@ -172,6 +172,10 @@ export enum ChatTelemetryEventName { ModifyCode = 'amazonq_modifyCode', ToolUseSuggested = 'amazonq_toolUseSuggested', InteractWithAgenticChat = 'amazonq_interactWithAgenticChat', + LoadHistory = 'amazonq_loadHistory', + ChatHistoryAction = 'amazonq_performChatHistoryAction', + ExportTab = 'amazonq_exportTab', + UiClick = 'ui_click', } export interface ChatTelemetryEventMap { @@ -187,6 +191,10 @@ export interface ChatTelemetryEventMap { [ChatTelemetryEventName.ModifyCode]: ModifyCodeEvent [ChatTelemetryEventName.ToolUseSuggested]: ToolUseSuggestedEvent [ChatTelemetryEventName.InteractWithAgenticChat]: InteractWithAgenticChatEvent + [ChatTelemetryEventName.LoadHistory]: LoadHistoryEvent + [ChatTelemetryEventName.ChatHistoryAction]: ChatHistoryActionEvent + [ChatTelemetryEventName.ExportTab]: ExportTabEvent + [ChatTelemetryEventName.UiClick]: UiClickEvent } export type ToolUseSuggestedEvent = { @@ -269,6 +277,33 @@ export type ExitFocusConversationEvent = { cwsprChatConversationId: string } +export type UiClickEvent = { + elementId: string +} + +export type LoadHistoryEvent = { + amazonqTimeToLoadHistory: number + amazonqHistoryFileSize: number + openTabCount: number + result: Result + languageServerVersion?: string +} + +export type ChatHistoryActionEvent = { + action: ChatHistoryActionType + result: Result + languageServerVersion?: string + filenameExt?: string + amazonqTimeToSearchHistory?: number + amazonqHistoryFileSize?: number +} + +export type ExportTabEvent = { + filenameExt: string + result: Result + languageServerVersion?: string +} + export enum ChatInteractionType { InsertAtCursor = 'insertAtCursor', CopySnippet = 'copySnippet', @@ -281,6 +316,13 @@ export enum ChatInteractionType { ClickBodyLink = 'clickBodyLink', } +export enum ChatHistoryActionType { + Search = 'search', + Export = 'export', + Open = 'open', + Delete = 'delete', +} + export type ChatConversationType = 'Chat' | 'Assign' | 'Transform' | 'AgenticChat' | 'AgenticChatWithToolUse' export type AgenticChatInteractionType = 'RejectDiff' | 'GeneratedDiff' | 'RunCommand' | 'GeneratedCommand' | 'StopChat'