From 57601a6fb6495fd5439dc35a0e8adb570c0a7fef Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 20 Aug 2024 14:49:44 +0200 Subject: [PATCH 01/11] chore(maintenance): extend e2e assertions --- .../tests/e2e/idempotentDecorator.test.ts | 34 +++++++++++-------- packages/testing/src/TestInvocationLogs.ts | 9 +++++ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/idempotency/tests/e2e/idempotentDecorator.test.ts b/packages/idempotency/tests/e2e/idempotentDecorator.test.ts index 37e0c4f197..7f5b7d5702 100644 --- a/packages/idempotency/tests/e2e/idempotentDecorator.test.ts +++ b/packages/idempotency/tests/e2e/idempotentDecorator.test.ts @@ -1,26 +1,26 @@ +import { createHash } from 'node:crypto'; +import { join } from 'node:path'; +import { + TestInvocationLogs, + TestStack, + invokeFunction, +} from '@aws-lambda-powertools/testing-utils'; /** * Test idempotency decorator * * @group e2e/idempotency/decorator */ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { ScanCommand } from '@aws-sdk/lib-dynamodb'; +import { Duration } from 'aws-cdk-lib'; +import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; +import { IdempotencyTestNodejsFunctionAndDynamoTable } from '../helpers/resources.js'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants.js'; -import { ScanCommand } from '@aws-sdk/lib-dynamodb'; -import { createHash } from 'node:crypto'; -import { - invokeFunction, - TestInvocationLogs, - TestStack, -} from '@aws-lambda-powertools/testing-utils'; -import { IdempotencyTestNodejsFunctionAndDynamoTable } from '../helpers/resources.js'; -import { join } from 'node:path'; -import { Duration } from 'aws-cdk-lib'; -import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; const dynamoDBClient = new DynamoDBClient({}); @@ -289,9 +289,15 @@ describe('Idempotency e2e test decorator, default settings', () => { }); expect(idempotencyRecord.Items?.[0].status).toEqual('COMPLETED'); - // During the first invocation the handler should be called, so the logs should contain 1 log - expect(functionLogs[0]).toHaveLength(2); - expect(functionLogs[0][0]).toContain('Task timed out after'); + try { + // During the first invocation the handler should be called, so the logs should contain 1 log + expect(functionLogs[0]).toHaveLength(2); + expect(functionLogs[0][0]).toContain('Task timed out after'); + } catch { + // During the first invocation the function should timeout so the logs should not contain any log and the report log should contain a timeout message + expect(functionLogs[0]).toHaveLength(0); + expect(logs[0].getReportLog()).toMatch(/Status: timeout$/); + } expect(functionLogs[1]).toHaveLength(1); expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( diff --git a/packages/testing/src/TestInvocationLogs.ts b/packages/testing/src/TestInvocationLogs.ts index 50bd418327..1ba52b419d 100644 --- a/packages/testing/src/TestInvocationLogs.ts +++ b/packages/testing/src/TestInvocationLogs.ts @@ -109,6 +109,15 @@ class TestInvocationLogs { ); } + /** + * Return the log that contains the report of the function `REPORT RequestId` + */ + public getReportLog(): string { + const endLogIndex = TestInvocationLogs.getReportLogIndex(this.logs); + + return this.logs[endLogIndex]; + } + /** * Return the index of the log that contains `REPORT RequestId` * @param logs From cbda58c5e8082107b400934ab9eefb3c72474a69 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 2 Sep 2024 16:42:49 +0100 Subject: [PATCH 02/11] test(tracer): improve xray trace retrieval --- package-lock.json | 15 +- packages/testing/package.json | 16 +- packages/testing/src/types.ts | 94 +++++ packages/testing/src/xray-traces-utils.ts | 346 ++++++++++++++++++ packages/tracer/package.json | 5 +- .../tests/e2e/allFeatures.decorator.test.ts | 299 ++++++--------- .../tests/e2e/allFeatures.manual.test.ts | 147 +++----- .../tests/e2e/allFeatures.middy.test.ts | 282 +++++--------- .../tests/e2e/asyncHandler.decorator.test.ts | 211 +++++------ .../helpers/FunctionSegmentNotDefinedError.ts | 13 - .../tracer/tests/helpers/invokeAllTests.ts | 37 ++ .../tracer/tests/helpers/traceAssertions.ts | 38 -- packages/tracer/tests/helpers/tracesUtils.ts | 321 ---------------- 13 files changed, 842 insertions(+), 982 deletions(-) create mode 100644 packages/testing/src/xray-traces-utils.ts delete mode 100644 packages/tracer/tests/helpers/FunctionSegmentNotDefinedError.ts create mode 100644 packages/tracer/tests/helpers/invokeAllTests.ts delete mode 100644 packages/tracer/tests/helpers/traceAssertions.ts delete mode 100644 packages/tracer/tests/helpers/tracesUtils.ts diff --git a/package-lock.json b/package-lock.json index a9ea473797..c6b8765b42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7852,8 +7852,7 @@ "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" }, "node_modules/error-ex": { "version": "1.3.2", @@ -13876,7 +13875,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" @@ -14500,7 +14498,6 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, "engines": { "node": ">= 4" } @@ -17523,7 +17520,11 @@ "@aws-sdk/client-lambda": "^3.637.0", "@smithy/util-utf8": "^3.0.0", "aws-cdk-lib": "^2.155.0", - "esbuild": "^0.23.1" + "esbuild": "^0.23.1", + "promise-retry": "^2.0.1" + }, + "devDependencies": { + "@types/promise-retry": "^1.1.6" } }, "packages/tracer": { @@ -17538,9 +17539,7 @@ "@aws-lambda-powertools/testing-utils": "file:../testing", "@aws-sdk/client-dynamodb": "^3.637.0", "@aws-sdk/client-xray": "^3.637.0", - "@types/promise-retry": "^1.1.6", - "aws-sdk": "^2.1686.0", - "promise-retry": "^2.0.1" + "aws-sdk": "^2.1686.0" }, "peerDependencies": { "@middy/core": "4.x || 5.x" diff --git a/packages/testing/package.json b/packages/testing/package.json index 0b627cccc1..7abd5370fb 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -53,6 +53,10 @@ "./types": { "import": "./lib/esm/types.js", "require": "./lib/cjs/types.js" + }, + "./utils/xray-traces": { + "import": "./lib/esm/xray-traces-utils.js", + "require": "./lib/cjs/xray-traces-utils.js" } }, "typesVersions": { @@ -72,6 +76,10 @@ "context": [ "lib/cjs/context.d.ts", "lib/esm/context.d.ts" + ], + "utils/xray-traces": [ + "lib/cjs/xray-traces-utils.d.ts", + "lib/esm/xray-traces-utils.d.ts" ] } }, @@ -94,6 +102,10 @@ "@aws-sdk/client-lambda": "^3.637.0", "@smithy/util-utf8": "^3.0.0", "aws-cdk-lib": "^2.155.0", - "esbuild": "^0.23.1" + "esbuild": "^0.23.1", + "promise-retry": "^2.0.1" + }, + "devDependencies": { + "@types/promise-retry": "^1.1.6" } -} \ No newline at end of file +} diff --git a/packages/testing/src/types.ts b/packages/testing/src/types.ts index 1aa85effe7..286f2bab7b 100644 --- a/packages/testing/src/types.ts +++ b/packages/testing/src/types.ts @@ -87,6 +87,94 @@ interface TestStackProps { stack?: Stack; } +// #region X-Ray Trace Utils + +type GetXRayTraceIdsOptions = { + startTime: Date; + resourceName: string; + expectedTracesCount: number; +}; + +type XRayTraceDocumentParsed = { + name: string; + id: string; + start_time: number; + end_time?: number; + // This flag may be set if the segment hasn't been fully processed + // The trace may have already appeared in the `getTraceSummaries` response + // but a segment may still be in_progress + in_progress?: boolean; + aws?: { + request_id: string; + }; + http?: { + response: { + status: number; + }; + }; + origin?: string; + resource_arn?: string; + trace_id?: string; + subsegments?: XRayTraceDocumentParsed[]; + annotations?: { + [key: string]: string | boolean | number; + }; + metadata?: { + [key: string]: { + [key: string]: unknown; + }; + }; + fault?: boolean; + cause?: { + working_directory: string; + exceptions: { + message: string; + type: string; + remote: boolean; + stack: { + path: string; + line: number; + label: string; + }[]; + }[]; + }; + exception: { + message: string; + }; + error?: boolean; +}; + +type XRaySegmentParsed = { + Id: string; + Document: XRayTraceDocumentParsed; +}; + +type XRayTraceParsed = { + Id: string; + Segments: XRaySegmentParsed[]; +}; + +type GetXRayTraceDetailsOptions = { + /** + * The trace IDs to get details for + */ + traceIds: string[]; + /** + * The expected number of segments in each trace + */ + expectedSegmentsCount: number; +}; + +/** + * Enriched X-Ray trace document parsed with subsegments as a map + */ +type EnrichedXRayTraceDocumentParsed = Omit< + XRayTraceDocumentParsed, + 'subsegments' +> & { + subsegments: Map; +}; + export type { ExtraTestProps, TestDynamodbTableProps, @@ -96,4 +184,10 @@ export type { FunctionLog, StackNameProps, TestStackProps, + GetXRayTraceIdsOptions, + GetXRayTraceDetailsOptions, + XRayTraceDocumentParsed, + XRaySegmentParsed, + XRayTraceParsed, + EnrichedXRayTraceDocumentParsed, }; diff --git a/packages/testing/src/xray-traces-utils.ts b/packages/testing/src/xray-traces-utils.ts new file mode 100644 index 0000000000..dda236f540 --- /dev/null +++ b/packages/testing/src/xray-traces-utils.ts @@ -0,0 +1,346 @@ +import { + BatchGetTracesCommand, + GetTraceSummariesCommand, + XRayClient, +} from '@aws-sdk/client-xray'; +import promiseRetry from 'promise-retry'; +import type { + EnrichedXRayTraceDocumentParsed, + GetXRayTraceDetailsOptions, + GetXRayTraceIdsOptions, + XRaySegmentParsed, + XRayTraceDocumentParsed, + XRayTraceParsed, +} from './types.js'; + +const retryOptions = { + retries: 20, + minTimeout: 5_000, + maxTimeout: 10_000, + factor: 1.25, +}; +const xrayClient = new XRayClient({}); + +/** + * Get the trace IDs for a given resource name from the AWS X-Ray API + * + * @param options - The options to get trace IDs, including the start time, resource name, and expected traces count + */ +const getTraceIds = async ( + options: GetXRayTraceIdsOptions +): Promise => { + const { startTime, resourceName, expectedTracesCount } = options; + const endTime = new Date(); + + const response = await xrayClient.send( + new GetTraceSummariesCommand({ + StartTime: startTime, + EndTime: endTime, + FilterExpression: `resource.arn ENDSWITH ":function:${resourceName}"`, + }) + ); + + const summaries = response.TraceSummaries; + + if (summaries === undefined || summaries.length !== expectedTracesCount) { + throw new Error( + `Expected ${expectedTracesCount} trace summaries, got ${summaries ? summaries.length : 0} for ${resourceName}` + ); + } + + const ids = []; + + for (const summary of summaries) { + if (summary.Id === undefined) { + throw new Error( + `Expected all trace summaries to have an ID for ${resourceName}` + ); + } + + ids.push(summary.Id); + } + + return ids; +}; + +/** + * Retriable version of {@link getTraceIds} + * + * @param options - The options to get trace IDs, including the start time, resource name, and expected traces count + */ +const retriableGetTraceIds = (options: GetXRayTraceIdsOptions) => + promiseRetry(async (retry, attempt) => { + try { + return await getTraceIds(options); + } catch (error) { + if (attempt === retryOptions.retries) { + const endTime = new Date(); + console.log( + `Manual query: aws xray get-trace-summaries --start-time ${Math.floor( + options.startTime.getTime() / 1000 + )} --end-time ${Math.floor( + endTime.getTime() / 1000 + )} --filter-expression 'resource.arn ENDSWITH ":function:${options.resourceName}"'` + ); + } + retry(error); + } + }); + +/** + * Get the trace details for a given trace ID from the AWS X-Ray API. + * + * When the trace is returned, the segments are parsed, since the document is returned + * stringified, and then sorted by start time. + * + * @param options - The options to get trace details, including the trace IDs and expected segments count + */ +const getTraceDetails = async ( + options: GetXRayTraceDetailsOptions +): Promise => { + const { traceIds, expectedSegmentsCount } = options; + const response = await xrayClient.send( + new BatchGetTracesCommand({ + TraceIds: traceIds, + }) + ); + + const traces = response.Traces; + + if (traces === undefined || traces.length !== traceIds.length) { + throw new Error( + `Expected ${traceIds.length} traces, got ${traces ? traces.length : 0}` + ); + } + + const parsedAndSortedTraces: XRayTraceParsed[] = []; + for (const trace of traces) { + const { Id: id, Segments: segments } = trace; + if (segments === undefined || segments.length !== expectedSegmentsCount) { + throw new Error( + `Expected ${expectedSegmentsCount} segments, got ${segments ? segments.length : 0} for traceId ${trace.Id}` + ); + } + + const parsedSegments: XRaySegmentParsed[] = []; + for (const segment of segments) { + const { Id, Document } = segment; + if (Document === undefined || Id === undefined) { + throw new Error( + `Segment document or id are missing for traceId ${trace.Id}` + ); + } + + parsedSegments.push({ + Id, + Document: JSON.parse(Document) as XRayTraceDocumentParsed, + }); + } + const sortedSegments = parsedSegments.sort( + (a, b) => a.Document.start_time - b.Document.start_time + ); + + parsedAndSortedTraces.push({ + Id: id as string, + Segments: sortedSegments, + }); + } + + return parsedAndSortedTraces.sort( + (a, b) => + a.Segments[0].Document.start_time - b.Segments[0].Document.start_time + ); +}; + +/** + * Retriable version of {@link getTraceDetails} + * + * @param options - The options to get trace details, including the trace IDs and expected segments count + */ +const retriableGetTraceDetails = (options: GetXRayTraceDetailsOptions) => + promiseRetry(async (retry) => { + try { + return await getTraceDetails(options); + } catch (error) { + retry(error); + } + }); + +/** + * Find the main function segment in the trace identified by the `## index.` suffix + */ +const findPowertoolsFunctionSegment = ( + trace: XRayTraceParsed, + functionName: string +): XRayTraceDocumentParsed => { + const functionSegment = trace.Segments.find( + (segment) => segment.Document.origin === 'AWS::Lambda::Function' + ); + + if (!functionSegment) { + throw new Error( + `AWS::Lambda::Function segment not found for ${functionName}` + ); + } + + 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; +}; + +/** + * Parse the subsegments of a segment by name. + * + * The subsegments are split into a map where the key is the name of the subsegment + * and the value is an array of subsegments with that name. + * + * This is useful to more easily assert the presence of specific subsegments in a segment. + * + * @param subsegments - The subsegments to parse + * @param expectedNames - The expected names to map the subsegments with + */ +const parseSubsegmentsByName = ( + subsegments: XRayTraceDocumentParsed[] +): Map => { + const subsegmentMap = new Map(); + + for (const subsegment of subsegments) { + subsegmentMap.set(subsegment.name, subsegment); + } + + return subsegmentMap; +}; + +/** + * Get the X-Ray trace data for a given resource name. + * + * @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 getXRayTraceData = async ( + options: GetXRayTraceIdsOptions & Omit +) => { + const { + startTime, + resourceName, + expectedTracesCount, + expectedSegmentsCount, + } = options; + + const traceIds = await retriableGetTraceIds({ + startTime, + resourceName, + expectedTracesCount, + }); + + if (!traceIds) { + throw new Error(`No trace IDs found for ${resourceName}`); + } + + const traces = await retriableGetTraceDetails({ + traceIds, + expectedSegmentsCount, + }); + + if (!traces) { + throw new Error(`No traces found for ${resourceName}`); + } + + return traces; +}; + +/** + * Get the X-Ray trace data for a given resource name and parse 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 getTraces = async ( + options: GetXRayTraceIdsOptions & Omit +): Promise => { + const traces = await getXRayTraceData(options); + + const { resourceName } = options; + + const mainSubsegments = []; + for (const trace of traces) { + const mainSubsegment = findPowertoolsFunctionSegment(trace, resourceName); + const enrichedMainSubsegment = { + ...mainSubsegment, + subsegments: parseSubsegmentsByName(mainSubsegment.subsegments ?? []), + }; + mainSubsegments.push(enrichedMainSubsegment); + } + + 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, + getTraceDetails, + retriableGetTraceDetails, + findPowertoolsFunctionSegment, + getTraces, + parseSubsegmentsByName, + getTracesWithoutMainSubsegments, +}; diff --git a/packages/tracer/package.json b/packages/tracer/package.json index 7238d9b571..e6257d5cd3 100644 --- a/packages/tracer/package.json +++ b/packages/tracer/package.json @@ -29,10 +29,7 @@ "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", "@aws-sdk/client-dynamodb": "^3.637.0", - "@aws-sdk/client-xray": "^3.637.0", - "@types/promise-retry": "^1.1.6", - "aws-sdk": "^2.1686.0", - "promise-retry": "^2.0.1" + "@aws-sdk/client-xray": "^3.637.0" }, "peerDependencies": { "@middy/core": "4.x || 5.x" diff --git a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts index d8160d8309..c44c73eff1 100644 --- a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts @@ -6,18 +6,12 @@ import { join } from 'node:path'; import { TestStack } from '@aws-lambda-powertools/testing-utils'; import { TestDynamodbTable } from '@aws-lambda-powertools/testing-utils/resources/dynamodb'; -import { TracerTestNodejsFunction } from '../helpers/resources.js'; -import { - assertAnnotation, - assertErrorAndFault, -} from '../helpers/traceAssertions.js'; import { - getFirstSubsegment, - getInvocationSubsegment, getTraces, - invokeAllTestCases, - splitSegmentsByName, -} from '../helpers/tracesUtils.js'; + 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, @@ -172,107 +166,77 @@ describe('Tracer E2E tests, all features with decorator instantiation', () => { }, TEARDOWN_TIMEOUT); it( - 'should generate all custom traces', - async () => { - const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = - commonEnvironmentVars; - - /** - * 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) - */ - const tracesWhenAllFlagsEnabled = await getTraces({ - startTime, - resourceName: fnNameAllFlagsEnabled, - expectedTracesCount: invocationCount, - expectedSegmentsCount: 4, - }); - - // Assess - for (let i = 0; i < invocationCount; i++) { - const trace = tracesWhenAllFlagsEnabled[i]; - const invocationSubsegment = getInvocationSubsegment(trace); - - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Tracer) - * '## index.handler' subsegment should have 3 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. docs.powertools.aws.dev (Remote call) - * 3. '### myMethod' (method decorator) - */ - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - expect(handlerSubsegment.name).toBe('## index.handler'); - expect(handlerSubsegment?.subsegments).toHaveLength(3); - - if (!handlerSubsegment.subsegments) { - fail('"## index.handler" subsegment should have subsegments'); - } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ - 'DynamoDB', - 'docs.powertools.aws.dev', - '### myMethod', - ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); - expect(subsegments.get('### myMethod')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = i === invocationCount - 1; - if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); - } - } - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should have correct annotations and metadata', + '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 tracesWhenAllFlagsEnabled = await getTraces({ + 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 trace = tracesWhenAllFlagsEnabled[i]; - const invocationSubsegment = getInvocationSubsegment(trace); - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - const { annotations, metadata } = handlerSubsegment; - - const isColdStart = i === 0; - assertAnnotation({ - annotations, - isColdStart, - expectedServiceName: 'AllFlagsOn', - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - }); + 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) { - fail('metadata is missing'); + throw new Error('No metadata found on the main segment'); } - expect(metadata.AllFlagsOn[expectedCustomMetadataKey]).toEqual( + expect(metadata[serviceName][expectedCustomMetadataKey]).toEqual( expectedCustomMetadataValue ); - const shouldThrowAnError = i === invocationCount - 1; - if (!shouldThrowAnError) { - // Assert that the metadata object contains the response - expect(metadata.AllFlagsOn['index.handler response']).toEqual( + // 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 ); } @@ -284,122 +248,55 @@ describe('Tracer E2E tests, all features with decorator instantiation', () => { it( 'should not capture error nor response when the flags are false', async () => { - /** - * 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) - */ - const tracesWithNoCaptureErrorOrResponse = await getTraces({ + 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 - for (let i = 0; i < invocationCount; i++) { - const trace = tracesWithNoCaptureErrorOrResponse[i]; - const invocationSubsegment = getInvocationSubsegment(trace); - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Tracer) - * '## index.handler' subsegment should have 3 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. docs.powertools.aws.dev (Remote call) - * 3. '### myMethod' (method decorator) - */ - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - expect(handlerSubsegment.name).toBe('## index.handler'); - expect(handlerSubsegment?.subsegments).toHaveLength(3); - - if (!handlerSubsegment.subsegments) { - fail('"## index.handler" subsegment should have subsegments'); - } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ - 'DynamoDB', - 'docs.powertools.aws.dev', - '### myMethod', - ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); - expect(subsegments.get('### myMethod')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = i === invocationCount - 1; - if (shouldThrowAnError) { - // Assert that the subsegment has the expected fault - expect(invocationSubsegment.error).toBe(true); - expect(handlerSubsegment.error).toBe(true); - // Assert that no error was captured on the subsegment - expect(Object.hasOwn(handlerSubsegment, 'cause')).toBe(false); - } - } + 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 response when captureResponse is set to false', + 'should not capture any custom traces when disabled', async () => { - const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = - commonEnvironmentVars; - - /** - * 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) - */ - const tracesWithCaptureResponseFalse = await getTraces({ + const lambdaFunctionSegments = await getTracesWithoutMainSubsegments({ startTime, - resourceName: fnNameCaptureResponseOff, + resourceName: fnNameTracerDisabled, expectedTracesCount: invocationCount, - expectedSegmentsCount: 4, + /** + * 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 trace = tracesWithCaptureResponseFalse[i]; - const invocationSubsegment = getInvocationSubsegment(trace); - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Tracer) - * '## index.handler' subsegment should have 3 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. docs.powertools.aws.dev (Remote call) - * 3. '### myMethod' (method decorator) - */ - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - expect(handlerSubsegment.name).toBe( - '## index.handlerWithCaptureResponseFalse' - ); - expect(handlerSubsegment?.subsegments).toHaveLength(3); - - if (!handlerSubsegment.subsegments) { - fail( - '"## index.handlerWithCaptureResponseFalse" subsegment should have subsegments' - ); - } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ - 'DynamoDB', - 'docs.powertools.aws.dev', - '### myMethod', - ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); - expect(subsegments.get('### myMethod')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); + const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture + const lambdaFunctionSegment = lambdaFunctionSegments[i]; + const { subsegments } = lambdaFunctionSegment; - // No metadata because capturing the response was disabled and that's - // the only metadata that could be in the subsegment for the test. - const myMethodSegment = subsegments.get('### myMethod')?.[0]; - expect(myMethodSegment).toBeDefined(); - expect(myMethodSegment).not.toHaveProperty('metadata'); + expect(subsegments.has('## index.handler')).toBe(false); - const shouldThrowAnError = i === invocationCount - 1; if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + expect(lambdaFunctionSegment.error).toBe(true); } } }, @@ -407,28 +304,36 @@ describe('Tracer E2E tests, all features with decorator instantiation', () => { ); it( - 'should not capture any custom traces when disabled', + 'should not capture response when captureResponse is set to false', async () => { - const tracesWithTracerDisabled = await getTraces({ + const mainSubsegments = await getTraces({ startTime, - resourceName: fnNameTracerDisabled, + resourceName: fnNameCaptureResponseOff, expectedTracesCount: invocationCount, - expectedSegmentsCount: 2, + /** + * 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 trace = tracesWithTracerDisabled[i]; - /** - * Expect no subsegment in the invocation - */ - const invocationSubsegment = getInvocationSubsegment(trace); - expect(invocationSubsegment?.subsegments).toBeUndefined(); + const mainSubsegment = mainSubsegments[i]; + const { subsegments } = mainSubsegment; - const shouldThrowAnError = i === invocationCount - 1; - if (shouldThrowAnError) { - expect(invocationSubsegment.error).toBe(true); - } + 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.ts b/packages/tracer/tests/e2e/allFeatures.manual.test.ts index 5b5823f475..dab6be98b5 100644 --- a/packages/tracer/tests/e2e/allFeatures.manual.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.manual.test.ts @@ -6,19 +6,9 @@ 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 { - assertAnnotation, - assertErrorAndFault, -} from '../helpers/traceAssertions.js'; -import { - type ParsedTrace, - getFirstSubsegment, - getInvocationSubsegment, - getTraces, - invokeAllTestCases, - splitSegmentsByName, -} from '../helpers/tracesUtils.js'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, @@ -63,37 +53,23 @@ describe('Tracer E2E tests, all features with manual instantiation', () => { }, }, { - nameSuffix: 'AllFlagsOn', + nameSuffix: 'AllFlagsManual', } ); testTable.grantWriteData(fnAllFlagsEnabled); const invocationCount = 3; - let sortedTraces: ParsedTrace[]; beforeAll(async () => { // Deploy the stack await testStack.deploy(); // Get the actual function names from the stack outputs - fnNameAllFlagsEnabled = testStack.findAndGetStackOutputValue('AllFlagsOn'); + fnNameAllFlagsEnabled = + testStack.findAndGetStackOutputValue('AllFlagsManual'); // Invoke all test cases await invokeAllTestCases(fnNameAllFlagsEnabled, 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) - */ - sortedTraces = await getTraces({ - startTime, - resourceName: fnNameAllFlagsEnabled, - expectedTracesCount: invocationCount, - expectedSegmentsCount: 4, - }); }, SETUP_TIMEOUT); afterAll(async () => { @@ -103,82 +79,75 @@ describe('Tracer E2E tests, all features with manual instantiation', () => { }, TEARDOWN_TIMEOUT); it( - 'should generate all custom traces', - async () => { - const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = - commonEnvironmentVars; - - // Assess - for (let i = 0; i < invocationCount; i++) { - const trace = sortedTraces[i]; - const invocationSubsegment = getInvocationSubsegment(trace); - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Tracer) - * '## index.handler' subsegment should have 2 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. docs.powertools.aws.dev (Remote call) - */ - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - expect(handlerSubsegment.name).toBe('## index.handler'); - expect(handlerSubsegment?.subsegments).toHaveLength(2); - - if (!handlerSubsegment.subsegments) { - fail('"## index.handler" subsegment should have subsegments'); - } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ - 'DynamoDB', - 'docs.powertools.aws.dev', - ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = i === invocationCount - 1; - if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); - } - } - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should have correct annotations and metadata', + '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 trace = sortedTraces[i]; - const invocationSubsegment = getInvocationSubsegment(trace); - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - const { annotations, metadata } = handlerSubsegment; - - const isColdStart = i === 0; - assertAnnotation({ - annotations, - isColdStart, - expectedServiceName: 'AllFlagsOn', - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - }); + 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) { - fail('metadata is missing'); + throw new Error('No metadata found on the main segment'); } - expect(metadata.AllFlagsOn[expectedCustomMetadataKey]).toEqual( + expect(metadata[serviceName][expectedCustomMetadataKey]).toEqual( expectedCustomMetadataValue ); - const shouldThrowAnError = i === invocationCount - 1; - if (!shouldThrowAnError) { - // Assert that the metadata object contains the response - expect(metadata.AllFlagsOn['index.handler response']).toEqual( + // 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 ); } diff --git a/packages/tracer/tests/e2e/allFeatures.middy.test.ts b/packages/tracer/tests/e2e/allFeatures.middy.test.ts index b22204e4aa..9d07a6a651 100644 --- a/packages/tracer/tests/e2e/allFeatures.middy.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.middy.test.ts @@ -6,18 +6,12 @@ import { join } from 'node:path'; import { TestStack } from '@aws-lambda-powertools/testing-utils'; import { TestDynamodbTable } from '@aws-lambda-powertools/testing-utils/resources/dynamodb'; -import { TracerTestNodejsFunction } from '../helpers/resources.js'; -import { - assertAnnotation, - assertErrorAndFault, -} from '../helpers/traceAssertions.js'; import { - getFirstSubsegment, - getInvocationSubsegment, getTraces, - invokeAllTestCases, - splitSegmentsByName, -} from '../helpers/tracesUtils.js'; + 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, @@ -172,104 +166,76 @@ describe('Tracer E2E tests, all features with middy instantiation', () => { }, TEARDOWN_TIMEOUT); it( - 'should generate all custom traces', - async () => { - const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = - commonEnvironmentVars; - - /** - * Expect the trace to have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 3. DynamoDB Table (AWS::DynamoDB::Table) - * 4. Remote call (docs.powertools.aws.dev) - */ - const tracesWhenAllFlagsEnabled = await getTraces({ - startTime, - resourceName: fnNameAllFlagsEnabled, - expectedTracesCount: invocationCount, - expectedSegmentsCount: 4, - }); - - // Assess - for (let i = 0; i < invocationCount; i++) { - const trace = tracesWhenAllFlagsEnabled[i]; - const invocationSubsegment = getInvocationSubsegment(trace); - - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Tracer) - * '## index.handler' subsegment should have 2 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. docs.powertools.aws.dev (Remote call) - */ - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - expect(handlerSubsegment.name).toBe('## index.handler'); - expect(handlerSubsegment?.subsegments).toHaveLength(2); - - if (!handlerSubsegment.subsegments) { - fail('"## index.handler" subsegment should have subsegments'); - } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ - 'DynamoDB', - 'docs.powertools.aws.dev', - ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = i === invocationCount - 1; - if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); - } - } - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should have correct annotations and metadata', + '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 tracesWhenAllFlagsEnabled = await getTraces({ + 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 trace = tracesWhenAllFlagsEnabled[i]; - const invocationSubsegment = getInvocationSubsegment(trace); - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - const { annotations, metadata } = handlerSubsegment; - - const isColdStart = i === 0; - assertAnnotation({ - annotations, - isColdStart, - expectedServiceName: 'AllFlagsOn', - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - }); + 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) { - fail('metadata is missing'); + throw new Error('No metadata found on the main segment'); } - expect(metadata.AllFlagsOn[expectedCustomMetadataKey]).toEqual( + expect(metadata[serviceName][expectedCustomMetadataKey]).toEqual( expectedCustomMetadataValue ); - const shouldThrowAnError = i === invocationCount - 1; - if (!shouldThrowAnError) { - // Assert that the metadata object contains the response - expect(metadata.AllFlagsOn['index.handler response']).toEqual( + // 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 ); } @@ -281,109 +247,55 @@ describe('Tracer E2E tests, all features with middy instantiation', () => { it( 'should not capture error nor response when the flags are false', async () => { - /** - * Expect the trace to have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 3. DynamoDB Table (AWS::DynamoDB::Table) - * 4. Remote call (docs.powertools.aws.dev) - */ - const tracesWithNoCaptureErrorOrResponse = await getTraces({ + const mainSubsegments = await getTraces({ startTime, resourceName: fnNameNoCaptureErrorOrResponse, expectedTracesCount: invocationCount, - expectedSegmentsCount: 4, - }); - // Assess - for (let i = 0; i < invocationCount; i++) { - const trace = tracesWithNoCaptureErrorOrResponse[i]; - const invocationSubsegment = getInvocationSubsegment(trace); /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Tracer) - * '## index.handler' subsegment should have 2 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. docs.powertools.aws.dev (Remote call) + * 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) */ - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - expect(handlerSubsegment.name).toBe('## index.handler'); - expect(handlerSubsegment?.subsegments).toHaveLength(2); - - if (!handlerSubsegment.subsegments) { - fail('"## index.handler" subsegment should have subsegments'); - } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ - 'DynamoDB', - 'docs.powertools.aws.dev', - ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); + expectedSegmentsCount: 4, + }); - const shouldThrowAnError = i === invocationCount - 1; - if (shouldThrowAnError) { - // Assert that the subsegment has the expected fault - expect(invocationSubsegment.error).toBe(true); - expect(handlerSubsegment.error).toBe(true); - // Assert that no error was captured on the subsegment - expect(Object.hasOwn(handlerSubsegment, 'cause')).toBe(false); - } - } + // 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 response when captureResponse is set to false', + 'should not capture any custom traces when disabled', async () => { - const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = - commonEnvironmentVars; - - /** - * Expect the trace to have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 3. DynamoDB Table (AWS::DynamoDB::Table) - * 4. Remote call (docs.powertools.aws.dev) - */ - const tracesWithNoCaptureResponse = await getTraces({ + const lambdaFunctionSegments = await getTracesWithoutMainSubsegments({ startTime, - resourceName: fnNameCaptureResponseOff, + resourceName: fnNameTracerDisabled, expectedTracesCount: invocationCount, - expectedSegmentsCount: 4, + /** + * 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 trace = tracesWithNoCaptureResponse[i]; - const invocationSubsegment = getInvocationSubsegment(trace); - /** - * Invocation subsegment should have a subsegment '## index.handlerWithNoCaptureResponseViaMiddlewareOption' (default behavior for Tracer) - * '## index.handlerWithNoCaptureResponseViaMiddlewareOption' subsegment should have 2 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. docs.powertools.aws.dev (Remote call) - */ - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - expect(handlerSubsegment.name).toBe( - '## index.handlerWithNoCaptureResponseViaMiddlewareOption' - ); - expect(handlerSubsegment?.subsegments).toHaveLength(2); + const shouldThrowAnError = i === invocationCount - 1; // Last invocation should throw - we are testing error capture + const lambdaFunctionSegment = lambdaFunctionSegments[i]; + const { subsegments } = lambdaFunctionSegment; - if (!handlerSubsegment.subsegments) { - fail( - '"## index.handlerWithNoCaptureResponseViaMiddlewareOption" subsegment should have subsegments' - ); - } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ - 'DynamoDB', - 'docs.powertools.aws.dev', - ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); + expect(subsegments.has('## index.handler')).toBe(false); - const shouldThrowAnError = i === invocationCount - 1; if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + expect(lambdaFunctionSegment.error).toBe(true); } } }, @@ -391,27 +303,35 @@ describe('Tracer E2E tests, all features with middy instantiation', () => { ); it( - 'should not capture any custom traces when disabled', + 'should not capture response when captureResponse is set to false', async () => { - const tracesWithTracerDisabled = await getTraces({ + const mainSubsegments = await getTraces({ startTime, - resourceName: fnNameTracerDisabled, + resourceName: fnNameCaptureResponseOff, expectedTracesCount: invocationCount, - expectedSegmentsCount: 2, + /** + * 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 trace = tracesWithTracerDisabled[i]; - /** - * Expect no subsegment in the invocation - */ - const invocationSubsegment = getInvocationSubsegment(trace); - expect(invocationSubsegment?.subsegments).toBeUndefined(); + const serviceName = 'CaptureResponseOff'; + const mainSubsegment = mainSubsegments[i]; + const { metadata } = mainSubsegment; - const shouldThrowAnError = i === invocationCount - 1; - if (shouldThrowAnError) { - expect(invocationSubsegment.error).toBe(true); + 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.ts b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts index e5ef278f8d..85995c1cf6 100644 --- a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts +++ b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts @@ -6,18 +6,12 @@ import { join } from 'node:path'; import { TestStack } from '@aws-lambda-powertools/testing-utils'; import { TestDynamodbTable } from '@aws-lambda-powertools/testing-utils/resources/dynamodb'; -import { TracerTestNodejsFunction } from '../helpers/resources.js'; -import { - assertAnnotation, - assertErrorAndFault, -} from '../helpers/traceAssertions.js'; import { - getFirstSubsegment, - getInvocationSubsegment, getTraces, - invokeAllTestCases, - splitSegmentsByName, -} from '../helpers/tracesUtils.js'; + 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, @@ -89,7 +83,7 @@ describe('Tracer E2E tests, async handler with decorator instantiation', () => { ); testTable.grantWriteData(fnCustomSubsegmentName); - const invocationsCount = 3; + const invocationCount = 3; beforeAll(async () => { // Deploy the stack @@ -103,8 +97,8 @@ describe('Tracer E2E tests, async handler with decorator instantiation', () => { // Act await Promise.all([ - invokeAllTestCases(fnNameAllFlagsEnabled, invocationsCount), - invokeAllTestCases(fnNameCustomSubsegment, invocationsCount), + invokeAllTestCases(fnNameAllFlagsEnabled, invocationCount), + invokeAllTestCases(fnNameCustomSubsegment, invocationCount), ]); }, SETUP_TIMEOUT); @@ -116,106 +110,76 @@ describe('Tracer E2E tests, async handler with decorator instantiation', () => { it( 'should generate all custom traces', - async () => { - const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = - commonEnvironmentVars; - - /** - * Expect the trace to have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 3. DynamoDB Table (AWS::DynamoDB::Table) - * 4. Remote call (docs.powertools.aws.dev) - */ - const tracesWhenAllFlagsEnabled = await getTraces({ - startTime, - resourceName: fnNameAllFlagsEnabled, - expectedTracesCount: invocationsCount, - expectedSegmentsCount: 4, - }); - - // Assess - for (let i = 0; i < invocationsCount; i++) { - const trace = tracesWhenAllFlagsEnabled[i]; - const invocationSubsegment = getInvocationSubsegment(trace); - - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Tracer) - * '## index.handler' subsegment should have 3 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. docs.powertools.aws.dev (Remote call) - * 3. '### myMethod' (method decorator) - */ - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - expect(handlerSubsegment.name).toBe('## index.handler'); - expect(handlerSubsegment?.subsegments).toHaveLength(3); - - if (!handlerSubsegment.subsegments) { - fail('"## index.handler" subsegment should have subsegments'); - } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ - 'DynamoDB', - 'docs.powertools.aws.dev', - '### myMethod', - ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); - expect(subsegments.get('### myMethod')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = i === invocationsCount - 1; - if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); - } - } - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should have correct 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 = 'AllFlagsOn'; - const traces = await getTraces({ + const mainSubsegments = await getTraces({ startTime, resourceName: fnNameAllFlagsEnabled, - expectedTracesCount: invocationsCount, + 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, }); - for (let i = 0; i < invocationsCount; i++) { - const trace = traces[i]; - const invocationSubsegment = getInvocationSubsegment(trace); - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - const { annotations, metadata } = handlerSubsegment; - - const isColdStart = i === 0; - assertAnnotation({ - annotations, - isColdStart, - expectedServiceName: 'AllFlagsOn', - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - }); + // Assess + // 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) { - fail('metadata is missing'); + throw new Error('No metadata found on the main segment'); } - expect(metadata.AllFlagsOn[expectedCustomMetadataKey]).toEqual( + expect(metadata[serviceName][expectedCustomMetadataKey]).toEqual( expectedCustomMetadataValue ); - const shouldThrowAnError = i === invocationsCount - 1; - if (!shouldThrowAnError) { - // Assert that the metadata object contains the response - expect(metadata.AllFlagsOn['index.handler response']).toEqual( + // 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 ); } @@ -232,54 +196,43 @@ describe('Tracer E2E tests, async handler with decorator instantiation', () => { EXPECTED_CUSTOM_SUBSEGMENT_NAME: expectedCustomSubSegmentName, } = commonEnvironmentVars; - /** - * Expect the trace to have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 3. DynamoDB Table (AWS::DynamoDB::Table) - * 4. Remote call (docs.powertools.aws.dev) - */ - const tracesWhenCustomSubsegmentNameInMethod = await getTraces({ + const mainSubsegments = await getTraces({ startTime, resourceName: fnNameCustomSubsegment, - expectedTracesCount: invocationsCount, + 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 < invocationsCount; i++) { - const trace = tracesWhenCustomSubsegmentNameInMethod[i]; - const invocationSubsegment = getInvocationSubsegment(trace); + 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; - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Tracer) - * '## index.handler' subsegment should have 3 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. docs.powertools.aws.dev (Remote call) - * 3. '### mySubsegment' (method decorator with custom name) - */ - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - expect(handlerSubsegment.name).toBe( + // Check the main segment name + expect(mainSubsegment.name).toBe( '## index.handlerWithCustomSubsegmentNameInMethod' ); - expect(handlerSubsegment?.subsegments).toHaveLength(3); - if (!handlerSubsegment.subsegments) { - fail('"## index.handler" subsegment should have subsegments'); - } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ - 'DynamoDB', - 'docs.powertools.aws.dev', - expectedCustomSubSegmentName, - ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); - expect(subsegments.get(expectedCustomSubSegmentName)?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); + // 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); - const shouldThrowAnError = i === invocationsCount - 1; if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + 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/FunctionSegmentNotDefinedError.ts b/packages/tracer/tests/helpers/FunctionSegmentNotDefinedError.ts deleted file mode 100644 index 9119bcaab7..0000000000 --- a/packages/tracer/tests/helpers/FunctionSegmentNotDefinedError.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Thrown when the function segement (AWS::Lambda::Function) is not found in a trace. - * - * X-Ray segments are process asynchronously. They may not be available even after - * the trace has already appeared. In that case, the function segment may be missing. - * We will throw this error to notify caller. - */ -export class FunctionSegmentNotDefinedError extends Error { - public constructor(msg: string) { - super(msg); - Object.setPrototypeOf(this, FunctionSegmentNotDefinedError.prototype); - } -} diff --git a/packages/tracer/tests/helpers/invokeAllTests.ts b/packages/tracer/tests/helpers/invokeAllTests.ts new file mode 100644 index 0000000000..5a6c934281 --- /dev/null +++ b/packages/tracer/tests/helpers/invokeAllTests.ts @@ -0,0 +1,37 @@ +import { invokeFunction } from '@aws-lambda-powertools/testing-utils'; + +/** + * Invoke function sequentially 3 times with different parameters + * + * invocation: is just a tracking number (it has to start from 1) + * sdkV2: define if we will use `captureAWSClient()` or `captureAWS()` for SDK V2 + * throw: forces the Lambda to throw an error + * + * @param functionName + */ +const invokeAllTestCases = async ( + functionName: string, + times: number +): Promise => { + await invokeFunction({ + functionName, + times, + invocationMode: 'SEQUENTIAL', + payload: [ + { + invocation: 1, + throw: false, + }, + { + invocation: 2, + throw: false, + }, + { + invocation: 3, + throw: true, // only last invocation should throw + }, + ], + }); +}; + +export { invokeAllTestCases }; diff --git a/packages/tracer/tests/helpers/traceAssertions.ts b/packages/tracer/tests/helpers/traceAssertions.ts deleted file mode 100644 index f2a6469abb..0000000000 --- a/packages/tracer/tests/helpers/traceAssertions.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - type AssertAnnotationParams, - type ParsedDocument, - getFirstSubsegment, -} from './tracesUtils.js'; - -export const assertAnnotation = (params: AssertAnnotationParams): void => { - const { - annotations, - isColdStart, - expectedServiceName, - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - } = params; - - if (!annotations) { - fail('annotation is missing'); - } - expect(annotations.ColdStart).toEqual(isColdStart); - expect(annotations.Service).toEqual(expectedServiceName); - expect(annotations[expectedCustomAnnotationKey]).toEqual( - expectedCustomAnnotationValue - ); -}; - -export const assertErrorAndFault = ( - invocationSubsegment: ParsedDocument, - expectedCustomErrorMessage: string -): void => { - expect(invocationSubsegment.error).toBe(true); - - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - expect(handlerSubsegment.fault).toBe(true); - expect(Object.hasOwn(handlerSubsegment, 'cause')).toBe(true); - expect(handlerSubsegment.cause?.exceptions[0].message).toBe( - expectedCustomErrorMessage - ); -}; diff --git a/packages/tracer/tests/helpers/tracesUtils.ts b/packages/tracer/tests/helpers/tracesUtils.ts deleted file mode 100644 index 77d8b4dcb9..0000000000 --- a/packages/tracer/tests/helpers/tracesUtils.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { invokeFunction } from '@aws-lambda-powertools/testing-utils'; -import { - BatchGetTracesCommand, - GetTraceSummariesCommand, - XRayClient, -} from '@aws-sdk/client-xray'; -import promiseRetry from 'promise-retry'; -import { FunctionSegmentNotDefinedError } from './FunctionSegmentNotDefinedError.js'; - -interface ParsedDocument { - name: string; - id: string; - start_time: number; - end_time?: number; - // This flag may be set if the segment hasn't been fully processed - // The trace may have already appeared in the `getTraceSummaries` response - // but a segment may still be in_progress - in_progress?: boolean; - aws?: { - request_id: string; - }; - http?: { - response: { - status: number; - }; - }; - origin?: string; - resource_arn?: string; - trace_id?: string; - subsegments?: ParsedDocument[]; - annotations?: { - [key: string]: string | boolean | number; - }; - metadata?: { - [key: string]: { - [key: string]: unknown; - }; - }; - fault?: boolean; - cause?: { - working_directory: string; - exceptions: { - message: string; - type: string; - remote: boolean; - stack: { - path: string; - line: number; - label: string; - }[]; - }[]; - }; - exception: { - message: string; - }; - error?: boolean; -} - -interface ParsedSegment { - Document: ParsedDocument; - Id: string; -} - -interface ParsedTrace { - Duration: number; - Id: string; - LimitExceeded: boolean; - Segments: ParsedSegment[]; -} - -interface AssertAnnotationParams { - annotations: ParsedDocument['annotations']; - isColdStart: boolean; - expectedServiceName: string; - expectedCustomAnnotationKey: string; - expectedCustomAnnotationValue: string | number | boolean; -} - -type GetTracesOptions = { - startTime: Date; - resourceName: string; - expectedTracesCount: number; - expectedSegmentsCount: number; -}; - -const retryOptions = { - retries: 20, - minTimeout: 5_000, - maxTimeout: 10_000, - factor: 1.25, -}; -const xrayClient = new XRayClient({}); - -const getTraces = async ({ - startTime, - resourceName, - expectedTracesCount, - expectedSegmentsCount, -}: GetTracesOptions): Promise => { - const endTime = new Date(); - console.log( - `Manual query: aws xray get-trace-summaries --start-time ${Math.floor( - startTime.getTime() / 1000 - )} --end-time ${Math.floor( - endTime.getTime() / 1000 - )} --filter-expression 'resource.arn ENDSWITH ":function:${resourceName}"'` - ); - - return promiseRetry(async (retry: (err?: Error) => never, _: number) => { - const traces = await xrayClient.send( - new GetTraceSummariesCommand({ - StartTime: startTime, - EndTime: endTime, - FilterExpression: `resource.arn ENDSWITH ":function:${resourceName}"`, - }) - ); - - if (traces.TraceSummaries?.length !== expectedTracesCount) { - retry( - new Error( - `Expected ${expectedTracesCount} traces, got ${traces.TraceSummaries?.length} for ${resourceName}` - ) - ); - } - - const traceIds = traces.TraceSummaries?.map( - (traceSummary) => traceSummary.Id - ); - if (!traceIds.every((traceId) => traceId !== undefined)) { - retry( - new Error( - `Expected all trace summaries to have an ID, got ${traceIds} for ${resourceName}` - ) - ); - } - - const traceDetails = await xrayClient.send( - new BatchGetTracesCommand({ - TraceIds: traceIds as string[], - }) - ); - - if (traceDetails.Traces?.length !== expectedTracesCount) { - retry( - new Error( - `Expected ${expectedTracesCount} trace summaries, got ${traceDetails.Traces?.length} for ${resourceName}` - ) - ); - } - - const sortedTraces = traceDetails.Traces?.map( - (trace): ParsedTrace => ({ - Duration: trace?.Duration as number, - Id: trace?.Id as string, - LimitExceeded: trace?.LimitExceeded as boolean, - Segments: trace.Segments?.map((segment) => ({ - Document: JSON.parse(segment?.Document as string) as ParsedDocument, - Id: segment.Id as string, - })).sort( - (a, b) => a.Document.start_time - b.Document.start_time - ) as ParsedSegment[], - }) - ).sort( - (a, b) => - a.Segments[0].Document.start_time - b.Segments[0].Document.start_time - ); - - // Verify that all trace has fully loaded invocation subsegments. - // The subsegments may be not available yet or still in progress. - for (const trace of sortedTraces) { - let retryFlag = false; - - let invocationSubsegment: ParsedDocument; - try { - invocationSubsegment = getInvocationSubsegment(trace); - } catch (error) { - if (error instanceof FunctionSegmentNotDefinedError) { - retry( - new Error( - 'There is no Function subsegment (AWS::Lambda::Function) yet. Retry.' - ) - ); - } else { - throw error; - } - } - - retryFlag = retryFlag || !!invocationSubsegment.in_progress; - if (retryFlag) { - retry( - new Error( - `There is at least an invocation subsegment that hasn't been fully processed yet. The "in_progress" flag is still "true" in the document.` - ) - ); - } - } - - if (sortedTraces === undefined) { - throw new Error(`Traces are undefined for ${resourceName}`); - } - - if (sortedTraces.length !== expectedTracesCount) { - throw new Error( - `Expected ${expectedTracesCount} sorted traces, but got ${sortedTraces.length} for ${resourceName}` - ); - } - - for (const trace of sortedTraces) { - if (trace.Segments?.length !== expectedSegmentsCount) { - retry( - new Error( - `Expected ${expectedSegmentsCount} segments, got ${trace.Segments?.length} for trace id ${trace.Id}` - ) - ); - } - } - - return sortedTraces; - }, retryOptions); -}; - -const getFunctionSegment = (trace: ParsedTrace): ParsedSegment => { - const functionSegment = trace.Segments.find( - (segment) => segment.Document.origin === 'AWS::Lambda::Function' - ); - - if (functionSegment === undefined) { - throw new FunctionSegmentNotDefinedError( - 'Function segment is undefined. This can be either due to eventual consistency or a bug in Tracer' - ); - } - - return functionSegment; -}; - -const getFirstSubsegment = (segment: ParsedDocument): ParsedDocument => { - const subsegments = segment.subsegments; - if (!subsegments || subsegments.length === 0) { - throw new Error('segment should have subsegments'); - } - - return subsegments[0]; -}; - -const getInvocationSubsegment = (trace: ParsedTrace): ParsedDocument => { - const functionSegment = getFunctionSegment(trace); - const invocationSubsegment = functionSegment.Document?.subsegments?.find( - (subsegment) => subsegment.name === 'Invocation' - ); - - if (invocationSubsegment === undefined) { - throw new Error('Invocation subsegment is undefined'); - } - - return invocationSubsegment; -}; - -const splitSegmentsByName = ( - subsegments: ParsedDocument[], - expectedNames: string[] -): Map => { - const splitSegments: Map = new Map( - [...expectedNames, 'other'].map((name) => [name, []]) - ); - for (const subsegment of subsegments) { - const name = - expectedNames.indexOf(subsegment.name) !== -1 ? subsegment.name : 'other'; - const newSegments = splitSegments.get(name) as ParsedDocument[]; - newSegments.push(subsegment); - splitSegments.set(name, newSegments); - } - - return splitSegments; -}; - -/** - * Invoke function sequentially 3 times with different parameters - * - * invocation: is just a tracking number (it has to start from 1) - * sdkV2: define if we will use `captureAWSClient()` or `captureAWS()` for SDK V2 - * throw: forces the Lambda to throw an error - * - * @param functionName - */ -const invokeAllTestCases = async ( - functionName: string, - times: number -): Promise => { - await invokeFunction({ - functionName, - times, - invocationMode: 'SEQUENTIAL', - payload: [ - { - invocation: 1, - throw: false, - }, - { - invocation: 2, - throw: false, - }, - { - invocation: 3, - throw: true, // only last invocation should throw - }, - ], - }); -}; - -export { - getTraces, - getFunctionSegment, - getFirstSubsegment, - getInvocationSubsegment, - splitSegmentsByName, - invokeAllTestCases, - type ParsedDocument, - type ParsedSegment, - type ParsedTrace, - type AssertAnnotationParams, -}; From 0b7686ecd98a0c7db9875778889f13ba2a1dedfe Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 2 Sep 2024 17:11:11 +0100 Subject: [PATCH 03/11] chore: override cdk lib & cli --- .github/dependabot.yml | 2 +- package-lock.json | 12 ++++++------ packages/testing/package.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bf7489b7cb..8e65137bed 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -44,7 +44,7 @@ updates: - "aws-sdk-client-mock-jest" aws-cdk: patterns: - - "@aws-cdk/*" + - "@aws-cdk/cli-lib-alpha" - "aws-cdk-lib" - "aws-cdk" typedoc: diff --git a/package-lock.json b/package-lock.json index c6b8765b42..00a55fc33c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,9 +89,9 @@ } }, "examples/app/node_modules/aws-cdk": { - "version": "2.145.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.145.0.tgz", - "integrity": "sha512-Jdw7nbrXiihYM/jReXK0/i8a+W/o+fLcn1f8Yzvns1jP58KBGQygqyiv5Dm+uqzS3D8/ZZnfPu3ph6aOVLPNSA==", + "version": "2.155.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.155.0.tgz", + "integrity": "sha512-AV7Ym/o7/xyDh6sqcGatWD6Bqa7Swe0OWJq+1srVww0MdBiy5yM3zYAA1+ZeqZNjFQThJPA+pYZQFTgojuaVBA==", "dev": true, "bin": { "cdk": "bin/cdk" @@ -146,9 +146,9 @@ } }, "layers/node_modules/aws-cdk": { - "version": "2.145.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.145.0.tgz", - "integrity": "sha512-Jdw7nbrXiihYM/jReXK0/i8a+W/o+fLcn1f8Yzvns1jP58KBGQygqyiv5Dm+uqzS3D8/ZZnfPu3ph6aOVLPNSA==", + "version": "2.155.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.155.0.tgz", + "integrity": "sha512-AV7Ym/o7/xyDh6sqcGatWD6Bqa7Swe0OWJq+1srVww0MdBiy5yM3zYAA1+ZeqZNjFQThJPA+pYZQFTgojuaVBA==", "bin": { "cdk": "bin/cdk" }, diff --git a/packages/testing/package.json b/packages/testing/package.json index 7abd5370fb..7483cf817a 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -98,7 +98,7 @@ }, "homepage": "https://github.com/aws-powertools/powertools-lambda-typescript/tree/main/packages/testing#readme", "dependencies": { - "@aws-cdk/cli-lib-alpha": "^2.121.1-alpha.0", + "@aws-cdk/cli-lib-alpha": "^2.155.0-alpha.0", "@aws-sdk/client-lambda": "^3.637.0", "@smithy/util-utf8": "^3.0.0", "aws-cdk-lib": "^2.155.0", From 6ab5310d82aeb9861a4937112cdeb87290f4ddbe Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 2 Sep 2024 17:13:36 +0100 Subject: [PATCH 04/11] chore: override cdk lib & cli --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 00a55fc33c..eb83e9e85b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -201,9 +201,9 @@ "integrity": "sha512-twhuEG+JPOYCYPx/xy5uH2+VUsIEhPTzDY0F1KuB+ocjWWB/KEDiOVL19nHvbPCB6fhWnkykXEMJ4HHcKvjtvg==" }, "node_modules/@aws-cdk/cli-lib-alpha": { - "version": "2.121.1-alpha.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cli-lib-alpha/-/cli-lib-alpha-2.121.1-alpha.0.tgz", - "integrity": "sha512-OG8VOukqFD4YFzmMN5+ppoVSvRugWaep7UVJle6JgvCN6/AvxTb2zCsAhhrCx4mDuckubD6WAkHNz2CRsFQAWg==", + "version": "2.155.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cli-lib-alpha/-/cli-lib-alpha-2.155.0-alpha.0.tgz", + "integrity": "sha512-2hXV65AP3h21aelU//ygGdQi/33kjzD2hEe+Giv7ISuMFPk3zCIS7L+uClmtXISLGLyyCLY9xS/OmMzb3m6pxQ==", "engines": { "node": ">= 14.15.0" } @@ -17516,7 +17516,7 @@ "version": "2.7.0", "license": "MIT-0", "dependencies": { - "@aws-cdk/cli-lib-alpha": "^2.121.1-alpha.0", + "@aws-cdk/cli-lib-alpha": "^2.155.0-alpha.0", "@aws-sdk/client-lambda": "^3.637.0", "@smithy/util-utf8": "^3.0.0", "aws-cdk-lib": "^2.155.0", From b1eff6efaa8aff14b0bffac1819fecd2660cf42b Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 2 Sep 2024 17:30:45 +0100 Subject: [PATCH 05/11] test(idempotency): extend assertions --- .../tests/e2e/makeHandlerIdempotent.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts index fb13ad73d2..84ac396499 100644 --- a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts @@ -263,9 +263,16 @@ describe('Idempotency E2E tests, middy middleware usage', () => { }); expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); - // During the first invocation the function should timeout so the logs should contain 2 logs - expect(functionLogs[0]).toHaveLength(2); - expect(functionLogs[0][0]).toContain('Task timed out after'); + try { + // During the first invocation the handler should be called, so the logs should contain 1 log + expect(functionLogs[0]).toHaveLength(2); + expect(functionLogs[0][0]).toContain('Task timed out after'); + } catch { + // During the first invocation the function should timeout so the logs should not contain any log and the report log should contain a timeout message + expect(functionLogs[0]).toHaveLength(0); + expect(logs[0].getReportLog()).toMatch(/Status: timeout$/); + } + // During the second invocation the handler should be called and complete, so the logs should // contain 1 log expect(functionLogs[1]).toHaveLength(1); From a264cca4f4179e863036147d0ab54a82a8bd178e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 2 Sep 2024 17:48:29 +0100 Subject: [PATCH 06/11] chore: format & deps --- package-lock.json | 68 +++++++++++++++---- .../tests/e2e/idempotentDecorator.test.ts | 10 +-- .../tests/e2e/makeHandlerIdempotent.test.ts | 4 +- .../tests/e2e/makeIdempotent.test.ts | 4 +- packages/tracer/package.json | 3 +- .../tests/e2e/asyncHandler.decorator.test.ts | 6 +- 6 files changed, 67 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb83e9e85b..c31a285989 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6264,12 +6264,6 @@ "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", "dev": true }, - "node_modules/aws-sdk/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, "node_modules/aws-sdk/node_modules/uuid": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", @@ -9401,6 +9395,12 @@ "node": ">=8" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -15545,12 +15545,6 @@ "xtend": "~4.0.1" } }, - "node_modules/through2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, "node_modules/through2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -17539,7 +17533,7 @@ "@aws-lambda-powertools/testing-utils": "file:../testing", "@aws-sdk/client-dynamodb": "^3.637.0", "@aws-sdk/client-xray": "^3.637.0", - "aws-sdk": "^2.1686.0" + "aws-sdk": "^2.1687.0" }, "peerDependencies": { "@middy/core": "4.x || 5.x" @@ -17549,6 +17543,54 @@ "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/idempotency/tests/e2e/idempotentDecorator.test.ts b/packages/idempotency/tests/e2e/idempotentDecorator.test.ts index 7f5b7d5702..ca22922e83 100644 --- a/packages/idempotency/tests/e2e/idempotentDecorator.test.ts +++ b/packages/idempotency/tests/e2e/idempotentDecorator.test.ts @@ -1,3 +1,8 @@ +/** + * Test idempotency decorator + * + * @group e2e/idempotency/decorator + */ import { createHash } from 'node:crypto'; import { join } from 'node:path'; import { @@ -5,11 +10,6 @@ import { TestStack, invokeFunction, } from '@aws-lambda-powertools/testing-utils'; -/** - * Test idempotency decorator - * - * @group e2e/idempotency/decorator - */ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { ScanCommand } from '@aws-sdk/lib-dynamodb'; import { Duration } from 'aws-cdk-lib'; diff --git a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts index 84ac396499..f8f5339c91 100644 --- a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts @@ -1,10 +1,10 @@ -import { createHash } from 'node:crypto'; -import { join } from 'node:path'; /** * Test makeHandlerIdempotent middleware * * @group e2e/idempotency/makeHandlerIdempotent */ +import { createHash } from 'node:crypto'; +import { join } from 'node:path'; import { TestInvocationLogs, TestStack, diff --git a/packages/idempotency/tests/e2e/makeIdempotent.test.ts b/packages/idempotency/tests/e2e/makeIdempotent.test.ts index 910060d474..9ae7609198 100644 --- a/packages/idempotency/tests/e2e/makeIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeIdempotent.test.ts @@ -1,10 +1,10 @@ -import { createHash } from 'node:crypto'; -import { join } from 'node:path'; /** * Test makeIdempotent function * * @group e2e/idempotency/makeIdempotent */ +import { createHash } from 'node:crypto'; +import { join } from 'node:path'; import { TestInvocationLogs, TestStack, diff --git a/packages/tracer/package.json b/packages/tracer/package.json index e6257d5cd3..4a2e90d0cd 100644 --- a/packages/tracer/package.json +++ b/packages/tracer/package.json @@ -29,7 +29,8 @@ "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", "@aws-sdk/client-dynamodb": "^3.637.0", - "@aws-sdk/client-xray": "^3.637.0" + "@aws-sdk/client-xray": "^3.637.0", + "aws-sdk": "^2.1687.0" }, "peerDependencies": { "@middy/core": "4.x || 5.x" diff --git a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts index 85995c1cf6..81e6504a91 100644 --- a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts +++ b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts @@ -6,10 +6,7 @@ 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 { getTraces } from '@aws-lambda-powertools/testing-utils/utils/xray-traces'; import { invokeAllTestCases } from '../helpers/invokeAllTests.js'; import { TracerTestNodejsFunction } from '../helpers/resources.js'; import { @@ -135,7 +132,6 @@ describe('Tracer E2E tests, async handler with decorator instantiation', () => { expectedSegmentsCount: 4, }); - // Assess // Assess for (let i = 0; i < invocationCount; i++) { const isColdStart = i === 0; // First invocation is a cold start From ab78a7081e215393b1dff6cfcc2e6add4a1efd9c Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 2 Sep 2024 17:57:32 +0100 Subject: [PATCH 07/11] chore: simplify further --- packages/testing/src/xray-traces-utils.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/testing/src/xray-traces-utils.ts b/packages/testing/src/xray-traces-utils.ts index dda236f540..1c1bbc438e 100644 --- a/packages/testing/src/xray-traces-utils.ts +++ b/packages/testing/src/xray-traces-utils.ts @@ -90,8 +90,7 @@ const retriableGetTraceIds = (options: GetXRayTraceIdsOptions) => /** * Get the trace details for a given trace ID from the AWS X-Ray API. * - * When the trace is returned, the segments are parsed, since the document is returned - * stringified, and then sorted by start time. + * When the trace is returned, the segments are parsed, since the document is returned as a string. * * @param options - The options to get trace details, including the trace IDs and expected segments count */ @@ -113,7 +112,7 @@ const getTraceDetails = async ( ); } - const parsedAndSortedTraces: XRayTraceParsed[] = []; + const parsedTraces: XRayTraceParsed[] = []; for (const trace of traces) { const { Id: id, Segments: segments } = trace; if (segments === undefined || segments.length !== expectedSegmentsCount) { @@ -136,20 +135,14 @@ const getTraceDetails = async ( Document: JSON.parse(Document) as XRayTraceDocumentParsed, }); } - const sortedSegments = parsedSegments.sort( - (a, b) => a.Document.start_time - b.Document.start_time - ); - parsedAndSortedTraces.push({ + parsedTraces.push({ Id: id as string, - Segments: sortedSegments, + Segments: parsedSegments, }); } - return parsedAndSortedTraces.sort( - (a, b) => - a.Segments[0].Document.start_time - b.Segments[0].Document.start_time - ); + return parsedTraces; }; /** From 93e63b2117beefb3d871f87442de30e91b1c6ce7 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 2 Sep 2024 18:13:21 +0100 Subject: [PATCH 08/11] chore: extract helper function --- packages/testing/src/xray-traces-utils.ts | 76 ++++++++++++++--------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/packages/testing/src/xray-traces-utils.ts b/packages/testing/src/xray-traces-utils.ts index 1c1bbc438e..6e05e47f57 100644 --- a/packages/testing/src/xray-traces-utils.ts +++ b/packages/testing/src/xray-traces-utils.ts @@ -1,6 +1,7 @@ import { BatchGetTracesCommand, GetTraceSummariesCommand, + type Trace, XRayClient, } from '@aws-sdk/client-xray'; import promiseRetry from 'promise-retry'; @@ -87,10 +88,49 @@ const retriableGetTraceIds = (options: GetXRayTraceIdsOptions) => } }); +/** + * 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 + */ +const parseAndSortTrace = (trace: Trace, expectedSegmentsCount: number) => { + const { Id: id, Segments: segments } = trace; + if (segments === undefined || segments.length !== expectedSegmentsCount) { + throw new Error( + `Expected ${expectedSegmentsCount} segments, got ${segments ? segments.length : 0} for traceId ${trace.Id}` + ); + } + + const parsedSegments: XRaySegmentParsed[] = []; + for (const segment of segments) { + const { Id, Document } = segment; + if (Document === undefined || Id === undefined) { + throw new Error( + `Segment document or id are missing for traceId ${trace.Id}` + ); + } + + parsedSegments.push({ + Id, + Document: JSON.parse(Document) as XRayTraceDocumentParsed, + }); + } + const sortedSegments = parsedSegments.sort( + (a, b) => a.Document.start_time - b.Document.start_time + ); + + return { + Id: id as string, + Segments: sortedSegments, + }; +}; + /** * Get the trace details for a given trace ID from the AWS X-Ray API. * - * When the trace is returned, the segments are parsed, since the document is returned as a string. + * When the trace is returned, the segments are parsed, since the document is returned + * stringified, and then sorted by start time. * * @param options - The options to get trace details, including the trace IDs and expected segments count */ @@ -112,37 +152,15 @@ const getTraceDetails = async ( ); } - const parsedTraces: XRayTraceParsed[] = []; + const parsedAndSortedTraces: XRayTraceParsed[] = []; for (const trace of traces) { - const { Id: id, Segments: segments } = trace; - if (segments === undefined || segments.length !== expectedSegmentsCount) { - throw new Error( - `Expected ${expectedSegmentsCount} segments, got ${segments ? segments.length : 0} for traceId ${trace.Id}` - ); - } - - const parsedSegments: XRaySegmentParsed[] = []; - for (const segment of segments) { - const { Id, Document } = segment; - if (Document === undefined || Id === undefined) { - throw new Error( - `Segment document or id are missing for traceId ${trace.Id}` - ); - } - - parsedSegments.push({ - Id, - Document: JSON.parse(Document) as XRayTraceDocumentParsed, - }); - } - - parsedTraces.push({ - Id: id as string, - Segments: parsedSegments, - }); + parsedAndSortedTraces.push(parseAndSortTrace(trace, expectedSegmentsCount)); } - return parsedTraces; + return parsedAndSortedTraces.sort( + (a, b) => + a.Segments[0].Document.start_time - b.Segments[0].Document.start_time + ); }; /** From e30138761f8e48d701548a8363cfa73a2ab54e0d Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 2 Sep 2024 18:18:18 +0100 Subject: [PATCH 09/11] chore: reduce verbosity --- packages/testing/src/xray-traces-utils.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/testing/src/xray-traces-utils.ts b/packages/testing/src/xray-traces-utils.ts index 6e05e47f57..27c641d508 100644 --- a/packages/testing/src/xray-traces-utils.ts +++ b/packages/testing/src/xray-traces-utils.ts @@ -116,13 +116,14 @@ const parseAndSortTrace = (trace: Trace, expectedSegmentsCount: number) => { Document: JSON.parse(Document) as XRayTraceDocumentParsed, }); } - const sortedSegments = parsedSegments.sort( - (a, b) => a.Document.start_time - b.Document.start_time - ); return { Id: id as string, - Segments: sortedSegments, + Segments: [ + ...parsedSegments.sort( + (a, b) => a.Document.start_time - b.Document.start_time + ), + ], }; }; From e2ee7fe99b01870d776db8466fc44df71a863665 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 2 Sep 2024 18:22:36 +0100 Subject: [PATCH 10/11] chore: fix sort usage --- packages/testing/src/xray-traces-utils.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/testing/src/xray-traces-utils.ts b/packages/testing/src/xray-traces-utils.ts index 27c641d508..9fb04c9137 100644 --- a/packages/testing/src/xray-traces-utils.ts +++ b/packages/testing/src/xray-traces-utils.ts @@ -119,11 +119,9 @@ const parseAndSortTrace = (trace: Trace, expectedSegmentsCount: number) => { return { Id: id as string, - Segments: [ - ...parsedSegments.sort( - (a, b) => a.Document.start_time - b.Document.start_time - ), - ], + Segments: [...parsedSegments].sort( + (a, b) => a.Document.start_time - b.Document.start_time + ), }; }; From ff6fb6e46a9d9f222d0ac9a94946e015eb6f40b3 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 4 Sep 2024 16:07:48 +0100 Subject: [PATCH 11/11] Update packages/tracer/tests/helpers/invokeAllTests.ts Co-authored-by: Alexander Schueren --- packages/tracer/tests/helpers/invokeAllTests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tracer/tests/helpers/invokeAllTests.ts b/packages/tracer/tests/helpers/invokeAllTests.ts index 5a6c934281..981a326397 100644 --- a/packages/tracer/tests/helpers/invokeAllTests.ts +++ b/packages/tracer/tests/helpers/invokeAllTests.ts @@ -4,7 +4,6 @@ import { invokeFunction } from '@aws-lambda-powertools/testing-utils'; * Invoke function sequentially 3 times with different parameters * * invocation: is just a tracking number (it has to start from 1) - * sdkV2: define if we will use `captureAWSClient()` or `captureAWS()` for SDK V2 * throw: forces the Lambda to throw an error * * @param functionName