Skip to content

Commit 9331045

Browse files
authored
telemetry: set more MetricBase fields aws#5255
Problem: `telemetry.run()` and `runCommand` do not set some "standard" fields on failure. Solution: Update `telemetry.run()` and `runCommand`. ref aws/aws-toolkit-common#769
1 parent 8d86a79 commit 9331045

File tree

11 files changed

+144
-75
lines changed

11 files changed

+144
-75
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"webpack-merge": "^5.10.0"
7070
},
7171
"dependencies": {
72-
"@aws-toolkits/telemetry": "^1.0.221",
72+
"@aws-toolkits/telemetry": "^1.0.223",
7373
"vscode-nls": "^5.2.0",
7474
"vscode-nls-dev": "^4.0.4"
7575
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ import {
2121
PlanIterationLimitError,
2222
UnknownApiError,
2323
} from '../errors'
24-
import { ToolkitError, isAwsError, isCodeWhispererStreamingServiceException } from '../../shared/errors'
24+
import {
25+
ToolkitError,
26+
isAwsError,
27+
isCodeWhispererStreamingServiceException,
28+
getHttpStatusCode,
29+
} from '../../shared/errors'
2530
import { getCodewhispererConfig } from '../../codewhisperer/client/codewhisperer'
2631
import { LLMResponseType } from '../types'
2732
import { createCodeWhispererChatStreamingClient } from '../../shared/clients/codewhispererChatClient'
@@ -191,7 +196,7 @@ export class FeatureDevClient {
191196
e.message,
192197
'GeneratePlan',
193198
e.name,
194-
e.$metadata?.httpStatusCode ?? streamResponseErrors[e.name] ?? 500
199+
getHttpStatusCode(e) ?? streamResponseErrors[e.name] ?? 500
195200
)
196201
}
197202

packages/core/src/codewhisperer/models/model.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
CodewhispererCompletionType,
1111
CodewhispererLanguage,
1212
CodewhispererTriggerType,
13+
MetricBase,
1314
Result,
1415
} from '../../shared/telemetry/telemetry'
1516
import { References } from '../client/codewhisperer'
@@ -673,7 +674,7 @@ export class TransformByQStoppedError extends ToolkitError {
673674
}
674675
}
675676

676-
export interface CodeScanTelemetryEntry {
677+
export interface CodeScanTelemetryEntry extends MetricBase {
677678
codewhispererCodeScanJobId?: string
678679
codewhispererLanguage: CodewhispererLanguage
679680
codewhispererCodeScanProjectBytes?: number

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { triggerPayloadToChatRequest } from './chatRequest/converter'
4545
import { AuthUtil } from '../../../codewhisperer/util/authUtil'
4646
import { openUrl } from '../../../shared/utilities/vsCodeUtils'
4747
import { randomUUID } from '../../../common/crypto'
48+
import { getHttpStatusCode } from '../../../shared/errors'
4849

4950
export interface ChatControllerMessagePublishers {
5051
readonly processPromptChatMessage: MessagePublisher<PromptMessage>
@@ -590,7 +591,7 @@ export class ChatController {
590591
)
591592
await this.messenger.sendAIResponse(response, session, tabID, triggerID, triggerPayload)
592593
} catch (e: any) {
593-
this.telemetryHelper.recordMessageResponseError(triggerPayload, tabID, e?.$metadata?.httpStatusCode ?? 0)
594+
this.telemetryHelper.recordMessageResponseError(triggerPayload, tabID, getHttpStatusCode(e) ?? 0)
594595
// clears session, record telemetry before this call
595596
this.processException(e, tabID)
596597
}

packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { ChatSession } from '../../../clients/chat/v0/chat'
2222
import { ChatException } from './model'
2323
import { CWCTelemetryHelper } from '../telemetryHelper'
2424
import { ChatPromptCommandType, TriggerPayload } from '../model'
25-
import { ToolkitError } from '../../../../shared/errors'
25+
import { getHttpStatusCode, getRequestId, ToolkitError } from '../../../../shared/errors'
2626
import { keys } from '../../../../shared/utilities/tsUtils'
2727
import { getLogger } from '../../../../shared/logger/logger'
2828
import { FeatureAuthState } from '../../../../codewhisperer/util/authUtil'
@@ -218,8 +218,8 @@ export class Messenger {
218218

219219
if (error instanceof CodeWhispererStreamingServiceException) {
220220
errorMessage = error.message
221-
statusCode = error.$metadata?.httpStatusCode ?? 0
222-
requestID = error.$metadata.requestId
221+
statusCode = getHttpStatusCode(error) ?? 0
222+
requestID = getRequestId(error)
223223
}
224224

225225
this.showChatExceptionMessage(

packages/core/src/shared/errors.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -522,11 +522,15 @@ function hasFault<T>(error: T): error is T & { $fault: 'client' | 'server' } {
522522
}
523523

524524
function hasMetadata<T>(error: T): error is T & Pick<CodeWhispererStreamingServiceException, '$metadata'> {
525-
return typeof (error as { $metadata?: unknown }).$metadata === 'object'
525+
return typeof (error as { $metadata?: unknown })?.$metadata === 'object'
526+
}
527+
528+
function hasResponse<T>(error: T): error is T & Pick<ServiceException, '$response'> {
529+
return typeof (error as { $response?: unknown })?.$response === 'object'
526530
}
527531

528532
function hasName<T>(error: T): error is T & { name: string } {
529-
return typeof (error as { name?: unknown }).name === 'string'
533+
return typeof (error as { name?: unknown })?.name === 'string'
530534
}
531535

532536
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -569,6 +573,17 @@ export function getRequestId(err: unknown): string | undefined {
569573
}
570574
}
571575

576+
export function getHttpStatusCode(err: unknown): number | undefined {
577+
if (hasResponse(err) && err?.$response?.statusCode !== undefined) {
578+
return err?.$response?.statusCode
579+
}
580+
if (hasMetadata(err) && err.$metadata?.httpStatusCode !== undefined) {
581+
return err.$metadata?.httpStatusCode
582+
}
583+
584+
return undefined
585+
}
586+
572587
export function isFilesystemError(err: unknown): boolean {
573588
if (
574589
err instanceof vscode.FileSystemError ||

packages/core/src/shared/telemetry/spans.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ import {
1515
MetricShapes,
1616
TelemetryBase,
1717
} from './telemetry.gen'
18-
import { getRequestId, getTelemetryReason, getTelemetryReasonDesc, getTelemetryResult } from '../errors'
18+
import {
19+
getHttpStatusCode,
20+
getRequestId,
21+
getTelemetryReason,
22+
getTelemetryReasonDesc,
23+
getTelemetryResult,
24+
} from '../errors'
1925
import { entries, NumericKeys } from '../utilities/tsUtils'
2026

2127
const AsyncLocalStorage: typeof AsyncLocalStorageClass =
@@ -151,14 +157,14 @@ export class TelemetrySpan<T extends MetricBase = MetricBase> {
151157
public stop(err?: unknown): void {
152158
const duration = this.startTime !== undefined ? globals.clock.Date.now() - this.startTime.getTime() : undefined
153159

154-
// TODO: add reasonDesc/requestId to `MetricBase` so we can remove `as any` below.
155160
this.emit({
156161
duration,
157162
result: getTelemetryResult(err),
158163
reason: getTelemetryReason(err),
159164
reasonDesc: getTelemetryReasonDesc(err),
160165
requestId: getRequestId(err),
161-
} as any as Partial<T>)
166+
httpStatusCode: getHttpStatusCode(err),
167+
} as Partial<T>)
162168

163169
this.#startTime = undefined
164170
}

packages/core/src/test/shared/errors.test.ts

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ import {
1515
resolveErrorMessageToDisplay,
1616
scrubNames,
1717
ToolkitError,
18+
UnknownError,
1819
} from '../../shared/errors'
1920
import { CancellationError } from '../../shared/utilities/timeoutUtils'
2021
import { UnauthorizedException } from '@aws-sdk/client-sso'
2122
import { AWSError } from 'aws-sdk'
2223
import { AccessDeniedException } from '@aws-sdk/client-sso-oidc'
24+
import { OidcClient } from '../../auth/sso/clients'
25+
import { SamCliError } from '../../shared/sam/cli/samCliInvokerUtils'
2326

2427
class TestAwsError extends Error implements AWSError {
2528
constructor(
@@ -67,33 +70,58 @@ function fakeAwsErrorUnauth() {
6770
return e as UnauthorizedException
6871
}
6972

73+
function trySetCause(err: Error | undefined, cause: unknown) {
74+
if (err && !(err instanceof ToolkitError)) {
75+
;(err as any).cause = cause
76+
}
77+
}
78+
7079
/** Creates a deep "cause chain", to test that error handler correctly gets the most relevant error. */
71-
function fakeErrorChain(rootCause?: Error) {
80+
export function fakeErrorChain(err1?: Error, err2?: Error, err3?: Error, err4?: Error) {
7281
try {
73-
if (rootCause) {
74-
throw rootCause
82+
if (err1) {
83+
throw err1
7584
} else {
7685
throw new Error('generic error 1')
7786
}
7887
} catch (e1) {
7988
try {
80-
const e = fakeAwsErrorAccessDenied()
81-
;(e as any).cause = e1
89+
const e = err2 ? err2 : new UnknownError(e1)
90+
trySetCause(e, e1)
8291
throw e
8392
} catch (e2) {
8493
try {
85-
const e = fakeAwsErrorUnauth()
86-
;(e as any).cause = e2
87-
return e
94+
// new Error('error 3')
95+
const e = err3 ? err3 : new SamCliError('sam error', { cause: e2 as Error })
96+
trySetCause(e, e2)
97+
throw e
8898
} catch (e3) {
89-
throw ToolkitError.chain(e3, 'ToolkitError message', {
90-
documentationUri: vscode.Uri.parse('https://docs.aws.amazon.com/toolkit-for-vscode/'),
91-
})
99+
const e = err4
100+
? err4
101+
: ToolkitError.chain(e3, 'ToolkitError message', {
102+
documentationUri: vscode.Uri.parse('https://docs.aws.amazon.com/toolkit-for-vscode/'),
103+
})
104+
trySetCause(e, e3)
105+
106+
return e
92107
}
93108
}
94109
}
95110
}
96111

112+
/** Sends a (broken) request to the AWS OIDC service, to get a "real" error response. */
113+
export async function getAwsServiceError(): Promise<Error> {
114+
const oidc = OidcClient.create('us-east-1')
115+
return oidc
116+
.createToken({
117+
clientId: 'AWS IDE Extensions',
118+
clientSecret: 'xx',
119+
deviceCode: 'xx',
120+
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
121+
})
122+
.catch(e => e)
123+
}
124+
97125
describe('ToolkitError', function () {
98126
it('can store an error message', function () {
99127
const error = new ToolkitError('uh oh!')
@@ -408,13 +436,20 @@ describe('util', function () {
408436

409437
it('formatError()', function () {
410438
assert.deepStrictEqual(
411-
formatError(fakeErrorChain()),
439+
formatError(
440+
fakeErrorChain(undefined, fakeAwsErrorAccessDenied(), new Error('err 3'), fakeAwsErrorUnauth())
441+
),
412442
'unauthorized-name: unauthorized message [unauthorized-code] (requestId: be62f79a-e9cf-41cd-a755-e6920c56e4fb)'
413443
)
414444
})
415445

416446
it('getErrorMsg()', function () {
417-
assert.deepStrictEqual(getErrorMsg(fakeErrorChain()), 'unauthorized message')
447+
assert.deepStrictEqual(
448+
getErrorMsg(
449+
fakeErrorChain(undefined, fakeAwsErrorAccessDenied(), new Error('err 3'), fakeAwsErrorUnauth())
450+
),
451+
'unauthorized message'
452+
)
418453
assert.deepStrictEqual(getErrorMsg(undefined), undefined)
419454
const err = new TestAwsError('ValidationException', 'aws validation msg 1', new Date())
420455
assert.deepStrictEqual(getErrorMsg(err), 'aws validation msg 1')

packages/core/src/test/shared/telemetry/spans.test.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { TelemetrySpan, TelemetryTracer } from '../../../shared/telemetry/spans'
99
import { MetricName, MetricShapes } from '../../../shared/telemetry/telemetry'
1010
import { assertTelemetry, getMetrics, installFakeClock } from '../../testUtil'
1111
import { selectFrom } from '../../../shared/utilities/tsUtils'
12+
import { getAwsServiceError } from '../errors.test'
1213

1314
describe('TelemetrySpan', function () {
1415
let clock: ReturnType<typeof installFakeClock>
@@ -77,7 +78,7 @@ describe('TelemetryTracer', function () {
7778
tracer = new TelemetryTracer()
7879
})
7980

80-
describe('record', function () {
81+
describe('record()', function () {
8182
it('only writes to the active span in the current context', function () {
8283
tracer.apigateway_copyUrl.run(() => {
8384
tracer.run(metricName, () => tracer.record({ source: 'bar' }))
@@ -122,7 +123,7 @@ describe('TelemetryTracer', function () {
122123
})
123124
})
124125

125-
describe('instrument', function () {
126+
describe('instrument()', function () {
126127
async function assertPositive(n: number): Promise<number> {
127128
if (n <= 0) {
128129
throw new Error()
@@ -165,7 +166,7 @@ describe('TelemetryTracer', function () {
165166
})
166167
})
167168

168-
describe('increment', function () {
169+
describe('increment()', function () {
169170
it('starts at 0 for uninitialized fields', function () {
170171
tracer.vscode_executeCommand.run(span => {
171172
span.record({ command: 'foo' })
@@ -195,7 +196,7 @@ describe('TelemetryTracer', function () {
195196
})
196197
})
197198

198-
describe('run', function () {
199+
describe('run()', function () {
199200
it('returns the result of the function', function () {
200201
const result = tracer.run(metricName, () => 'foo')
201202

@@ -214,7 +215,27 @@ describe('TelemetryTracer', function () {
214215
assertTelemetry(metricName, { result: 'Succeeded', source: 'bar' })
215216
})
216217

217-
describe('nested run', function () {
218+
it('records standard fields on failed service request', async function () {
219+
await assert.rejects(async () => {
220+
await tracer.aws_loginWithBrowser.run(async () => {
221+
throw await getAwsServiceError()
222+
})
223+
})
224+
225+
assertTelemetry('aws_loginWithBrowser', {
226+
result: 'Failed',
227+
reason: 'InvalidRequestException',
228+
reasonDesc: 'Invalid client ID provided',
229+
httpStatusCode: '400',
230+
})
231+
const metric = getMetrics('aws_loginWithBrowser')[0]
232+
assert.match(metric.awsAccount ?? '', /not-set|n\/a|\d+/)
233+
assert.match(metric.awsRegion ?? '', /not-set|\w+-\w+-\d+/)
234+
assert.match(String(metric.duration) ?? '', /\d+/)
235+
assert.match(metric.requestId ?? '', /[a-z0-9-]+/)
236+
})
237+
238+
describe('nested run()', function () {
218239
const nestedName = 'nested_metric' as MetricName
219240

220241
it('can record metadata in nested spans', function () {

0 commit comments

Comments
 (0)