diff --git a/package-lock.json b/package-lock.json index 38ed65eece..35aafa18e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17544,8 +17544,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" @@ -17555,54 +17554,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..454c2d87b4 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 = { @@ -163,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 9fb04c9137..d172e40580 100644 --- a/packages/testing/src/xray-traces-utils.ts +++ b/packages/testing/src/xray-traces-utils.ts @@ -83,18 +83,66 @@ 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); + +/** + * 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( @@ -111,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, }); } @@ -136,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}` @@ -153,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( @@ -168,16 +222,28 @@ 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 + * Find the main function segment within the `AWS::Lambda::Function` segment */ const findPowertoolsFunctionSegment = ( trace: XRayTraceParsed, @@ -194,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); }; /** @@ -271,6 +314,7 @@ const getXRayTraceData = async ( const traces = await retriableGetTraceDetails({ traceIds, expectedSegmentsCount, + functionName: resourceName, }); if (!traces) { @@ -286,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; @@ -305,45 +355,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 +363,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.manual.test.ts b/packages/tracer/tests/e2e/allFeatures.manual.test.ts deleted file mode 100644 index dab6be98b5..0000000000 --- a/packages/tracer/tests/e2e/allFeatures.manual.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Test tracer manual mode - * - * @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 { 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, all features with manual instantiation', () => { - const testStack = new TestStack({ - stackNameProps: { - stackNamePrefix: RESOURCE_NAME_PREFIX, - testName: 'AllFeatures-Manual', - }, - }); - - // Location of the lambda function code - const lambdaFunctionCodeFilePath = join( - __dirname, - 'allFeatures.manual.test.functionCode.ts' - ); - const startTime = new Date(); - - /** - * Table used by all functions to make an SDK call - */ - const testTable = new TestDynamodbTable( - testStack, - {}, - { - nameSuffix: 'TestTable', - } - ); - - let fnNameAllFlagsEnabled: string; - const fnAllFlagsEnabled = new TracerTestNodejsFunction( - testStack, - { - entry: lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, - }, - }, - { - nameSuffix: 'AllFlagsManual', - } - ); - testTable.grantWriteData(fnAllFlagsEnabled); - - const invocationCount = 3; - - beforeAll(async () => { - // Deploy the stack - await testStack.deploy(); - - // Get the actual function names from the stack outputs - fnNameAllFlagsEnabled = - testStack.findAndGetStackOutputValue('AllFlagsManual'); - - // Invoke all test cases - await invokeAllTestCases(fnNameAllFlagsEnabled, 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 () => { - 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 { 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 - ); -}); 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..273464000b --- /dev/null +++ b/packages/tracer/tests/e2e/decorator.test.ts @@ -0,0 +1,181 @@ +/** + * 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, + 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 = 2; + 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 + 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]; + 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.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/manual.test.ts b/packages/tracer/tests/e2e/manual.test.ts new file mode 100644 index 0000000000..62cb605152 --- /dev/null +++ b/packages/tracer/tests/e2e/manual.test.ts @@ -0,0 +1,146 @@ +/** + * 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 { + RESOURCE_NAME_PREFIX, + SETUP_TIMEOUT, + TEARDOWN_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, +} from './constants.js'; + +describe('Tracer E2E tests, manual instantiation', () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'Manual', + }, + }); + + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'manual.test.functionCode.ts' + ); + const startTime = new Date(); + + const testTable = new TestDynamodbTable( + testStack, + {}, + { + nameSuffix: 'TestTable', + } + ); + + const fnManual = new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_SERVICE_NAME: 'Manual', + }, + }, + { + nameSuffix: 'Manual', + } + ); + testTable.grantWriteData(fnManual); + + const invocationCount = 2; + let traceData: EnrichedXRayTraceDocumentParsed[] = []; + + beforeAll(async () => { + // Deploy the stack + await testStack.deploy(); + + // Get the actual function names from the stack outputs + const fnNameManual = testStack.findAndGetStackOutputValue('Manual'); + + // Invoke all test cases + 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 () => { + if (!process.env.DISABLE_TEARDOWN) { + await testStack.destroy(); + } + }, 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 + ); + }); + + 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.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..e7b4e74cd2 --- /dev/null +++ b/packages/tracer/tests/e2e/middy.test.ts @@ -0,0 +1,171 @@ +/** + * 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, + 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 = 2; + 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', () => { + // 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 }, ], 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 };