Skip to content

Commit b0b2639

Browse files
authored
telemetry(featureDev): missing "result" field, tests #4167
Problem: - Missing "result" for some metrics. - Missing test coverage for telemetry events. Solution: - Add tests. - Set "result" field for amazonq_isReviewedChanges, amazonq_startChat metrics.
1 parent b3a843a commit b0b2639

File tree

8 files changed

+223
-73
lines changed

8 files changed

+223
-73
lines changed

src/amazonqFeatureDev/client/featureDev.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const streamResponseErrors: Record<string, number> = {
6363
}
6464

6565
export class FeatureDevClient {
66-
private async getClient() {
66+
public async getClient() {
6767
// Should not be stored for the whole session.
6868
// Client has to be reinitialized for each request so we always have a fresh bearerToken
6969
return await createFeatureDevProxyClient()

src/amazonqFeatureDev/controllers/chat/controller.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,13 @@ export class FeatureDevController {
115115
telemetry.amazonq_approachThumbsUp.emit({
116116
amazonqConversationId: session?.conversationId,
117117
value: 1,
118+
result: 'Succeeded',
118119
})
119120
} else if (vote === 'downvote') {
120121
telemetry.amazonq_approachThumbsDown.emit({
121122
amazonqConversationId: session?.conversationId,
122123
value: 1,
124+
result: 'Succeeded',
123125
})
124126
}
125127
break
@@ -357,7 +359,11 @@ To learn more, visit the _[Amazon Q User Guide](${userGuideURL})_.
357359

358360
private async openDiff(message: OpenDiffMessage) {
359361
const session = await this.sessionStorage.getSession(message.tabID)
360-
telemetry.amazonq_isReviewedChanges.emit({ amazonqConversationId: session.conversationId, enabled: true })
362+
telemetry.amazonq_isReviewedChanges.emit({
363+
amazonqConversationId: session.conversationId,
364+
enabled: true,
365+
result: 'Succeeded',
366+
})
361367

362368
if (message.deleted) {
363369
const fileUri = this.getOriginalFileUri(message, session)

src/amazonqFeatureDev/session/session.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,15 @@ export class Session {
2727
// Used to keep track of whether or not the current session is currently authenticating/needs authenticating
2828
public isAuthenticating: boolean
2929

30-
constructor(public readonly config: SessionConfig, private messenger: Messenger, public readonly tabID: string) {
31-
this._state = new ConversationNotStartedState('', tabID)
32-
this.proxyClient = new FeatureDevClient()
30+
constructor(
31+
public readonly config: SessionConfig,
32+
private messenger: Messenger,
33+
public readonly tabID: string,
34+
initialState: Omit<SessionState, 'uploadId'> = new ConversationNotStartedState('', tabID),
35+
proxyClient: FeatureDevClient = new FeatureDevClient()
36+
) {
37+
this._state = initialState
38+
this.proxyClient = proxyClient
3339

3440
this.approachRetries = approachRetryLimit
3541

@@ -45,7 +51,11 @@ export class Session {
4551
await this.setupConversation(msg)
4652
this.preloaderFinished = true
4753

48-
telemetry.amazonq_startChat.emit({ amazonqConversationId: this.conversationId, value: 1 })
54+
telemetry.amazonq_startChat.emit({
55+
amazonqConversationId: this.conversationId,
56+
value: 1,
57+
result: 'Succeeded',
58+
})
4959

5060
this.messenger.sendAsyncEventProgress(this.tabID, true, undefined)
5161
}

src/shared/telemetry/exemptMetrics.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
*/
1717
const validationExemptMetrics: Set<string> = new Set([
1818
'amazonq_runCommand',
19-
'amazonq_isReviewedChanges',
2019
'apigateway_copyUrl',
2120
'aws_loadCredentials',
2221
'aws_validateCredentials',

src/test/amazonqFeatureDev/controllers/chat/controller.test.ts

Lines changed: 107 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,40 @@ import * as assert from 'assert'
88
import * as path from 'path'
99
import sinon from 'sinon'
1010
import { waitUntil } from '../../../../shared/utilities/timeoutUtils'
11-
import { ControllerSetup, createController } from '../../utils'
12-
import { ChatControllerEventEmitters } from '../../../../amazonqFeatureDev/controllers/chat/controller'
11+
import { ControllerSetup, createController, createSession } from '../../utils'
1312
import { FollowUpTypes, createUri } from '../../../../amazonqFeatureDev/types'
1413
import { Session } from '../../../../amazonqFeatureDev/session/session'
1514
import { Prompter } from '../../../../shared/ui/prompter'
16-
import { toFile } from '../../../testUtil'
15+
import { assertTelemetry, toFile } from '../../../testUtil'
1716
import { SelectedFolderNotInWorkspaceFolderError } from '../../../../amazonqFeatureDev/errors'
17+
import { PrepareRefinementState } from '../../../../amazonqFeatureDev/session/sessionState'
18+
import { FeatureDevClient } from '../../../../amazonqFeatureDev/client/featureDev'
1819

1920
describe('Controller', () => {
2021
const tabID = '123'
2122
const conversationID = '456'
2223
const uploadID = '789'
2324

25+
let session: Session
2426
let controllerSetup: ControllerSetup
2527

28+
before(() => {
29+
sinon.stub(performance, 'now').returns(0)
30+
})
31+
2632
beforeEach(async () => {
27-
controllerSetup = await createController({
28-
conversationID,
29-
tabID,
30-
uploadID,
31-
})
33+
controllerSetup = await createController()
34+
session = await createSession({ messenger: controllerSetup.messenger, conversationID, tabID, uploadID })
3235
})
3336

3437
afterEach(() => {
3538
sinon.restore()
3639
})
3740

3841
describe('openDiff', async () => {
39-
async function openDiff(
40-
controllerEventEmitter: ChatControllerEventEmitters,
41-
filePath: string,
42-
deleted = false
43-
) {
42+
async function openDiff(filePath: string, deleted = false) {
4443
const executeDiff = sinon.stub(vscode.commands, 'executeCommand').returns(Promise.resolve(undefined))
45-
controllerEventEmitter.openDiff.fire({ tabID, filePath, deleted })
44+
controllerSetup.emitters.openDiff.fire({ tabID, filePath, deleted })
4645

4746
// Wait until the controller has time to process the event
4847
await waitUntil(() => {
@@ -53,7 +52,8 @@ describe('Controller', () => {
5352
}
5453

5554
it('uses empty file when file is not found locally', async () => {
56-
const executedDiff = await openDiff(controllerSetup.emitters, path.join('src', 'mynewfile.js'))
55+
sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session)
56+
const executedDiff = await openDiff(path.join('src', 'mynewfile.js'))
5757
assert.strictEqual(
5858
executedDiff.calledWith(
5959
'vscode.diff',
@@ -62,12 +62,15 @@ describe('Controller', () => {
6262
),
6363
true
6464
)
65+
66+
assertTelemetry('amazonq_isReviewedChanges', { amazonqConversationId: conversationID, enabled: true })
6567
})
6668

6769
it('uses file location when file is found locally and /src is not available', async () => {
70+
sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session)
6871
const newFileLocation = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'mynewfile.js')
6972
toFile('', newFileLocation)
70-
const executedDiff = await openDiff(controllerSetup.emitters, 'mynewfile.js')
73+
const executedDiff = await openDiff('mynewfile.js')
7174
assert.strictEqual(
7275
executedDiff.calledWith(
7376
'vscode.diff',
@@ -76,12 +79,15 @@ describe('Controller', () => {
7679
),
7780
true
7881
)
82+
83+
assertTelemetry('amazonq_isReviewedChanges', { amazonqConversationId: conversationID, enabled: true })
7984
})
8085

8186
it('uses file location when file is found locally and /src is available', async () => {
87+
sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session)
8288
const newFileLocation = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'src', 'mynewfile.js')
8389
toFile('', newFileLocation)
84-
const executedDiff = await openDiff(controllerSetup.emitters, path.join('src', 'mynewfile.js'))
90+
const executedDiff = await openDiff(path.join('src', 'mynewfile.js'))
8591
assert.strictEqual(
8692
executedDiff.calledWith(
8793
'vscode.diff',
@@ -90,18 +96,17 @@ describe('Controller', () => {
9096
),
9197
true
9298
)
99+
100+
assertTelemetry('amazonq_isReviewedChanges', { amazonqConversationId: conversationID, enabled: true })
93101
})
94102

95103
it('uses file location when file is found locally and source folder was picked', async () => {
104+
sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session)
96105
const newFileLocation = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'foo', 'fi', 'mynewfile.js')
97106
toFile('', newFileLocation)
98107
sinon.stub(vscode.workspace, 'getWorkspaceFolder').returns(controllerSetup.workspaceFolder)
99-
controllerSetup.session.config.sourceRoot = path.join(
100-
controllerSetup.workspaceFolder.uri.fsPath,
101-
'foo',
102-
'fi'
103-
)
104-
const executedDiff = await openDiff(controllerSetup.emitters, path.join('foo', 'fi', 'mynewfile.js'))
108+
session.config.sourceRoot = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'foo', 'fi')
109+
const executedDiff = await openDiff(path.join('foo', 'fi', 'mynewfile.js'))
105110
assert.strictEqual(
106111
executedDiff.calledWith(
107112
'vscode.diff',
@@ -110,17 +115,15 @@ describe('Controller', () => {
110115
),
111116
true
112117
)
118+
119+
assertTelemetry('amazonq_isReviewedChanges', { amazonqConversationId: conversationID, enabled: true })
113120
})
114121
})
115122

116123
describe('modifyDefaultSourceFolder', () => {
117-
async function modifyDefaultSourceFolder(
118-
controllerEventEmitter: ChatControllerEventEmitters,
119-
session: Session,
120-
sourceRoot: string
121-
) {
124+
async function modifyDefaultSourceFolder(sourceRoot: string) {
122125
const promptStub = sinon.stub(Prompter.prototype, 'prompt').resolves(vscode.Uri.file(sourceRoot))
123-
controllerEventEmitter.followUpClicked.fire({
126+
controllerSetup.emitters.followUpClicked.fire({
124127
tabID,
125128
followUp: {
126129
type: FollowUpTypes.ModifyDefaultSourceFolder,
@@ -131,14 +134,12 @@ describe('Controller', () => {
131134
await waitUntil(() => {
132135
return Promise.resolve(promptStub.callCount > 0)
133136
}, {})
134-
135-
return session
136137
}
137138

138139
it('fails if selected folder is not under a workspace folder', async () => {
139140
sinon.stub(vscode.workspace, 'getWorkspaceFolder').returns(undefined)
140141
const messengerSpy = sinon.spy(controllerSetup.messenger, 'sendAnswer')
141-
await modifyDefaultSourceFolder(controllerSetup.emitters, controllerSetup.session, '../../')
142+
await modifyDefaultSourceFolder('../../')
142143
assert.deepStrictEqual(
143144
messengerSpy.calledWith({
144145
tabID,
@@ -158,23 +159,84 @@ describe('Controller', () => {
158159
})
159160

160161
it('accepts valid source folders under a workspace root', async () => {
161-
const controllerSetup = await createController({
162+
sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session)
163+
sinon.stub(vscode.workspace, 'getWorkspaceFolder').returns(controllerSetup.workspaceFolder)
164+
const expectedSourceRoot = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'src')
165+
await modifyDefaultSourceFolder(expectedSourceRoot)
166+
assert.strictEqual(session.config.sourceRoot, expectedSourceRoot)
167+
assert.strictEqual(session.config.workspaceRoot, controllerSetup.workspaceFolder.uri.fsPath)
168+
})
169+
})
170+
171+
describe('processChatItemVotedMessage', () => {
172+
async function processChatItemVotedMessage(vote: 'upvote' | 'downvote') {
173+
const initialState = new PrepareRefinementState(
174+
{
175+
conversationId: conversationID,
176+
proxyClient: new FeatureDevClient(),
177+
sourceRoot: '',
178+
workspaceRoot: '',
179+
},
180+
'',
181+
tabID
182+
)
183+
const newSession = await createSession({
184+
messenger: controllerSetup.messenger,
185+
sessionState: initialState,
162186
conversationID,
163187
tabID,
164188
uploadID,
165189
})
166-
sinon.stub(vscode.workspace, 'getWorkspaceFolder').returns(controllerSetup.workspaceFolder)
167-
const expectedSourceRoot = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'src')
168-
const modifiedSourceFolderSession = await modifyDefaultSourceFolder(
169-
controllerSetup.emitters,
170-
controllerSetup.session,
171-
expectedSourceRoot
172-
)
173-
assert.strictEqual(modifiedSourceFolderSession.config.sourceRoot, expectedSourceRoot)
174-
assert.strictEqual(
175-
modifiedSourceFolderSession.config.workspaceRoot,
176-
controllerSetup.workspaceFolder.uri.fsPath
177-
)
190+
const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(newSession)
191+
controllerSetup.emitters.processChatItemVotedMessage.fire({
192+
tabID,
193+
messageID: '',
194+
vote,
195+
})
196+
197+
// Wait until the controller has time to process the event
198+
await waitUntil(() => {
199+
return Promise.resolve(getSessionStub.callCount > 0)
200+
}, {})
201+
}
202+
203+
it('incoming upvoted message sends telemetry', async () => {
204+
await processChatItemVotedMessage('upvote')
205+
206+
assertTelemetry('amazonq_approachThumbsUp', { amazonqConversationId: conversationID, result: 'Succeeded' })
207+
})
208+
209+
it('incoming downvoted message sends telemetry', async () => {
210+
await processChatItemVotedMessage('downvote')
211+
212+
assertTelemetry('amazonq_approachThumbsDown', {
213+
amazonqConversationId: conversationID,
214+
result: 'Succeeded',
215+
})
216+
})
217+
})
218+
219+
describe('newPlan', () => {
220+
async function newPlanClicked() {
221+
const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session)
222+
223+
controllerSetup.emitters.followUpClicked.fire({
224+
tabID,
225+
followUp: {
226+
type: FollowUpTypes.NewPlan,
227+
},
228+
})
229+
230+
// Wait until the controller has time to process the event
231+
await waitUntil(() => {
232+
return Promise.resolve(getSessionStub.callCount > 0)
233+
}, {})
234+
}
235+
236+
it('end chat telemetry is sent', async () => {
237+
await newPlanClicked()
238+
239+
assertTelemetry('amazonq_endChat', { amazonqConversationId: conversationID, result: 'Succeeded' })
178240
})
179241
})
180242
})
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import sinon from 'sinon'
7+
import { createMessenger, createSession } from '../utils'
8+
import { Session } from '../../../amazonqFeatureDev/session/session'
9+
import { assertTelemetry } from '../../testUtil'
10+
11+
describe('session', () => {
12+
const conversationID = '12345'
13+
14+
let session: Session
15+
16+
beforeEach(async () => {
17+
const messenger = createMessenger()
18+
session = await createSession({ messenger, conversationID })
19+
})
20+
21+
afterEach(() => {
22+
sinon.restore()
23+
})
24+
25+
describe('preloader', () => {
26+
it('emits start chat telemetry', async () => {
27+
await session.preloader('implement twosum in typescript')
28+
29+
assertTelemetry('amazonq_startChat', { amazonqConversationId: conversationID })
30+
31+
assertTelemetry('amazonq_startConversationInvoke', {
32+
amazonqConversationId: conversationID,
33+
})
34+
})
35+
})
36+
})

0 commit comments

Comments
 (0)