From 4f613fe1263e0f9ef0a230ab8f710df79c841e53 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 5 Sep 2024 21:09:37 +0200 Subject: [PATCH 1/4] test(tracer): streamline e2e test cases --- package-lock.json | 51 +-- packages/testing/src/types.ts | 8 +- packages/testing/src/xray-traces-utils.ts | 63 +--- packages/tracer/package.json | 3 +- ...allFeatures.decorator.test.functionCode.ts | 121 ------- .../tests/e2e/allFeatures.decorator.test.ts | 341 ------------------ .../allFeatures.manual.test.functionCode.ts | 79 ---- .../allFeatures.middy.test.functionCode.ts | 65 ---- .../tests/e2e/allFeatures.middy.test.ts | 339 ----------------- ...syncHandler.decorator.test.functionCode.ts | 111 ------ .../tests/e2e/asyncHandler.decorator.test.ts | 237 ------------ packages/tracer/tests/e2e/constants.ts | 27 +- .../tests/e2e/decorator.test.functionCode.ts | 81 +++++ packages/tracer/tests/e2e/decorator.test.ts | 187 ++++++++++ .../tests/e2e/manual.test.functionCode.ts | 54 +++ ...Features.manual.test.ts => manual.test.ts} | 90 ++--- .../tests/e2e/middy.test.functionCode.ts | 45 +++ packages/tracer/tests/e2e/middy.test.ts | 162 +++++++++ packages/tracer/tests/helpers/resources.ts | 36 -- 19 files changed, 611 insertions(+), 1489 deletions(-) delete mode 100644 packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts delete mode 100644 packages/tracer/tests/e2e/allFeatures.decorator.test.ts delete mode 100644 packages/tracer/tests/e2e/allFeatures.manual.test.functionCode.ts delete mode 100644 packages/tracer/tests/e2e/allFeatures.middy.test.functionCode.ts delete mode 100644 packages/tracer/tests/e2e/allFeatures.middy.test.ts delete mode 100644 packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts delete mode 100644 packages/tracer/tests/e2e/asyncHandler.decorator.test.ts create mode 100644 packages/tracer/tests/e2e/decorator.test.functionCode.ts create mode 100644 packages/tracer/tests/e2e/decorator.test.ts create mode 100644 packages/tracer/tests/e2e/manual.test.functionCode.ts rename packages/tracer/tests/e2e/{allFeatures.manual.test.ts => manual.test.ts} (55%) create mode 100644 packages/tracer/tests/e2e/middy.test.functionCode.ts create mode 100644 packages/tracer/tests/e2e/middy.test.ts delete mode 100644 packages/tracer/tests/helpers/resources.ts diff --git a/package-lock.json b/package-lock.json index ef3242e32e..804080f8b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17532,8 +17532,7 @@ "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", "@aws-sdk/client-dynamodb": "^3.637.0", - "@aws-sdk/client-xray": "^3.637.0", - "aws-sdk": "^2.1688.0" + "@aws-sdk/client-xray": "^3.637.0" }, "peerDependencies": { "@middy/core": "4.x || 5.x" @@ -17543,54 +17542,6 @@ "optional": true } } - }, - "packages/tracer/node_modules/aws-sdk": { - "version": "2.1687.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1687.0.tgz", - "integrity": "sha512-Pk7RbIxJ8yDmFJRKzaapiUsAvz5cTPKCz7soomU+lASx1jvO29Z9KAPB6KJR22m7rDDMO/HNNN9OJRzfdvh7xQ==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.16.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "util": "^0.12.4", - "uuid": "8.0.0", - "xml2js": "0.6.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "packages/tracer/node_modules/buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "packages/tracer/node_modules/ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true - }, - "packages/tracer/node_modules/uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } } } } diff --git a/packages/testing/src/types.ts b/packages/testing/src/types.ts index 286f2bab7b..28ca556b8c 100644 --- a/packages/testing/src/types.ts +++ b/packages/testing/src/types.ts @@ -108,8 +108,13 @@ type XRayTraceDocumentParsed = { request_id: string; }; http?: { - response: { + request: { + url: string; + method: string; + }; + response?: { status: number; + content_length?: number; }; }; origin?: string; @@ -142,6 +147,7 @@ type XRayTraceDocumentParsed = { message: string; }; error?: boolean; + namespace?: string; }; type XRaySegmentParsed = { diff --git a/packages/testing/src/xray-traces-utils.ts b/packages/testing/src/xray-traces-utils.ts index 9fb04c9137..c25a9c2939 100644 --- a/packages/testing/src/xray-traces-utils.ts +++ b/packages/testing/src/xray-traces-utils.ts @@ -83,10 +83,15 @@ const retriableGetTraceIds = (options: GetXRayTraceIdsOptions) => endTime.getTime() / 1000 )} --filter-expression 'resource.arn ENDSWITH ":function:${options.resourceName}"'` ); + + throw new Error( + `Failed to get trace IDs after ${retryOptions.retries} retries`, + { cause: error } + ); } retry(error); } - }); + }, retryOptions); /** * Parse and sort the trace segments by start time @@ -168,13 +173,25 @@ const getTraceDetails = async ( * @param options - The options to get trace details, including the trace IDs and expected segments count */ const retriableGetTraceDetails = (options: GetXRayTraceDetailsOptions) => - promiseRetry(async (retry) => { + promiseRetry(async (retry, attempt) => { try { return await getTraceDetails(options); } catch (error) { + if (attempt === retryOptions.retries) { + console.log( + `Manual query: aws xray batch-get-traces --trace-ids ${ + options.traceIds + }` + ); + + throw new Error( + `Failed to get trace details after ${retryOptions.retries} retries`, + { cause: error } + ); + } retry(error); } - }); + }, retryOptions); /** * Find the main function segment in the trace identified by the `## index.` suffix @@ -305,45 +322,6 @@ const getTraces = async ( return mainSubsegments; }; -/** - * Get the X-Ray trace data for a given resource name without the main subsegments. - * - * This is useful when we are testing cases where Active Tracing is disabled and we don't have the main subsegments. - * - * @param options - The options to get the X-Ray trace data, including the start time, resource name, expected traces count, and expected segments count - */ -const getTracesWithoutMainSubsegments = async ( - options: GetXRayTraceIdsOptions & Omit -): Promise => { - const traces = await getXRayTraceData(options); - - const { resourceName } = options; - - const lambdaFunctionSegments = []; - for (const trace of traces) { - const functionSegment = trace.Segments.find( - (segment) => segment.Document.origin === 'AWS::Lambda::Function' - ); - - if (!functionSegment) { - throw new Error( - `AWS::Lambda::Function segment not found for ${resourceName}` - ); - } - - const lambdaFunctionSegment = functionSegment.Document; - const enrichedSubsegment = { - ...lambdaFunctionSegment, - subsegments: parseSubsegmentsByName( - lambdaFunctionSegment.subsegments ?? [] - ), - }; - lambdaFunctionSegments.push(enrichedSubsegment); - } - - return lambdaFunctionSegments; -}; - export { getTraceIds, retriableGetTraceIds, @@ -352,5 +330,4 @@ export { findPowertoolsFunctionSegment, getTraces, parseSubsegmentsByName, - getTracesWithoutMainSubsegments, }; diff --git a/packages/tracer/package.json b/packages/tracer/package.json index 3cb3110793..e6257d5cd3 100644 --- a/packages/tracer/package.json +++ b/packages/tracer/package.json @@ -29,8 +29,7 @@ "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", "@aws-sdk/client-dynamodb": "^3.637.0", - "@aws-sdk/client-xray": "^3.637.0", - "aws-sdk": "^2.1688.0" + "@aws-sdk/client-xray": "^3.637.0" }, "peerDependencies": { "@middy/core": "4.x || 5.x" diff --git a/packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts b/packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts deleted file mode 100644 index 8e9ed4aba1..0000000000 --- a/packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { Callback, Context } from 'aws-lambda'; -import AWS from 'aws-sdk'; -import { Tracer } from '../../src/Tracer.js'; -import { httpRequest } from '../helpers/httpRequest.js'; - -const serviceName = - process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; -const customAnnotationKey = - process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; -const customAnnotationValue = - process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; -const customMetadataKey = - process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; -const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE - ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) - : { bar: 'baz' }; -const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE - ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) - : { foo: 'bar' }; -const customErrorMessage = - process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; -const testTableName = process.env.TEST_TABLE_NAME ?? 'TestTable'; - -interface CustomEvent { - throw: boolean; - invocation: number; -} - -const tracer = new Tracer({ serviceName: serviceName }); -tracer.captureAWS(AWS); -const dynamoDB = new AWS.DynamoDB.DocumentClient(); - -export class MyFunctionBase { - private readonly returnValue: string; - - public constructor() { - this.returnValue = customResponseValue; - } - - public handler( - event: CustomEvent, - _context: Context, - _callback: Callback - ): unknown { - tracer.putAnnotation(customAnnotationKey, customAnnotationValue); - tracer.putMetadata(customMetadataKey, customMetadataValue); - - return Promise.all([ - dynamoDB - .put({ - TableName: testTableName, - Item: { id: `${serviceName}-${event.invocation}-sdkv2` }, - }) - .promise(), - httpRequest({ - hostname: 'docs.powertools.aws.dev', - path: '/lambda/typescript/latest/', - }), - new Promise((resolve, reject) => { - setTimeout(() => { - const res = this.myMethod(); - if (event.throw) { - reject(new Error(customErrorMessage)); - } else { - resolve(res); - } - }, 2000); // We need to wait for to make sure previous calls are finished - }), - ]) - .then(([_dynamoDBRes, _httpRes, promiseRes]) => promiseRes) - .catch((err) => { - throw err; - }); - } - - public myMethod(): string { - return this.returnValue; - } -} - -class MyFunctionWithDecorator extends MyFunctionBase { - @tracer.captureLambdaHandler() - public handler( - event: CustomEvent, - _context: Context, - _callback: Callback - ): unknown { - return super.handler(event, _context, _callback); - } - - @tracer.captureMethod() - public myMethod(): string { - return super.myMethod(); - } -} - -const handlerClass = new MyFunctionWithDecorator(); -export const handler = handlerClass.handler.bind(handlerClass); - -class MyFunctionWithDecoratorCaptureResponseFalse extends MyFunctionBase { - @tracer.captureLambdaHandler({ captureResponse: false }) - public handler( - event: CustomEvent, - _context: Context, - _callback: Callback - ): unknown { - return super.handler(event, _context, _callback); - } - - @tracer.captureMethod({ captureResponse: false }) - public myMethod(): string { - return super.myMethod(); - } -} - -const handlerWithCaptureResponseFalseClass = - new MyFunctionWithDecoratorCaptureResponseFalse(); -export const handlerWithCaptureResponseFalse = - handlerWithCaptureResponseFalseClass.handler.bind( - handlerWithCaptureResponseFalseClass - ); diff --git a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts deleted file mode 100644 index c44c73eff1..0000000000 --- a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts +++ /dev/null @@ -1,341 +0,0 @@ -/** - * Test tracer in decorator setup - * - * @group e2e/tracer/decorator - */ -import { join } from 'node:path'; -import { TestStack } from '@aws-lambda-powertools/testing-utils'; -import { TestDynamodbTable } from '@aws-lambda-powertools/testing-utils/resources/dynamodb'; -import { - getTraces, - getTracesWithoutMainSubsegments, -} from '@aws-lambda-powertools/testing-utils/utils/xray-traces'; -import { invokeAllTestCases } from '../helpers/invokeAllTests.js'; -import { TracerTestNodejsFunction } from '../helpers/resources.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, - commonEnvironmentVars, -} from './constants.js'; - -/** - * The test includes one stack with 4 Lambda functions that correspond to the following test cases: - * 1. With all flags enabled (capture both response and error) - * 2. Do not capture error or response - * 3. Do not enable tracer - * 4. Disable capture response via decorator options - * Each stack must use a unique `serviceName` as it's used to for retrieving the trace. - * Using the same one will result in traces from different test cases mixing up. - */ -describe('Tracer E2E tests, all features with decorator instantiation', () => { - const testStack = new TestStack({ - stackNameProps: { - stackNamePrefix: RESOURCE_NAME_PREFIX, - testName: 'AllFeatures-Decorator', - }, - }); - - // Location of the lambda function code - const lambdaFunctionCodeFilePath = join( - __dirname, - 'allFeatures.decorator.test.functionCode.ts' - ); - const startTime = new Date(); - - /** - * Table used by all functions to make an SDK call - */ - const testTable = new TestDynamodbTable( - testStack, - {}, - { - nameSuffix: 'TestTable', - } - ); - - /** - * Function #1 is with all flags enabled. - */ - let fnNameAllFlagsEnabled: string; - const fnAllFlagsEnabled = new TracerTestNodejsFunction( - testStack, - { - entry: lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, - }, - }, - { - nameSuffix: 'AllFlagsOn', - outputFormat: 'ESM', - } - ); - testTable.grantWriteData(fnAllFlagsEnabled); - - /** - * Function #2 doesn't capture error or response - */ - let fnNameNoCaptureErrorOrResponse: string; - const fnNoCaptureErrorOrResponse = new TracerTestNodejsFunction( - testStack, - { - entry: lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', - }, - }, - { - nameSuffix: 'NoCaptureErrOrResp', - outputFormat: 'ESM', - } - ); - testTable.grantWriteData(fnNoCaptureErrorOrResponse); - - /** - * Function #3 disables tracer - */ - let fnNameTracerDisabled: string; - const fnTracerDisabled = new TracerTestNodejsFunction( - testStack, - { - entry: lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, - POWERTOOLS_TRACE_ENABLED: 'false', - }, - }, - { - nameSuffix: 'TracerDisabled', - outputFormat: 'ESM', - } - ); - testTable.grantWriteData(fnTracerDisabled); - - /** - * Function #4 disables capture response via decorator options - */ - let fnNameCaptureResponseOff: string; - const fnCaptureResponseOff = new TracerTestNodejsFunction( - testStack, - { - entry: lambdaFunctionCodeFilePath, - handler: 'handlerWithCaptureResponseFalse', - environment: { - TEST_TABLE_NAME: testTable.tableName, - }, - }, - { - nameSuffix: 'CaptureResponseOff', - outputFormat: 'ESM', - } - ); - testTable.grantWriteData(fnCaptureResponseOff); - - const invocationCount = 3; - - beforeAll(async () => { - // Deploy the stack - await testStack.deploy(); - - // Get the actual function names from the stack outputs - fnNameAllFlagsEnabled = testStack.findAndGetStackOutputValue('AllFlagsOn'); - fnNameNoCaptureErrorOrResponse = - testStack.findAndGetStackOutputValue('NoCaptureErrOrResp'); - fnNameTracerDisabled = - testStack.findAndGetStackOutputValue('TracerDisabled'); - fnNameCaptureResponseOff = - testStack.findAndGetStackOutputValue('CaptureResponseOff'); - - // Invoke all functions - await Promise.all([ - invokeAllTestCases(fnNameAllFlagsEnabled, invocationCount), - invokeAllTestCases(fnNameNoCaptureErrorOrResponse, invocationCount), - invokeAllTestCases(fnNameTracerDisabled, invocationCount), - invokeAllTestCases(fnNameCaptureResponseOff, invocationCount), - ]); - }, SETUP_TIMEOUT); - - afterAll(async () => { - if (!process.env.DISABLE_TEARDOWN) { - await testStack.destroy(); - } - }, TEARDOWN_TIMEOUT); - - it( - 'should generate all custom traces with correct subsegments, annotations, and metadata', - async () => { - // Prepare - const { - EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage, - EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, - EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, - EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, - EXPECTED_CUSTOM_METADATA_VALUE: expectedCustomMetadataValue, - EXPECTED_CUSTOM_RESPONSE_VALUE: expectedCustomResponseValue, - } = commonEnvironmentVars; - const serviceName = 'AllFlagsOn'; - - const mainSubsegments = await getTraces({ - startTime, - resourceName: fnNameAllFlagsEnabled, - expectedTracesCount: invocationCount, - /** - * The trace should have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 4. DynamoDB (AWS::DynamoDB) - * 4. Remote call (docs.powertools.aws.dev) - */ - expectedSegmentsCount: 4, - }); - - // Assess - for (let i = 0; i < invocationCount; i++) { - const isColdStart = i === 0; // First invocation is a cold start - const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture - const mainSubsegment = mainSubsegments[i]; - const { subsegments, annotations, metadata } = mainSubsegment; - - // Check the main segment name - expect(mainSubsegment.name).toBe('## index.handler'); - - // Check the subsegments - expect(subsegments.size).toBe(3); - expect(subsegments.has('DynamoDB')).toBe(true); - expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); - expect(subsegments.has('### myMethod')).toBe(true); - - // Check the annotations - if (!annotations) { - throw new Error('No annotations found on the main segment'); - } - expect(annotations.ColdStart).toEqual(isColdStart); - expect(annotations.Service).toEqual(serviceName); - expect(annotations[expectedCustomAnnotationKey]).toEqual( - expectedCustomAnnotationValue - ); - - // Check the metadata - if (!metadata) { - throw new Error('No metadata found on the main segment'); - } - expect(metadata[serviceName][expectedCustomMetadataKey]).toEqual( - expectedCustomMetadataValue - ); - - // Check the error recording (only on invocations that should throw) - if (shouldThrowAnError) { - expect(mainSubsegment.fault).toBe(true); - expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(true); - expect(mainSubsegment.cause?.exceptions[0].message).toBe( - expectedCustomErrorMessage - ); - // Check the response in the metadata (only on invocations that DON'T throw) - } else { - expect(metadata[serviceName]['index.handler response']).toEqual( - expectedCustomResponseValue - ); - } - } - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should not capture error nor response when the flags are false', - async () => { - const mainSubsegments = await getTraces({ - startTime, - resourceName: fnNameNoCaptureErrorOrResponse, - expectedTracesCount: invocationCount, - /** - * Expect the trace to have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 3. DynamoDB (AWS::DynamoDB) - * 4. Remote call (docs.powertools.aws.dev) - */ - expectedSegmentsCount: 4, - }); - - // Assess - const mainSubsegment = mainSubsegments[2]; // Only the last invocation should throw - // Assert that the subsegment has the expected fault - expect(mainSubsegment.error).toBe(true); - // Assert that no error was captured on the subsegment - expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(false); - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should not capture any custom traces when disabled', - async () => { - const lambdaFunctionSegments = await getTracesWithoutMainSubsegments({ - startTime, - resourceName: fnNameTracerDisabled, - expectedTracesCount: invocationCount, - /** - * Expect the trace to have 2 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - */ - expectedSegmentsCount: 2, - }); - - // Assess - for (let i = 0; i < invocationCount; i++) { - const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture - const lambdaFunctionSegment = lambdaFunctionSegments[i]; - const { subsegments } = lambdaFunctionSegment; - - expect(subsegments.has('## index.handler')).toBe(false); - - if (shouldThrowAnError) { - expect(lambdaFunctionSegment.error).toBe(true); - } - } - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should not capture response when captureResponse is set to false', - async () => { - const mainSubsegments = await getTraces({ - startTime, - resourceName: fnNameCaptureResponseOff, - expectedTracesCount: invocationCount, - /** - * Expect the trace to have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 3. DynamoDB (AWS::DynamoDB) - * 4. Remote call (docs.powertools.aws.dev) - */ - expectedSegmentsCount: 4, - }); - - // Assess - for (let i = 0; i < invocationCount; i++) { - const mainSubsegment = mainSubsegments[i]; - const { subsegments } = mainSubsegment; - - expect(mainSubsegment.name).toBe( - '## index.handlerWithCaptureResponseFalse' - ); - const customSubsegment = subsegments.get('### myMethod'); - expect(customSubsegment).toBeDefined(); - - // No metadata because capturing the response was disabled and that's - // the only metadata that could be in the subsegment for the test. - expect(customSubsegment).not.toHaveProperty('metadata'); - } - }, - TEST_CASE_TIMEOUT - ); -}); diff --git a/packages/tracer/tests/e2e/allFeatures.manual.test.functionCode.ts b/packages/tracer/tests/e2e/allFeatures.manual.test.functionCode.ts deleted file mode 100644 index 6cde17b438..0000000000 --- a/packages/tracer/tests/e2e/allFeatures.manual.test.functionCode.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { Context } from 'aws-lambda'; -import AWS from 'aws-sdk'; -import type { Subsegment } from 'aws-xray-sdk-core'; -import { Tracer } from '../../src/index.js'; -import { httpRequest } from '../helpers/httpRequest.js'; - -const serviceName = - process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; -const customAnnotationKey = - process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; -const customAnnotationValue = - process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; -const customMetadataKey = - process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; -const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE - ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) - : { bar: 'baz' }; -const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE - ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) - : { foo: 'bar' }; -const customErrorMessage = - process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; -const testTableName = process.env.TEST_TABLE_NAME ?? 'TestTable'; - -interface CustomEvent { - throw: boolean; - invocation: number; -} - -const tracer = new Tracer({ serviceName: serviceName }); -const dynamoDB = tracer.captureAWSClient(new AWS.DynamoDB.DocumentClient()); - -export const handler = async ( - event: CustomEvent, - _context: Context -): Promise => { - const segment = tracer.getSegment(); - let subsegment: Subsegment | undefined; - if (segment) { - subsegment = segment.addNewSubsegment(`## ${process.env._HANDLER}`); - tracer.setSegment(subsegment); - } - - tracer.annotateColdStart(); - tracer.addServiceNameAnnotation(); - - tracer.putAnnotation('invocation', event.invocation); - tracer.putAnnotation(customAnnotationKey, customAnnotationValue); - tracer.putMetadata(customMetadataKey, customMetadataValue); - - try { - await dynamoDB - .put({ - TableName: testTableName, - Item: { id: `${serviceName}-${event.invocation}-sdkv2` }, - }) - .promise(); - await httpRequest({ - hostname: 'docs.powertools.aws.dev', - path: '/lambda/typescript/latest/', - }); - - const res = customResponseValue; - if (event.throw) { - throw new Error(customErrorMessage); - } - tracer.addResponseAsMetadata(res, process.env._HANDLER); - - return res; - } catch (err) { - tracer.addErrorAsMetadata(err as Error); - throw err; - } finally { - if (segment && subsegment) { - subsegment.close(); - tracer.setSegment(segment); - } - } -}; diff --git a/packages/tracer/tests/e2e/allFeatures.middy.test.functionCode.ts b/packages/tracer/tests/e2e/allFeatures.middy.test.functionCode.ts deleted file mode 100644 index 07cc915c12..0000000000 --- a/packages/tracer/tests/e2e/allFeatures.middy.test.functionCode.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb'; -import type { Context } from 'aws-lambda'; -import middy from 'middy5'; -import { Tracer } from '../../src/index.js'; -import { captureLambdaHandler } from '../../src/middleware/middy.js'; -import { httpRequest } from '../helpers/httpRequest.js'; - -const serviceName = - process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; -const customAnnotationKey = - process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; -const customAnnotationValue = - process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; -const customMetadataKey = - process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; -const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE - ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) - : { bar: 'baz' }; -const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE - ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) - : { foo: 'bar' }; -const customErrorMessage = - process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; -const testTableName = process.env.TEST_TABLE_NAME ?? 'TestTable'; - -interface CustomEvent { - throw: boolean; - invocation: number; -} - -const tracer = new Tracer({ serviceName: serviceName }); -const dynamoDB = tracer.captureAWSv3Client(new DynamoDBClient({})); - -const testHandler = async ( - event: CustomEvent, - _context: Context -): Promise => { - tracer.putAnnotation('invocation', event.invocation); - tracer.putAnnotation(customAnnotationKey, customAnnotationValue); - tracer.putMetadata(customMetadataKey, customMetadataValue); - - await dynamoDB.send( - new PutItemCommand({ - TableName: testTableName, - Item: { id: { S: `${serviceName}-${event.invocation}-sdkv3` } }, - }) - ); - await httpRequest({ - hostname: 'docs.powertools.aws.dev', - path: '/lambda/typescript/latest/', - }); - - const res = customResponseValue; - if (event.throw) { - throw new Error(customErrorMessage); - } - - return res; -}; - -export const handler = middy(testHandler).use(captureLambdaHandler(tracer)); - -export const handlerWithNoCaptureResponseViaMiddlewareOption = middy( - testHandler -).use(captureLambdaHandler(tracer, { captureResponse: false })); diff --git a/packages/tracer/tests/e2e/allFeatures.middy.test.ts b/packages/tracer/tests/e2e/allFeatures.middy.test.ts deleted file mode 100644 index 9d07a6a651..0000000000 --- a/packages/tracer/tests/e2e/allFeatures.middy.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -/** - * Test tracer in middy setup - * - * @group e2e/tracer/middy - */ -import { join } from 'node:path'; -import { TestStack } from '@aws-lambda-powertools/testing-utils'; -import { TestDynamodbTable } from '@aws-lambda-powertools/testing-utils/resources/dynamodb'; -import { - getTraces, - getTracesWithoutMainSubsegments, -} from '@aws-lambda-powertools/testing-utils/utils/xray-traces'; -import { invokeAllTestCases } from '../helpers/invokeAllTests.js'; -import { TracerTestNodejsFunction } from '../helpers/resources.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, - commonEnvironmentVars, -} from './constants.js'; - -/** - * The test includes one stack with 4 Lambda functions that correspond to the following test cases: - * 1. With all flags enabled (capture both response and error) - * 2. Do not capture error or response - * 3. Do not enable tracer - * 4. Disable response capture via middleware option - * Each stack must use a unique `serviceName` as it's used to for retrieving the trace. - * Using the same one will result in traces from different test cases mixing up. - */ -describe('Tracer E2E tests, all features with middy instantiation', () => { - const testStack = new TestStack({ - stackNameProps: { - stackNamePrefix: RESOURCE_NAME_PREFIX, - testName: 'AllFeatures-Middy', - }, - }); - - // Location of the lambda function code - const lambdaFunctionCodeFilePath = join( - __dirname, - 'allFeatures.middy.test.functionCode.ts' - ); - const startTime = new Date(); - - /** - * Table used by all functions to make an SDK call - */ - const testTable = new TestDynamodbTable( - testStack, - {}, - { - nameSuffix: 'TestTable', - } - ); - - /** - * Function #1 is with all flags enabled. - */ - let fnNameAllFlagsEnabled: string; - const fnAllFlagsEnabled = new TracerTestNodejsFunction( - testStack, - { - entry: lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, - }, - }, - { - nameSuffix: 'AllFlagsOn', - outputFormat: 'ESM', - } - ); - testTable.grantWriteData(fnAllFlagsEnabled); - - /** - * Function #2 doesn't capture error or response - */ - let fnNameNoCaptureErrorOrResponse: string; - const fnNoCaptureErrorOrResponse = new TracerTestNodejsFunction( - testStack, - { - entry: lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', - }, - }, - { - nameSuffix: 'NoCaptureErrOrResp', - outputFormat: 'ESM', - } - ); - testTable.grantWriteData(fnNoCaptureErrorOrResponse); - - /** - * Function #3 disables tracer - */ - let fnNameTracerDisabled: string; - const fnTracerDisabled = new TracerTestNodejsFunction( - testStack, - { - entry: lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, - POWERTOOLS_TRACE_ENABLED: 'false', - }, - }, - { - nameSuffix: 'TracerDisabled', - outputFormat: 'ESM', - } - ); - testTable.grantWriteData(fnTracerDisabled); - - /** - * Function #4 disables response capture via middleware option - */ - let fnNameCaptureResponseOff: string; - const fnCaptureResponseOff = new TracerTestNodejsFunction( - testStack, - { - entry: lambdaFunctionCodeFilePath, - handler: 'handlerWithNoCaptureResponseViaMiddlewareOption', - environment: { - TEST_TABLE_NAME: testTable.tableName, - }, - }, - { - nameSuffix: 'CaptureResponseOff', - outputFormat: 'ESM', - } - ); - testTable.grantWriteData(fnCaptureResponseOff); - - const invocationCount = 3; - - beforeAll(async () => { - // Deploy the stack - await testStack.deploy(); - - // Get the actual function names from the stack outputs - fnNameAllFlagsEnabled = testStack.findAndGetStackOutputValue('AllFlagsOn'); - fnNameNoCaptureErrorOrResponse = - testStack.findAndGetStackOutputValue('NoCaptureErrOrResp'); - fnNameTracerDisabled = - testStack.findAndGetStackOutputValue('TracerDisabled'); - fnNameCaptureResponseOff = - testStack.findAndGetStackOutputValue('CaptureResponseOff'); - - // Invoke all functions - await Promise.all([ - invokeAllTestCases(fnNameAllFlagsEnabled, invocationCount), - invokeAllTestCases(fnNameNoCaptureErrorOrResponse, invocationCount), - invokeAllTestCases(fnNameTracerDisabled, invocationCount), - invokeAllTestCases(fnNameCaptureResponseOff, invocationCount), - ]); - }, SETUP_TIMEOUT); - - afterAll(async () => { - if (!process.env.DISABLE_TEARDOWN) { - await testStack.destroy(); - } - }, TEARDOWN_TIMEOUT); - - it( - 'should generate all custom traces with correct subsegments, annotations, and metadata', - async () => { - // Prepare - const { - EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage, - EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, - EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, - EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, - EXPECTED_CUSTOM_METADATA_VALUE: expectedCustomMetadataValue, - EXPECTED_CUSTOM_RESPONSE_VALUE: expectedCustomResponseValue, - } = commonEnvironmentVars; - const serviceName = 'AllFlagsOn'; - - const mainSubsegments = await getTraces({ - startTime, - resourceName: fnNameAllFlagsEnabled, - expectedTracesCount: invocationCount, - /** - * The trace should have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 4. DynamoDB (AWS::DynamoDB) - * 4. Remote call (docs.powertools.aws.dev) - */ - expectedSegmentsCount: 4, - }); - - // Assess - for (let i = 0; i < invocationCount; i++) { - const isColdStart = i === 0; // First invocation is a cold start - const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture - const mainSubsegment = mainSubsegments[i]; - const { subsegments, annotations, metadata } = mainSubsegment; - - // Check the main segment name - expect(mainSubsegment.name).toBe('## index.handler'); - - // Check the subsegments - expect(subsegments.size).toBe(2); - expect(subsegments.has('DynamoDB')).toBe(true); - expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); - - // Check the annotations - if (!annotations) { - throw new Error('No annotations found on the main segment'); - } - expect(annotations.ColdStart).toEqual(isColdStart); - expect(annotations.Service).toEqual(serviceName); - expect(annotations[expectedCustomAnnotationKey]).toEqual( - expectedCustomAnnotationValue - ); - - // Check the metadata - if (!metadata) { - throw new Error('No metadata found on the main segment'); - } - expect(metadata[serviceName][expectedCustomMetadataKey]).toEqual( - expectedCustomMetadataValue - ); - - // Check the error recording (only on invocations that should throw) - if (shouldThrowAnError) { - expect(mainSubsegment.fault).toBe(true); - expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(true); - expect(mainSubsegment.cause?.exceptions[0].message).toBe( - expectedCustomErrorMessage - ); - // Check the response in the metadata (only on invocations that DON'T throw) - } else { - expect(metadata[serviceName]['index.handler response']).toEqual( - expectedCustomResponseValue - ); - } - } - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should not capture error nor response when the flags are false', - async () => { - const mainSubsegments = await getTraces({ - startTime, - resourceName: fnNameNoCaptureErrorOrResponse, - expectedTracesCount: invocationCount, - /** - * Expect the trace to have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 3. DynamoDB (AWS::DynamoDB) - * 4. Remote call (docs.powertools.aws.dev) - */ - expectedSegmentsCount: 4, - }); - - // Assess - const mainSubsegment = mainSubsegments[2]; // Only the last invocation should throw - // Assert that the subsegment has the expected fault - expect(mainSubsegment.error).toBe(true); - // Assert that no error was captured on the subsegment - expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(false); - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should not capture any custom traces when disabled', - async () => { - const lambdaFunctionSegments = await getTracesWithoutMainSubsegments({ - startTime, - resourceName: fnNameTracerDisabled, - expectedTracesCount: invocationCount, - /** - * Expect the trace to have 2 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - */ - expectedSegmentsCount: 2, - }); - - // Assess - for (let i = 0; i < invocationCount; i++) { - const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture - const lambdaFunctionSegment = lambdaFunctionSegments[i]; - const { subsegments } = lambdaFunctionSegment; - - expect(subsegments.has('## index.handler')).toBe(false); - - if (shouldThrowAnError) { - expect(lambdaFunctionSegment.error).toBe(true); - } - } - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should not capture response when captureResponse is set to false', - async () => { - const mainSubsegments = await getTraces({ - startTime, - resourceName: fnNameCaptureResponseOff, - expectedTracesCount: invocationCount, - /** - * Expect the trace to have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 3. DynamoDB (AWS::DynamoDB) - * 4. Remote call (docs.powertools.aws.dev) - */ - expectedSegmentsCount: 4, - }); - - // Assess - for (let i = 0; i < invocationCount; i++) { - const serviceName = 'CaptureResponseOff'; - const mainSubsegment = mainSubsegments[i]; - const { metadata } = mainSubsegment; - - expect(mainSubsegment.name).toBe( - '## index.handlerWithNoCaptureResponseViaMiddlewareOption' - ); - if (!metadata) { - throw new Error('No metadata found on the main segment'); - } - expect(metadata[serviceName]['index.handler response']).toBeUndefined(); - } - }, - TEST_CASE_TIMEOUT - ); -}); diff --git a/packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts b/packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts deleted file mode 100644 index cb9fc99c59..0000000000 --- a/packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb'; -import type { Context } from 'aws-lambda'; -import { Tracer } from '../../src/index.js'; -import { httpRequest } from '../helpers/httpRequest.js'; - -const serviceName = - process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; -const customAnnotationKey = - process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; -const customAnnotationValue = - process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; -const customMetadataKey = - process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; -const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE - ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) - : { bar: 'baz' }; -const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE - ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) - : { foo: 'bar' }; -const customErrorMessage = - process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; -const testTableName = process.env.TEST_TABLE_NAME ?? 'TestTable'; -const customSubSegmentName = - process.env.EXPECTED_CUSTOM_SUBSEGMENT_NAME ?? 'mySubsegment'; - -interface CustomEvent { - throw: boolean; - invocation: number; -} - -const tracer = new Tracer({ serviceName: serviceName }); -const dynamoDB = tracer.captureAWSv3Client( - DynamoDBDocumentClient.from(new DynamoDBClient({})) -); - -export class MyFunctionBase { - private readonly returnValue: string; - - public constructor() { - this.returnValue = customResponseValue; - } - - public async handler( - event: CustomEvent, - _context: Context - ): Promise { - tracer.putAnnotation(customAnnotationKey, customAnnotationValue); - tracer.putMetadata(customMetadataKey, customMetadataValue); - - await dynamoDB.send( - new PutCommand({ - TableName: testTableName, - Item: { id: `${serviceName}-${event.invocation}-sdkv3` }, - }) - ); - await httpRequest({ - hostname: 'docs.powertools.aws.dev', - path: '/lambda/typescript/latest/', - }); - - const res = this.myMethod(); - if (event.throw) { - throw new Error(customErrorMessage); - } - - return res; - } - - public myMethod(): string { - return this.returnValue; - } -} - -class MyFunctionWithDecorator extends MyFunctionBase { - @tracer.captureLambdaHandler() - public async handler( - event: CustomEvent, - _context: Context - ): Promise { - return super.handler(event, _context); - } - - @tracer.captureMethod() - public myMethod(): string { - return super.myMethod(); - } -} - -const handlerClass = new MyFunctionWithDecorator(); -export const handler = handlerClass.handler.bind(handlerClass); - -export class MyFunctionWithDecoratorAndCustomNamedSubSegmentForMethod extends MyFunctionBase { - @tracer.captureLambdaHandler() - public async handler( - event: CustomEvent, - _context: Context - ): Promise { - return super.handler(event, _context); - } - - @tracer.captureMethod({ subSegmentName: customSubSegmentName }) - public myMethod(): string { - return super.myMethod(); - } -} - -const handlerWithCustomSubsegmentNameInMethodClass = - new MyFunctionWithDecoratorAndCustomNamedSubSegmentForMethod(); -export const handlerWithCustomSubsegmentNameInMethod = - handlerClass.handler.bind(handlerWithCustomSubsegmentNameInMethodClass); diff --git a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts deleted file mode 100644 index 81e6504a91..0000000000 --- a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Test tracer in decorator setup - * - * @group e2e/tracer/decorator-async-handler - */ -import { join } from 'node:path'; -import { TestStack } from '@aws-lambda-powertools/testing-utils'; -import { TestDynamodbTable } from '@aws-lambda-powertools/testing-utils/resources/dynamodb'; -import { getTraces } from '@aws-lambda-powertools/testing-utils/utils/xray-traces'; -import { invokeAllTestCases } from '../helpers/invokeAllTests.js'; -import { TracerTestNodejsFunction } from '../helpers/resources.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, - commonEnvironmentVars, -} from './constants.js'; - -describe('Tracer E2E tests, async handler with decorator instantiation', () => { - const testStack = new TestStack({ - stackNameProps: { - stackNamePrefix: RESOURCE_NAME_PREFIX, - testName: 'AllFeatures-AsyncDecorator', - }, - }); - - // Location of the lambda function code - const lambdaFunctionCodeFilePath = join( - __dirname, - 'asyncHandler.decorator.test.functionCode.ts' - ); - const startTime = new Date(); - - /** - * Table used by all functions to make an SDK call - */ - const testTable = new TestDynamodbTable( - testStack, - {}, - { - nameSuffix: 'TestTable', - } - ); - - /** - * Function #1 is with all flags enabled. - */ - let fnNameAllFlagsEnabled: string; - const fnAllFlagsEnabled = new TracerTestNodejsFunction( - testStack, - { - entry: lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, - }, - }, - { - nameSuffix: 'AllFlagsOn', - } - ); - testTable.grantWriteData(fnAllFlagsEnabled); - - /** - * Function #2 sets a custom subsegment name in the decorated method - */ - let fnNameCustomSubsegment: string; - const fnCustomSubsegmentName = new TracerTestNodejsFunction( - testStack, - { - entry: lambdaFunctionCodeFilePath, - handler: 'handlerWithCustomSubsegmentNameInMethod', - environment: { - TEST_TABLE_NAME: testTable.tableName, - }, - }, - { - nameSuffix: 'CustomSubsegmentName', - } - ); - testTable.grantWriteData(fnCustomSubsegmentName); - - const invocationCount = 3; - - beforeAll(async () => { - // Deploy the stack - await testStack.deploy(); - - // Get the actual function names from the stack outputs - fnNameAllFlagsEnabled = testStack.findAndGetStackOutputValue('AllFlagsOn'); - fnNameCustomSubsegment = testStack.findAndGetStackOutputValue( - 'CustomSubsegmentName' - ); - - // Act - await Promise.all([ - invokeAllTestCases(fnNameAllFlagsEnabled, invocationCount), - invokeAllTestCases(fnNameCustomSubsegment, invocationCount), - ]); - }, SETUP_TIMEOUT); - - afterAll(async () => { - if (!process.env.DISABLE_TEARDOWN) { - await testStack.destroy(); - } - }, TEARDOWN_TIMEOUT); - - it( - 'should generate all custom traces', - async () => { - const { - EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage, - EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, - EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, - EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, - EXPECTED_CUSTOM_METADATA_VALUE: expectedCustomMetadataValue, - EXPECTED_CUSTOM_RESPONSE_VALUE: expectedCustomResponseValue, - } = commonEnvironmentVars; - const serviceName = 'AllFlagsOn'; - - const mainSubsegments = await getTraces({ - startTime, - resourceName: fnNameAllFlagsEnabled, - expectedTracesCount: invocationCount, - /** - * The trace should have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 4. DynamoDB (AWS::DynamoDB) - * 4. Remote call (docs.powertools.aws.dev) - */ - expectedSegmentsCount: 4, - }); - - // Assess - for (let i = 0; i < invocationCount; i++) { - const isColdStart = i === 0; // First invocation is a cold start - const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture - const mainSubsegment = mainSubsegments[i]; - const { subsegments, annotations, metadata } = mainSubsegment; - - // Check the main segment name - expect(mainSubsegment.name).toBe('## index.handler'); - - // Check the subsegments - expect(subsegments.size).toBe(3); - expect(subsegments.has('DynamoDB')).toBe(true); - expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); - expect(subsegments.has('### myMethod')).toBe(true); - - // Check the annotations - if (!annotations) { - throw new Error('No annotations found on the main segment'); - } - expect(annotations.ColdStart).toEqual(isColdStart); - expect(annotations.Service).toEqual(serviceName); - expect(annotations[expectedCustomAnnotationKey]).toEqual( - expectedCustomAnnotationValue - ); - - // Check the metadata - if (!metadata) { - throw new Error('No metadata found on the main segment'); - } - expect(metadata[serviceName][expectedCustomMetadataKey]).toEqual( - expectedCustomMetadataValue - ); - - // Check the error recording (only on invocations that should throw) - if (shouldThrowAnError) { - expect(mainSubsegment.fault).toBe(true); - expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(true); - expect(mainSubsegment.cause?.exceptions[0].message).toBe( - expectedCustomErrorMessage - ); - // Check the response in the metadata (only on invocations that DON'T throw) - } else { - expect(metadata[serviceName]['index.handler response']).toEqual( - expectedCustomResponseValue - ); - } - } - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should have a custom name as the subsegment name for the decorated method', - async () => { - const { - EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage, - EXPECTED_CUSTOM_SUBSEGMENT_NAME: expectedCustomSubSegmentName, - } = commonEnvironmentVars; - - const mainSubsegments = await getTraces({ - startTime, - resourceName: fnNameCustomSubsegment, - expectedTracesCount: invocationCount, - /** - * The trace should have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 4. DynamoDB (AWS::DynamoDB) - * 4. Remote call (docs.powertools.aws.dev) - */ - expectedSegmentsCount: 4, - }); - - // Assess - for (let i = 0; i < invocationCount; i++) { - const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture - const mainSubsegment = mainSubsegments[i]; - const { subsegments } = mainSubsegment; - - // Check the main segment name - expect(mainSubsegment.name).toBe( - '## index.handlerWithCustomSubsegmentNameInMethod' - ); - - // Check the subsegments - expect(subsegments.size).toBe(3); - expect(subsegments.has('DynamoDB')).toBe(true); - expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); - expect(subsegments.has(expectedCustomSubSegmentName)).toBe(true); - - if (shouldThrowAnError) { - expect(mainSubsegment.fault).toBe(true); - expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(true); - expect(mainSubsegment.cause?.exceptions[0].message).toBe( - expectedCustomErrorMessage - ); - } - } - }, - TEST_CASE_TIMEOUT - ); -}); diff --git a/packages/tracer/tests/e2e/constants.ts b/packages/tracer/tests/e2e/constants.ts index 907976c469..77ea5c9ed2 100644 --- a/packages/tracer/tests/e2e/constants.ts +++ b/packages/tracer/tests/e2e/constants.ts @@ -7,18 +7,13 @@ const SETUP_TIMEOUT = 7 * ONE_MINUTE; const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; // Expected values for custom annotations, metadata, and response -const commonEnvironmentVars = { - EXPECTED_CUSTOM_ANNOTATION_KEY: 'myAnnotation', - EXPECTED_CUSTOM_ANNOTATION_VALUE: 'myValue', - EXPECTED_CUSTOM_METADATA_KEY: 'myMetadata', - EXPECTED_CUSTOM_METADATA_VALUE: { bar: 'baz' }, - EXPECTED_CUSTOM_RESPONSE_VALUE: { foo: 'bar' }, - EXPECTED_CUSTOM_ERROR_MESSAGE: 'An error has occurred', - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - EXPECTED_CUSTOM_SUBSEGMENT_NAME: 'mySubsegment', -}; +const EXPECTED_ANNOTATION_KEY = 'myAnnotation'; +const EXPECTED_ANNOTATION_VALUE = 'myValue'; +const EXPECTED_METADATA_KEY = 'myMetadata'; +const EXPECTED_METADATA_VALUE = { bar: 'baz' }; +const EXPECTED_RESPONSE_VALUE = { foo: 'bar' }; +const EXPECTED_ERROR_MESSAGE = 'An error has occurred'; +const EXPECTED_SUBSEGMENT_NAME = '### mySubsegment'; export { RESOURCE_NAME_PREFIX, @@ -26,5 +21,11 @@ export { TEST_CASE_TIMEOUT, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, - commonEnvironmentVars, + EXPECTED_ANNOTATION_KEY, + EXPECTED_ANNOTATION_VALUE, + EXPECTED_METADATA_KEY, + EXPECTED_METADATA_VALUE, + EXPECTED_RESPONSE_VALUE, + EXPECTED_ERROR_MESSAGE, + EXPECTED_SUBSEGMENT_NAME, }; diff --git a/packages/tracer/tests/e2e/decorator.test.functionCode.ts b/packages/tracer/tests/e2e/decorator.test.functionCode.ts new file mode 100644 index 0000000000..678d256058 --- /dev/null +++ b/packages/tracer/tests/e2e/decorator.test.functionCode.ts @@ -0,0 +1,81 @@ +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { + DynamoDBDocumentClient, + PutCommand, + type PutCommandOutput, +} from '@aws-sdk/lib-dynamodb'; +import type { Context } from 'aws-lambda'; +import { Tracer } from '../../src/index.js'; +import { httpRequest } from '../helpers/httpRequest.js'; +import { + EXPECTED_ANNOTATION_KEY as customAnnotationKey, + EXPECTED_ANNOTATION_VALUE as customAnnotationValue, + EXPECTED_ERROR_MESSAGE as customErrorMessage, + EXPECTED_METADATA_KEY as customMetadataKey, + EXPECTED_METADATA_VALUE as customMetadataValue, + EXPECTED_RESPONSE_VALUE as customResponseValue, + EXPECTED_SUBSEGMENT_NAME as customSubSegmentName, +} from './constants.js'; + +type CustomEvent = { + throw: boolean; + invocation: number; +}; + +const tracer = new Tracer(); +const dynamoDB = tracer.captureAWSv3Client( + DynamoDBDocumentClient.from(new DynamoDBClient({})) +); + +export class LambdaFunction { + private readonly returnValue: Record; + + public constructor() { + this.returnValue = customResponseValue; + } + + @tracer.captureLambdaHandler() + public async handler( + event: CustomEvent, + _context: Context + ): Promise { + tracer.putAnnotation(customAnnotationKey, customAnnotationValue); + tracer.putMetadata(customMetadataKey, customMetadataValue); + + await this.methodNoResponse(event.invocation); + await httpRequest({ + hostname: 'docs.powertools.aws.dev', + path: '/lambda/typescript/latest/', + }); + + const res = this.myMethod(); + + if (event.throw) { + throw new Error(customErrorMessage); + } + + return res; + } + + @tracer.captureMethod({ subSegmentName: customSubSegmentName }) + public myMethod(): Record { + return this.returnValue; + } + + @tracer.captureMethod({ captureResponse: false }) + private async methodNoResponse( + invocationIdx: number + ): Promise { + return await dynamoDB.send( + new PutCommand({ + TableName: process.env.TEST_TABLE_NAME ?? 'TestTable', + Item: { + id: `${process.env.POWERTOOLS_SERVICE_NAME ?? 'service'}-${invocationIdx}-sdkv3`, + }, + }) + ); + } +} + +const lambda = new LambdaFunction(); +export const handler = lambda.handler.bind(lambda); diff --git a/packages/tracer/tests/e2e/decorator.test.ts b/packages/tracer/tests/e2e/decorator.test.ts new file mode 100644 index 0000000000..4cfe3fa862 --- /dev/null +++ b/packages/tracer/tests/e2e/decorator.test.ts @@ -0,0 +1,187 @@ +/** + * Test tracer when using the decorator instrumentation + * + * @group e2e/tracer/decorator + */ +import { join } from 'node:path'; +import { TestStack } from '@aws-lambda-powertools/testing-utils'; +import { TestDynamodbTable } from '@aws-lambda-powertools/testing-utils/resources/dynamodb'; +import { TestNodejsFunction } from '@aws-lambda-powertools/testing-utils/resources/lambda'; +import { getTraces } from '@aws-lambda-powertools/testing-utils/utils/xray-traces'; +import type { EnrichedXRayTraceDocumentParsed } from 'packages/testing/lib/cjs/types.js'; +import { invokeAllTestCases } from '../helpers/invokeAllTests.js'; +import { + RESOURCE_NAME_PREFIX, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, + EXPECTED_ANNOTATION_KEY as expectedCustomAnnotationKey, + EXPECTED_ANNOTATION_VALUE as expectedCustomAnnotationValue, + EXPECTED_ERROR_MESSAGE as expectedCustomErrorMessage, + EXPECTED_METADATA_KEY as expectedCustomMetadataKey, + EXPECTED_METADATA_VALUE as expectedCustomMetadataValue, + EXPECTED_RESPONSE_VALUE as expectedCustomResponseValue, + EXPECTED_SUBSEGMENT_NAME as expectedCustomSubSegmentName, +} from './constants.js'; + +describe('Tracer E2E tests, decorator instrumentation', () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'Decorator', + }, + }); + + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'decorator.test.functionCode.ts' + ); + const startTime = new Date(); + + const testTable = new TestDynamodbTable( + testStack, + {}, + { + nameSuffix: 'TestTable', + } + ); + + const fnDecorator = new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_SERVICE_NAME: 'Decorator', + }, + }, + { + nameSuffix: 'Decorator', + } + ); + testTable.grantWriteData(fnDecorator); + + const invocationCount = 3; + let traceData: EnrichedXRayTraceDocumentParsed[] = []; + + beforeAll(async () => { + // Deploy the stack + await testStack.deploy(); + + // Get the actual function names from the stack outputs + const fnNameDecorator = testStack.findAndGetStackOutputValue('Decorator'); + + // Act + await invokeAllTestCases(fnNameDecorator, invocationCount); + traceData = await getTraces({ + startTime, + resourceName: fnNameDecorator, + expectedTracesCount: invocationCount, + /** + * The trace should have 4 segments: + * 1. Lambda Context (AWS::Lambda) + * 2. Lambda Function (AWS::Lambda::Function) + * 4. DynamoDB (AWS::DynamoDB) + * 4. Remote call (docs.powertools.aws.dev) + */ + expectedSegmentsCount: 4, + }); + }, SETUP_TIMEOUT); + + afterAll(async () => { + if (!process.env.DISABLE_TEARDOWN) { + await testStack.destroy(); + } + }, TEARDOWN_TIMEOUT); + + it( + 'should generate all trace data correctly', + async () => { + // Assess + for (let i = 0; i < invocationCount; i++) { + const isColdStart = i === 0; // First invocation is a cold start + const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture + const mainSubsegment = traceData[i]; + const { subsegments, annotations, metadata } = mainSubsegment; + + // Check the main segment name + expect(mainSubsegment.name).toBe('## index.handler'); + + // Check the subsegments of the main segment + expect(subsegments.size).toBe(3); + + // Check remote call subsegment + expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); + const httpSubsegment = subsegments.get('docs.powertools.aws.dev'); + expect(httpSubsegment?.namespace).toBe('remote'); + expect(httpSubsegment?.http?.request?.url).toEqual( + 'https://docs.powertools.aws.dev/lambda/typescript/latest/' + ); + expect(httpSubsegment?.http?.request?.method).toBe('GET'); + expect(httpSubsegment?.http?.response?.status).toEqual( + expect.any(Number) + ); + expect(httpSubsegment?.http?.response?.status).toEqual( + expect.any(Number) + ); + + // Check the custom subsegment name & metadata + expect(subsegments.has(expectedCustomSubSegmentName)).toBe(true); + expect( + subsegments.get(expectedCustomSubSegmentName)?.metadata + ).toStrictEqual({ + Decorator: { + 'myMethod response': expectedCustomResponseValue, + }, + }); + + // Check the other custom subsegment and its subsegments + expect(subsegments.has('### methodNoResponse')).toBe(true); + expect( + subsegments.get('### methodNoResponse')?.metadata + ).toBeUndefined(); + expect( + subsegments.get('### methodNoResponse')?.subsegments?.length + ).toBe(1); + expect( + subsegments.get('### methodNoResponse')?.subsegments?.[0]?.name === + 'DynamoDB' + ).toBe(true); + + // Check the annotations of the main segment + if (!annotations) { + throw new Error('No annotations found on the main segment'); + } + expect(annotations.ColdStart).toEqual(isColdStart); + expect(annotations.Service).toEqual('Decorator'); + expect(annotations[expectedCustomAnnotationKey]).toEqual( + expectedCustomAnnotationValue + ); + + // Check the metadata of the main segment + if (!metadata) { + throw new Error('No metadata found on the main segment'); + } + expect(metadata.Decorator[expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); + + // Check the error recording (only on invocations that should throw) + if (shouldThrowAnError) { + expect(mainSubsegment.fault).toBe(true); + expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(true); + expect(mainSubsegment.cause?.exceptions[0].message).toBe( + expectedCustomErrorMessage + ); + // Check the response in the metadata (only on invocations that DON'T throw) + } else { + expect(metadata.Decorator['index.handler response']).toEqual( + expectedCustomResponseValue + ); + } + } + }, + TEST_CASE_TIMEOUT + ); +}); diff --git a/packages/tracer/tests/e2e/manual.test.functionCode.ts b/packages/tracer/tests/e2e/manual.test.functionCode.ts new file mode 100644 index 0000000000..5ec65c6946 --- /dev/null +++ b/packages/tracer/tests/e2e/manual.test.functionCode.ts @@ -0,0 +1,54 @@ +import type { Subsegment } from 'aws-xray-sdk-core'; +import { Tracer } from '../../src/index.js'; +import { + EXPECTED_ANNOTATION_KEY as customAnnotationKey, + EXPECTED_ANNOTATION_VALUE as customAnnotationValue, + EXPECTED_ERROR_MESSAGE as customErrorMessage, + EXPECTED_METADATA_KEY as customMetadataKey, + EXPECTED_METADATA_VALUE as customMetadataValue, + EXPECTED_RESPONSE_VALUE as customResponseValue, +} from './constants.js'; + +type CustomEvent = { + throw: boolean; + invocation: number; +}; + +const tracer = new Tracer({ captureHTTPsRequests: false }); + +export const handler = async ( + event: CustomEvent +): Promise> => { + const segment = tracer.getSegment(); + let subsegment: Subsegment | undefined; + if (segment) { + subsegment = segment.addNewSubsegment(`## ${process.env._HANDLER}`); + tracer.setSegment(subsegment); + } + + tracer.annotateColdStart(); + tracer.addServiceNameAnnotation(); + + tracer.putAnnotation(customAnnotationKey, customAnnotationValue); + tracer.putMetadata(customMetadataKey, customMetadataValue); + + try { + await fetch('https://docs.powertools.aws.dev/lambda/typescript/latest/'); + + const res = customResponseValue; + if (event.throw) { + throw new Error(customErrorMessage); + } + tracer.addResponseAsMetadata(res, process.env._HANDLER); + + return res; + } catch (err) { + tracer.addErrorAsMetadata(err as Error); + throw err; + } finally { + if (segment && subsegment) { + subsegment.close(); + tracer.setSegment(segment); + } + } +}; diff --git a/packages/tracer/tests/e2e/allFeatures.manual.test.ts b/packages/tracer/tests/e2e/manual.test.ts similarity index 55% rename from packages/tracer/tests/e2e/allFeatures.manual.test.ts rename to packages/tracer/tests/e2e/manual.test.ts index dab6be98b5..54c9259126 100644 --- a/packages/tracer/tests/e2e/allFeatures.manual.test.ts +++ b/packages/tracer/tests/e2e/manual.test.ts @@ -1,40 +1,43 @@ /** - * Test tracer manual mode + * Test tracer when instrumenting the lambda function manually * * @group e2e/tracer/manual */ import { join } from 'node:path'; import { TestStack } from '@aws-lambda-powertools/testing-utils'; import { TestDynamodbTable } from '@aws-lambda-powertools/testing-utils/resources/dynamodb'; +import { TestNodejsFunction } from '@aws-lambda-powertools/testing-utils/resources/lambda'; import { getTraces } from '@aws-lambda-powertools/testing-utils/utils/xray-traces'; +import type { EnrichedXRayTraceDocumentParsed } from 'packages/testing/lib/cjs/types.js'; import { invokeAllTestCases } from '../helpers/invokeAllTests.js'; -import { TracerTestNodejsFunction } from '../helpers/resources.js'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, - commonEnvironmentVars, + EXPECTED_ANNOTATION_KEY as expectedCustomAnnotationKey, + EXPECTED_ANNOTATION_VALUE as expectedCustomAnnotationValue, + EXPECTED_ERROR_MESSAGE as expectedCustomErrorMessage, + EXPECTED_METADATA_KEY as expectedCustomMetadataKey, + EXPECTED_METADATA_VALUE as expectedCustomMetadataValue, + EXPECTED_RESPONSE_VALUE as expectedCustomResponseValue, } from './constants.js'; -describe('Tracer E2E tests, all features with manual instantiation', () => { +describe('Tracer E2E tests, manual instantiation', () => { const testStack = new TestStack({ stackNameProps: { stackNamePrefix: RESOURCE_NAME_PREFIX, - testName: 'AllFeatures-Manual', + testName: 'Manual', }, }); // Location of the lambda function code const lambdaFunctionCodeFilePath = join( __dirname, - 'allFeatures.manual.test.functionCode.ts' + 'manual.test.functionCode.ts' ); const startTime = new Date(); - /** - * Table used by all functions to make an SDK call - */ const testTable = new TestDynamodbTable( testStack, {}, @@ -43,33 +46,44 @@ describe('Tracer E2E tests, all features with manual instantiation', () => { } ); - let fnNameAllFlagsEnabled: string; - const fnAllFlagsEnabled = new TracerTestNodejsFunction( + const fnManual = new TestNodejsFunction( testStack, { entry: lambdaFunctionCodeFilePath, environment: { TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_SERVICE_NAME: 'Manual', }, }, { - nameSuffix: 'AllFlagsManual', + nameSuffix: 'Manual', } ); - testTable.grantWriteData(fnAllFlagsEnabled); + testTable.grantWriteData(fnManual); const invocationCount = 3; + let traceData: EnrichedXRayTraceDocumentParsed[] = []; beforeAll(async () => { // Deploy the stack await testStack.deploy(); // Get the actual function names from the stack outputs - fnNameAllFlagsEnabled = - testStack.findAndGetStackOutputValue('AllFlagsManual'); + const fnNameManual = testStack.findAndGetStackOutputValue('Manual'); // Invoke all test cases - await invokeAllTestCases(fnNameAllFlagsEnabled, invocationCount); + await invokeAllTestCases(fnNameManual, invocationCount); + traceData = await getTraces({ + startTime, + resourceName: fnNameManual, + expectedTracesCount: invocationCount, + /** + * The trace should have 2 segments: + * 1. Lambda Context (AWS::Lambda) + * 2. Lambda Function (AWS::Lambda::Function) + */ + expectedSegmentsCount: 2, + }); }, SETUP_TIMEOUT); afterAll(async () => { @@ -79,62 +93,36 @@ describe('Tracer E2E tests, all features with manual instantiation', () => { }, TEARDOWN_TIMEOUT); it( - 'should generate all custom traces with correct subsegments, annotations, and metadata', + 'should generate all trace data correctly', async () => { - const { - EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage, - EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, - EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, - EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, - EXPECTED_CUSTOM_METADATA_VALUE: expectedCustomMetadataValue, - EXPECTED_CUSTOM_RESPONSE_VALUE: expectedCustomResponseValue, - } = commonEnvironmentVars; - const serviceName = 'AllFlagsManual'; - - const mainSubsegments = await getTraces({ - startTime, - resourceName: fnNameAllFlagsEnabled, - expectedTracesCount: invocationCount, - /** - * The trace should have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 4. DynamoDB (AWS::DynamoDB) - * 4. Remote call (docs.powertools.aws.dev) - */ - expectedSegmentsCount: 4, - }); - // Assess for (let i = 0; i < invocationCount; i++) { const isColdStart = i === 0; // First invocation is a cold start const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture - const mainSubsegment = mainSubsegments[i]; + const mainSubsegment = traceData[i]; const { subsegments, annotations, metadata } = mainSubsegment; // Check the main segment name expect(mainSubsegment.name).toBe('## index.handler'); - // Check the subsegments - expect(subsegments.size).toBe(2); - expect(subsegments.has('DynamoDB')).toBe(true); - expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); + // Since CaptureHTTPsRequests is disabled, we should not have any subsegments + expect(subsegments.size).toBe(0); - // Check the annotations + // Check the annotations of the main segment if (!annotations) { throw new Error('No annotations found on the main segment'); } expect(annotations.ColdStart).toEqual(isColdStart); - expect(annotations.Service).toEqual(serviceName); + expect(annotations.Service).toEqual('Manual'); expect(annotations[expectedCustomAnnotationKey]).toEqual( expectedCustomAnnotationValue ); - // Check the metadata + // Check the metadata of the main segment if (!metadata) { throw new Error('No metadata found on the main segment'); } - expect(metadata[serviceName][expectedCustomMetadataKey]).toEqual( + expect(metadata.Manual?.[expectedCustomMetadataKey]).toEqual( expectedCustomMetadataValue ); @@ -147,7 +135,7 @@ describe('Tracer E2E tests, all features with manual instantiation', () => { ); // Check the response in the metadata (only on invocations that DON'T throw) } else { - expect(metadata[serviceName]['index.handler response']).toEqual( + expect(metadata.Manual?.['index.handler response']).toEqual( expectedCustomResponseValue ); } diff --git a/packages/tracer/tests/e2e/middy.test.functionCode.ts b/packages/tracer/tests/e2e/middy.test.functionCode.ts new file mode 100644 index 0000000000..cff6e205f4 --- /dev/null +++ b/packages/tracer/tests/e2e/middy.test.functionCode.ts @@ -0,0 +1,45 @@ +import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb'; +import type { Context } from 'aws-lambda'; +import middy from 'middy5'; +import { Tracer } from '../../src/index.js'; +import { captureLambdaHandler } from '../../src/middleware/middy.js'; +import { + EXPECTED_ANNOTATION_KEY as customAnnotationKey, + EXPECTED_ANNOTATION_VALUE as customAnnotationValue, + EXPECTED_ERROR_MESSAGE as customErrorMessage, + EXPECTED_METADATA_KEY as customMetadataKey, + EXPECTED_METADATA_VALUE as customMetadataValue, +} from './constants.js'; + +type CustomEvent = { + throw: boolean; + invocation: number; +}; + +const tracer = new Tracer(); +const dynamoDB = tracer.captureAWSv3Client(new DynamoDBClient({})); + +export const handler = middy( + async (event: CustomEvent, _context: Context): Promise => { + tracer.putAnnotation(customAnnotationKey, customAnnotationValue); + tracer.putMetadata(customMetadataKey, customMetadataValue); + + await dynamoDB.send( + new PutItemCommand({ + TableName: process.env.TEST_TABLE_NAME ?? 'TestTable', + Item: { + id: { + S: `${process.env.POWERTOOLS_SERVICE_NAME ?? 'service'}-${event.invocation}-sdkv3`, + }, + }, + }) + ); + await fetch('https://docs.powertools.aws.dev/lambda/typescript/latest/'); + + if (event.throw) { + throw new Error(customErrorMessage); + } + + return 'success'; + } +).use(captureLambdaHandler(tracer, { captureResponse: false })); diff --git a/packages/tracer/tests/e2e/middy.test.ts b/packages/tracer/tests/e2e/middy.test.ts new file mode 100644 index 0000000000..bb9a2f32ae --- /dev/null +++ b/packages/tracer/tests/e2e/middy.test.ts @@ -0,0 +1,162 @@ +/** + * Test tracer when using the Middy.js instrumentation + * + * @group e2e/tracer/middy + */ +import { join } from 'node:path'; +import { TestStack } from '@aws-lambda-powertools/testing-utils'; +import { TestDynamodbTable } from '@aws-lambda-powertools/testing-utils/resources/dynamodb'; +import { TestNodejsFunction } from '@aws-lambda-powertools/testing-utils/resources/lambda'; +import { getTraces } from '@aws-lambda-powertools/testing-utils/utils/xray-traces'; +import type { EnrichedXRayTraceDocumentParsed } from 'packages/testing/lib/cjs/types.js'; +import { invokeAllTestCases } from '../helpers/invokeAllTests.js'; +import { + RESOURCE_NAME_PREFIX, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, + EXPECTED_ANNOTATION_KEY as expectedCustomAnnotationKey, + EXPECTED_ANNOTATION_VALUE as expectedCustomAnnotationValue, + EXPECTED_ERROR_MESSAGE as expectedCustomErrorMessage, + EXPECTED_METADATA_KEY as expectedCustomMetadataKey, + EXPECTED_METADATA_VALUE as expectedCustomMetadataValue, +} from './constants.js'; + +describe('Tracer E2E tests, middy instrumentation', () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'Middy', + }, + }); + + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'middy.test.functionCode.ts' + ); + const startTime = new Date(); + + const testTable = new TestDynamodbTable( + testStack, + {}, + { + nameSuffix: 'TestTable', + } + ); + + const fnMiddy = new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_SERVICE_NAME: 'Middy', + }, + }, + { + nameSuffix: 'Middy', + outputFormat: 'ESM', + } + ); + testTable.grantWriteData(fnMiddy); + + const invocationCount = 3; + let traceData: EnrichedXRayTraceDocumentParsed[] = []; + + beforeAll(async () => { + // Deploy the stack + await testStack.deploy(); + + // Get the actual function names from the stack outputs + const fnNameMiddy = testStack.findAndGetStackOutputValue('Middy'); + + // Invoke all functions + await invokeAllTestCases(fnNameMiddy, invocationCount); + traceData = await getTraces({ + startTime, + resourceName: fnNameMiddy, + expectedTracesCount: invocationCount, + /** + * The trace should have 4 segments: + * 1. Lambda Context (AWS::Lambda) + * 2. Lambda Function (AWS::Lambda::Function) + * 4. DynamoDB (AWS::DynamoDB) + * 4. Remote call (docs.powertools.aws.dev) + */ + expectedSegmentsCount: 4, + }); + }, SETUP_TIMEOUT); + + afterAll(async () => { + if (!process.env.DISABLE_TEARDOWN) { + await testStack.destroy(); + } + }, TEARDOWN_TIMEOUT); + + it( + 'should generate all trace data correctly', + async () => { + // Assess + for (let i = 0; i < invocationCount; i++) { + const isColdStart = i === 0; // First invocation is a cold start + const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture + const mainSubsegment = traceData[i]; + const { subsegments, annotations, metadata } = mainSubsegment; + + // Check the main segment name + expect(mainSubsegment.name).toBe('## index.handler'); + + // Check the subsegments + expect(subsegments.size).toBe(2); + expect(subsegments.has('DynamoDB')).toBe(true); + + // Check remote call subsegment + expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); + const httpSubsegment = subsegments.get('docs.powertools.aws.dev'); + expect(httpSubsegment?.namespace).toBe('remote'); + expect(httpSubsegment?.http?.request?.url).toEqual( + 'docs.powertools.aws.dev' + ); + expect(httpSubsegment?.http?.request?.method).toBe('GET'); + expect(httpSubsegment?.http?.response?.status).toEqual( + expect.any(Number) + ); + expect(httpSubsegment?.http?.response?.status).toEqual( + expect.any(Number) + ); + + // Check the annotations on the main segment + if (!annotations) { + throw new Error('No annotations found on the main segment'); + } + expect(annotations.ColdStart).toEqual(isColdStart); + expect(annotations.Service).toEqual('Middy'); + expect(annotations[expectedCustomAnnotationKey]).toEqual( + expectedCustomAnnotationValue + ); + + // Check the metadata on the main segment + if (!metadata) { + throw new Error('No metadata found on the main segment'); + } + expect(metadata.Middy?.[expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); + + // Check the error recording (only on invocations that should throw) + if (shouldThrowAnError) { + expect(mainSubsegment.fault).toBe(true); + expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(true); + expect(mainSubsegment.cause?.exceptions[0].message).toBe( + expectedCustomErrorMessage + ); + // Capture response is disabled, so no response should be recorded + } else { + expect(metadata.Middy?.['index.handler response']).toBeUndefined(); + } + } + }, + TEST_CASE_TIMEOUT + ); +}); diff --git a/packages/tracer/tests/helpers/resources.ts b/packages/tracer/tests/helpers/resources.ts deleted file mode 100644 index 447ac238ee..0000000000 --- a/packages/tracer/tests/helpers/resources.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { TestStack } from '@aws-lambda-powertools/testing-utils'; -import { TestNodejsFunction } from '@aws-lambda-powertools/testing-utils/resources/lambda'; -import type { - ExtraTestProps, - TestNodejsFunctionProps, -} from '@aws-lambda-powertools/testing-utils/types'; -import { commonEnvironmentVars } from '../e2e/constants.js'; - -class TracerTestNodejsFunction extends TestNodejsFunction { - public constructor( - scope: TestStack, - props: TestNodejsFunctionProps, - extraProps: ExtraTestProps - ) { - super( - scope, - { - ...props, - environment: { - ...commonEnvironmentVars, - EXPECTED_SERVICE_NAME: extraProps.nameSuffix, - EXPECTED_CUSTOM_METADATA_VALUE: JSON.stringify( - commonEnvironmentVars.EXPECTED_CUSTOM_METADATA_VALUE - ), - EXPECTED_CUSTOM_RESPONSE_VALUE: JSON.stringify( - commonEnvironmentVars.EXPECTED_CUSTOM_RESPONSE_VALUE - ), - ...props.environment, - }, - }, - extraProps - ); - } -} - -export { TracerTestNodejsFunction }; From a4eead1d604ceb87d990f103f72ace781925c981 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 6 Sep 2024 10:36:46 +0200 Subject: [PATCH 2/4] chore: account for not loaded subsegments --- packages/testing/src/types.ts | 4 + packages/testing/src/xray-traces-utils.ts | 99 +++++++++++++++-------- 2 files changed, 70 insertions(+), 33 deletions(-) diff --git a/packages/testing/src/types.ts b/packages/testing/src/types.ts index 28ca556b8c..454c2d87b4 100644 --- a/packages/testing/src/types.ts +++ b/packages/testing/src/types.ts @@ -169,6 +169,10 @@ type GetXRayTraceDetailsOptions = { * The expected number of segments in each trace */ expectedSegmentsCount: number; + /** + * The name of the function that the trace is expected to be associated with + */ + functionName: string; }; /** diff --git a/packages/testing/src/xray-traces-utils.ts b/packages/testing/src/xray-traces-utils.ts index c25a9c2939..d172e40580 100644 --- a/packages/testing/src/xray-traces-utils.ts +++ b/packages/testing/src/xray-traces-utils.ts @@ -93,13 +93,56 @@ const retriableGetTraceIds = (options: GetXRayTraceIdsOptions) => } }, retryOptions); +/** + * Find the main Powertools subsegment in the trace + * + * A main Powertools subsegment is identified by the `## index.` suffix. Depending on the + * runtime, it may also be identified by the `Invocation` name. + * + * @param trace - The trace to find the main Powertools subsegment + * @param functionName - The function name to find the main Powertools subsegment + */ +const findMainPowertoolsSubsegment = ( + trace: XRayTraceDocumentParsed, + functionName: string +) => { + const maybePowertoolsSubsegment = trace.subsegments?.find( + (subsegment) => + subsegment.name.startsWith('## index.') || + subsegment.name === 'Invocation' + ); + + if (!maybePowertoolsSubsegment) { + throw new Error(`Main subsegment not found for ${functionName} segment`); + } + + if (maybePowertoolsSubsegment.name === 'Invocation') { + const powertoolsSubsegment = maybePowertoolsSubsegment.subsegments?.find( + (subsegment) => subsegment.name.startsWith('## index.') + ); + + if (!powertoolsSubsegment) { + throw new Error(`Main subsegment not found for ${functionName} segment`); + } + + return powertoolsSubsegment; + } + + return maybePowertoolsSubsegment; +}; + /** * Parse and sort the trace segments by start time * * @param trace - The trace to parse and sort * @param expectedSegmentsCount - The expected segments count for the trace + * @param functionName - The function name to find the main Powertools subsegment */ -const parseAndSortTrace = (trace: Trace, expectedSegmentsCount: number) => { +const parseAndSortTrace = ( + trace: Trace, + expectedSegmentsCount: number, + functionName: string +) => { const { Id: id, Segments: segments } = trace; if (segments === undefined || segments.length !== expectedSegmentsCount) { throw new Error( @@ -116,9 +159,14 @@ const parseAndSortTrace = (trace: Trace, expectedSegmentsCount: number) => { ); } + const parsedDocument = JSON.parse(Document) as XRayTraceDocumentParsed; + if (parsedDocument.origin === 'AWS::Lambda::Function') { + findMainPowertoolsSubsegment(parsedDocument, functionName); + } + parsedSegments.push({ Id, - Document: JSON.parse(Document) as XRayTraceDocumentParsed, + Document: parsedDocument, }); } @@ -141,15 +189,14 @@ const parseAndSortTrace = (trace: Trace, expectedSegmentsCount: number) => { const getTraceDetails = async ( options: GetXRayTraceDetailsOptions ): Promise => { - const { traceIds, expectedSegmentsCount } = options; + const { traceIds, expectedSegmentsCount, functionName } = options; const response = await xrayClient.send( new BatchGetTracesCommand({ TraceIds: traceIds, }) ); - const traces = response.Traces; - + const { Traces: traces } = response; if (traces === undefined || traces.length !== traceIds.length) { throw new Error( `Expected ${traceIds.length} traces, got ${traces ? traces.length : 0}` @@ -158,7 +205,9 @@ const getTraceDetails = async ( const parsedAndSortedTraces: XRayTraceParsed[] = []; for (const trace of traces) { - parsedAndSortedTraces.push(parseAndSortTrace(trace, expectedSegmentsCount)); + parsedAndSortedTraces.push( + parseAndSortTrace(trace, expectedSegmentsCount, functionName) + ); } return parsedAndSortedTraces.sort( @@ -194,7 +243,7 @@ const retriableGetTraceDetails = (options: GetXRayTraceDetailsOptions) => }, retryOptions); /** - * Find the main function segment in the trace identified by the `## index.` suffix + * Find the main function segment within the `AWS::Lambda::Function` segment */ const findPowertoolsFunctionSegment = ( trace: XRayTraceParsed, @@ -211,30 +260,7 @@ const findPowertoolsFunctionSegment = ( } const document = functionSegment.Document; - - const maybePowertoolsSubsegment = document.subsegments?.find( - (subsegment) => - subsegment.name.startsWith('## index.') || - subsegment.name === 'Invocation' - ); - - if (!maybePowertoolsSubsegment) { - throw new Error(`Main subsegment not found for ${functionName} segment`); - } - - if (maybePowertoolsSubsegment.name === 'Invocation') { - const powertoolsSubsegment = maybePowertoolsSubsegment.subsegments?.find( - (subsegment) => subsegment.name.startsWith('## index.') - ); - - if (!powertoolsSubsegment) { - throw new Error(`Main subsegment not found for ${functionName} segment`); - } - - return powertoolsSubsegment; - } - - return maybePowertoolsSubsegment; + return findMainPowertoolsSubsegment(document, functionName); }; /** @@ -288,6 +314,7 @@ const getXRayTraceData = async ( const traces = await retriableGetTraceDetails({ traceIds, expectedSegmentsCount, + functionName: resourceName, }); if (!traces) { @@ -303,9 +330,15 @@ const getXRayTraceData = async ( * @param options - The options to get the X-Ray trace data, including the start time, resource name, expected traces count, and expected segments count */ const getTraces = async ( - options: GetXRayTraceIdsOptions & Omit + options: GetXRayTraceIdsOptions & + Omit & { + resourceName: string; + } ): Promise => { - const traces = await getXRayTraceData(options); + const traces = await getXRayTraceData({ + ...options, + functionName: options.resourceName, + }); const { resourceName } = options; From 22012b8fe49269e189f75028988105603e594468 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 9 Sep 2024 16:31:59 +0200 Subject: [PATCH 3/4] chore: refactor out for loop in tests --- packages/tracer/tests/e2e/decorator.test.ts | 167 +++++++++--------- packages/tracer/tests/e2e/manual.test.ts | 93 +++++----- packages/tracer/tests/e2e/middy.test.ts | 142 ++++++++------- .../tracer/tests/helpers/invokeAllTests.ts | 4 - 4 files changed, 210 insertions(+), 196 deletions(-) diff --git a/packages/tracer/tests/e2e/decorator.test.ts b/packages/tracer/tests/e2e/decorator.test.ts index 4cfe3fa862..2055e9ea7e 100644 --- a/packages/tracer/tests/e2e/decorator.test.ts +++ b/packages/tracer/tests/e2e/decorator.test.ts @@ -62,7 +62,7 @@ describe('Tracer E2E tests, decorator instrumentation', () => { ); testTable.grantWriteData(fnDecorator); - const invocationCount = 3; + const invocationCount = 2; let traceData: EnrichedXRayTraceDocumentParsed[] = []; beforeAll(async () => { @@ -99,89 +99,92 @@ describe('Tracer E2E tests, decorator instrumentation', () => { 'should generate all trace data correctly', async () => { // Assess - for (let i = 0; i < invocationCount; i++) { - const isColdStart = i === 0; // First invocation is a cold start - const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture - const mainSubsegment = traceData[i]; - const { subsegments, annotations, metadata } = mainSubsegment; - - // Check the main segment name - expect(mainSubsegment.name).toBe('## index.handler'); - - // Check the subsegments of the main segment - expect(subsegments.size).toBe(3); - - // Check remote call subsegment - expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); - const httpSubsegment = subsegments.get('docs.powertools.aws.dev'); - expect(httpSubsegment?.namespace).toBe('remote'); - expect(httpSubsegment?.http?.request?.url).toEqual( - 'https://docs.powertools.aws.dev/lambda/typescript/latest/' - ); - expect(httpSubsegment?.http?.request?.method).toBe('GET'); - expect(httpSubsegment?.http?.response?.status).toEqual( - expect.any(Number) - ); - expect(httpSubsegment?.http?.response?.status).toEqual( - expect.any(Number) - ); - - // Check the custom subsegment name & metadata - expect(subsegments.has(expectedCustomSubSegmentName)).toBe(true); - expect( - subsegments.get(expectedCustomSubSegmentName)?.metadata - ).toStrictEqual({ - Decorator: { - 'myMethod response': expectedCustomResponseValue, - }, - }); - - // Check the other custom subsegment and its subsegments - expect(subsegments.has('### methodNoResponse')).toBe(true); - expect( - subsegments.get('### methodNoResponse')?.metadata - ).toBeUndefined(); - expect( - subsegments.get('### methodNoResponse')?.subsegments?.length - ).toBe(1); - expect( - subsegments.get('### methodNoResponse')?.subsegments?.[0]?.name === - 'DynamoDB' - ).toBe(true); - - // Check the annotations of the main segment - if (!annotations) { - throw new Error('No annotations found on the main segment'); - } - expect(annotations.ColdStart).toEqual(isColdStart); - expect(annotations.Service).toEqual('Decorator'); - expect(annotations[expectedCustomAnnotationKey]).toEqual( - expectedCustomAnnotationValue - ); - - // Check the metadata of the main segment - if (!metadata) { - throw new Error('No metadata found on the main segment'); - } - expect(metadata.Decorator[expectedCustomMetadataKey]).toEqual( - expectedCustomMetadataValue - ); - - // Check the error recording (only on invocations that should throw) - if (shouldThrowAnError) { - expect(mainSubsegment.fault).toBe(true); - expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(true); - expect(mainSubsegment.cause?.exceptions[0].message).toBe( - expectedCustomErrorMessage - ); - // Check the response in the metadata (only on invocations that DON'T throw) - } else { - expect(metadata.Decorator['index.handler response']).toEqual( - expectedCustomResponseValue - ); - } + const mainSubsegment = traceData[0]; + const { subsegments, annotations, metadata } = mainSubsegment; + + // Check the main segment name + expect(mainSubsegment.name).toBe('## index.handler'); + + // Check the subsegments of the main segment + expect(subsegments.size).toBe(3); + + // Check remote call subsegment + expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); + const httpSubsegment = subsegments.get('docs.powertools.aws.dev'); + expect(httpSubsegment?.namespace).toBe('remote'); + expect(httpSubsegment?.http?.request?.url).toEqual( + 'https://docs.powertools.aws.dev/lambda/typescript/latest/' + ); + expect(httpSubsegment?.http?.request?.method).toBe('GET'); + expect(httpSubsegment?.http?.response?.status).toEqual( + expect.any(Number) + ); + expect(httpSubsegment?.http?.response?.status).toEqual( + expect.any(Number) + ); + + // Check the custom subsegment name & metadata + expect(subsegments.has(expectedCustomSubSegmentName)).toBe(true); + expect( + subsegments.get(expectedCustomSubSegmentName)?.metadata + ).toStrictEqual({ + Decorator: { + 'myMethod response': expectedCustomResponseValue, + }, + }); + + // Check the other custom subsegment and its subsegments + expect(subsegments.has('### methodNoResponse')).toBe(true); + expect(subsegments.get('### methodNoResponse')?.metadata).toBeUndefined(); + expect(subsegments.get('### methodNoResponse')?.subsegments?.length).toBe( + 1 + ); + expect( + subsegments.get('### methodNoResponse')?.subsegments?.[0]?.name === + 'DynamoDB' + ).toBe(true); + + // Check the annotations of the main segment + if (!annotations) { + throw new Error('No annotations found on the main segment'); } + expect(annotations.ColdStart).toEqual(true); + expect(annotations.Service).toEqual('Decorator'); + expect(annotations[expectedCustomAnnotationKey]).toEqual( + expectedCustomAnnotationValue + ); + + // Check the metadata of the main segment + if (!metadata) { + throw new Error('No metadata found on the main segment'); + } + expect(metadata.Decorator[expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); + + // Check the response is present in the metadata + expect(metadata.Decorator['index.handler response']).toEqual( + expectedCustomResponseValue + ); }, TEST_CASE_TIMEOUT ); + + it('should annotate the trace with error data correctly', () => { + const mainSubsegment = traceData[1]; + const { annotations } = mainSubsegment; + + // Check the annotations of the main segment + if (!annotations) { + throw new Error('No annotations found on the main segment'); + } + expect(annotations.ColdStart).toEqual(false); + + // Check that the main segment has error data + expect(mainSubsegment.fault).toBe(true); + expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(true); + expect(mainSubsegment.cause?.exceptions[0].message).toBe( + expectedCustomErrorMessage + ); + }); }); diff --git a/packages/tracer/tests/e2e/manual.test.ts b/packages/tracer/tests/e2e/manual.test.ts index 54c9259126..4a809f1164 100644 --- a/packages/tracer/tests/e2e/manual.test.ts +++ b/packages/tracer/tests/e2e/manual.test.ts @@ -61,7 +61,7 @@ describe('Tracer E2E tests, manual instantiation', () => { ); testTable.grantWriteData(fnManual); - const invocationCount = 3; + const invocationCount = 2; let traceData: EnrichedXRayTraceDocumentParsed[] = []; beforeAll(async () => { @@ -96,51 +96,56 @@ describe('Tracer E2E tests, manual instantiation', () => { 'should generate all trace data correctly', async () => { // Assess - for (let i = 0; i < invocationCount; i++) { - const isColdStart = i === 0; // First invocation is a cold start - const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture - const mainSubsegment = traceData[i]; - const { subsegments, annotations, metadata } = mainSubsegment; - - // Check the main segment name - expect(mainSubsegment.name).toBe('## index.handler'); - - // Since CaptureHTTPsRequests is disabled, we should not have any subsegments - expect(subsegments.size).toBe(0); - - // Check the annotations of the main segment - if (!annotations) { - throw new Error('No annotations found on the main segment'); - } - expect(annotations.ColdStart).toEqual(isColdStart); - expect(annotations.Service).toEqual('Manual'); - expect(annotations[expectedCustomAnnotationKey]).toEqual( - expectedCustomAnnotationValue - ); - - // Check the metadata of the main segment - if (!metadata) { - throw new Error('No metadata found on the main segment'); - } - expect(metadata.Manual?.[expectedCustomMetadataKey]).toEqual( - expectedCustomMetadataValue - ); - - // Check the error recording (only on invocations that should throw) - if (shouldThrowAnError) { - expect(mainSubsegment.fault).toBe(true); - expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(true); - expect(mainSubsegment.cause?.exceptions[0].message).toBe( - expectedCustomErrorMessage - ); - // Check the response in the metadata (only on invocations that DON'T throw) - } else { - expect(metadata.Manual?.['index.handler response']).toEqual( - expectedCustomResponseValue - ); - } + const mainSubsegment = traceData[0]; + const { subsegments, annotations, metadata } = mainSubsegment; + + // Check the main segment name + expect(mainSubsegment.name).toBe('## index.handler'); + + // Since CaptureHTTPsRequests is disabled, we should not have any subsegments + expect(subsegments.size).toBe(0); + + // Check the annotations of the main segment + if (!annotations) { + throw new Error('No annotations found on the main segment'); } + expect(annotations.ColdStart).toEqual(true); + expect(annotations.Service).toEqual('Manual'); + expect(annotations[expectedCustomAnnotationKey]).toEqual( + expectedCustomAnnotationValue + ); + + // Check the metadata of the main segment + if (!metadata) { + throw new Error('No metadata found on the main segment'); + } + expect(metadata.Manual?.[expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); + + // Check the response is present in the metadata + expect(metadata.Manual?.['index.handler response']).toEqual( + expectedCustomResponseValue + ); }, TEST_CASE_TIMEOUT ); + + it('should annotate the trace with error data correctly', () => { + const mainSubsegment = traceData[1]; + const { annotations } = mainSubsegment; + + // Check the annotations of the main segment + if (!annotations) { + throw new Error('No annotations found on the main segment'); + } + expect(annotations.ColdStart).toEqual(false); + + // Check that the main segment has error data + expect(mainSubsegment.fault).toBe(true); + expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(true); + expect(mainSubsegment.cause?.exceptions[0].message).toBe( + expectedCustomErrorMessage + ); + }); }); diff --git a/packages/tracer/tests/e2e/middy.test.ts b/packages/tracer/tests/e2e/middy.test.ts index bb9a2f32ae..19e380c81e 100644 --- a/packages/tracer/tests/e2e/middy.test.ts +++ b/packages/tracer/tests/e2e/middy.test.ts @@ -61,7 +61,7 @@ describe('Tracer E2E tests, middy instrumentation', () => { ); testTable.grantWriteData(fnMiddy); - const invocationCount = 3; + const invocationCount = 2; let traceData: EnrichedXRayTraceDocumentParsed[] = []; beforeAll(async () => { @@ -94,69 +94,79 @@ describe('Tracer E2E tests, middy instrumentation', () => { } }, TEARDOWN_TIMEOUT); - it( - 'should generate all trace data correctly', - async () => { - // Assess - for (let i = 0; i < invocationCount; i++) { - const isColdStart = i === 0; // First invocation is a cold start - const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture - const mainSubsegment = traceData[i]; - const { subsegments, annotations, metadata } = mainSubsegment; - - // Check the main segment name - expect(mainSubsegment.name).toBe('## index.handler'); - - // Check the subsegments - expect(subsegments.size).toBe(2); - expect(subsegments.has('DynamoDB')).toBe(true); - - // Check remote call subsegment - expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); - const httpSubsegment = subsegments.get('docs.powertools.aws.dev'); - expect(httpSubsegment?.namespace).toBe('remote'); - expect(httpSubsegment?.http?.request?.url).toEqual( - 'docs.powertools.aws.dev' - ); - expect(httpSubsegment?.http?.request?.method).toBe('GET'); - expect(httpSubsegment?.http?.response?.status).toEqual( - expect.any(Number) - ); - expect(httpSubsegment?.http?.response?.status).toEqual( - expect.any(Number) - ); - - // Check the annotations on the main segment - if (!annotations) { - throw new Error('No annotations found on the main segment'); - } - expect(annotations.ColdStart).toEqual(isColdStart); - expect(annotations.Service).toEqual('Middy'); - expect(annotations[expectedCustomAnnotationKey]).toEqual( - expectedCustomAnnotationValue - ); - - // Check the metadata on the main segment - if (!metadata) { - throw new Error('No metadata found on the main segment'); - } - expect(metadata.Middy?.[expectedCustomMetadataKey]).toEqual( - expectedCustomMetadataValue - ); - - // Check the error recording (only on invocations that should throw) - if (shouldThrowAnError) { - expect(mainSubsegment.fault).toBe(true); - expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(true); - expect(mainSubsegment.cause?.exceptions[0].message).toBe( - expectedCustomErrorMessage - ); - // Capture response is disabled, so no response should be recorded - } else { - expect(metadata.Middy?.['index.handler response']).toBeUndefined(); - } - } - }, - TEST_CASE_TIMEOUT - ); + it('should generate all trace data correctly', () => { + // Assess + const mainSubsegment = traceData[0]; + const { subsegments, annotations, metadata } = mainSubsegment; + + // Check the main segment name + expect(mainSubsegment.name).toBe('## index.handler'); + + // Check the subsegments + expect(subsegments.size).toBe(2); + expect(subsegments.has('DynamoDB')).toBe(true); + + // Check remote call subsegment + expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); + const httpSubsegment = subsegments.get('docs.powertools.aws.dev'); + expect(httpSubsegment?.namespace).toBe('remote'); + expect(httpSubsegment?.http?.request?.url).toEqual( + 'docs.powertools.aws.dev' + ); + expect(httpSubsegment?.http?.request?.method).toBe('GET'); + expect(httpSubsegment?.http?.response?.status).toEqual(expect.any(Number)); + expect(httpSubsegment?.http?.response?.status).toEqual(expect.any(Number)); + + // Check the annotations on the main segment + if (!annotations) { + throw new Error('No annotations found on the main segment'); + } + expect(annotations.ColdStart).toEqual(true); + expect(annotations.Service).toEqual('Middy'); + expect(annotations[expectedCustomAnnotationKey]).toEqual( + expectedCustomAnnotationValue + ); + + // Check the metadata on the main segment + if (!metadata) { + throw new Error('No metadata found on the main segment'); + } + expect(metadata.Middy?.[expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); + + expect(metadata.Middy?.['index.handler response']).toBeUndefined(); + + // Check the metadata on the main segment + if (!metadata) { + throw new Error('No metadata found on the main segment'); + } + expect(metadata.Middy?.[expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); + + // Check that the response is not present in the metadata because we disabled the feature + expect(metadata.Middy?.['index.handler response']).toBeUndefined(); + }); + + it('should annotate the trace with error data correctly', () => { + const mainSubsegment = traceData[1]; + const { annotations } = mainSubsegment; + + // Check the main segment name + expect(mainSubsegment.name).toBe('## index.handler'); + + // Check the annotations of the main segment + if (!annotations) { + throw new Error('No annotations found on the main segment'); + } + expect(annotations.ColdStart).toEqual(false); + + // Check that the main segment has error data + expect(mainSubsegment.fault).toBe(true); + expect(Object.hasOwn(mainSubsegment, 'cause')).toBe(true); + expect(mainSubsegment.cause?.exceptions[0].message).toBe( + expectedCustomErrorMessage + ); + }); }); diff --git a/packages/tracer/tests/helpers/invokeAllTests.ts b/packages/tracer/tests/helpers/invokeAllTests.ts index 981a326397..696cdceb59 100644 --- a/packages/tracer/tests/helpers/invokeAllTests.ts +++ b/packages/tracer/tests/helpers/invokeAllTests.ts @@ -23,10 +23,6 @@ const invokeAllTestCases = async ( }, { invocation: 2, - throw: false, - }, - { - invocation: 3, throw: true, // only last invocation should throw }, ], From 8a241c142d6bca7edac0da4cc07b3254e6774f68 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 9 Sep 2024 16:42:54 +0200 Subject: [PATCH 4/4] chore: remove test case timeout --- packages/tracer/tests/e2e/decorator.test.ts | 141 +++++++++----------- packages/tracer/tests/e2e/manual.test.ts | 73 +++++----- packages/tracer/tests/e2e/middy.test.ts | 1 - 3 files changed, 100 insertions(+), 115 deletions(-) diff --git a/packages/tracer/tests/e2e/decorator.test.ts b/packages/tracer/tests/e2e/decorator.test.ts index 2055e9ea7e..273464000b 100644 --- a/packages/tracer/tests/e2e/decorator.test.ts +++ b/packages/tracer/tests/e2e/decorator.test.ts @@ -14,7 +14,6 @@ import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, EXPECTED_ANNOTATION_KEY as expectedCustomAnnotationKey, EXPECTED_ANNOTATION_VALUE as expectedCustomAnnotationValue, EXPECTED_ERROR_MESSAGE as expectedCustomErrorMessage, @@ -95,80 +94,72 @@ describe('Tracer E2E tests, decorator instrumentation', () => { } }, TEARDOWN_TIMEOUT); - it( - 'should generate all trace data correctly', - async () => { - // Assess - const mainSubsegment = traceData[0]; - const { subsegments, annotations, metadata } = mainSubsegment; - - // Check the main segment name - expect(mainSubsegment.name).toBe('## index.handler'); - - // Check the subsegments of the main segment - expect(subsegments.size).toBe(3); - - // Check remote call subsegment - expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); - const httpSubsegment = subsegments.get('docs.powertools.aws.dev'); - expect(httpSubsegment?.namespace).toBe('remote'); - expect(httpSubsegment?.http?.request?.url).toEqual( - 'https://docs.powertools.aws.dev/lambda/typescript/latest/' - ); - expect(httpSubsegment?.http?.request?.method).toBe('GET'); - expect(httpSubsegment?.http?.response?.status).toEqual( - expect.any(Number) - ); - expect(httpSubsegment?.http?.response?.status).toEqual( - expect.any(Number) - ); - - // Check the custom subsegment name & metadata - expect(subsegments.has(expectedCustomSubSegmentName)).toBe(true); - expect( - subsegments.get(expectedCustomSubSegmentName)?.metadata - ).toStrictEqual({ - Decorator: { - 'myMethod response': expectedCustomResponseValue, - }, - }); - - // Check the other custom subsegment and its subsegments - expect(subsegments.has('### methodNoResponse')).toBe(true); - expect(subsegments.get('### methodNoResponse')?.metadata).toBeUndefined(); - expect(subsegments.get('### methodNoResponse')?.subsegments?.length).toBe( - 1 - ); - expect( - subsegments.get('### methodNoResponse')?.subsegments?.[0]?.name === - 'DynamoDB' - ).toBe(true); - - // Check the annotations of the main segment - if (!annotations) { - throw new Error('No annotations found on the main segment'); - } - expect(annotations.ColdStart).toEqual(true); - expect(annotations.Service).toEqual('Decorator'); - expect(annotations[expectedCustomAnnotationKey]).toEqual( - expectedCustomAnnotationValue - ); - - // Check the metadata of the main segment - if (!metadata) { - throw new Error('No metadata found on the main segment'); - } - expect(metadata.Decorator[expectedCustomMetadataKey]).toEqual( - expectedCustomMetadataValue - ); - - // Check the response is present in the metadata - expect(metadata.Decorator['index.handler response']).toEqual( - expectedCustomResponseValue - ); - }, - TEST_CASE_TIMEOUT - ); + it('should generate all trace data correctly', async () => { + // Assess + const mainSubsegment = traceData[0]; + const { subsegments, annotations, metadata } = mainSubsegment; + + // Check the main segment name + expect(mainSubsegment.name).toBe('## index.handler'); + + // Check the subsegments of the main segment + expect(subsegments.size).toBe(3); + + // Check remote call subsegment + expect(subsegments.has('docs.powertools.aws.dev')).toBe(true); + const httpSubsegment = subsegments.get('docs.powertools.aws.dev'); + expect(httpSubsegment?.namespace).toBe('remote'); + expect(httpSubsegment?.http?.request?.url).toEqual( + 'https://docs.powertools.aws.dev/lambda/typescript/latest/' + ); + expect(httpSubsegment?.http?.request?.method).toBe('GET'); + expect(httpSubsegment?.http?.response?.status).toEqual(expect.any(Number)); + expect(httpSubsegment?.http?.response?.status).toEqual(expect.any(Number)); + + // Check the custom subsegment name & metadata + expect(subsegments.has(expectedCustomSubSegmentName)).toBe(true); + expect( + subsegments.get(expectedCustomSubSegmentName)?.metadata + ).toStrictEqual({ + Decorator: { + 'myMethod response': expectedCustomResponseValue, + }, + }); + + // Check the other custom subsegment and its subsegments + expect(subsegments.has('### methodNoResponse')).toBe(true); + expect(subsegments.get('### methodNoResponse')?.metadata).toBeUndefined(); + expect(subsegments.get('### methodNoResponse')?.subsegments?.length).toBe( + 1 + ); + expect( + subsegments.get('### methodNoResponse')?.subsegments?.[0]?.name === + 'DynamoDB' + ).toBe(true); + + // Check the annotations of the main segment + if (!annotations) { + throw new Error('No annotations found on the main segment'); + } + expect(annotations.ColdStart).toEqual(true); + expect(annotations.Service).toEqual('Decorator'); + expect(annotations[expectedCustomAnnotationKey]).toEqual( + expectedCustomAnnotationValue + ); + + // Check the metadata of the main segment + if (!metadata) { + throw new Error('No metadata found on the main segment'); + } + expect(metadata.Decorator[expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); + + // Check the response is present in the metadata + expect(metadata.Decorator['index.handler response']).toEqual( + expectedCustomResponseValue + ); + }); it('should annotate the trace with error data correctly', () => { const mainSubsegment = traceData[1]; diff --git a/packages/tracer/tests/e2e/manual.test.ts b/packages/tracer/tests/e2e/manual.test.ts index 4a809f1164..62cb605152 100644 --- a/packages/tracer/tests/e2e/manual.test.ts +++ b/packages/tracer/tests/e2e/manual.test.ts @@ -14,7 +14,6 @@ import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, EXPECTED_ANNOTATION_KEY as expectedCustomAnnotationKey, EXPECTED_ANNOTATION_VALUE as expectedCustomAnnotationValue, EXPECTED_ERROR_MESSAGE as expectedCustomErrorMessage, @@ -92,44 +91,40 @@ describe('Tracer E2E tests, manual instantiation', () => { } }, TEARDOWN_TIMEOUT); - it( - 'should generate all trace data correctly', - async () => { - // Assess - const mainSubsegment = traceData[0]; - const { subsegments, annotations, metadata } = mainSubsegment; - - // Check the main segment name - expect(mainSubsegment.name).toBe('## index.handler'); - - // Since CaptureHTTPsRequests is disabled, we should not have any subsegments - expect(subsegments.size).toBe(0); - - // Check the annotations of the main segment - if (!annotations) { - throw new Error('No annotations found on the main segment'); - } - expect(annotations.ColdStart).toEqual(true); - expect(annotations.Service).toEqual('Manual'); - expect(annotations[expectedCustomAnnotationKey]).toEqual( - expectedCustomAnnotationValue - ); - - // Check the metadata of the main segment - if (!metadata) { - throw new Error('No metadata found on the main segment'); - } - expect(metadata.Manual?.[expectedCustomMetadataKey]).toEqual( - expectedCustomMetadataValue - ); - - // Check the response is present in the metadata - expect(metadata.Manual?.['index.handler response']).toEqual( - expectedCustomResponseValue - ); - }, - TEST_CASE_TIMEOUT - ); + it('should generate all trace data correctly', async () => { + // Assess + const mainSubsegment = traceData[0]; + const { subsegments, annotations, metadata } = mainSubsegment; + + // Check the main segment name + expect(mainSubsegment.name).toBe('## index.handler'); + + // Since CaptureHTTPsRequests is disabled, we should not have any subsegments + expect(subsegments.size).toBe(0); + + // Check the annotations of the main segment + if (!annotations) { + throw new Error('No annotations found on the main segment'); + } + expect(annotations.ColdStart).toEqual(true); + expect(annotations.Service).toEqual('Manual'); + expect(annotations[expectedCustomAnnotationKey]).toEqual( + expectedCustomAnnotationValue + ); + + // Check the metadata of the main segment + if (!metadata) { + throw new Error('No metadata found on the main segment'); + } + expect(metadata.Manual?.[expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); + + // Check the response is present in the metadata + expect(metadata.Manual?.['index.handler response']).toEqual( + expectedCustomResponseValue + ); + }); it('should annotate the trace with error data correctly', () => { const mainSubsegment = traceData[1]; diff --git a/packages/tracer/tests/e2e/middy.test.ts b/packages/tracer/tests/e2e/middy.test.ts index 19e380c81e..e7b4e74cd2 100644 --- a/packages/tracer/tests/e2e/middy.test.ts +++ b/packages/tracer/tests/e2e/middy.test.ts @@ -14,7 +14,6 @@ import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, EXPECTED_ANNOTATION_KEY as expectedCustomAnnotationKey, EXPECTED_ANNOTATION_VALUE as expectedCustomAnnotationValue, EXPECTED_ERROR_MESSAGE as expectedCustomErrorMessage,