Skip to content

Commit c449b0d

Browse files
authored
telemetry(amazonq): send metric data in onCodeGeneration #6226
## Problem This is a part of the task to implement client side alarms in order to track success rate for the client. ## Solution - Emit metric data telemetry on success/failure.
1 parent df7a3b6 commit c449b0d

File tree

5 files changed

+224
-32
lines changed

5 files changed

+224
-32
lines changed

packages/core/src/amazonqFeatureDev/client/featureDev.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ import { createCodeWhispererChatStreamingClient } from '../../shared/clients/cod
2525
import { getClientId, getOptOutPreference, getOperatingSystem } from '../../shared/telemetry/util'
2626
import { extensionVersion } from '../../shared/vscode/env'
2727
import apiConfig = require('./codewhispererruntime-2022-11-11.json')
28-
import { FeatureDevCodeAcceptanceEvent, FeatureDevCodeGenerationEvent, TelemetryEvent } from './featuredevproxyclient'
28+
import {
29+
FeatureDevCodeAcceptanceEvent,
30+
FeatureDevCodeGenerationEvent,
31+
MetricData,
32+
TelemetryEvent,
33+
} from './featuredevproxyclient'
2934

3035
// Re-enable once BE is able to handle retries.
3136
const writeAPIRetryOptions = {
@@ -299,6 +304,11 @@ export class FeatureDevClient {
299304
await this.sendFeatureDevEvent('featureDevCodeAcceptanceEvent', event)
300305
}
301306

307+
public async sendMetricData(event: MetricData) {
308+
getLogger().debug(`featureDevCodeGenerationMetricData: dimensions: ${event.dimensions}`)
309+
await this.sendFeatureDevEvent('metricData', event)
310+
}
311+
302312
public async sendFeatureDevEvent<T extends keyof TelemetryEvent>(
303313
eventName: T,
304314
event: NonNullable<TelemetryEvent[T]>

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
import { codeGenRetryLimit, defaultRetryLimit } from '../../limits'
3131
import { Session } from '../../session/session'
3232
import { featureDevScheme, featureName } from '../../constants'
33-
import { DeletedFileInfo, DevPhase, type NewFileInfo } from '../../types'
33+
import { DeletedFileInfo, DevPhase, MetricDataOperationName, MetricDataResult, type NewFileInfo } from '../../types'
3434
import { AuthUtil } from '../../../codewhisperer/util/authUtil'
3535
import { AuthController } from '../../../amazonq/auth/controller'
3636
import { getLogger } from '../../../shared/logger'
@@ -413,6 +413,7 @@ export class FeatureDevController {
413413
canBeVoted: true,
414414
})
415415
this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.generatingCode'))
416+
await session.sendMetricDataTelemetry(MetricDataOperationName.StartCodeGeneration, MetricDataResult.Success)
416417
await session.send(message)
417418
const filePaths = session.state.filePaths ?? []
418419
const deletedFiles = session.state.deletedFiles ?? []
@@ -486,6 +487,31 @@ export class FeatureDevController {
486487
await session.sendLinesOfCodeGeneratedTelemetry()
487488
}
488489
this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption'))
490+
} catch (err: any) {
491+
getLogger().error(`${featureName}: Error during code generation: ${err}`)
492+
493+
let result: string
494+
switch (err.constructor.name) {
495+
case FeatureDevServiceError.name:
496+
if (err.code === 'EmptyPatchException') {
497+
result = MetricDataResult.LlmFailure
498+
} else if (err.code === 'GuardrailsException' || err.code === 'ThrottlingException') {
499+
result = MetricDataResult.Error
500+
} else {
501+
result = MetricDataResult.Fault
502+
}
503+
break
504+
case PromptRefusalException.name:
505+
case NoChangeRequiredException.name:
506+
result = MetricDataResult.Error
507+
break
508+
default:
509+
result = MetricDataResult.Fault
510+
break
511+
}
512+
513+
await session.sendMetricDataTelemetry(MetricDataOperationName.EndCodeGeneration, result)
514+
throw err
489515
} finally {
490516
// Finish processing the event
491517

@@ -517,6 +543,7 @@ export class FeatureDevController {
517543
}
518544
}
519545
}
546+
await session.sendMetricDataTelemetry(MetricDataOperationName.EndCodeGeneration, MetricDataResult.Success)
520547
}
521548

522549
private sendUpdateCodeMessage(tabID: string) {

packages/core/src/amazonqFeatureDev/session/session.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,25 @@ export class Session {
285285
return { leftPath, rightPath, ...diff }
286286
}
287287

288+
public async sendMetricDataTelemetry(operationName: string, result: string) {
289+
await this.proxyClient.sendMetricData({
290+
metricName: 'Operation',
291+
metricValue: 1,
292+
timestamp: new Date(),
293+
product: 'FeatureDev',
294+
dimensions: [
295+
{
296+
name: 'operationName',
297+
value: operationName,
298+
},
299+
{
300+
name: 'result',
301+
value: result,
302+
},
303+
],
304+
})
305+
}
306+
288307
public async sendLinesOfCodeGeneratedTelemetry() {
289308
let charactersOfCodeGenerated = 0
290309
let linesOfCodeGenerated = 0

packages/core/src/amazonqFeatureDev/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,15 @@ export interface UpdateFilesPathsParams {
115115
messageId: string
116116
disableFileActions?: boolean
117117
}
118+
119+
export enum MetricDataOperationName {
120+
StartCodeGeneration = 'StartCodeGeneration',
121+
EndCodeGeneration = 'EndCodeGeneration',
122+
}
123+
124+
export enum MetricDataResult {
125+
Success = 'Success',
126+
Fault = 'Fault',
127+
Error = 'Error',
128+
LlmFailure = 'LLMFailure',
129+
}

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

Lines changed: 154 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ import * as path from 'path'
99
import sinon from 'sinon'
1010
import { waitUntil } from '../../../../shared/utilities/timeoutUtils'
1111
import { ControllerSetup, createController, createSession, generateVirtualMemoryUri } from '../../utils'
12-
import { CurrentWsFolders, DeletedFileInfo, NewFileInfo } from '../../../../amazonqFeatureDev/types'
12+
import {
13+
CurrentWsFolders,
14+
DeletedFileInfo,
15+
MetricDataOperationName,
16+
MetricDataResult,
17+
NewFileInfo,
18+
} from '../../../../amazonqFeatureDev/types'
1319
import { Session } from '../../../../amazonqFeatureDev/session/session'
1420
import { Prompter } from '../../../../shared/ui/prompter'
1521
import { assertTelemetry, toFile } from '../../../testUtil'
@@ -36,6 +42,7 @@ import { AuthUtil } from '../../../../codewhisperer'
3642
import { featureDevScheme, featureName, messageWithConversationId } from '../../../../amazonqFeatureDev'
3743
import { i18n } from '../../../../shared/i18n-helper'
3844
import { FollowUpTypes } from '../../../../amazonq/commons/types'
45+
import { ToolkitError } from '../../../../shared'
3946

4047
let mockGetCodeGeneration: sinon.SinonStub
4148
describe('Controller', () => {
@@ -395,7 +402,47 @@ describe('Controller', () => {
395402
})
396403

397404
describe('processUserChatMessage', function () {
398-
async function fireChatMessage() {
405+
// TODO: fix disablePreviousFileList error
406+
const runs = [
407+
{ name: 'ContentLengthError', error: new ContentLengthError() },
408+
{
409+
name: 'MonthlyConversationLimitError',
410+
error: new MonthlyConversationLimitError('Service Quota Exceeded'),
411+
},
412+
{
413+
name: 'FeatureDevServiceErrorGuardrailsException',
414+
error: new FeatureDevServiceError(
415+
i18n('AWS.amazonq.featureDev.error.codeGen.default'),
416+
'GuardrailsException'
417+
),
418+
},
419+
{
420+
name: 'FeatureDevServiceErrorEmptyPatchException',
421+
error: new FeatureDevServiceError(
422+
i18n('AWS.amazonq.featureDev.error.throttling'),
423+
'EmptyPatchException'
424+
),
425+
},
426+
{
427+
name: 'FeatureDevServiceErrorThrottlingException',
428+
error: new FeatureDevServiceError(
429+
i18n('AWS.amazonq.featureDev.error.codeGen.default'),
430+
'ThrottlingException'
431+
),
432+
},
433+
{ name: 'UploadCodeError', error: new UploadCodeError('403: Forbiden') },
434+
{ name: 'UserMessageNotFoundError', error: new UserMessageNotFoundError() },
435+
{ name: 'TabIdNotFoundError', error: new TabIdNotFoundError() },
436+
{ name: 'PrepareRepoFailedError', error: new PrepareRepoFailedError() },
437+
{ name: 'PromptRefusalException', error: new PromptRefusalException() },
438+
{ name: 'ZipFileError', error: new ZipFileError() },
439+
{ name: 'CodeIterationLimitError', error: new CodeIterationLimitError() },
440+
{ name: 'UploadURLExpired', error: new UploadURLExpired() },
441+
{ name: 'NoChangeRequiredException', error: new NoChangeRequiredException() },
442+
{ name: 'default', error: new ToolkitError('Default', { code: 'Default' }) },
443+
]
444+
445+
async function fireChatMessage(session: Session) {
399446
const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session)
400447

401448
controllerSetup.emitters.processHumanChatMessage.fire({
@@ -410,44 +457,121 @@ describe('Controller', () => {
410457
}, {})
411458
}
412459

413-
describe('processErrorChatMessage', function () {
414-
// TODO: fix disablePreviousFileList error
415-
const runs = [
416-
{ name: 'ContentLengthError', error: new ContentLengthError() },
417-
{
418-
name: 'MonthlyConversationLimitError',
419-
error: new MonthlyConversationLimitError('Service Quota Exceeded'),
420-
},
421-
{
422-
name: 'FeatureDevServiceError',
423-
error: new FeatureDevServiceError(
424-
i18n('AWS.amazonq.featureDev.error.codeGen.default'),
425-
'GuardrailsException'
426-
),
427-
},
428-
{ name: 'UploadCodeError', error: new UploadCodeError('403: Forbiden') },
429-
{ name: 'UserMessageNotFoundError', error: new UserMessageNotFoundError() },
430-
{ name: 'TabIdNotFoundError', error: new TabIdNotFoundError() },
431-
{ name: 'PrepareRepoFailedError', error: new PrepareRepoFailedError() },
432-
{ name: 'PromptRefusalException', error: new PromptRefusalException() },
433-
{ name: 'ZipFileError', error: new ZipFileError() },
434-
{ name: 'CodeIterationLimitError', error: new CodeIterationLimitError() },
435-
{ name: 'UploadURLExpired', error: new UploadURLExpired() },
436-
{ name: 'NoChangeRequiredException', error: new NoChangeRequiredException() },
437-
{ name: 'default', error: new Error() },
438-
]
460+
describe('onCodeGeneration', function () {
461+
let session: any
462+
let sendMetricDataTelemetrySpy: sinon.SinonStub
463+
464+
const errorResultMapping = new Map([
465+
['EmptyPatchException', MetricDataResult.LlmFailure],
466+
[PromptRefusalException.name, MetricDataResult.Error],
467+
[NoChangeRequiredException.name, MetricDataResult.Error],
468+
])
469+
470+
function getMetricResult(error: ToolkitError): MetricDataResult {
471+
if (error instanceof FeatureDevServiceError && error.code) {
472+
return errorResultMapping.get(error.code) ?? MetricDataResult.Error
473+
}
474+
return errorResultMapping.get(error.constructor.name) ?? MetricDataResult.Fault
475+
}
476+
477+
async function createCodeGenState() {
478+
mockGetCodeGeneration = sinon.stub().resolves({ codeGenerationStatus: { status: 'Complete' } })
479+
480+
const workspaceFolders = [controllerSetup.workspaceFolder] as CurrentWsFolders
481+
const testConfig = {
482+
conversationId: conversationID,
483+
proxyClient: {
484+
createConversation: () => sinon.stub(),
485+
createUploadUrl: () => sinon.stub(),
486+
generatePlan: () => sinon.stub(),
487+
startCodeGeneration: () => sinon.stub(),
488+
getCodeGeneration: () => mockGetCodeGeneration(),
489+
exportResultArchive: () => sinon.stub(),
490+
} as unknown as FeatureDevClient,
491+
workspaceRoots: [''],
492+
uploadId: uploadID,
493+
workspaceFolders,
494+
}
495+
496+
const codeGenState = new CodeGenState(testConfig, getFilePaths(controllerSetup), [], [], tabID, 0, {})
497+
const newSession = await createSession({
498+
messenger: controllerSetup.messenger,
499+
sessionState: codeGenState,
500+
conversationID,
501+
tabID,
502+
uploadID,
503+
scheme: featureDevScheme,
504+
})
505+
return newSession
506+
}
507+
508+
async function verifyException(error: ToolkitError) {
509+
sinon.stub(session, 'send').throws(error)
510+
511+
await fireChatMessage(session)
512+
await verifyMetricsCalled()
513+
assert.ok(
514+
sendMetricDataTelemetrySpy.calledWith(
515+
MetricDataOperationName.StartCodeGeneration,
516+
MetricDataResult.Success
517+
)
518+
)
519+
const metricResult = getMetricResult(error)
520+
assert.ok(
521+
sendMetricDataTelemetrySpy.calledWith(MetricDataOperationName.EndCodeGeneration, metricResult)
522+
)
523+
}
524+
525+
async function verifyMetricsCalled() {
526+
await waitUntil(() => Promise.resolve(sendMetricDataTelemetrySpy.callCount >= 2), {})
527+
}
528+
529+
beforeEach(async () => {
530+
session = await createCodeGenState()
531+
sinon.stub(session, 'preloader').resolves()
532+
sendMetricDataTelemetrySpy = sinon.stub(session, 'sendMetricDataTelemetry')
533+
})
534+
535+
it('sends success operation telemetry', async () => {
536+
sinon.stub(session, 'send').resolves()
537+
sinon.stub(session, 'sendLinesOfCodeGeneratedTelemetry').resolves() // Avoid sending extra telemetry
439538

539+
await fireChatMessage(session)
540+
await verifyMetricsCalled()
541+
542+
assert.ok(
543+
sendMetricDataTelemetrySpy.calledWith(
544+
MetricDataOperationName.StartCodeGeneration,
545+
MetricDataResult.Success
546+
)
547+
)
548+
assert.ok(
549+
sendMetricDataTelemetrySpy.calledWith(
550+
MetricDataOperationName.EndCodeGeneration,
551+
MetricDataResult.Success
552+
)
553+
)
554+
})
555+
556+
runs.forEach(({ name, error }) => {
557+
it(`sends failure operation telemetry on ${name}`, async () => {
558+
await verifyException(error)
559+
})
560+
})
561+
})
562+
563+
describe('processErrorChatMessage', function () {
440564
function createTestErrorMessage(message: string) {
441565
return createUserFacingErrorMessage(`${featureName} request failed: ${message}`)
442566
}
443567

444-
async function verifyException(error: Error) {
568+
async function verifyException(error: ToolkitError) {
445569
sinon.stub(session, 'preloader').throws(error)
446570
const sendAnswerSpy = sinon.stub(controllerSetup.messenger, 'sendAnswer')
447571
const sendErrorMessageSpy = sinon.stub(controllerSetup.messenger, 'sendErrorMessage')
448572
const sendMonthlyLimitErrorSpy = sinon.stub(controllerSetup.messenger, 'sendMonthlyLimitError')
449573

450-
await fireChatMessage()
574+
await fireChatMessage(session)
451575

452576
switch (error.constructor.name) {
453577
case ContentLengthError.name:

0 commit comments

Comments
 (0)