Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

telemetry(amazonq): sending metric data in onCodeGeneration #6226

Merged
merged 9 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "Feature",
"description": "send metric data in onCodeGeneration"
}
12 changes: 11 additions & 1 deletion packages/core/src/amazonqFeatureDev/client/featureDev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import { createCodeWhispererChatStreamingClient } from '../../shared/clients/cod
import { getClientId, getOptOutPreference, getOperatingSystem } from '../../shared/telemetry/util'
import { extensionVersion } from '../../shared/vscode/env'
import apiConfig = require('./codewhispererruntime-2022-11-11.json')
import { FeatureDevCodeAcceptanceEvent, FeatureDevCodeGenerationEvent, TelemetryEvent } from './featuredevproxyclient'
import {
FeatureDevCodeAcceptanceEvent,
FeatureDevCodeGenerationEvent,
MetricData,
TelemetryEvent,
} from './featuredevproxyclient'

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

public async sendMetricData(event: MetricData) {
getLogger().debug(`featureDevCodeGenerationMetricData: dimensions: ${event.dimensions}`)
await this.sendFeatureDevEvent('metricData', event)
}

public async sendFeatureDevEvent<T extends keyof TelemetryEvent>(
eventName: T,
event: NonNullable<TelemetryEvent[T]>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
import { codeGenRetryLimit, defaultRetryLimit } from '../../limits'
import { Session } from '../../session/session'
import { featureDevScheme, featureName } from '../../constants'
import { DeletedFileInfo, DevPhase, type NewFileInfo } from '../../types'
import { DeletedFileInfo, DevPhase, MetricDataOperationName, MetricDataResult, type NewFileInfo } from '../../types'
import { AuthUtil } from '../../../codewhisperer/util/authUtil'
import { AuthController } from '../../../amazonq/auth/controller'
import { getLogger } from '../../../shared/logger'
Expand Down Expand Up @@ -413,6 +413,10 @@ export class FeatureDevController {
canBeVoted: true,
})
this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.generatingCode'))
await session.sendMetricDataTelemetry(
MetricDataOperationName.START_CODE_GENERATION,
MetricDataResult.SUCCESS
)
await session.send(message)
const filePaths = session.state.filePaths ?? []
const deletedFiles = session.state.deletedFiles ?? []
Expand Down Expand Up @@ -484,8 +488,45 @@ export class FeatureDevController {
})
await session.updateChatAnswer(tabID, i18n('AWS.amazonq.featureDev.pillText.acceptAllChanges'))
await session.sendLinesOfCodeGeneratedTelemetry()
await session.sendMetricDataTelemetry(
MetricDataOperationName.END_CODE_GENERATION,
MetricDataResult.SUCCESS
)
}
this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption'))
} catch (err: any) {
getLogger().error(`${featureName}: Error during code generation: ${err}`)

switch (err.constructor.name) {
case FeatureDevServiceError.name:
if (err.code === 'EmptyPatchException') {
await session.sendMetricDataTelemetry(
MetricDataOperationName.END_CODE_GENERATION,
MetricDataResult.LLMFAILURE
)
} else {
await session.sendMetricDataTelemetry(
MetricDataOperationName.END_CODE_GENERATION,
MetricDataResult.ERROR
)
}
break
case PromptRefusalException.name:
case NoChangeRequiredException.name:
await session.sendMetricDataTelemetry(
MetricDataOperationName.END_CODE_GENERATION,
MetricDataResult.ERROR
)
break
default:
await session.sendMetricDataTelemetry(
MetricDataOperationName.END_CODE_GENERATION,
MetricDataResult.FAULT
)

break
}
throw err
} finally {
// Finish processing the event

Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/amazonqFeatureDev/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,25 @@ export class Session {
return { leftPath, rightPath, ...diff }
}

public async sendMetricDataTelemetry(operationName: string, result: string) {
await this.proxyClient.sendMetricData({
metricName: 'Operation',
metricValue: 1,
timestamp: new Date(Date.now()),
product: 'FeatureDev',
dimensions: [
{
name: 'operationName',
value: operationName,
},
{
name: 'result',
value: result,
},
],
})
}

public async sendLinesOfCodeGeneratedTelemetry() {
let charactersOfCodeGenerated = 0
let linesOfCodeGenerated = 0
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/amazonqFeatureDev/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,15 @@ export interface UpdateFilesPathsParams {
messageId: string
disableFileActions?: boolean
}

export enum MetricDataOperationName {
START_CODE_GENERATION = 'StartCodeGeneration',
END_CODE_GENERATION = 'EndCodeGeneration',
}

export enum MetricDataResult {
SUCCESS = 'Success',
FAULT = 'Fault',
ERROR = 'Error',
LLMFAILURE = 'LLMFailure',
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import * as path from 'path'
import sinon from 'sinon'
import { waitUntil } from '../../../../shared/utilities/timeoutUtils'
import { ControllerSetup, createController, createSession, generateVirtualMemoryUri } from '../../utils'
import { CurrentWsFolders, DeletedFileInfo, NewFileInfo } from '../../../../amazonqFeatureDev/types'
import {
CurrentWsFolders,
DeletedFileInfo,
MetricDataOperationName,
MetricDataResult,
NewFileInfo,
} from '../../../../amazonqFeatureDev/types'
import { Session } from '../../../../amazonqFeatureDev/session/session'
import { Prompter } from '../../../../shared/ui/prompter'
import { assertTelemetry, toFile } from '../../../testUtil'
Expand All @@ -36,6 +42,7 @@ import { AuthUtil } from '../../../../codewhisperer'
import { featureDevScheme, featureName, messageWithConversationId } from '../../../../amazonqFeatureDev'
import { i18n } from '../../../../shared/i18n-helper'
import { FollowUpTypes } from '../../../../amazonq/commons/types'
import { ToolkitError } from '../../../../shared'

let mockGetCodeGeneration: sinon.SinonStub
describe('Controller', () => {
Expand Down Expand Up @@ -395,7 +402,47 @@ describe('Controller', () => {
})

describe('processUserChatMessage', function () {
async function fireChatMessage() {
// TODO: fix disablePreviousFileList error
const runs = [
{ name: 'ContentLengthError', error: new ContentLengthError() },
{
name: 'MonthlyConversationLimitError',
error: new MonthlyConversationLimitError('Service Quota Exceeded'),
},
{
name: 'FeatureDevServiceErrorGuardrailsException',
error: new FeatureDevServiceError(
i18n('AWS.amazonq.featureDev.error.codeGen.default'),
'GuardrailsException'
),
},
{
name: 'FeatureDevServiceErrorEmptyPatchException',
error: new FeatureDevServiceError(
i18n('AWS.amazonq.featureDev.error.throttling'),
'EmptyPatchException'
),
},
{
name: 'FeatureDevServiceErrorThrottlingException',
error: new FeatureDevServiceError(
i18n('AWS.amazonq.featureDev.error.codeGen.default'),
'ThrottlingException'
),
},
{ name: 'UploadCodeError', error: new UploadCodeError('403: Forbiden') },
{ name: 'UserMessageNotFoundError', error: new UserMessageNotFoundError() },
{ name: 'TabIdNotFoundError', error: new TabIdNotFoundError() },
{ name: 'PrepareRepoFailedError', error: new PrepareRepoFailedError() },
{ name: 'PromptRefusalException', error: new PromptRefusalException() },
{ name: 'ZipFileError', error: new ZipFileError() },
{ name: 'CodeIterationLimitError', error: new CodeIterationLimitError() },
{ name: 'UploadURLExpired', error: new UploadURLExpired() },
{ name: 'NoChangeRequiredException', error: new NoChangeRequiredException() },
{ name: 'default', error: new ToolkitError('Default', { code: 'Default' }) },
]

async function fireChatMessage(session: Session) {
const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session)

controllerSetup.emitters.processHumanChatMessage.fire({
Expand All @@ -410,44 +457,121 @@ describe('Controller', () => {
}, {})
}

describe('processErrorChatMessage', function () {
// TODO: fix disablePreviousFileList error
const runs = [
{ name: 'ContentLengthError', error: new ContentLengthError() },
{
name: 'MonthlyConversationLimitError',
error: new MonthlyConversationLimitError('Service Quota Exceeded'),
},
{
name: 'FeatureDevServiceError',
error: new FeatureDevServiceError(
i18n('AWS.amazonq.featureDev.error.codeGen.default'),
'GuardrailsException'
),
},
{ name: 'UploadCodeError', error: new UploadCodeError('403: Forbiden') },
{ name: 'UserMessageNotFoundError', error: new UserMessageNotFoundError() },
{ name: 'TabIdNotFoundError', error: new TabIdNotFoundError() },
{ name: 'PrepareRepoFailedError', error: new PrepareRepoFailedError() },
{ name: 'PromptRefusalException', error: new PromptRefusalException() },
{ name: 'ZipFileError', error: new ZipFileError() },
{ name: 'CodeIterationLimitError', error: new CodeIterationLimitError() },
{ name: 'UploadURLExpired', error: new UploadURLExpired() },
{ name: 'NoChangeRequiredException', error: new NoChangeRequiredException() },
{ name: 'default', error: new Error() },
]
describe('onCodeGeneration', function () {
let session: any
let sendMetricDataTelemetrySpy: sinon.SinonStub

const errorResultMapping = new Map([
['EmptyPatchException', MetricDataResult.LLMFAILURE],
[PromptRefusalException.name, MetricDataResult.ERROR],
[NoChangeRequiredException.name, MetricDataResult.ERROR],
])

function getMetricResult(error: ToolkitError): MetricDataResult {
if (error instanceof FeatureDevServiceError && error.code) {
return errorResultMapping.get(error.code) ?? MetricDataResult.ERROR
}
return errorResultMapping.get(error.constructor.name) ?? MetricDataResult.FAULT
}

async function createCodeGenState() {
mockGetCodeGeneration = sinon.stub().resolves({ codeGenerationStatus: { status: 'Complete' } })

const workspaceFolders = [controllerSetup.workspaceFolder] as CurrentWsFolders
const testConfig = {
conversationId: conversationID,
proxyClient: {
createConversation: () => sinon.stub(),
createUploadUrl: () => sinon.stub(),
generatePlan: () => sinon.stub(),
startCodeGeneration: () => sinon.stub(),
getCodeGeneration: () => mockGetCodeGeneration(),
exportResultArchive: () => sinon.stub(),
} as unknown as FeatureDevClient,
workspaceRoots: [''],
uploadId: uploadID,
workspaceFolders,
}

const codeGenState = new CodeGenState(testConfig, getFilePaths(controllerSetup), [], [], tabID, 0, {})
const newSession = await createSession({
messenger: controllerSetup.messenger,
sessionState: codeGenState,
conversationID,
tabID,
uploadID,
scheme: featureDevScheme,
})
return newSession
}

async function verifyException(error: ToolkitError) {
sinon.stub(session, 'send').throws(error)

await fireChatMessage(session)
await verifyMetricsCalled()
assert.ok(
sendMetricDataTelemetrySpy.calledWith(
MetricDataOperationName.START_CODE_GENERATION,
MetricDataResult.SUCCESS
)
)
const metricResult = getMetricResult(error)
assert.ok(
sendMetricDataTelemetrySpy.calledWith(MetricDataOperationName.END_CODE_GENERATION, metricResult)
)
}

async function verifyMetricsCalled() {
await waitUntil(() => Promise.resolve(sendMetricDataTelemetrySpy.callCount >= 2), {})
}

beforeEach(async () => {
session = await createCodeGenState()
sinon.stub(session, 'preloader').resolves()
sendMetricDataTelemetrySpy = sinon.stub(session, 'sendMetricDataTelemetry')
})

it('sends success operation telemetry', async () => {
sinon.stub(session, 'send').resolves()
sinon.stub(session, 'sendLinesOfCodeGeneratedTelemetry').resolves() // Avoid sending extra telemetry

await fireChatMessage(session)
await verifyMetricsCalled()

assert.ok(
sendMetricDataTelemetrySpy.calledWith(
MetricDataOperationName.START_CODE_GENERATION,
MetricDataResult.SUCCESS
)
)
assert.ok(
sendMetricDataTelemetrySpy.calledWith(
MetricDataOperationName.END_CODE_GENERATION,
MetricDataResult.SUCCESS
)
)
})

runs.forEach(({ name, error }) => {
it(`sends failure operation telemetry on ${name}`, async () => {
await verifyException(error)
})
})
})

describe('processErrorChatMessage', function () {
function createTestErrorMessage(message: string) {
return createUserFacingErrorMessage(`${featureName} request failed: ${message}`)
}

async function verifyException(error: Error) {
async function verifyException(error: ToolkitError) {
sinon.stub(session, 'preloader').throws(error)
const sendAnswerSpy = sinon.stub(controllerSetup.messenger, 'sendAnswer')
const sendErrorMessageSpy = sinon.stub(controllerSetup.messenger, 'sendErrorMessage')
const sendMonthlyLimitErrorSpy = sinon.stub(controllerSetup.messenger, 'sendMonthlyLimitError')

await fireChatMessage()
await fireChatMessage(session)

switch (error.constructor.name) {
case ContentLengthError.name:
Expand Down
Loading