Merge "Instrument telemetry for AI Review Agent chat requests"
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index 5502a73..1b3c5c0 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -103,6 +103,8 @@
COPY_TO_CLIPBOARD = 'CopyToClipboard',
// Time to autocomplete a comment
COMMENT_COMPLETION = 'CommentCompletion',
+ // Time for AI chat requests to complete
+ AI_CHAT_REQUEST = 'AiChatRequest',
}
export enum Interaction {
@@ -184,6 +186,8 @@
FLOWS_TAB_RENDERED = 'flows-tab-rendered',
CREATE_FLOW_DIALOG_OPENED = 'create-flow-dialog-opened',
FLOW_CREATED = 'flow-created',
+ // AI Chat interaction request failures
+ AI_CHAT_FAILURE = 'ai-chat-failure',
}
/**
diff --git a/polygerrit-ui/app/models/chat/chat-model.ts b/polygerrit-ui/app/models/chat/chat-model.ts
index 619424b..2711edb 100644
--- a/polygerrit-ui/app/models/chat/chat-model.ts
+++ b/polygerrit-ui/app/models/chat/chat-model.ts
@@ -37,6 +37,8 @@
import {contextItemEquals} from './context-item-util';
import {FilesModel, NormalizedFileInfo} from '../change/files-model';
import {isMagicPath} from '../../utils/path-list-util';
+import {getAppContext} from '../../services/app-context';
+import {Interaction, Timing} from '../../constants/reporting';
/** The available display modes in the chat panel. */
export enum ChatPanelMode {
@@ -616,6 +618,19 @@
});
},
emitError: (errorMessage: string) => {
+ getAppContext().reportingService.timeEnd(Timing.AI_CHAT_REQUEST, {
+ modelName: request.model_name,
+ actionId: action.id,
+ error: errorMessage,
+ });
+ getAppContext().reportingService.reportInteraction(
+ Interaction.AI_CHAT_FAILURE,
+ {
+ modelName: request.model_name,
+ actionId: action.id,
+ error: errorMessage,
+ }
+ );
const state = this.getState();
if (state.id !== conversationId) return;
const turns: readonly Turn[] = state.turns;
@@ -630,6 +645,10 @@
});
},
done: () => {
+ getAppContext().reportingService.timeEnd(Timing.AI_CHAT_REQUEST, {
+ modelName: request.model_name,
+ actionId: action.id,
+ });
const state = this.getState();
if (state.id !== conversationId) return;
assert(turnIndex < state.turns.length, 'turn index out of bounds');
@@ -642,6 +661,7 @@
});
},
};
+ getAppContext().reportingService.time(Timing.AI_CHAT_REQUEST);
this.plugin?.chat?.(request, listener);
}
diff --git a/polygerrit-ui/app/models/chat/chat-model_test.ts b/polygerrit-ui/app/models/chat/chat-model_test.ts
index 9f70830..d244bd4 100644
--- a/polygerrit-ui/app/models/chat/chat-model_test.ts
+++ b/polygerrit-ui/app/models/chat/chat-model_test.ts
@@ -16,6 +16,8 @@
import sinon from 'sinon';
import {ParsedChangeInfo} from '../../types/types';
+import {getAppContext} from '../../services/app-context';
+import {Interaction, Timing} from '../../constants/reporting';
suite('chat-model tests', () => {
let model: ChatModel;
@@ -345,4 +347,99 @@
const state = model.getState();
assert.equal(state.turns[0].geminiMessage.regenerationIndex, 0);
});
+
+ suite('telemetry reporting', () => {
+ let timeStub: sinon.SinonStub;
+ let timeEndStub: sinon.SinonStub;
+ let reportInteractionStub: sinon.SinonStub;
+
+ setup(() => {
+ timeStub = sinon.stub(getAppContext().reportingService, 'time');
+ timeEndStub = sinon.stub(getAppContext().reportingService, 'timeEnd');
+ reportInteractionStub = sinon.stub(
+ getAppContext().reportingService,
+ 'reportInteraction'
+ );
+
+ // Set up a change, models, and actions
+ const models = {
+ models: [
+ {
+ model_id: 'test-model',
+ full_display_text: 'Test Model',
+ short_text: 'Test',
+ },
+ ],
+ default_model_id: 'test-model',
+ };
+ const actions = {
+ actions: [
+ {
+ id: 'test-action',
+ display_text: 'Test Action',
+ initial_user_prompt: 'Test Prompt',
+ },
+ ],
+ default_action_id: 'test-action',
+ };
+ (provider.getActions as sinon.SinonStub).resolves(actions);
+ (provider.getModels as sinon.SinonStub).resolves(models);
+
+ changeModel.updateStateChange(createParsedChange());
+ });
+
+ test('chat request starts a timer', async () => {
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ model.updateUserInput('hello');
+ model.chat('hello', 'test-action', 0);
+
+ assert.isTrue(timeStub.calledOnceWith(Timing.AI_CHAT_REQUEST));
+ });
+
+ test('chat request success stops the timer', async () => {
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ (provider.chat as sinon.SinonStub).callsFake((_, listener) => {
+ listener.done();
+ });
+
+ model.updateUserInput('hello');
+ model.chat('hello', 'test-action', 0);
+
+ assert.isTrue(
+ timeEndStub.calledOnceWith(Timing.AI_CHAT_REQUEST, {
+ modelName: 'test-model',
+ actionId: 'test-action',
+ })
+ );
+ });
+
+ test('chat request failure stops the timer and logs interaction', async () => {
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ (provider.chat as sinon.SinonStub).callsFake((_, listener) => {
+ listener.emitError('some error');
+ });
+
+ model.updateUserInput('hello');
+ model.chat('hello', 'test-action', 0);
+
+ assert.isTrue(
+ timeEndStub.calledOnceWith(Timing.AI_CHAT_REQUEST, {
+ modelName: 'test-model',
+ actionId: 'test-action',
+ error: 'some error',
+ })
+ );
+
+ assert.isTrue(
+ reportInteractionStub.calledOnceWith(Interaction.AI_CHAT_FAILURE, {
+ modelName: 'test-model',
+ actionId: 'test-action',
+ error: 'some error',
+ })
+ );
+ });
+ });
});