From 587d58f17571a52b3637ce97a24113ac38d36376 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 11 May 2023 19:59:52 +0200 Subject: [PATCH 1/2] style(tracer): apply standardized formatting --- packages/tracer/.eslintrc.js | 72 ++ packages/tracer/jest.config.js | 55 +- packages/tracer/package.json | 7 +- packages/tracer/src/Tracer.ts | 464 +++++---- packages/tracer/src/TracerInterface.ts | 49 +- .../src/config/ConfigServiceInterface.ts | 17 +- .../src/config/EnvironmentVariablesService.ts | 18 +- packages/tracer/src/config/index.ts | 2 +- packages/tracer/src/helpers.ts | 7 +- packages/tracer/src/index.ts | 2 +- packages/tracer/src/middleware/middy.ts | 43 +- .../tracer/src/provider/ProviderService.ts | 47 +- .../src/provider/ProviderServiceInterface.ts | 42 +- packages/tracer/src/provider/index.ts | 2 +- packages/tracer/src/types/Tracer.ts | 82 +- packages/tracer/src/types/index.ts | 2 +- ...allFeatures.decorator.test.functionCode.ts | 71 +- .../tests/e2e/allFeatures.decorator.test.ts | 585 ++++++----- .../allFeatures.manual.test.functionCode.ts | 46 +- .../tests/e2e/allFeatures.manual.test.ts | 225 +++-- .../allFeatures.middy.test.functionCode.ts | 48 +- .../tests/e2e/allFeatures.middy.test.ts | 593 ++++++----- ...syncHandler.decorator.test.functionCode.ts | 69 +- .../tests/e2e/asyncHandler.decorator.test.ts | 340 ++++--- packages/tracer/tests/e2e/constants.ts | 2 +- .../helpers/FunctionSegmentNotDefinedError.ts | 4 +- .../helpers/populateEnvironmentVariables.ts | 7 +- .../tracer/tests/helpers/traceAssertions.ts | 22 +- .../tracer/tests/helpers/traceUtils.types.ts | 106 +- packages/tracer/tests/helpers/tracesUtils.ts | 205 ++-- .../tracer/tests/unit/ProviderService.test.ts | 149 +-- packages/tracer/tests/unit/Tracer.test.ts | 943 +++++++++++------- .../EnvironmentVariablesService.test.ts | 22 +- packages/tracer/tests/unit/helpers.test.ts | 241 ++--- packages/tracer/tests/unit/middy.test.ts | 155 +-- 35 files changed, 2745 insertions(+), 1999 deletions(-) create mode 100644 packages/tracer/.eslintrc.js diff --git a/packages/tracer/.eslintrc.js b/packages/tracer/.eslintrc.js new file mode 100644 index 0000000000..2e4ca18fb2 --- /dev/null +++ b/packages/tracer/.eslintrc.js @@ -0,0 +1,72 @@ +module.exports = { + env: { + browser: false, + es2020: true, + jest: true, + node: true, + }, + ignorePatterns: ['coverage', 'lib'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint', 'prettier'], + settings: { + 'import/resolver': { + node: {}, + typescript: { + project: './tsconfig.json', + alwaysTryTypes: true, + }, + }, + }, + rules: { + '@typescript-eslint/explicit-function-return-type': [ + 'error', + { allowExpressions: true }, + ], // Enforce return type definitions for functions + '@typescript-eslint/explicit-member-accessibility': 'error', // Enforce explicit accessibility modifiers on class properties and methods (public, private, protected) + '@typescript-eslint/member-ordering': [ + // Standardize the order of class members + 'error', + { + default: { + memberTypes: [ + 'signature', + 'public-field', + 'protected-field', + 'private-field', + 'constructor', + 'public-method', + 'protected-method', + 'private-method', + ], + order: 'alphabetically', + }, + }, + ], + '@typescript-eslint/no-explicit-any': 'error', // Disallow usage of the any type + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // Disallow unused variables, except for variables starting with an underscore + '@typescript-eslint/no-use-before-define': ['off'], // Check if this rule is needed + 'no-unused-vars': 'off', // Disable eslint core rule, since it's replaced by @typescript-eslint/no-unused-vars + // Rules from eslint core https://eslint.org/docs/latest/rules/ + 'array-bracket-spacing': ['error', 'never'], // Disallow spaces inside of array brackets + 'computed-property-spacing': ['error', 'never'], // Disallow spaces inside of computed properties + 'func-style': ['warn', 'expression'], // Enforce function expressions instead of function declarations + 'keyword-spacing': 'error', // Enforce spaces after keywords and before parenthesis, e.g. if (condition) instead of if(condition) + 'padding-line-between-statements': [ + // Require an empty line before return statements + 'error', + { blankLine: 'always', prev: '*', next: 'return' }, + ], + 'no-console': 0, // Allow console.log statements + 'no-multi-spaces': ['error', { ignoreEOLComments: false }], // Disallow multiple spaces except for comments + 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], // Enforce no empty line at the beginning & end of files and max 1 empty line between consecutive statements + 'no-throw-literal': 'error', // Disallow throwing literals as exceptions, e.g. throw 'error' instead of throw new Error('error') + 'object-curly-spacing': ['error', 'always'], // Enforce spaces inside of curly braces in objects + 'prefer-arrow-callback': 'error', // Enforce arrow functions instead of anonymous functions for callbacks + quotes: ['error', 'single', { allowTemplateLiterals: true }], // Enforce single quotes except for template strings + semi: ['error', 'always'], // Require semicolons instead of ASI (automatic semicolon insertion) at the end of statements + }, +}; diff --git a/packages/tracer/jest.config.js b/packages/tracer/jest.config.js index d23175955a..87da06ecbb 100644 --- a/packages/tracer/jest.config.js +++ b/packages/tracer/jest.config.js @@ -3,43 +3,26 @@ module.exports = { name: 'AWS Lambda Powertools utility: TRACER', color: 'white', }, - 'runner': 'groups', - 'preset': 'ts-jest', - 'transform': { + runner: 'groups', + preset: 'ts-jest', + transform: { '^.+\\.ts?$': 'ts-jest', }, - moduleFileExtensions: [ 'js', 'ts' ], - 'collectCoverageFrom': [ - '**/src/**/*.ts', - '!**/node_modules/**', - ], - 'testMatch': ['**/?(*.)+(spec|test).ts'], - 'roots': [ - '/src', - '/tests', - ], - 'testPathIgnorePatterns': [ - '/node_modules/', - ], - 'testEnvironment': 'node', - 'coveragePathIgnorePatterns': [ - '/node_modules/', - '/types/', - ], - 'coverageThreshold': { - 'global': { - 'statements': 100, - 'branches': 100, - 'functions': 100, - 'lines': 100, + moduleFileExtensions: ['js', 'ts'], + collectCoverageFrom: ['**/src/**/*.ts', '!**/node_modules/**'], + testMatch: ['**/?(*.)+(spec|test).ts'], + roots: ['/src', '/tests'], + testPathIgnorePatterns: ['/node_modules/'], + testEnvironment: 'node', + coveragePathIgnorePatterns: ['/node_modules/', '/types/'], + coverageThreshold: { + global: { + statements: 100, + branches: 100, + functions: 100, + lines: 100, }, }, - 'coverageReporters': [ - 'json-summary', - 'text', - 'lcov' - ], - 'setupFiles': [ - '/tests/helpers/populateEnvironmentVariables.ts' - ] -}; \ No newline at end of file + coverageReporters: ['json-summary', 'text', 'lcov'], + setupFiles: ['/tests/helpers/populateEnvironmentVariables.ts'], +}; diff --git a/packages/tracer/package.json b/packages/tracer/package.json index b0c1d993b6..9da5c77f2b 100644 --- a/packages/tracer/package.json +++ b/packages/tracer/package.json @@ -19,14 +19,15 @@ "test:e2e": "jest --group=e2e", "watch": "jest --watch", "build": "tsc", - "lint": "eslint --ext .ts --no-error-on-unmatched-pattern src tests", - "lint-fix": "eslint --fix --ext .ts --no-error-on-unmatched-pattern src tests", + "lint": "eslint --ext .ts,.js --no-error-on-unmatched-pattern .", + "lint-fix": "eslint --fix --ext .ts,.js --no-error-on-unmatched-pattern .", "package": "mkdir -p dist/ && npm pack && mv *.tgz dist/", "package-bundle": "../../package-bundler.sh tracer-bundle ./dist", "prepare": "npm run build" }, "lint-staged": { - "*.ts": "npm run lint-fix" + "*.ts": "npm run lint-fix", + "*.js": "npm run lint-fix" }, "homepage": "https://github.com/awslabs/aws-lambda-powertools-typescript/tree/main/packages/tracer#readme", "license": "MIT-0", diff --git a/packages/tracer/src/Tracer.ts b/packages/tracer/src/Tracer.ts index 6b8cff3009..29f3078f2c 100644 --- a/packages/tracer/src/Tracer.ts +++ b/packages/tracer/src/Tracer.ts @@ -1,88 +1,98 @@ import { Handler } from 'aws-lambda'; -import { AsyncHandler, SyncHandler, Utility } from '@aws-lambda-powertools/commons'; +import { + AsyncHandler, + SyncHandler, + Utility, +} from '@aws-lambda-powertools/commons'; import { TracerInterface } from '.'; import { ConfigServiceInterface, EnvironmentVariablesService } from './config'; -import { HandlerMethodDecorator, TracerOptions, MethodDecorator, CaptureLambdaHandlerOptions, CaptureMethodOptions } from './types'; +import { + HandlerMethodDecorator, + TracerOptions, + MethodDecorator, + CaptureLambdaHandlerOptions, + CaptureMethodOptions, +} from './types'; import { ProviderService, ProviderServiceInterface } from './provider'; import { Segment, Subsegment } from 'aws-xray-sdk-core'; /** * ## Intro * Tracer is an opinionated thin wrapper for [AWS X-Ray SDK for Node.js](https://github.com/aws/aws-xray-sdk-node). - * + * * Tracing data can be visualized through AWS X-Ray Console. - * + * * ## Key features * * Auto capture cold start as annotation, and responses or full exceptions as metadata * * Auto-disable when not running in AWS Lambda environment * * Automatically trace HTTP(s) clients and generate segments for each request * * Support tracing functions via decorators, middleware, and manual instrumentation * * Support tracing AWS SDK v2 and v3 via AWS X-Ray SDK for Node.js - * + * * ## Usage - * + * * For more usage examples, see [our documentation](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/core/tracer/). - * + * * ### Functions usage with middleware - * + * * If you use function-based Lambda handlers you can use the [captureLambdaHandler()](./_aws_lambda_powertools_tracer.Tracer.html) middy middleware to automatically: - * * handle the subsegment lifecycle + * * handle the subsegment lifecycle * * add the `ServiceName` and `ColdStart` annotations * * add the function response as metadata * * add the function error as metadata (if any) - * + * * @example * ```typescript * import { captureLambdaHandler, Tracer } from '@aws-lambda-powertools/tracer'; * import middy from '@middy/core'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - * + * * const lambdaHandler = async (_event: any, _context: any) => { * ... * }; - * + * * export const handler = middy(lambdaHandler).use(captureLambdaHandler(tracer)); * ``` - * + * * ### Object oriented usage with decorators - * + * * If instead you use TypeScript Classes to wrap your Lambda handler you can use the [@tracer.captureLambdaHandler()](./_aws_lambda_powertools_tracer.Tracer.html#captureLambdaHandler) decorator to automatically: - * * handle the subsegment lifecycle + * * handle the subsegment lifecycle * * add the `ServiceName` and `ColdStart` annotations * * add the function response as metadata * * add the function error as metadata (if any) - * + * * @example * ```typescript * import { Tracer } from '@aws-lambda-powertools/tracer'; * import { LambdaInterface } from '@aws-lambda-powertools/commons'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - * + * * // FYI: Decorator might not render properly in VSCode mouse over due to https://github.com/microsoft/TypeScript/issues/47679 and might show as *@tracer* instead of `@tracer.captureLambdaHandler` - * + * * class Lambda implements LambdaInterface { * @tracer.captureLambdaHandler() * public handler(event: any, context: any) { * ... * } * } - * + * * const handlerClass = new Lambda(); * export const handler = handlerClass.handler.bind(handlerClass); * ``` - * + * * ### Functions usage with manual instrumentation - * + * * If you prefer to manually instrument your Lambda handler you can use the methods in the tracer class directly. * * @example * ```typescript * import { Tracer } from '@aws-lambda-powertools/tracer'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - * + * * export const handler = async (_event: any, context: any) => { * const segment = tracer.getSegment(); // This is the facade segment (the one that is created by AWS Lambda) * // Create subsegment for the function & set it as active @@ -96,7 +106,7 @@ import { Segment, Subsegment } from 'aws-xray-sdk-core'; * let res; * try { * // ... your own logic goes here - * // Add the response as metadata + * // Add the response as metadata * tracer.addResponseAsMetadata(res, process.env._HANDLER); * } catch (err) { * // Add the error as metadata @@ -114,24 +124,23 @@ import { Segment, Subsegment } from 'aws-xray-sdk-core'; * ``` */ class Tracer extends Utility implements TracerInterface { - public provider: ProviderServiceInterface; - - private captureError: boolean = true; - private captureHTTPsRequests: boolean = true; - - private captureResponse: boolean = true; + private captureError = true; + + private captureHTTPsRequests = true; + + private captureResponse = true; private customConfigService?: ConfigServiceInterface; - + // envVarsService is always initialized in the constructor in setOptions() private envVarsService!: EnvironmentVariablesService; - + // serviceName is always initialized in the constructor in setOptions() private serviceName!: string; - - private tracingEnabled: boolean = true; + + private tracingEnabled = true; public constructor(options: TracerOptions = {}) { super(); @@ -148,13 +157,13 @@ class Tracer extends Utility implements TracerInterface { } /** - * Add an error to the current segment or subsegment as metadata. - * - * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-errors - * - * @param error - Error to serialize as metadata - * @param [remote] - Whether the error was thrown by a remote service. Defaults to `false` - */ + * Add an error to the current segment or subsegment as metadata. + * + * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-errors + * + * @param error - Error to serialize as metadata + * @param [remote] - Whether the error was thrown by a remote service. Defaults to `false` + */ public addErrorAsMetadata(error: Error, remote?: boolean): void { if (!this.isTracingEnabled()) { return; @@ -162,7 +171,6 @@ class Tracer extends Utility implements TracerInterface { const subsegment = this.getSegment(); if (subsegment === undefined) { - return; } @@ -176,15 +184,19 @@ class Tracer extends Utility implements TracerInterface { } /** - * Add response data to the current segment or subsegment as metadata. - * - * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-annotations - * - * @param data - Data to serialize as metadata - * @param methodName - Name of the method that is being traced - */ + * Add response data to the current segment or subsegment as metadata. + * + * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-annotations + * + * @param data - Data to serialize as metadata + * @param methodName - Name of the method that is being traced + */ public addResponseAsMetadata(data?: unknown, methodName?: string): void { - if (data === undefined || !this.captureResponse || !this.isTracingEnabled()) { + if ( + data === undefined || + !this.captureResponse || + !this.isTracingEnabled() + ) { return; } @@ -193,7 +205,7 @@ class Tracer extends Utility implements TracerInterface { /** * Add service name to the current segment or subsegment as annotation. - * + * */ public addServiceNameAnnotation(): void { if (!this.isTracingEnabled()) { @@ -204,11 +216,11 @@ class Tracer extends Utility implements TracerInterface { /** * Add ColdStart annotation to the current segment or subsegment. - * + * * If Tracer has been initialized outside the Lambda handler then the same instance * of Tracer will be reused throughout the lifecycle of that same Lambda execution environment * and this method will annotate `ColdStart: false` after the first invocation. - * + * * @see https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html */ public annotateColdStart(): void { @@ -219,23 +231,23 @@ class Tracer extends Utility implements TracerInterface { /** * Patch all AWS SDK v2 clients and create traces when your application makes calls to AWS services. - * + * * If you want to patch a specific client use {@link captureAWSClient} and if you are using AWS SDK v3 use {@link captureAWSv3Client} instead. - * + * * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-awssdkclients.html - * + * * @example * ```typescript * import { Tracer } from '@aws-lambda-powertools/tracer'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); * const AWS = tracer.captureAWS(require('aws-sdk')); - * + * * export const handler = async (_event: any, _context: any) => { * ... * } * ``` - * + * * @param aws - AWS SDK v2 import * @returns AWS - Instrumented AWS SDK */ @@ -247,24 +259,24 @@ class Tracer extends Utility implements TracerInterface { /** * Patch a specific AWS SDK v2 client and create traces when your application makes calls to that AWS service. - * + * * If you want to patch all clients use {@link captureAWS} and if you are using AWS SDK v3 use {@link captureAWSv3Client} instead. - * + * * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-awssdkclients.html - * + * * @example * ```typescript * import { S3 } from 'aws-sdk'; * import { Tracer } from '@aws-lambda-powertools/tracer'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); * const s3 = tracer.captureAWSClient(new S3({ apiVersion: '2006-03-01' })); - * + * * export const handler = async (_event: any, _context: any) => { * ... * } * ``` - * + * * @param service - AWS SDK v2 client * @returns service - Instrumented AWS SDK v2 client */ @@ -276,10 +288,10 @@ class Tracer extends Utility implements TracerInterface { } catch (error) { try { // This is needed because some aws-sdk clients like AWS.DynamoDB.DocumentDB don't comply with the same - // instrumentation contract like most base clients. + // instrumentation contract like most base clients. // For detailed explanation see: https://github.com/awslabs/aws-lambda-powertools-typescript/issues/524#issuecomment-1024493662 this.provider.captureAWSClient((service as T & { service: T }).service); - + return service; } catch { throw error; @@ -289,25 +301,25 @@ class Tracer extends Utility implements TracerInterface { /** * Patch an AWS SDK v3 client and create traces when your application makes calls to that AWS service. - * + * * If you are using AWS SDK v2 use {@link captureAWSClient} instead. - * + * * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-awssdkclients.html - * + * * @example * ```typescript * import { S3Client } from '@aws-sdk/client-s3'; * import { Tracer } from '@aws-lambda-powertools/tracer'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); * const client = new S3Client({}); * tracer.captureAWSv3Client(client); - * + * * export const handler = async (_event: any, _context: any) => { * ... * } * ``` - * + * * @param service - AWS SDK v3 client * @returns service - Instrumented AWS SDK v3 client */ @@ -319,42 +331,44 @@ class Tracer extends Utility implements TracerInterface { /** * A decorator automating capture of metadata and annotations on segments or subsegments for a Lambda Handler. - * + * * Using this decorator on your handler function will automatically: - * * handle the subsegment lifecycle + * * handle the subsegment lifecycle * * add the `ColdStart` annotation * * add the function response as metadata * * add the function error as metadata (if any) - * + * * Note: Currently TypeScript only supports decorators on classes and methods. If you are using the * function syntax, you should use the middleware instead. - * + * * @example * ```typescript * import { Tracer } from '@aws-lambda-powertools/tracer'; * import { LambdaInterface } from '@aws-lambda-powertools/commons'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - * + * * class Lambda implements LambdaInterface { * @tracer.captureLambdaHandler() * public handler(event: any, context: any) { * ... * } * } - * + * * const handlerClass = new Lambda(); * export const handler = handlerClass.handler.bind(handlerClass); * ``` - * + * * @decorator Class * @param options - (_optional_) Options for the decorator */ - public captureLambdaHandler(options?: CaptureLambdaHandlerOptions): HandlerMethodDecorator { + public captureLambdaHandler( + options?: CaptureLambdaHandlerOptions + ): HandlerMethodDecorator { return (_target, _propertyKey, descriptor) => { /** * The descriptor.value is the method this decorator decorates, it cannot be undefined. - */ + */ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const originalMethod = descriptor.value!; @@ -362,35 +376,41 @@ class Tracer extends Utility implements TracerInterface { const tracerRef = this; // Use a function() {} instead of an () => {} arrow function so that we can // access `myClass` as `this` in a decorated `myClass.myMethod()`. - descriptor.value = (function (this: Handler, event, context, callback) { + descriptor.value = function (this: Handler, event, context, callback) { // eslint-disable-next-line @typescript-eslint/no-this-alias const handlerRef: Handler = this; if (!tracerRef.isTracingEnabled()) { - return originalMethod.apply(handlerRef, [ event, context, callback ]); + return originalMethod.apply(handlerRef, [event, context, callback]); } - return tracerRef.provider.captureAsyncFunc(`## ${process.env._HANDLER}`, async subsegment => { - tracerRef.annotateColdStart(); - tracerRef.addServiceNameAnnotation(); - let result: unknown; - try { - result = await originalMethod.apply(handlerRef, [ event, context, callback ]); - if (options?.captureResponse ?? true) { - tracerRef.addResponseAsMetadata(result, process.env._HANDLER); + return tracerRef.provider.captureAsyncFunc( + `## ${process.env._HANDLER}`, + async (subsegment) => { + tracerRef.annotateColdStart(); + tracerRef.addServiceNameAnnotation(); + let result: unknown; + try { + result = await originalMethod.apply(handlerRef, [ + event, + context, + callback, + ]); + if (options?.captureResponse ?? true) { + tracerRef.addResponseAsMetadata(result, process.env._HANDLER); + } + } catch (error) { + tracerRef.addErrorAsMetadata(error as Error); + throw error; + } finally { + subsegment?.close(); + subsegment?.flush(); } - } catch (error) { - tracerRef.addErrorAsMetadata(error as Error); - throw error; - } finally { - subsegment?.close(); - subsegment?.flush(); + return result; } - - return result; - }); - }) as SyncHandler | AsyncHandler; + ); + } as SyncHandler | AsyncHandler; return descriptor; }; @@ -398,37 +418,37 @@ class Tracer extends Utility implements TracerInterface { /** * A decorator automating capture of metadata and annotations on segments or subsegments for an arbitrary function. - * + * * Using this decorator on your function will automatically: - * * handle the subsegment lifecycle + * * handle the subsegment lifecycle * * add the function response as metadata * * add the function error as metadata (if any) - * + * * Note: Currently TypeScript only supports decorators on classes and methods. If you are using the * function syntax, you should use the middleware instead. - * + * * @example * ```typescript * import { Tracer } from '@aws-lambda-powertools/tracer'; * import { LambdaInterface } from '@aws-lambda-powertools/commons'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - * + * * class Lambda implements LambdaInterface { * @tracer.captureMethod() * public myMethod(param: any) { * ... * } - * + * * public handler(event: any, context: any) { * ... * } * } - * + * * const handlerClass = new Lambda(); - * export const handler = handlerClass.handler.bind(handlerClass);; + * export const handler = handlerClass.handler.bind(handlerClass);; * ``` - * + * * @decorator Class * @param options - (_optional_) Options for the decorator */ @@ -437,7 +457,7 @@ class Tracer extends Utility implements TracerInterface { // The descriptor.value is the method this decorator decorates, it cannot be undefined. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const originalMethod = descriptor.value!; - + // eslint-disable-next-line @typescript-eslint/no-this-alias const tracerRef = this; // Use a function() {} instead of an () => {} arrow function so that we can @@ -448,26 +468,31 @@ class Tracer extends Utility implements TracerInterface { } const methodName = String(propertyKey); - const subsegmentName = options?.subSegmentName ? options.subSegmentName : `### ${methodName}`; - - return tracerRef.provider.captureAsyncFunc(subsegmentName, async subsegment => { - let result; - try { - result = await originalMethod.apply(this, [...args]); - if (options?.captureResponse ?? true) { - tracerRef.addResponseAsMetadata(result, methodName); + const subsegmentName = options?.subSegmentName + ? options.subSegmentName + : `### ${methodName}`; + + return tracerRef.provider.captureAsyncFunc( + subsegmentName, + async (subsegment) => { + let result; + try { + result = await originalMethod.apply(this, [...args]); + if (options?.captureResponse ?? true) { + tracerRef.addResponseAsMetadata(result, methodName); + } + } catch (error) { + tracerRef.addErrorAsMetadata(error as Error); + + throw error; + } finally { + subsegment?.close(); + subsegment?.flush(); } - } catch (error) { - tracerRef.addErrorAsMetadata(error as Error); - - throw error; - } finally { - subsegment?.close(); - subsegment?.flush(); + + return result; } - - return result; - }); + ); }; return descriptor; @@ -476,17 +501,17 @@ class Tracer extends Utility implements TracerInterface { /** * Get the current root AWS X-Ray trace id. - * + * * Utility method that returns the current AWS X-Ray Root trace id. Useful as correlation id for downstream processes. * * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-traces - * + * * @example * ```typescript * import { Tracer } from '@aws-lambda-powertools/tracer'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - * + * * export const handler = async () => { * try { * ... @@ -504,7 +529,7 @@ class Tracer extends Utility implements TracerInterface { * } * } * ``` - * + * * @returns string - The root X-Ray trace id. */ public getRootXrayTraceId(): string | undefined { @@ -512,50 +537,50 @@ class Tracer extends Utility implements TracerInterface { return this.envVarsService.getXrayTraceId(); } - + /** * Get the active segment or subsegment (if any) in the current scope. - * + * * Usually you won't need to call this method unless you are creating custom subsegments or using manual mode. * * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-segments * @see https://awslabs.github.io/aws-lambda-powertools-typescript/latest/core/tracer/#escape-hatch-mechanism - * + * * @example * ```typescript * import { Tracer } from '@aws-lambda-powertools/tracer'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - * + * * export const handler = async (_event: any, _context: any) => { * const currentSegment = tracer.getSegment(); * ... // Do something with segment * } * ``` - * + * * @returns The active segment or subsegment in the current scope. Will log a warning and return `undefined` if no segment is found. */ public getSegment(): Segment | Subsegment | undefined { if (!this.isTracingEnabled()) { return new Subsegment('## Dummy segment'); } - const segment = this.provider.getSegment(); + const segment = this.provider.getSegment(); if (segment === undefined) { console.warn( 'Failed to get the current sub/segment from the context, this is likely because you are not using the Tracer in a Lambda function.' ); } - + return segment; } /** * Get the current value of the AWS X-Ray Sampled flag. - * + * * Utility method that returns the current AWS X-Ray Sampled flag. * * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-traces - * + * * @returns boolean - `true` if the trace is sampled, `false` if tracing is disabled or the trace is not sampled. */ public isTraceSampled(): boolean { @@ -566,11 +591,11 @@ class Tracer extends Utility implements TracerInterface { /** * Get the current value of the `tracingEnabled` property. - * + * * You can use this method during manual instrumentation to determine * if tracer is currently enabled. - * - * @returns tracingEnabled - `true` if tracing is enabled, `false` otherwise. + * + * @returns tracingEnabled - `true` if tracing is enabled, `false` otherwise. */ public isTracingEnabled(): boolean { return this.tracingEnabled; @@ -578,20 +603,20 @@ class Tracer extends Utility implements TracerInterface { /** * Adds annotation to existing segment or subsegment. - * + * * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-segment.html#xray-sdk-nodejs-segment-annotations - * + * * @example * ```typescript * import { Tracer } from '@aws-lambda-powertools/tracer'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - * + * * export const handler = async (_event: any, _context: any) => { * tracer.putAnnotation('successfulBooking', true); * } * ``` - * + * * @param key - Annotation key * @param value - Value for annotation */ @@ -600,59 +625,63 @@ class Tracer extends Utility implements TracerInterface { this.provider.putAnnotation(key, value); } - + /** * Adds metadata to existing segment or subsegment. - * + * * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-segment.html#xray-sdk-nodejs-segment-metadata - * + * * @example * ```typescript * import { Tracer } from '@aws-lambda-powertools/tracer'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - * + * * export const handler = async (_event: any, _context: any) => { * const res = someLogic(); * tracer.putMetadata('paymentResponse', res); * } * ``` - * + * * @param key - Metadata key * @param value - Value for metadata * @param namespace - Namespace that metadata will lie under, if none is passed it will use the serviceName */ - public putMetadata(key: string, value: unknown, namespace?: string | undefined): void { + public putMetadata( + key: string, + value: unknown, + namespace?: string | undefined + ): void { if (!this.isTracingEnabled()) return; this.provider.putMetadata(key, value, namespace || this.serviceName); } - + /** * Sets the passed subsegment as the current active subsegment. - * + * * If you are using a middleware or a decorator this is done automatically for you. - * + * * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-subsegments.html - * + * * @example * ```typescript * import { Tracer } from '@aws-lambda-powertools/tracer'; * import { Subsegment } from 'aws-xray-sdk-core'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - * + * * export const handler = async (_event: any, _context: any) => { * const subsegment = new Subsegment('### foo.bar'); * tracer.setSegment(subsegment); * } * ``` - * + * * @param segment - Subsegment to set as the current segment */ public setSegment(segment: Segment | Subsegment): void { if (!this.isTracingEnabled()) return; - + return this.provider.setSegment(segment); } @@ -677,9 +706,12 @@ class Tracer extends Utility implements TracerInterface { * Used internally during initialization. */ private isAmplifyCli(): boolean { - return this.getEnvVarsService().getAwsExecutionEnv() === 'AWS_Lambda_amplify-mock'; + return ( + this.getEnvVarsService().getAwsExecutionEnv() === + 'AWS_Lambda_amplify-mock' + ); } - + /** * Determine if we are running in a Lambda execution environment. * Used internally during initialization. @@ -687,7 +719,7 @@ class Tracer extends Utility implements TracerInterface { private isLambdaExecutionEnv(): boolean { return this.getEnvVarsService().getAwsExecutionEnv() !== ''; } - + /** * Determine if we are running inside a SAM CLI process. * Used internally during initialization. @@ -701,8 +733,12 @@ class Tracer extends Utility implements TracerInterface { * Used internally during initialization. */ private setCaptureError(): void { - const customConfigValue = this.getCustomConfigService()?.getTracingCaptureError(); - if (customConfigValue !== undefined && customConfigValue.toLowerCase() === 'false') { + const customConfigValue = + this.getCustomConfigService()?.getTracingCaptureError(); + if ( + customConfigValue !== undefined && + customConfigValue.toLowerCase() === 'false' + ) { this.captureError = false; return; @@ -721,9 +757,9 @@ class Tracer extends Utility implements TracerInterface { * * Calls using third-party HTTP request libraries, such as Axios, are supported as long as they use the native http * module under the hood. Support for third-party HTTP request libraries is provided on a best effort basis. - * + * * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-httpclients.html - * + * * @param enabled - Whether or not to patch all HTTP clients * @returns void */ @@ -734,8 +770,12 @@ class Tracer extends Utility implements TracerInterface { return; } - const customConfigValue = this.getCustomConfigService()?.getCaptureHTTPsRequests(); - if (customConfigValue !== undefined && customConfigValue.toLowerCase() === 'false') { + const customConfigValue = + this.getCustomConfigService()?.getCaptureHTTPsRequests(); + if ( + customConfigValue !== undefined && + customConfigValue.toLowerCase() === 'false' + ) { this.captureHTTPsRequests = false; return; @@ -754,8 +794,12 @@ class Tracer extends Utility implements TracerInterface { * Used internally during initialization. */ private setCaptureResponse(): void { - const customConfigValue = this.getCustomConfigService()?.getTracingCaptureResponse(); - if (customConfigValue !== undefined && customConfigValue.toLowerCase() === 'false') { + const customConfigValue = + this.getCustomConfigService()?.getTracingCaptureResponse(); + if ( + customConfigValue !== undefined && + customConfigValue.toLowerCase() === 'false' + ) { this.captureResponse = false; return; @@ -772,11 +816,15 @@ class Tracer extends Utility implements TracerInterface { /** * Setter for `customConfigService` based on configuration passed. * Used internally during initialization. - * + * * @param customConfigService - Custom configuration service to use */ - private setCustomConfigService(customConfigService?: ConfigServiceInterface): void { - this.customConfigService = customConfigService ? customConfigService : undefined; + private setCustomConfigService( + customConfigService?: ConfigServiceInterface + ): void { + this.customConfigService = customConfigService + ? customConfigService + : undefined; } /** @@ -790,16 +838,12 @@ class Tracer extends Utility implements TracerInterface { /** * Method that reconciles the configuration passed with the environment variables. * Used internally during initialization. - * + * * @param options - Configuration passed to the tracer */ private setOptions(options: TracerOptions): Tracer { - const { - enabled, - serviceName, - captureHTTPsRequests, - customConfigService - } = options; + const { enabled, serviceName, captureHTTPsRequests, customConfigService } = + options; this.setEnvVarsService(); this.setCustomConfigService(customConfigService); @@ -815,7 +859,7 @@ class Tracer extends Utility implements TracerInterface { /** * Setter for `customConfigService` based on configurations passed and environment variables. * Used internally during initialization. - * + * * @param serviceName - Name of the service to use */ private setServiceName(serviceName?: string): void { @@ -826,7 +870,10 @@ class Tracer extends Utility implements TracerInterface { } const customConfigValue = this.getCustomConfigService()?.getServiceName(); - if (customConfigValue !== undefined && this.isValidServiceName(customConfigValue)) { + if ( + customConfigValue !== undefined && + this.isValidServiceName(customConfigValue) + ) { this.serviceName = customConfigValue; return; @@ -844,37 +891,42 @@ class Tracer extends Utility implements TracerInterface { /** * Setter for `tracingEnabled` based on configurations passed and environment variables. * Used internally during initialization. - * + * * @param enabled - Whether or not tracing is enabled */ private setTracingEnabled(enabled?: boolean): void { if (enabled !== undefined && !enabled) { this.tracingEnabled = enabled; - + return; } - const customConfigValue = this.getCustomConfigService()?.getTracingEnabled(); - if (customConfigValue !== undefined && customConfigValue.toLowerCase() === 'false') { + const customConfigValue = + this.getCustomConfigService()?.getTracingEnabled(); + if ( + customConfigValue !== undefined && + customConfigValue.toLowerCase() === 'false' + ) { this.tracingEnabled = false; - + return; } const envVarsValue = this.getEnvVarsService().getTracingEnabled(); if (envVarsValue.toLowerCase() === 'false') { this.tracingEnabled = false; - + return; } - if (this.isAmplifyCli() || this.isLambdaSamCli() || !this.isLambdaExecutionEnv()) { + if ( + this.isAmplifyCli() || + this.isLambdaSamCli() || + !this.isLambdaExecutionEnv() + ) { this.tracingEnabled = false; } } - } -export { - Tracer -}; \ No newline at end of file +export { Tracer }; diff --git a/packages/tracer/src/TracerInterface.ts b/packages/tracer/src/TracerInterface.ts index 30a78d86cc..65d7d4a10a 100644 --- a/packages/tracer/src/TracerInterface.ts +++ b/packages/tracer/src/TracerInterface.ts @@ -1,25 +1,34 @@ -import { CaptureLambdaHandlerOptions, CaptureMethodOptions, HandlerMethodDecorator, MethodDecorator } from './types'; +import { + CaptureLambdaHandlerOptions, + CaptureMethodOptions, + HandlerMethodDecorator, + MethodDecorator, +} from './types'; import { Segment, Subsegment } from 'aws-xray-sdk-core'; interface TracerInterface { - addErrorAsMetadata(error: Error, remote?: boolean): void - addResponseAsMetadata(data?: unknown, methodName?: string): void - addServiceNameAnnotation(): void - annotateColdStart(): void - captureAWS(aws: T): void | T - captureAWSv3Client(service: T): void | T - captureAWSClient(service: T): void | T - captureLambdaHandler(options?: CaptureLambdaHandlerOptions): HandlerMethodDecorator - captureMethod(options?: CaptureMethodOptions): MethodDecorator - getSegment(): Segment | Subsegment | undefined - getRootXrayTraceId(): string | undefined - isTraceSampled(): boolean - isTracingEnabled(): boolean - putAnnotation: (key: string, value: string | number | boolean) => void - putMetadata: (key: string, value: unknown, namespace?: string | undefined) => void - setSegment(segment: Segment | Subsegment): void + addErrorAsMetadata(error: Error, remote?: boolean): void; + addResponseAsMetadata(data?: unknown, methodName?: string): void; + addServiceNameAnnotation(): void; + annotateColdStart(): void; + captureAWS(aws: T): void | T; + captureAWSv3Client(service: T): void | T; + captureAWSClient(service: T): void | T; + captureLambdaHandler( + options?: CaptureLambdaHandlerOptions + ): HandlerMethodDecorator; + captureMethod(options?: CaptureMethodOptions): MethodDecorator; + getSegment(): Segment | Subsegment | undefined; + getRootXrayTraceId(): string | undefined; + isTraceSampled(): boolean; + isTracingEnabled(): boolean; + putAnnotation: (key: string, value: string | number | boolean) => void; + putMetadata: ( + key: string, + value: unknown, + namespace?: string | undefined + ) => void; + setSegment(segment: Segment | Subsegment): void; } -export { - TracerInterface -}; \ No newline at end of file +export { TracerInterface }; diff --git a/packages/tracer/src/config/ConfigServiceInterface.ts b/packages/tracer/src/config/ConfigServiceInterface.ts index 67db36b5c5..78169c4de3 100644 --- a/packages/tracer/src/config/ConfigServiceInterface.ts +++ b/packages/tracer/src/config/ConfigServiceInterface.ts @@ -1,18 +1,15 @@ interface ConfigServiceInterface { + get(name: string): string; - get(name: string): string + getCaptureHTTPsRequests(): string; - getCaptureHTTPsRequests(): string + getTracingEnabled(): string; - getTracingEnabled(): string + getServiceName(): string; - getServiceName(): string + getTracingCaptureResponse(): string; - getTracingCaptureResponse(): string - - getTracingCaptureError(): string + getTracingCaptureError(): string; } -export { - ConfigServiceInterface -}; \ No newline at end of file +export { ConfigServiceInterface }; diff --git a/packages/tracer/src/config/EnvironmentVariablesService.ts b/packages/tracer/src/config/EnvironmentVariablesService.ts index d8503864c1..ecb46b3e23 100644 --- a/packages/tracer/src/config/EnvironmentVariablesService.ts +++ b/packages/tracer/src/config/EnvironmentVariablesService.ts @@ -1,13 +1,16 @@ import { ConfigServiceInterface } from './ConfigServiceInterface'; import { EnvironmentVariablesService as CommonEnvironmentVariablesService } from '@aws-lambda-powertools/commons'; -class EnvironmentVariablesService extends CommonEnvironmentVariablesService implements ConfigServiceInterface { - +class EnvironmentVariablesService + extends CommonEnvironmentVariablesService + implements ConfigServiceInterface +{ // Environment variables private awsExecutionEnv = 'AWS_EXECUTION_ENV'; private samLocalVariable = 'AWS_SAM_LOCAL'; private tracerCaptureErrorVariable = 'POWERTOOLS_TRACER_CAPTURE_ERROR'; - private tracerCaptureHTTPsRequestsVariable = 'POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS'; + private tracerCaptureHTTPsRequestsVariable = + 'POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS'; private tracerCaptureResponseVariable = 'POWERTOOLS_TRACER_CAPTURE_RESPONSE'; private tracingEnabledVariable = 'POWERTOOLS_TRACE_ENABLED'; @@ -26,17 +29,14 @@ class EnvironmentVariablesService extends CommonEnvironmentVariablesService impl public getTracingCaptureError(): string { return this.get(this.tracerCaptureErrorVariable); } - + public getTracingCaptureResponse(): string { return this.get(this.tracerCaptureResponseVariable); } - + public getTracingEnabled(): string { return this.get(this.tracingEnabledVariable); } - } -export { - EnvironmentVariablesService, -}; \ No newline at end of file +export { EnvironmentVariablesService }; diff --git a/packages/tracer/src/config/index.ts b/packages/tracer/src/config/index.ts index 0f036d810e..11fd37677e 100644 --- a/packages/tracer/src/config/index.ts +++ b/packages/tracer/src/config/index.ts @@ -1,2 +1,2 @@ export * from './ConfigServiceInterface'; -export * from './EnvironmentVariablesService'; \ No newline at end of file +export * from './EnvironmentVariablesService'; diff --git a/packages/tracer/src/helpers.ts b/packages/tracer/src/helpers.ts index 17d50a5ef0..ff198f88f5 100644 --- a/packages/tracer/src/helpers.ts +++ b/packages/tracer/src/helpers.ts @@ -1,8 +1,7 @@ import { Tracer } from '.'; import { TracerOptions } from './types'; -const createTracer = (options: TracerOptions = {}): Tracer => new Tracer(options); +const createTracer = (options: TracerOptions = {}): Tracer => + new Tracer(options); -export { - createTracer -}; \ No newline at end of file +export { createTracer }; diff --git a/packages/tracer/src/index.ts b/packages/tracer/src/index.ts index 4e2259de59..1303ff1fb2 100644 --- a/packages/tracer/src/index.ts +++ b/packages/tracer/src/index.ts @@ -1,4 +1,4 @@ export * from './helpers'; export * from './Tracer'; export * from './TracerInterface'; -export * from './middleware/middy'; \ No newline at end of file +export * from './middleware/middy'; diff --git a/packages/tracer/src/middleware/middy.ts b/packages/tracer/src/middleware/middy.ts index 399945c453..132228390d 100644 --- a/packages/tracer/src/middleware/middy.ts +++ b/packages/tracer/src/middleware/middy.ts @@ -3,37 +3,40 @@ import type { Segment, Subsegment } from 'aws-xray-sdk-core'; import type { CaptureLambdaHandlerOptions } from '../types'; import type { MiddlewareLikeObj, - MiddyLikeRequest + MiddyLikeRequest, } from '@aws-lambda-powertools/commons'; /** * A middy middleware automating capture of metadata and annotations on segments or subsegments for a Lambda Handler. - * + * * Using this middleware on your handler function will automatically: - * * handle the subsegment lifecycle + * * handle the subsegment lifecycle * * add the `ColdStart` annotation * * add the function response as metadata * * add the function error as metadata (if any) - * + * * @example * ```typescript * import { Tracer, captureLambdaHandler } from '@aws-lambda-powertools/tracer'; * import middy from '@middy/core'; - * + * * const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - * + * * const lambdaHandler = async (_event: any, _context: any) => { * ... * }; - * + * * export const handler = middy(lambdaHandler).use(captureLambdaHandler(tracer)); * ``` - * + * * @param target - The Tracer instance to use for tracing * @param options - (_optional_) Options for the middleware * @returns middleware - The middy middleware object */ -const captureLambdaHandler = (target: Tracer, options?: CaptureLambdaHandlerOptions): MiddlewareLikeObj => { +const captureLambdaHandler = ( + target: Tracer, + options?: CaptureLambdaHandlerOptions +): MiddlewareLikeObj => { let lambdaSegment: Segment; let handlerSegment: Subsegment; @@ -44,7 +47,9 @@ const captureLambdaHandler = (target: Tracer, options?: CaptureLambdaHandlerOpti } // If segment is defined, then it is a Segment as this middleware is only used for Lambda Handlers lambdaSegment = segment as Segment; - handlerSegment = lambdaSegment.addNewSubsegment(`## ${process.env._HANDLER}`); + handlerSegment = lambdaSegment.addNewSubsegment( + `## ${process.env._HANDLER}` + ); target.setSegment(handlerSegment); }; @@ -63,8 +68,10 @@ const captureLambdaHandler = (target: Tracer, options?: CaptureLambdaHandlerOpti target.addServiceNameAnnotation(); } }; - - const captureLambdaHandlerAfter = async (request: MiddyLikeRequest): Promise => { + + const captureLambdaHandlerAfter = async ( + request: MiddyLikeRequest + ): Promise => { if (target.isTracingEnabled()) { if (options?.captureResponse ?? true) { target.addResponseAsMetadata(request.response, process.env._HANDLER); @@ -72,8 +79,10 @@ const captureLambdaHandler = (target: Tracer, options?: CaptureLambdaHandlerOpti close(); } }; - - const captureLambdaHandlerError = async (request: MiddyLikeRequest): Promise => { + + const captureLambdaHandlerError = async ( + request: MiddyLikeRequest + ): Promise => { if (target.isTracingEnabled()) { target.addErrorAsMetadata(request.error as Error); close(); @@ -83,10 +92,8 @@ const captureLambdaHandler = (target: Tracer, options?: CaptureLambdaHandlerOpti return { before: captureLambdaHandlerBefore, after: captureLambdaHandlerAfter, - onError: captureLambdaHandlerError + onError: captureLambdaHandlerError, }; }; -export { - captureLambdaHandler, -}; \ No newline at end of file +export { captureLambdaHandler }; diff --git a/packages/tracer/src/provider/ProviderService.ts b/packages/tracer/src/provider/ProviderService.ts index 56abd43dfd..b3397cbc03 100644 --- a/packages/tracer/src/provider/ProviderService.ts +++ b/packages/tracer/src/provider/ProviderService.ts @@ -1,6 +1,4 @@ -import { - ContextMissingStrategy -} from 'aws-xray-sdk-core/dist/lib/context_utils'; +import { ContextMissingStrategy } from 'aws-xray-sdk-core/dist/lib/context_utils'; import { Namespace } from 'cls-hooked'; import { ProviderServiceInterface } from '.'; import { @@ -18,7 +16,7 @@ import { setContextMissingStrategy, setDaemonAddress, setLogger, - Logger + Logger, } from 'aws-xray-sdk-core'; class ProviderService implements ProviderServiceInterface { @@ -36,11 +34,19 @@ class ProviderService implements ProviderServiceInterface { return captureAWSv3Client(service as any); } - public captureAsyncFunc(name: string, fcn: (subsegment?: Subsegment) => unknown, _parent?: Segment | Subsegment): unknown { + public captureAsyncFunc( + name: string, + fcn: (subsegment?: Subsegment) => unknown, + _parent?: Segment | Subsegment + ): unknown { return captureAsyncFunc(name, fcn); } - - public captureFunc(name: string, fcn: (subsegment?: Subsegment) => unknown, _parent?: Segment | Subsegment): unknown { + + public captureFunc( + name: string, + fcn: (subsegment?: Subsegment) => unknown, + _parent?: Segment | Subsegment + ): unknown { return captureFunc(name, fcn); } @@ -62,13 +68,17 @@ class ProviderService implements ProviderServiceInterface { public putAnnotation(key: string, value: string | number | boolean): void { const segment = this.getSegment(); if (segment === undefined) { - console.warn('No active segment or subsegment found, skipping annotation'); + console.warn( + 'No active segment or subsegment found, skipping annotation' + ); return; } if (segment instanceof Segment) { - console.warn('You cannot annotate the main segment in a Lambda execution environment'); - + console.warn( + 'You cannot annotate the main segment in a Lambda execution environment' + ); + return; } segment.addAnnotation(key, value); @@ -77,13 +87,17 @@ class ProviderService implements ProviderServiceInterface { public putMetadata(key: string, value: unknown, namespace?: string): void { const segment = this.getSegment(); if (segment === undefined) { - console.warn('No active segment or subsegment found, skipping metadata addition'); + console.warn( + 'No active segment or subsegment found, skipping metadata addition' + ); return; } if (segment instanceof Segment) { - console.warn('You cannot add metadata to the main segment in a Lambda execution environment'); - + console.warn( + 'You cannot add metadata to the main segment in a Lambda execution environment' + ); + return; } segment.addMetadata(key, value, namespace); @@ -100,13 +114,10 @@ class ProviderService implements ProviderServiceInterface { public setLogger(logObj: unknown): void { setLogger(logObj as Logger); } - + public setSegment(segment: Segment | Subsegment): void { setSegment(segment); } - } -export { - ProviderService -}; \ No newline at end of file +export { ProviderService }; diff --git a/packages/tracer/src/provider/ProviderServiceInterface.ts b/packages/tracer/src/provider/ProviderServiceInterface.ts index 2376255767..a8cd4a4f91 100644 --- a/packages/tracer/src/provider/ProviderServiceInterface.ts +++ b/packages/tracer/src/provider/ProviderServiceInterface.ts @@ -2,35 +2,41 @@ import { Namespace } from 'cls-hooked'; import { Segment, Subsegment } from 'aws-xray-sdk-core'; interface ProviderServiceInterface { - getNamespace(): Namespace + getNamespace(): Namespace; - getSegment(): Segment | Subsegment | undefined + getSegment(): Segment | Subsegment | undefined; - setSegment(segment: Segment | Subsegment): void + setSegment(segment: Segment | Subsegment): void; - setLogger(logObj: unknown): void + setLogger(logObj: unknown): void; - setDaemonAddress(address: string): void + setDaemonAddress(address: string): void; - setContextMissingStrategy(strategy: unknown): void + setContextMissingStrategy(strategy: unknown): void; - captureAWS(awsservice: T): T + captureAWS(awsservice: T): T; - captureAWSClient(awsservice: T): T + captureAWSClient(awsservice: T): T; - captureAWSv3Client(awsservice: T): T + captureAWSv3Client(awsservice: T): T; - captureAsyncFunc(name: string, fcn: (subsegment?: Subsegment) => unknown, parent?: Segment | Subsegment): unknown - - captureFunc(name: string, fcn: (subsegment?: Subsegment) => unknown, parent?: Segment | Subsegment): unknown + captureAsyncFunc( + name: string, + fcn: (subsegment?: Subsegment) => unknown, + parent?: Segment | Subsegment + ): unknown; - captureHTTPsGlobal(): void + captureFunc( + name: string, + fcn: (subsegment?: Subsegment) => unknown, + parent?: Segment | Subsegment + ): unknown; - putAnnotation(key: string, value: string | number | boolean): void + captureHTTPsGlobal(): void; - putMetadata(key: string, value: unknown, namespace?: string): void + putAnnotation(key: string, value: string | number | boolean): void; + + putMetadata(key: string, value: unknown, namespace?: string): void; } -export { - ProviderServiceInterface -}; \ No newline at end of file +export { ProviderServiceInterface }; diff --git a/packages/tracer/src/provider/index.ts b/packages/tracer/src/provider/index.ts index 91ce4f416a..ef648fd32d 100644 --- a/packages/tracer/src/provider/index.ts +++ b/packages/tracer/src/provider/index.ts @@ -1,2 +1,2 @@ export * from './ProviderService'; -export * from './ProviderServiceInterface'; \ No newline at end of file +export * from './ProviderServiceInterface'; diff --git a/packages/tracer/src/types/Tracer.ts b/packages/tracer/src/types/Tracer.ts index 90b8db2d49..eea9113326 100644 --- a/packages/tracer/src/types/Tracer.ts +++ b/packages/tracer/src/types/Tracer.ts @@ -1,29 +1,33 @@ import { ConfigServiceInterface } from '../config'; import { Handler } from 'aws-lambda'; -import { AsyncHandler, LambdaInterface, SyncHandler } from '@aws-lambda-powertools/commons'; +import { + AsyncHandler, + LambdaInterface, + SyncHandler, +} from '@aws-lambda-powertools/commons'; /** - * Options for the tracer class to be used during initialization. - * - * Usage: - * @example - * ```typescript - * const customConfigService: ConfigServiceInterface; - * const tracerOptions: TracerOptions = { - * enabled?: true, - * serviceName?: 'serverlessAirline', - * captureHTTPsRequests?: true, - * customConfigService?: customConfigService, // Only needed for advanced uses - * }; - * - * const tracer = new Tracer(tracerOptions); - * ``` - */ + * Options for the tracer class to be used during initialization. + * + * Usage: + * @example + * ```typescript + * const customConfigService: ConfigServiceInterface; + * const tracerOptions: TracerOptions = { + * enabled?: true, + * serviceName?: 'serverlessAirline', + * captureHTTPsRequests?: true, + * customConfigService?: customConfigService, // Only needed for advanced uses + * }; + * + * const tracer = new Tracer(tracerOptions); + * ``` + */ type TracerOptions = { - enabled?: boolean - serviceName?: string - captureHTTPsRequests?: boolean - customConfigService?: ConfigServiceInterface + enabled?: boolean; + serviceName?: string; + captureHTTPsRequests?: boolean; + customConfigService?: ConfigServiceInterface; }; /** @@ -31,20 +35,20 @@ type TracerOptions = { * * Options supported: * * `captureResponse` - (_optional_) - Disable response serialization as subsegment metadata - * + * * Middleware usage: * @example * ```typescript * import middy from '@middy/core'; - * + * * const tracer = new Tracer(); - * + * * const lambdaHandler = async (_event: any, _context: any): Promise => {}; - * + * * export const handler = middy(lambdaHandler) * .use(captureLambdaHandler(tracer, { captureResponse: false })); * ``` - * + * * Decorator usage: * @example * ```typescript @@ -60,12 +64,12 @@ type TracerOptions = { * ``` */ type CaptureLambdaHandlerOptions = { - captureResponse?: boolean + captureResponse?: boolean; }; /** * Options for method decorators. - * + * * Options supported: * * `subSegmentName` - (_optional_) - Set a custom name for the subsegment * * `captureResponse` - (_optional_) - Disable response serialization as subsegment metadata @@ -92,24 +96,32 @@ type CaptureLambdaHandlerOptions = { * ``` */ type CaptureMethodOptions = { - subSegmentName?: string - captureResponse?: boolean + subSegmentName?: string; + captureResponse?: boolean; }; type HandlerMethodDecorator = ( target: LambdaInterface, propertyKey: string | symbol, - descriptor: TypedPropertyDescriptor> | TypedPropertyDescriptor> + descriptor: + | TypedPropertyDescriptor> + | TypedPropertyDescriptor> ) => void; // TODO: Revisit type below & make it more specific -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => any; +type MethodDecorator = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + target: any, + propertyKey: string | symbol, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor: TypedPropertyDescriptor + // eslint-disable-next-line @typescript-eslint/no-explicit-any +) => any; export { TracerOptions, CaptureLambdaHandlerOptions, CaptureMethodOptions, HandlerMethodDecorator, - MethodDecorator -}; \ No newline at end of file + MethodDecorator, +}; diff --git a/packages/tracer/src/types/index.ts b/packages/tracer/src/types/index.ts index cf0ac29bb3..8f8f55c173 100644 --- a/packages/tracer/src/types/index.ts +++ b/packages/tracer/src/types/index.ts @@ -1 +1 @@ -export * from './Tracer'; \ No newline at end of file +export * from './Tracer'; diff --git a/packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts b/packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts index 2ecf33dd15..1c3f683592 100644 --- a/packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts +++ b/packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts @@ -3,18 +3,27 @@ import { Callback, Context } from 'aws-lambda'; import AWS from 'aws-sdk'; import axios from 'axios'; -const serviceName = process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; -const customAnnotationKey = process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; -const customAnnotationValue = process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; -const customMetadataKey = process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; -const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) : { bar: 'baz' }; -const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) : { foo: 'bar' }; -const customErrorMessage = process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; +const serviceName = + process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; +const customAnnotationKey = + process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; +const customAnnotationValue = + process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; +const customMetadataKey = + process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; +const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE + ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) + : { bar: 'baz' }; +const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE + ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) + : { foo: 'bar' }; +const customErrorMessage = + process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; const testTableName = process.env.TEST_TABLE_NAME ?? 'TestTable'; interface CustomEvent { - throw: boolean - invocation: number + throw: boolean; + invocation: number; } const tracer = new Tracer({ serviceName: serviceName }); @@ -28,13 +37,25 @@ export class MyFunctionBase { this.returnValue = customResponseValue; } - public handler(event: CustomEvent, _context: Context, _callback: Callback): void | Promise { + public handler( + event: CustomEvent, + _context: Context, + _callback: Callback + ): void | Promise { tracer.putAnnotation(customAnnotationKey, customAnnotationValue); tracer.putMetadata(customMetadataKey, customMetadataValue); - + return Promise.all([ - dynamoDB.put({ TableName: testTableName, Item: { id: `${serviceName}-${event.invocation}-sdkv2` } }).promise(), - axios.get('https://awslabs.github.io/aws-lambda-powertools-typescript/latest/', { timeout: 5000 }), + dynamoDB + .put({ + TableName: testTableName, + Item: { id: `${serviceName}-${event.invocation}-sdkv2` }, + }) + .promise(), + axios.get( + 'https://awslabs.github.io/aws-lambda-powertools-typescript/latest/', + { timeout: 5000 } + ), new Promise((resolve, reject) => { setTimeout(() => { const res = this.myMethod(); @@ -44,9 +65,9 @@ export class MyFunctionBase { resolve(res); } }, 2000); // We need to wait for to make sure previous calls are finished - }) + }), ]) - .then(([ _dynamoDBRes, _axiosRes, promiseRes ]) => promiseRes) + .then(([_dynamoDBRes, _axiosRes, promiseRes]) => promiseRes) .catch((err) => { throw err; }); @@ -61,7 +82,11 @@ class MyFunctionWithDecorator extends MyFunctionBase { @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public handler(event: CustomEvent, _context: Context, _callback: Callback): void | Promise { + public handler( + event: CustomEvent, + _context: Context, + _callback: Callback + ): void | Promise { return super.handler(event, _context, _callback); } @@ -80,7 +105,11 @@ class MyFunctionWithDecoratorCaptureResponseFalse extends MyFunctionBase { @tracer.captureLambdaHandler({ captureResponse: false }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public handler(event: CustomEvent, _context: Context, _callback: Callback): void | Promise { + public handler( + event: CustomEvent, + _context: Context, + _callback: Callback + ): void | Promise { return super.handler(event, _context, _callback); } @@ -92,5 +121,9 @@ class MyFunctionWithDecoratorCaptureResponseFalse extends MyFunctionBase { } } -const handlerWithCaptureResponseFalseClass = new MyFunctionWithDecoratorCaptureResponseFalse(); -export const handlerWithCaptureResponseFalse = handlerWithCaptureResponseFalseClass.handler.bind(handlerWithCaptureResponseFalseClass); \ No newline at end of file +const handlerWithCaptureResponseFalseClass = + new MyFunctionWithDecoratorCaptureResponseFalse(); +export const handlerWithCaptureResponseFalse = + handlerWithCaptureResponseFalseClass.handler.bind( + handlerWithCaptureResponseFalseClass + ); diff --git a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts index 434d00506f..d9210ccced 100644 --- a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts @@ -10,7 +10,10 @@ import { App, Stack, RemovalPolicy } from 'aws-cdk-lib'; import { XRayClient } from '@aws-sdk/client-xray'; import { STSClient } from '@aws-sdk/client-sts'; import { v4 } from 'uuid'; -import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli'; +import { + deployStack, + destroyStack, +} from '../../../commons/tests/utils/cdk-cli'; import { getTraces, getInvocationSubsegment, @@ -24,19 +27,19 @@ import { generateUniqueName, isValidRuntimeKey, } from '../../../commons/tests/utils/e2eUtils'; -import { +import { RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + expectedCustomMetadataKey, + expectedCustomMetadataValue, + expectedCustomResponseValue, expectedCustomErrorMessage, } from './constants'; -import { +import { assertAnnotation, assertErrorAndFault, } from '../helpers/traceAssertions'; @@ -55,7 +58,12 @@ if (!isValidRuntimeKey(runtime)) { * Each stack must use a unique `serviceName` as it's used to for retrieving the trace. * Using the same one will result in traces from different test cases mixing up. */ -const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, v4(), runtime, 'AllFeatures-Decorator'); +const stackName = generateUniqueName( + RESOURCE_NAME_PREFIX, + v4(), + runtime, + 'AllFeatures-Decorator' +); const lambdaFunctionCodeFile = 'allFeatures.decorator.test.functionCode.ts'; let startTime: Date; @@ -63,28 +71,50 @@ let startTime: Date; * Function #1 is with all flags enabled. */ const uuidFunction1 = v4(); -const functionNameWithAllFlagsEnabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction1, runtime, 'AllFeatures-Decorator-AllFlagsEnabled'); -const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled; +const functionNameWithAllFlagsEnabled = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuidFunction1, + runtime, + 'AllFeatures-Decorator-AllFlagsEnabled' +); +const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled; /** * Function #2 doesn't capture error or response */ const uuidFunction2 = v4(); -const functionNameWithNoCaptureErrorOrResponse = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction2, runtime, 'AllFeatures-Decorator-NoCaptureErrorOrResponse'); -const serviceNameWithNoCaptureErrorOrResponse = functionNameWithNoCaptureErrorOrResponse; +const functionNameWithNoCaptureErrorOrResponse = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuidFunction2, + runtime, + 'AllFeatures-Decorator-NoCaptureErrorOrResponse' +); +const serviceNameWithNoCaptureErrorOrResponse = + functionNameWithNoCaptureErrorOrResponse; /** * Function #3 disables tracer */ const uuidFunction3 = v4(); -const functionNameWithTracerDisabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction3, runtime, 'AllFeatures-Decorator-TracerDisabled'); -const serviceNameWithTracerDisabled = functionNameWithNoCaptureErrorOrResponse; +const functionNameWithTracerDisabled = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuidFunction3, + runtime, + 'AllFeatures-Decorator-TracerDisabled' +); +const serviceNameWithTracerDisabled = functionNameWithNoCaptureErrorOrResponse; /** * Function #4 disables capture response via decorator options */ const uuidFunction4 = v4(); -const functionNameWithCaptureResponseFalse = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction4, runtime, 'AllFeatures-Decorator-CaptureResponseFalse'); -const serviceNameWithCaptureResponseFalse = functionNameWithCaptureResponseFalse; +const functionNameWithCaptureResponseFalse = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuidFunction4, + runtime, + 'AllFeatures-Decorator-CaptureResponseFalse' +); +const serviceNameWithCaptureResponseFalse = + functionNameWithCaptureResponseFalse; const xrayClient = new XRayClient({}); const stsClient = new STSClient({}); @@ -94,9 +124,7 @@ const integTestApp = new App(); let stack: Stack; describe(`Tracer E2E tests, all features with decorator instantiation for runtime: ${runtime}`, () => { - beforeAll(async () => { - // Prepare startTime = new Date(); const ddbTableName = stackName + '-table'; @@ -106,10 +134,10 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim tableName: ddbTableName, partitionKey: { name: 'id', - type: AttributeType.STRING + type: AttributeType.STRING, }, billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY + removalPolicy: RemovalPolicy.DESTROY, }); const entry = path.join(__dirname, lambdaFunctionCodeFile); @@ -124,23 +152,24 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', POWERTOOLS_TRACE_ENABLED: 'true', }, - runtime + runtime, }); ddbTable.grantWriteData(functionWithAllFlagsEnabled); - const functionThatDoesNotCapturesErrorAndResponse = createTracerTestFunction({ - stack, - functionName: functionNameWithNoCaptureErrorOrResponse, - entry, - expectedServiceName: serviceNameWithNoCaptureErrorOrResponse, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', - POWERTOOLS_TRACE_ENABLED: 'true', - }, - runtime - }); + const functionThatDoesNotCapturesErrorAndResponse = + createTracerTestFunction({ + stack, + functionName: functionNameWithNoCaptureErrorOrResponse, + entry, + expectedServiceName: serviceNameWithNoCaptureErrorOrResponse, + environmentParams: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', + POWERTOOLS_TRACE_ENABLED: 'true', + }, + runtime, + }); ddbTable.grantWriteData(functionThatDoesNotCapturesErrorAndResponse); const functionWithTracerDisabled = createTracerTestFunction({ @@ -154,7 +183,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', POWERTOOLS_TRACE_ENABLED: 'false', }, - runtime + runtime, }); ddbTable.grantWriteData(functionWithTracerDisabled); @@ -170,7 +199,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', POWERTOOLS_TRACE_ENABLED: 'true', }, - runtime + runtime, }); ddbTable.grantWriteData(functionWithCaptureResponseFalse); @@ -183,7 +212,6 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim invokeAllTestCases(functionNameWithTracerDisabled), invokeAllTestCases(functionNameWithCaptureResponseFalse), ]); - }, SETUP_TIMEOUT); afterAll(async () => { @@ -192,216 +220,279 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim } }, TEARDOWN_TIMEOUT); - it('should generate all custom traces', async () => { - - const tracesWhenAllFlagsEnabled = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), invocations, 4); - - expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); - - // Assess - for (let i = 0; i < invocations; i++) { - const trace = tracesWhenAllFlagsEnabled[i]; - - /** - * Expect the trace to have 4 segments: - * 1. Lambda Context (AWS::Lambda) - * 2. Lambda Function (AWS::Lambda::Function) - * 4. DynamoDB (AWS::DynamoDB) - * 4. Remote call (awslabs.github.io) - */ - expect(trace.Segments.length).toBe(4); - const invocationSubsegment = getInvocationSubsegment(trace); - - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) - * '## index.handler' subsegment should have 3 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. awslabs.github.io (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', 'awslabs.github.io', '### myMethod' ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('awslabs.github.io')?.length).toBe(1); - expect(subsegments.get('### myMethod')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = (i === (invocations - 1)); - if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); - } - } - - }, TEST_CASE_TIMEOUT); - - it('should have correct annotations and metadata', async () => { - const tracesWhenAllFlagsEnabled = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), invocations, 4); - - for (let i = 0; i < invocations; 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: serviceNameWithAllFlagsEnabled, - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - }); - - if (!metadata) { - fail('metadata is missing'); - } - expect(metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey]) - .toEqual(expectedCustomMetadataValue); - - const shouldThrowAnError = (i === (invocations - 1)); - if (!shouldThrowAnError) { - // Assert that the metadata object contains the response - expect(metadata[serviceNameWithAllFlagsEnabled]['index.handler response']) - .toEqual(expectedCustomResponseValue); - } - } - }, TEST_CASE_TIMEOUT); - - it('should not capture error nor response when the flags are false', async () => { - - const tracesWithNoCaptureErrorOrResponse = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithNoCaptureErrorOrResponse), invocations, 4); - - expect(tracesWithNoCaptureErrorOrResponse.length).toBe(invocations); - - // Assess - for (let i = 0; i < invocations; i++) { - const trace = tracesWithNoCaptureErrorOrResponse[i]; - - /** - * 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 (awslabs.github.io) - */ - expect(trace.Segments.length).toBe(4); - const invocationSubsegment = getInvocationSubsegment(trace); - - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) - * '## index.handler' subsegment should have 3 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. awslabs.github.io (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'); + it( + 'should generate all custom traces', + async () => { + const tracesWhenAllFlagsEnabled = await getTraces( + xrayClient, + startTime, + await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), + invocations, + 4 + ); + + expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWhenAllFlagsEnabled[i]; + + /** + * Expect the trace to have 4 segments: + * 1. Lambda Context (AWS::Lambda) + * 2. Lambda Function (AWS::Lambda::Function) + * 4. DynamoDB (AWS::DynamoDB) + * 4. Remote call (awslabs.github.io) + */ + expect(trace.Segments.length).toBe(4); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) + * '## index.handler' subsegment should have 3 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. awslabs.github.io (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', + 'awslabs.github.io', + '### myMethod', + ]); + expect(subsegments.get('DynamoDB')?.length).toBe(1); + expect(subsegments.get('awslabs.github.io')?.length).toBe(1); + expect(subsegments.get('### myMethod')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = i === invocations - 1; + if (shouldThrowAnError) { + assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + } } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'awslabs.github.io', '### myMethod' ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('awslabs.github.io')?.length).toBe(1); - expect(subsegments.get('### myMethod')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = (i === (invocations - 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(handlerSubsegment.hasOwnProperty('cause')).toBe(false); + }, + TEST_CASE_TIMEOUT + ); + + it( + 'should have correct annotations and metadata', + async () => { + const tracesWhenAllFlagsEnabled = await getTraces( + xrayClient, + startTime, + await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), + invocations, + 4 + ); + + for (let i = 0; i < invocations; 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: serviceNameWithAllFlagsEnabled, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + }); + + if (!metadata) { + fail('metadata is missing'); + } + expect( + metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey] + ).toEqual(expectedCustomMetadataValue); + + const shouldThrowAnError = i === invocations - 1; + if (!shouldThrowAnError) { + // Assert that the metadata object contains the response + expect( + metadata[serviceNameWithAllFlagsEnabled]['index.handler response'] + ).toEqual(expectedCustomResponseValue); + } } - } - - }, TEST_CASE_TIMEOUT); - - it('should not capture response when the decorator\'s captureResponse is set to false', async () => { - - const tracesWithCaptureResponseFalse = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithCaptureResponseFalse), invocations, 4); - - expect(tracesWithCaptureResponseFalse.length).toBe(invocations); - - // Assess - for (let i = 0; i < invocations; i++) { - const trace = tracesWithCaptureResponseFalse[i]; - - /** - * 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 (awslabs.github.io) - */ - expect(trace.Segments.length).toBe(4); - const invocationSubsegment = getInvocationSubsegment(trace); - - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) - * '## index.handler' subsegment should have 3 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. awslabs.github.io (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'); + }, + TEST_CASE_TIMEOUT + ); + + it( + 'should not capture error nor response when the flags are false', + async () => { + const tracesWithNoCaptureErrorOrResponse = await getTraces( + xrayClient, + startTime, + await getFunctionArn( + stsClient, + functionNameWithNoCaptureErrorOrResponse + ), + invocations, + 4 + ); + + expect(tracesWithNoCaptureErrorOrResponse.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWithNoCaptureErrorOrResponse[i]; + + /** + * 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 (awslabs.github.io) + */ + expect(trace.Segments.length).toBe(4); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) + * '## index.handler' subsegment should have 3 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. awslabs.github.io (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', + 'awslabs.github.io', + '### myMethod', + ]); + expect(subsegments.get('DynamoDB')?.length).toBe(1); + expect(subsegments.get('awslabs.github.io')?.length).toBe(1); + expect(subsegments.get('### myMethod')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = i === invocations - 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(handlerSubsegment.hasOwnProperty('cause')).toBe(false); + } } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'awslabs.github.io', '### myMethod' ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('awslabs.github.io')?.length).toBe(1); - expect(subsegments.get('### myMethod')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - // 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'); - - const shouldThrowAnError = (i === (invocations - 1)); - if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + }, + TEST_CASE_TIMEOUT + ); + + it( + 'should not capture response when captureResponse is set to false', + async () => { + const tracesWithCaptureResponseFalse = await getTraces( + xrayClient, + startTime, + await getFunctionArn(stsClient, functionNameWithCaptureResponseFalse), + invocations, + 4 + ); + + expect(tracesWithCaptureResponseFalse.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWithCaptureResponseFalse[i]; + + /** + * 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 (awslabs.github.io) + */ + expect(trace.Segments.length).toBe(4); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) + * '## index.handler' subsegment should have 3 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. awslabs.github.io (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', + 'awslabs.github.io', + '### myMethod', + ]); + expect(subsegments.get('DynamoDB')?.length).toBe(1); + expect(subsegments.get('awslabs.github.io')?.length).toBe(1); + expect(subsegments.get('### myMethod')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + // 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'); + + const shouldThrowAnError = i === invocations - 1; + if (shouldThrowAnError) { + assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + } } - } - - }, TEST_CASE_TIMEOUT); - - it('should not capture any custom traces when disabled', async () => { - const expectedNoOfTraces = 2; - const tracesWithTracerDisabled = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithTracerDisabled), invocations, expectedNoOfTraces); - - expect(tracesWithTracerDisabled.length).toBe(invocations); - - // Assess - for (let i = 0; i < invocations; i++) { - const trace = tracesWithTracerDisabled[i]; - expect(trace.Segments.length).toBe(2); - - /** - * Expect no subsegment in the invocation - */ - const invocationSubsegment = getInvocationSubsegment(trace); - expect(invocationSubsegment?.subsegments).toBeUndefined(); - - const shouldThrowAnError = (i === (invocations - 1)); - if (shouldThrowAnError) { - expect(invocationSubsegment.error).toBe(true); + }, + TEST_CASE_TIMEOUT + ); + + it( + 'should not capture any custom traces when disabled', + async () => { + const expectedNoOfTraces = 2; + const tracesWithTracerDisabled = await getTraces( + xrayClient, + startTime, + await getFunctionArn(stsClient, functionNameWithTracerDisabled), + invocations, + expectedNoOfTraces + ); + + expect(tracesWithTracerDisabled.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWithTracerDisabled[i]; + expect(trace.Segments.length).toBe(2); + + /** + * Expect no subsegment in the invocation + */ + const invocationSubsegment = getInvocationSubsegment(trace); + expect(invocationSubsegment?.subsegments).toBeUndefined(); + + const shouldThrowAnError = i === invocations - 1; + if (shouldThrowAnError) { + expect(invocationSubsegment.error).toBe(true); + } } - } - - }, TEST_CASE_TIMEOUT); + }, + TEST_CASE_TIMEOUT + ); }); - diff --git a/packages/tracer/tests/e2e/allFeatures.manual.test.functionCode.ts b/packages/tracer/tests/e2e/allFeatures.manual.test.functionCode.ts index 3eae40b329..fb16b19681 100644 --- a/packages/tracer/tests/e2e/allFeatures.manual.test.functionCode.ts +++ b/packages/tracer/tests/e2e/allFeatures.manual.test.functionCode.ts @@ -4,24 +4,36 @@ import axios from 'axios'; import AWS from 'aws-sdk'; import type { Subsegment } from 'aws-xray-sdk-core'; -const serviceName = process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; -const customAnnotationKey = process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; -const customAnnotationValue = process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; -const customMetadataKey = process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; -const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) : { bar: 'baz' }; -const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) : { foo: 'bar' }; -const customErrorMessage = process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; +const serviceName = + process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; +const customAnnotationKey = + process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; +const customAnnotationValue = + process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; +const customMetadataKey = + process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; +const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE + ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) + : { bar: 'baz' }; +const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE + ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) + : { foo: 'bar' }; +const customErrorMessage = + process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; const testTableName = process.env.TEST_TABLE_NAME ?? 'TestTable'; interface CustomEvent { - throw: boolean - invocation: number + throw: boolean; + invocation: number; } const tracer = new Tracer({ serviceName: serviceName }); const dynamoDB = tracer.captureAWSClient(new AWS.DynamoDB.DocumentClient()); -export const handler = async (event: CustomEvent, _context: Context): Promise => { +export const handler = async ( + event: CustomEvent, + _context: Context +): Promise => { const segment = tracer.getSegment(); let subsegment: Subsegment | undefined; if (segment) { @@ -37,8 +49,16 @@ export const handler = async (event: CustomEvent, _context: Context): Promise { - +describe(`Tracer E2E tests, all features with manual instantiation for runtime: ${runtime}`, () => { beforeAll(async () => { - // Prepare const startTime = new Date(); const ddbTableName = stackName + '-table'; @@ -86,17 +95,17 @@ describe(`Tracer E2E tests, all features with manual instantiation for runtime: entry, expectedServiceName, environmentParams, - runtime + runtime, }); - + const ddbTable = new Table(stack, 'Table', { tableName: ddbTableName, partitionKey: { name: 'id', - type: AttributeType.STRING + type: AttributeType.STRING, }, billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY + removalPolicy: RemovalPolicy.DESTROY, }); ddbTable.grantWriteData(testFunction); @@ -108,8 +117,13 @@ describe(`Tracer E2E tests, all features with manual instantiation for runtime: // Retrieve traces from X-Ray for assertion const lambdaFunctionArn = await getFunctionArn(stsClient, functionName); - sortedTraces = await getTraces(xrayClient, startTime, lambdaFunctionArn, invocations, 4); - + sortedTraces = await getTraces( + xrayClient, + startTime, + lambdaFunctionArn, + invocations, + 4 + ); }, SETUP_TIMEOUT); afterAll(async () => { @@ -118,80 +132,89 @@ describe(`Tracer E2E tests, all features with manual instantiation for runtime: } }, TEARDOWN_TIMEOUT); - it('should generate all custom traces', async () => { - - expect(sortedTraces.length).toBe(invocations); - - // Assess - for (let i = 0; i < invocations; i++) { - const trace = sortedTraces[i]; - - /** - * 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 (awslabs.github.io) - */ - expect(trace.Segments.length).toBe(4); - const invocationSubsegment = getInvocationSubsegment(trace); - - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) - * '## index.handler' subsegment should have 2 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. awslabs.github.io (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'); + it( + 'should generate all custom traces', + async () => { + expect(sortedTraces.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = sortedTraces[i]; + + /** + * 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 (awslabs.github.io) + */ + expect(trace.Segments.length).toBe(4); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) + * '## index.handler' subsegment should have 2 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. awslabs.github.io (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', + 'awslabs.github.io', + ]); + expect(subsegments.get('DynamoDB')?.length).toBe(1); + expect(subsegments.get('awslabs.github.io')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = i === invocations - 1; + if (shouldThrowAnError) { + assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + } } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'awslabs.github.io' ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('awslabs.github.io')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = (i === (invocations - 1)); - if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + }, + TEST_CASE_TIMEOUT + ); + + it( + 'should have correct annotations and metadata', + async () => { + for (let i = 0; i < invocations; 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, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + }); + + if (!metadata) { + fail('metadata is missing'); + } + expect( + metadata[expectedServiceName][expectedCustomMetadataKey] + ).toEqual(expectedCustomMetadataValue); + + const shouldThrowAnError = i === invocations - 1; + if (!shouldThrowAnError) { + // Assert that the metadata object contains the response + expect( + metadata[expectedServiceName]['index.handler response'] + ).toEqual(expectedCustomResponseValue); + } } - } - - }, TEST_CASE_TIMEOUT); - - it('should have correct annotations and metadata', async () => { - for (let i = 0; i < invocations; 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, - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - }); - - if (!metadata) { - fail('metadata is missing'); - } - expect(metadata[expectedServiceName][expectedCustomMetadataKey]) - .toEqual(expectedCustomMetadataValue); - - const shouldThrowAnError = (i === (invocations - 1)); - if (!shouldThrowAnError) { - // Assert that the metadata object contains the response - expect(metadata[expectedServiceName]['index.handler response']) - .toEqual(expectedCustomResponseValue); - } - } - }, TEST_CASE_TIMEOUT); - + }, + TEST_CASE_TIMEOUT + ); }); - diff --git a/packages/tracer/tests/e2e/allFeatures.middy.test.functionCode.ts b/packages/tracer/tests/e2e/allFeatures.middy.test.functionCode.ts index 198e40de7f..42bc4d8ee1 100644 --- a/packages/tracer/tests/e2e/allFeatures.middy.test.functionCode.ts +++ b/packages/tracer/tests/e2e/allFeatures.middy.test.functionCode.ts @@ -4,31 +4,51 @@ import { Context } from 'aws-lambda'; import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb'; import axios from 'axios'; -const serviceName = process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; -const customAnnotationKey = process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; -const customAnnotationValue = process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; -const customMetadataKey = process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; -const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) : { bar: 'baz' }; -const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) : { foo: 'bar' }; -const customErrorMessage = process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; +const serviceName = + process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; +const customAnnotationKey = + process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; +const customAnnotationValue = + process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; +const customMetadataKey = + process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; +const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE + ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) + : { bar: 'baz' }; +const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE + ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) + : { foo: 'bar' }; +const customErrorMessage = + process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; const testTableName = process.env.TEST_TABLE_NAME ?? 'TestTable'; interface CustomEvent { - throw: boolean - invocation: number + throw: boolean; + invocation: number; } const tracer = new Tracer({ serviceName: serviceName }); const dynamoDB = tracer.captureAWSv3Client(new DynamoDBClient({})); -const testHandler = async (event: CustomEvent, _context: Context): Promise => { +const testHandler = async ( + event: CustomEvent, + _context: Context +): Promise => { tracer.putAnnotation('invocation', event.invocation); tracer.putAnnotation(customAnnotationKey, customAnnotationValue); tracer.putMetadata(customMetadataKey, customMetadataValue); try { - await dynamoDB.send(new PutItemCommand({ TableName: testTableName, Item: { id: { 'S': `${serviceName}-${event.invocation}-sdkv3` } } })); - await axios.get('https://awslabs.github.io/aws-lambda-powertools-typescript/latest/', { timeout: 5000 }); + await dynamoDB.send( + new PutItemCommand({ + TableName: testTableName, + Item: { id: { S: `${serviceName}-${event.invocation}-sdkv3` } }, + }) + ); + await axios.get( + 'https://awslabs.github.io/aws-lambda-powertools-typescript/latest/', + { timeout: 5000 } + ); const res = customResponseValue; if (event.throw) { @@ -43,4 +63,6 @@ const testHandler = async (event: CustomEvent, _context: Context): Promise export const handler = middy(testHandler).use(captureLambdaHandler(tracer)); -export const handlerWithNoCaptureResponseViaMiddlewareOption = middy(testHandler).use(captureLambdaHandler(tracer, { captureResponse: false })); \ No newline at end of file +export const handlerWithNoCaptureResponseViaMiddlewareOption = middy( + testHandler +).use(captureLambdaHandler(tracer, { captureResponse: false })); diff --git a/packages/tracer/tests/e2e/allFeatures.middy.test.ts b/packages/tracer/tests/e2e/allFeatures.middy.test.ts index 771dce6ddc..609b48df73 100644 --- a/packages/tracer/tests/e2e/allFeatures.middy.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.middy.test.ts @@ -10,7 +10,10 @@ import { App, Stack, RemovalPolicy } from 'aws-cdk-lib'; import { XRayClient } from '@aws-sdk/client-xray'; import { STSClient } from '@aws-sdk/client-sts'; import { v4 } from 'uuid'; -import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli'; +import { + deployStack, + destroyStack, +} from '../../../commons/tests/utils/cdk-cli'; import { getTraces, getInvocationSubsegment, @@ -24,19 +27,19 @@ import { generateUniqueName, isValidRuntimeKey, } from '../../../commons/tests/utils/e2eUtils'; -import { +import { RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + expectedCustomMetadataKey, + expectedCustomMetadataValue, + expectedCustomResponseValue, expectedCustomErrorMessage, } from './constants'; -import { +import { assertAnnotation, assertErrorAndFault, } from '../helpers/traceAssertions'; @@ -55,7 +58,12 @@ if (!isValidRuntimeKey(runtime)) { * Each stack must use a unique `serviceName` as it's used to for retrieving the trace. * Using the same one will result in traces from different test cases mixing up. */ -const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, v4(), runtime, 'AllFeatures-Middy'); +const stackName = generateUniqueName( + RESOURCE_NAME_PREFIX, + v4(), + runtime, + 'AllFeatures-Middy' +); const lambdaFunctionCodeFile = 'allFeatures.middy.test.functionCode.ts'; let startTime: Date; @@ -63,28 +71,50 @@ let startTime: Date; * Function #1 is with all flags enabled. */ const uuidFunction1 = v4(); -const functionNameWithAllFlagsEnabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction1, runtime, 'AllFeatures-Middy-AllFlagsEnabled'); -const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled; +const functionNameWithAllFlagsEnabled = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuidFunction1, + runtime, + 'AllFeatures-Middy-AllFlagsEnabled' +); +const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled; /** * Function #2 doesn't capture error or response */ const uuidFunction2 = v4(); -const functionNameWithNoCaptureErrorOrResponse = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction2, runtime, 'AllFeatures-Middy-NoCaptureErrorOrResponse'); -const serviceNameWithNoCaptureErrorOrResponse = functionNameWithNoCaptureErrorOrResponse; +const functionNameWithNoCaptureErrorOrResponse = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuidFunction2, + runtime, + 'AllFeatures-Middy-NoCaptureErrorOrResponse' +); +const serviceNameWithNoCaptureErrorOrResponse = + functionNameWithNoCaptureErrorOrResponse; /** * Function #3 disables tracer */ const uuidFunction3 = v4(); -const functionNameWithTracerDisabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction3, runtime, 'AllFeatures-Middy-TracerDisabled'); -const serviceNameWithTracerDisabled = functionNameWithNoCaptureErrorOrResponse; +const functionNameWithTracerDisabled = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuidFunction3, + runtime, + 'AllFeatures-Middy-TracerDisabled' +); +const serviceNameWithTracerDisabled = functionNameWithNoCaptureErrorOrResponse; /** * Function #4 doesn't capture response */ const uuidFunction4 = v4(); -const functionNameWithNoCaptureResponseViaMiddlewareOption = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction4, runtime, 'AllFeatures-Middy-NoCaptureResponse2'); -const serviceNameWithNoCaptureResponseViaMiddlewareOption = functionNameWithNoCaptureResponseViaMiddlewareOption; +const functionNameWithNoCaptureResponseViaMiddlewareOption = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuidFunction4, + runtime, + 'AllFeatures-Middy-NoCaptureResponse2' +); +const serviceNameWithNoCaptureResponseViaMiddlewareOption = + functionNameWithNoCaptureResponseViaMiddlewareOption; const xrayClient = new XRayClient({}); const stsClient = new STSClient({}); @@ -94,9 +124,7 @@ const integTestApp = new App(); let stack: Stack; describe(`Tracer E2E tests, all features with middy instantiation for runtime: ${runtime}`, () => { - beforeAll(async () => { - // Prepare startTime = new Date(); const ddbTableName = stackName + '-table'; @@ -106,10 +134,10 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ tableName: ddbTableName, partitionKey: { name: 'id', - type: AttributeType.STRING + type: AttributeType.STRING, }, billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY + removalPolicy: RemovalPolicy.DESTROY, }); const entry = path.join(__dirname, lambdaFunctionCodeFile); @@ -124,23 +152,24 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', POWERTOOLS_TRACE_ENABLED: 'true', }, - runtime + runtime, }); ddbTable.grantWriteData(functionWithAllFlagsEnabled); - const functionThatDoesNotCapturesErrorAndResponse = createTracerTestFunction({ - stack, - functionName: functionNameWithNoCaptureErrorOrResponse, - entry, - expectedServiceName: serviceNameWithNoCaptureErrorOrResponse, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', - POWERTOOLS_TRACE_ENABLED: 'true', - }, - runtime - }); + const functionThatDoesNotCapturesErrorAndResponse = + createTracerTestFunction({ + stack, + functionName: functionNameWithNoCaptureErrorOrResponse, + entry, + expectedServiceName: serviceNameWithNoCaptureErrorOrResponse, + environmentParams: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', + POWERTOOLS_TRACE_ENABLED: 'true', + }, + runtime, + }); ddbTable.grantWriteData(functionThatDoesNotCapturesErrorAndResponse); const functionWithTracerDisabled = createTracerTestFunction({ @@ -154,25 +183,29 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', POWERTOOLS_TRACE_ENABLED: 'false', }, - runtime + runtime, }); ddbTable.grantWriteData(functionWithTracerDisabled); - const functionThatDoesNotCaptureResponseViaMiddlewareOption = createTracerTestFunction({ - stack, - functionName: functionNameWithNoCaptureResponseViaMiddlewareOption, - entry, - handler: 'handlerWithNoCaptureResponseViaMiddlewareOption', - expectedServiceName: serviceNameWithNoCaptureResponseViaMiddlewareOption, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - }, - runtime - }); - ddbTable.grantWriteData(functionThatDoesNotCaptureResponseViaMiddlewareOption); + const functionThatDoesNotCaptureResponseViaMiddlewareOption = + createTracerTestFunction({ + stack, + functionName: functionNameWithNoCaptureResponseViaMiddlewareOption, + entry, + handler: 'handlerWithNoCaptureResponseViaMiddlewareOption', + expectedServiceName: + serviceNameWithNoCaptureResponseViaMiddlewareOption, + environmentParams: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + }, + runtime, + }); + ddbTable.grantWriteData( + functionThatDoesNotCaptureResponseViaMiddlewareOption + ); await deployStack(integTestApp, stack); @@ -183,7 +216,6 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ invokeAllTestCases(functionNameWithTracerDisabled), invokeAllTestCases(functionNameWithNoCaptureResponseViaMiddlewareOption), ]); - }, SETUP_TIMEOUT); afterAll(async () => { @@ -192,204 +224,267 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ } }, TEARDOWN_TIMEOUT); - it('should generate all custom traces', async () => { - - const tracesWhenAllFlagsEnabled = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), invocations, 4); - - expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); - - // Assess - for (let i = 0; i < invocations; i++) { - const trace = tracesWhenAllFlagsEnabled[i]; - - /** - * 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 (awslabs.github.io) - */ - expect(trace.Segments.length).toBe(4); - const invocationSubsegment = getInvocationSubsegment(trace); - - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) - * '## index.handler' subsegment should have 2 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. awslabs.github.io (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'); + it( + 'should generate all custom traces', + async () => { + const tracesWhenAllFlagsEnabled = await getTraces( + xrayClient, + startTime, + await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), + invocations, + 4 + ); + + expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWhenAllFlagsEnabled[i]; + + /** + * 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 (awslabs.github.io) + */ + expect(trace.Segments.length).toBe(4); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) + * '## index.handler' subsegment should have 2 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. awslabs.github.io (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', + 'awslabs.github.io', + ]); + expect(subsegments.get('DynamoDB')?.length).toBe(1); + expect(subsegments.get('awslabs.github.io')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = i === invocations - 1; + if (shouldThrowAnError) { + assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + } } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'awslabs.github.io' ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('awslabs.github.io')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = (i === (invocations - 1)); - if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + }, + TEST_CASE_TIMEOUT + ); + + it( + 'should have correct annotations and metadata', + async () => { + const tracesWhenAllFlagsEnabled = await getTraces( + xrayClient, + startTime, + await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), + invocations, + 4 + ); + + for (let i = 0; i < invocations; 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: serviceNameWithAllFlagsEnabled, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + }); + + if (!metadata) { + fail('metadata is missing'); + } + expect( + metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey] + ).toEqual(expectedCustomMetadataValue); + + const shouldThrowAnError = i === invocations - 1; + if (!shouldThrowAnError) { + // Assert that the metadata object contains the response + expect( + metadata[serviceNameWithAllFlagsEnabled]['index.handler response'] + ).toEqual(expectedCustomResponseValue); + } } - } - - }, TEST_CASE_TIMEOUT); - - it('should have correct annotations and metadata', async () => { - const tracesWhenAllFlagsEnabled = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), invocations, 4); - - for (let i = 0; i < invocations; 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: serviceNameWithAllFlagsEnabled, - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - }); - - if (!metadata) { - fail('metadata is missing'); + }, + TEST_CASE_TIMEOUT + ); + + it( + 'should not capture error nor response when the flags are false', + async () => { + const tracesWithNoCaptureErrorOrResponse = await getTraces( + xrayClient, + startTime, + await getFunctionArn( + stsClient, + functionNameWithNoCaptureErrorOrResponse + ), + invocations, + 4 + ); + + expect(tracesWithNoCaptureErrorOrResponse.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWithNoCaptureErrorOrResponse[i]; + + /** + * 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 (awslabs.github.io) + */ + expect(trace.Segments.length).toBe(4); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) + * '## index.handler' subsegment should have 2 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. awslabs.github.io (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', + 'awslabs.github.io', + ]); + expect(subsegments.get('DynamoDB')?.length).toBe(1); + expect(subsegments.get('awslabs.github.io')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = i === invocations - 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(handlerSubsegment.hasOwnProperty('cause')).toBe(false); + } } - expect(metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey]) - .toEqual(expectedCustomMetadataValue); - - const shouldThrowAnError = (i === (invocations - 1)); - if (!shouldThrowAnError) { - // Assert that the metadata object contains the response - expect(metadata[serviceNameWithAllFlagsEnabled]['index.handler response']) - .toEqual(expectedCustomResponseValue); - } - } - }, TEST_CASE_TIMEOUT); - - it('should not capture error nor response when the flags are false', async () => { - - const tracesWithNoCaptureErrorOrResponse = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithNoCaptureErrorOrResponse), invocations, 4); - - expect(tracesWithNoCaptureErrorOrResponse.length).toBe(invocations); - - // Assess - for (let i = 0; i < invocations; i++) { - const trace = tracesWithNoCaptureErrorOrResponse[i]; - - /** - * 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 (awslabs.github.io) - */ - expect(trace.Segments.length).toBe(4); - const invocationSubsegment = getInvocationSubsegment(trace); - - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) - * '## index.handler' subsegment should have 2 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. awslabs.github.io (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'); + }, + TEST_CASE_TIMEOUT + ); + + it( + 'should not capture response when captureResponse is set to false', + async () => { + const tracesWithNoCaptureResponse = await getTraces( + xrayClient, + startTime, + await getFunctionArn( + stsClient, + functionNameWithNoCaptureResponseViaMiddlewareOption + ), + invocations, + 4 + ); + + expect(tracesWithNoCaptureResponse.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWithNoCaptureResponse[i]; + + /** + * 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 (awslabs.github.io) + */ + expect(trace.Segments.length).toBe(4); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handlerWithNoCaptureResponseViaMiddlewareOption' (default behavior for Powertools Tracer) + * '## index.handlerWithNoCaptureResponseViaMiddlewareOption' subsegment should have 2 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. awslabs.github.io (Remote call) + */ + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + expect(handlerSubsegment.name).toBe( + '## index.handlerWithNoCaptureResponseViaMiddlewareOption' + ); + expect(handlerSubsegment?.subsegments).toHaveLength(2); + + if (!handlerSubsegment.subsegments) { + fail( + '"## index.handlerWithNoCaptureResponseViaMiddlewareOption" subsegment should have subsegments' + ); + } + const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ + 'DynamoDB', + 'awslabs.github.io', + ]); + expect(subsegments.get('DynamoDB')?.length).toBe(1); + expect(subsegments.get('awslabs.github.io')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = i === invocations - 1; + if (shouldThrowAnError) { + assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + } } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'awslabs.github.io' ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('awslabs.github.io')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = (i === (invocations - 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(handlerSubsegment.hasOwnProperty('cause')).toBe(false); + }, + TEST_CASE_TIMEOUT + ); + + it( + 'should not capture any custom traces when disabled', + async () => { + const expectedNoOfTraces = 2; + const tracesWithTracerDisabled = await getTraces( + xrayClient, + startTime, + await getFunctionArn(stsClient, functionNameWithTracerDisabled), + invocations, + expectedNoOfTraces + ); + + expect(tracesWithTracerDisabled.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWithTracerDisabled[i]; + expect(trace.Segments.length).toBe(2); + + /** + * Expect no subsegment in the invocation + */ + const invocationSubsegment = getInvocationSubsegment(trace); + expect(invocationSubsegment?.subsegments).toBeUndefined(); + + const shouldThrowAnError = i === invocations - 1; + if (shouldThrowAnError) { + expect(invocationSubsegment.error).toBe(true); + } } - } - - }, TEST_CASE_TIMEOUT); - - it('should not capture response when the middleware\'s captureResponse is set to false', async () => { - - const tracesWithNoCaptureResponse = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithNoCaptureResponseViaMiddlewareOption), invocations, 4); - - expect(tracesWithNoCaptureResponse.length).toBe(invocations); - - // Assess - for (let i = 0; i < invocations; i++) { - const trace = tracesWithNoCaptureResponse[i]; - - /** - * 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 (awslabs.github.io) - */ - expect(trace.Segments.length).toBe(4); - const invocationSubsegment = getInvocationSubsegment(trace); - - /** - * Invocation subsegment should have a subsegment '## index.handlerWithNoCaptureResponseViaMiddlewareOption' (default behavior for Powertools Tracer) - * '## index.handlerWithNoCaptureResponseViaMiddlewareOption' subsegment should have 2 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. awslabs.github.io (Remote call) - */ - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - expect(handlerSubsegment.name).toBe('## index.handlerWithNoCaptureResponseViaMiddlewareOption'); - expect(handlerSubsegment?.subsegments).toHaveLength(2); - - if (!handlerSubsegment.subsegments) { - fail('"## index.handlerWithNoCaptureResponseViaMiddlewareOption" subsegment should have subsegments'); - } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'awslabs.github.io' ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('awslabs.github.io')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = (i === (invocations - 1)); - if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); - } - } - - }, TEST_CASE_TIMEOUT); - - it('should not capture any custom traces when disabled', async () => { - const expectedNoOfTraces = 2; - const tracesWithTracerDisabled = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithTracerDisabled), invocations, expectedNoOfTraces); - - expect(tracesWithTracerDisabled.length).toBe(invocations); - - // Assess - for (let i = 0; i < invocations; i++) { - const trace = tracesWithTracerDisabled[i]; - expect(trace.Segments.length).toBe(2); - - /** - * Expect no subsegment in the invocation - */ - const invocationSubsegment = getInvocationSubsegment(trace); - expect(invocationSubsegment?.subsegments).toBeUndefined(); - - const shouldThrowAnError = (i === (invocations - 1)); - if (shouldThrowAnError) { - expect(invocationSubsegment.error).toBe(true); - } - } - - }, TEST_CASE_TIMEOUT); + }, + TEST_CASE_TIMEOUT + ); }); - diff --git a/packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts b/packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts index f960881a8b..b39d6361d0 100644 --- a/packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts +++ b/packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts @@ -4,23 +4,35 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb'; import axios from 'axios'; -const serviceName = process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; -const customAnnotationKey = process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; -const customAnnotationValue = process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; -const customMetadataKey = process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; -const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) : { bar: 'baz' }; -const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) : { foo: 'bar' }; -const customErrorMessage = process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; +const serviceName = + process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; +const customAnnotationKey = + process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; +const customAnnotationValue = + process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; +const customMetadataKey = + process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; +const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE + ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) + : { bar: 'baz' }; +const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE + ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) + : { foo: 'bar' }; +const customErrorMessage = + process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; const testTableName = process.env.TEST_TABLE_NAME ?? 'TestTable'; -const customSubSegmentName = process.env.EXPECTED_CUSTOM_SUBSEGMENT_NAME ?? 'mySubsegment'; +const customSubSegmentName = + process.env.EXPECTED_CUSTOM_SUBSEGMENT_NAME ?? 'mySubsegment'; interface CustomEvent { - throw: boolean - invocation: number + throw: boolean; + invocation: number; } const tracer = new Tracer({ serviceName: serviceName }); -const dynamoDB = tracer.captureAWSv3Client(DynamoDBDocumentClient.from(new DynamoDBClient({}))); +const dynamoDB = tracer.captureAWSv3Client( + DynamoDBDocumentClient.from(new DynamoDBClient({})) +); export class MyFunctionBase { private readonly returnValue: string; @@ -29,13 +41,24 @@ export class MyFunctionBase { this.returnValue = customResponseValue; } - public async handler(event: CustomEvent, _context: Context): Promise { + public async handler( + event: CustomEvent, + _context: Context + ): Promise { tracer.putAnnotation(customAnnotationKey, customAnnotationValue); tracer.putMetadata(customMetadataKey, customMetadataValue); try { - await dynamoDB.send(new PutCommand({ TableName: testTableName, Item: { id: `${serviceName}-${event.invocation}-sdkv3` } })); - await axios.get('https://awslabs.github.io/aws-lambda-powertools-typescript/latest/', { timeout: 5000 }); + await dynamoDB.send( + new PutCommand({ + TableName: testTableName, + Item: { id: `${serviceName}-${event.invocation}-sdkv3` }, + }) + ); + await axios.get( + 'https://awslabs.github.io/aws-lambda-powertools-typescript/latest/', + { timeout: 5000 } + ); const res = this.myMethod(); if (event.throw) { @@ -57,7 +80,11 @@ class MyFunctionWithDecorator extends MyFunctionBase { @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public async handler(event: CustomEvent, _context: Context, _callback: Callback): void | Promise { + public async handler( + event: CustomEvent, + _context: Context, + _callback: Callback + ): void | Promise { return super.handler(event, _context); } @@ -76,7 +103,11 @@ export class MyFunctionWithDecoratorAndCustomNamedSubSegmentForMethod extends My @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public async handler(event: CustomEvent, _context: Context, _callback: Callback): void | Promise { + public async handler( + event: CustomEvent, + _context: Context, + _callback: Callback + ): void | Promise { return super.handler(event, _context); } @@ -88,5 +119,7 @@ export class MyFunctionWithDecoratorAndCustomNamedSubSegmentForMethod extends My } } -const handlerWithCustomSubsegmentNameInMethodClass = new MyFunctionWithDecoratorAndCustomNamedSubSegmentForMethod(); -export const handlerWithCustomSubsegmentNameInMethod = handlerClass.handler.bind(handlerWithCustomSubsegmentNameInMethodClass); \ No newline at end of file +const handlerWithCustomSubsegmentNameInMethodClass = + new MyFunctionWithDecoratorAndCustomNamedSubSegmentForMethod(); +export const handlerWithCustomSubsegmentNameInMethod = + handlerClass.handler.bind(handlerWithCustomSubsegmentNameInMethodClass); diff --git a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts index 0fb284c40a..3126414c3d 100644 --- a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts +++ b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts @@ -10,7 +10,10 @@ import { App, Stack, RemovalPolicy } from 'aws-cdk-lib'; import { XRayClient } from '@aws-sdk/client-xray'; import { STSClient } from '@aws-sdk/client-sts'; import { v4 } from 'uuid'; -import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli'; +import { + deployStack, + destroyStack, +} from '../../../commons/tests/utils/cdk-cli'; import { getTraces, getInvocationSubsegment, @@ -24,11 +27,11 @@ import { generateUniqueName, isValidRuntimeKey, } from '../../../commons/tests/utils/e2eUtils'; -import { +import { RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, expectedCustomErrorMessage, expectedCustomAnnotationKey, expectedCustomAnnotationValue, @@ -37,7 +40,7 @@ import { expectedCustomResponseValue, expectedCustomSubSegmentName, } from './constants'; -import { +import { assertAnnotation, assertErrorAndFault, } from '../helpers/traceAssertions'; @@ -48,7 +51,12 @@ if (!isValidRuntimeKey(runtime)) { throw new Error(`Invalid runtime key value: ${runtime}`); } -const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, v4(), runtime, 'AllFeatures-Decorator'); +const stackName = generateUniqueName( + RESOURCE_NAME_PREFIX, + v4(), + runtime, + 'AllFeatures-Decorator' +); const lambdaFunctionCodeFile = 'asyncHandler.decorator.test.functionCode.ts'; let startTime: Date; @@ -56,15 +64,26 @@ let startTime: Date; * Function #1 is with all flags enabled. */ const uuidFunction1 = v4(); -const functionNameWithAllFlagsEnabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction1, runtime, 'AllFeatures-Decorator-Async-AllFlagsEnabled'); +const functionNameWithAllFlagsEnabled = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuidFunction1, + runtime, + 'AllFeatures-Decorator-Async-AllFlagsEnabled' +); const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled; /** * Function #2 sets a custom subsegment name in the decorated method */ const uuidFunction2 = v4(); -const functionNameWithCustomSubsegmentNameInMethod = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction2, runtime, 'AllFeatures-Decorator-Async-CustomSubsegmentNameInMethod'); -const serviceNameWithCustomSubsegmentNameInMethod = functionNameWithCustomSubsegmentNameInMethod; +const functionNameWithCustomSubsegmentNameInMethod = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuidFunction2, + runtime, + 'AllFeatures-Decorator-Async-CustomSubsegmentNameInMethod' +); +const serviceNameWithCustomSubsegmentNameInMethod = + functionNameWithCustomSubsegmentNameInMethod; const xrayClient = new XRayClient({}); const stsClient = new STSClient({}); @@ -74,9 +93,7 @@ const integTestApp = new App(); let stack: Stack; describe(`Tracer E2E tests, asynchronous handler with decorator instantiation for runtime: ${runtime}`, () => { - beforeAll(async () => { - // Prepare startTime = new Date(); const ddbTableName = stackName + '-table'; @@ -86,10 +103,10 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo tableName: ddbTableName, partitionKey: { name: 'id', - type: AttributeType.STRING + type: AttributeType.STRING, }, billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY + removalPolicy: RemovalPolicy.DESTROY, }); const entry = path.join(__dirname, lambdaFunctionCodeFile); @@ -104,7 +121,7 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', POWERTOOLS_TRACE_ENABLED: 'true', }, - runtime + runtime, }); ddbTable.grantWriteData(functionWithAllFlagsEnabled); @@ -121,7 +138,7 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', POWERTOOLS_TRACE_ENABLED: 'true', }, - runtime + runtime, }); ddbTable.grantWriteData(functionWithCustomSubsegmentNameInMethod); @@ -132,7 +149,6 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo invokeAllTestCases(functionNameWithAllFlagsEnabled), invokeAllTestCases(functionNameWithCustomSubsegmentNameInMethod), ]); - }, SETUP_TIMEOUT); afterAll(async () => { @@ -141,133 +157,173 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo } }, TEARDOWN_TIMEOUT); - it('should generate all custom traces', async () => { - - const tracesWhenAllFlagsEnabled = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), invocations, 4); - - expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); - - // Assess - for (let i = 0; i < invocations; i++) { - const trace = tracesWhenAllFlagsEnabled[i]; - - /** - * 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 (awslabs.github.io) - */ - expect(trace.Segments.length).toBe(4); - const invocationSubsegment = getInvocationSubsegment(trace); - - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) - * '## index.handler' subsegment should have 3 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. awslabs.github.io (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', 'awslabs.github.io', '### myMethod' ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('awslabs.github.io')?.length).toBe(1); - expect(subsegments.get('### myMethod')?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = (i === (invocations - 1)); - if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); - } - } + it( + 'should generate all custom traces', + async () => { + const tracesWhenAllFlagsEnabled = await getTraces( + xrayClient, + startTime, + await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), + invocations, + 4 + ); - }, TEST_CASE_TIMEOUT); - - it('should have correct annotations and metadata', async () => { - const traces = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), invocations, 4); - - for (let i = 0; i < invocations; 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: serviceNameWithAllFlagsEnabled, - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - }); - - if (!metadata) { - fail('metadata is missing'); - } - expect(metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey]) - .toEqual(expectedCustomMetadataValue); - - const shouldThrowAnError = (i === (invocations - 1)); - if (!shouldThrowAnError) { - // Assert that the metadata object contains the response - expect(metadata[serviceNameWithAllFlagsEnabled]['index.handler response']) - .toEqual(expectedCustomResponseValue); - } - } - }, TEST_CASE_TIMEOUT); - - it('should have a custom name as the subsegment\'s name for the decorated method', async () => { - - const tracesWhenCustomSubsegmentNameInMethod = await getTraces(xrayClient, startTime, await getFunctionArn(stsClient, functionNameWithCustomSubsegmentNameInMethod), invocations, 4); - - expect(tracesWhenCustomSubsegmentNameInMethod.length).toBe(invocations); - - // Assess - for (let i = 0; i < invocations; i++) { - const trace = tracesWhenCustomSubsegmentNameInMethod[i]; - - /** - * 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 (awslabs.github.io) - */ - expect(trace.Segments.length).toBe(4); - const invocationSubsegment = getInvocationSubsegment(trace); - - /** - * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) - * '## index.handler' subsegment should have 3 subsegments - * 1. DynamoDB (PutItem on the table) - * 2. awslabs.github.io (Remote call) - * 3. '### mySubsegment' (method decorator with custom name) - */ - const handlerSubsegment = getFirstSubsegment(invocationSubsegment); - expect(handlerSubsegment.name).toBe('## index.handlerWithCustomSubsegmentNameInMethod'); - expect(handlerSubsegment?.subsegments).toHaveLength(3); - - if (!handlerSubsegment.subsegments) { - fail('"## index.handler" subsegment should have subsegments'); + expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWhenAllFlagsEnabled[i]; + + /** + * 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 (awslabs.github.io) + */ + expect(trace.Segments.length).toBe(4); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) + * '## index.handler' subsegment should have 3 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. awslabs.github.io (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', + 'awslabs.github.io', + '### myMethod', + ]); + expect(subsegments.get('DynamoDB')?.length).toBe(1); + expect(subsegments.get('awslabs.github.io')?.length).toBe(1); + expect(subsegments.get('### myMethod')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = i === invocations - 1; + if (shouldThrowAnError) { + assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + } } - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'awslabs.github.io', expectedCustomSubSegmentName ]); - expect(subsegments.get('DynamoDB')?.length).toBe(1); - expect(subsegments.get('awslabs.github.io')?.length).toBe(1); - expect(subsegments.get(expectedCustomSubSegmentName)?.length).toBe(1); - expect(subsegments.get('other')?.length).toBe(0); - - const shouldThrowAnError = (i === (invocations - 1)); - if (shouldThrowAnError) { - assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + }, + TEST_CASE_TIMEOUT + ); + + it( + 'should have correct annotations and metadata', + async () => { + const traces = await getTraces( + xrayClient, + startTime, + await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), + invocations, + 4 + ); + + for (let i = 0; i < invocations; 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: serviceNameWithAllFlagsEnabled, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + }); + + if (!metadata) { + fail('metadata is missing'); + } + expect( + metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey] + ).toEqual(expectedCustomMetadataValue); + + const shouldThrowAnError = i === invocations - 1; + if (!shouldThrowAnError) { + // Assert that the metadata object contains the response + expect( + metadata[serviceNameWithAllFlagsEnabled]['index.handler response'] + ).toEqual(expectedCustomResponseValue); + } } - } + }, + TEST_CASE_TIMEOUT + ); - }, TEST_CASE_TIMEOUT); -}); + it( + 'should have a custom name as the subsegment name for the decorated method', + async () => { + const tracesWhenCustomSubsegmentNameInMethod = await getTraces( + xrayClient, + startTime, + await getFunctionArn( + stsClient, + functionNameWithCustomSubsegmentNameInMethod + ), + invocations, + 4 + ); + + expect(tracesWhenCustomSubsegmentNameInMethod.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWhenCustomSubsegmentNameInMethod[i]; + + /** + * 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 (awslabs.github.io) + */ + expect(trace.Segments.length).toBe(4); + const invocationSubsegment = getInvocationSubsegment(trace); + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for Powertools Tracer) + * '## index.handler' subsegment should have 3 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. awslabs.github.io (Remote call) + * 3. '### mySubsegment' (method decorator with custom name) + */ + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + expect(handlerSubsegment.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', + 'awslabs.github.io', + expectedCustomSubSegmentName, + ]); + expect(subsegments.get('DynamoDB')?.length).toBe(1); + expect(subsegments.get('awslabs.github.io')?.length).toBe(1); + expect(subsegments.get(expectedCustomSubSegmentName)?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = i === invocations - 1; + if (shouldThrowAnError) { + assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + } + } + }, + TEST_CASE_TIMEOUT + ); +}); diff --git a/packages/tracer/tests/e2e/constants.ts b/packages/tracer/tests/e2e/constants.ts index b16ff806dd..7d27f4cde4 100644 --- a/packages/tracer/tests/e2e/constants.ts +++ b/packages/tracer/tests/e2e/constants.ts @@ -10,4 +10,4 @@ export const expectedCustomMetadataKey = 'myMetadata'; export const expectedCustomMetadataValue = { bar: 'baz' }; export const expectedCustomResponseValue = { foo: 'bar' }; export const expectedCustomErrorMessage = 'An error has occurred'; -export const expectedCustomSubSegmentName = 'mySubsegment'; \ No newline at end of file +export const expectedCustomSubSegmentName = 'mySubsegment'; diff --git a/packages/tracer/tests/helpers/FunctionSegmentNotDefinedError.ts b/packages/tracer/tests/helpers/FunctionSegmentNotDefinedError.ts index 1b3d6ad96b..9119bcaab7 100644 --- a/packages/tracer/tests/helpers/FunctionSegmentNotDefinedError.ts +++ b/packages/tracer/tests/helpers/FunctionSegmentNotDefinedError.ts @@ -1,7 +1,7 @@ /** * 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 + * + * 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. */ diff --git a/packages/tracer/tests/helpers/populateEnvironmentVariables.ts b/packages/tracer/tests/helpers/populateEnvironmentVariables.ts index 3dd17a6aa7..2fe6ddd9e7 100644 --- a/packages/tracer/tests/helpers/populateEnvironmentVariables.ts +++ b/packages/tracer/tests/helpers/populateEnvironmentVariables.ts @@ -3,11 +3,14 @@ process.env._X_AMZN_TRACE_ID = '1-abcdef12-3456abcdef123456abcdef12'; process.env.AWS_LAMBDA_FUNCTION_NAME = 'my-lambda-function'; process.env.AWS_EXECUTION_ENV = 'nodejs16.x'; process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '128'; -if (process.env.AWS_REGION === undefined && process.env.CDK_DEFAULT_REGION === undefined) { +if ( + process.env.AWS_REGION === undefined && + process.env.CDK_DEFAULT_REGION === undefined +) { process.env.AWS_REGION = 'eu-west-1'; } process.env._HANDLER = 'index.handler'; // Powertools variables process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; -process.env.AWS_XRAY_LOGGING_LEVEL = 'silent'; \ No newline at end of file +process.env.AWS_XRAY_LOGGING_LEVEL = 'silent'; diff --git a/packages/tracer/tests/helpers/traceAssertions.ts b/packages/tracer/tests/helpers/traceAssertions.ts index 128c1f592a..8ad7b31ee8 100644 --- a/packages/tracer/tests/helpers/traceAssertions.ts +++ b/packages/tracer/tests/helpers/traceAssertions.ts @@ -1,5 +1,8 @@ import { getFirstSubsegment } from './tracesUtils'; -import type { AssertAnnotationParams, ParsedDocument } from './traceUtils.types'; +import type { + AssertAnnotationParams, + ParsedDocument, +} from './traceUtils.types'; export const assertAnnotation = (params: AssertAnnotationParams): void => { const { @@ -7,7 +10,7 @@ export const assertAnnotation = (params: AssertAnnotationParams): void => { isColdStart, expectedServiceName, expectedCustomAnnotationKey, - expectedCustomAnnotationValue + expectedCustomAnnotationValue, } = params; if (!annotations) { @@ -15,14 +18,21 @@ export const assertAnnotation = (params: AssertAnnotationParams): void => { } expect(annotations['ColdStart']).toEqual(isColdStart); expect(annotations['Service']).toEqual(expectedServiceName); - expect(annotations[expectedCustomAnnotationKey]).toEqual(expectedCustomAnnotationValue); + expect(annotations[expectedCustomAnnotationKey]).toEqual( + expectedCustomAnnotationValue + ); }; -export const assertErrorAndFault = (invocationSubsegment: ParsedDocument, expectedCustomErrorMessage: string): void => { +export const assertErrorAndFault = ( + invocationSubsegment: ParsedDocument, + expectedCustomErrorMessage: string +): void => { expect(invocationSubsegment.error).toBe(true); const handlerSubsegment = getFirstSubsegment(invocationSubsegment); expect(handlerSubsegment.fault).toBe(true); expect(handlerSubsegment.hasOwnProperty('cause')).toBe(true); - expect(handlerSubsegment.cause?.exceptions[0].message).toBe(expectedCustomErrorMessage); -}; \ No newline at end of file + expect(handlerSubsegment.cause?.exceptions[0].message).toBe( + expectedCustomErrorMessage + ); +}; diff --git a/packages/tracer/tests/helpers/traceUtils.types.ts b/packages/tracer/tests/helpers/traceUtils.types.ts index 76ad9c47f8..6b537deced 100644 --- a/packages/tracer/tests/helpers/traceUtils.types.ts +++ b/packages/tracer/tests/helpers/traceUtils.types.ts @@ -1,82 +1,82 @@ import type { Stack } from 'aws-cdk-lib'; interface ParsedDocument { - name: string - id: string - start_time: number - end_time?: number + 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 + // The trace may have already appeared in the `getTraceSummaries` response // but a segment may still be in_progress - in_progress?: boolean + in_progress?: boolean; aws?: { - request_id: string - } + request_id: string; + }; http?: { response: { - status: number - } - } - origin?: string - resource_arn?: string - trace_id?: string - subsegments?: ParsedDocument[] + status: number; + }; + }; + origin?: string; + resource_arn?: string; + trace_id?: string; + subsegments?: ParsedDocument[]; annotations?: { - [key: string]: string | boolean | number - } + [key: string]: string | boolean | number; + }; metadata?: { [key: string]: { - [key: string]: unknown - } - } - fault?: boolean + [key: string]: unknown; + }; + }; + fault?: boolean; cause?: { - working_directory: string + working_directory: string; exceptions: { - message: string - type: string - remote: boolean + message: string; + type: string; + remote: boolean; stack: { - path: string - line: number - label: string - }[] - }[] - } + path: string; + line: number; + label: string; + }[]; + }[]; + }; exception: { - message: string - } - error?: boolean + message: string; + }; + error?: boolean; } interface ParsedSegment { - Document: ParsedDocument - Id: string + Document: ParsedDocument; + Id: string; } interface ParsedTrace { - Duration: number - Id: string - LimitExceeded: boolean - Segments: ParsedSegment[] + Duration: number; + Id: string; + LimitExceeded: boolean; + Segments: ParsedSegment[]; } interface TracerTestFunctionParams { - stack: Stack - functionName: string - handler?: string - entry: string - expectedServiceName: string - environmentParams: { [key: string]: string } - runtime: string + stack: Stack; + functionName: string; + handler?: string; + entry: string; + expectedServiceName: string; + environmentParams: { [key: string]: string }; + runtime: string; } interface AssertAnnotationParams { - annotations: ParsedDocument['annotations'] - isColdStart: boolean - expectedServiceName: string - expectedCustomAnnotationKey: string - expectedCustomAnnotationValue: string | number | boolean + annotations: ParsedDocument['annotations']; + isColdStart: boolean; + expectedServiceName: string; + expectedCustomAnnotationKey: string; + expectedCustomAnnotationValue: string | number | boolean; } export { @@ -85,4 +85,4 @@ export { ParsedTrace, TracerTestFunctionParams, AssertAnnotationParams, -}; \ No newline at end of file +}; diff --git a/packages/tracer/tests/helpers/tracesUtils.ts b/packages/tracer/tests/helpers/tracesUtils.ts index f888f928dd..b742a1f9b7 100644 --- a/packages/tracer/tests/helpers/tracesUtils.ts +++ b/packages/tracer/tests/helpers/tracesUtils.ts @@ -8,23 +8,21 @@ import { } from '@aws-sdk/client-xray'; import type { XRayClient } from '@aws-sdk/client-xray'; import type { STSClient } from '@aws-sdk/client-sts'; +import { GetCallerIdentityCommand } from '@aws-sdk/client-sts'; import { - GetCallerIdentityCommand -} from '@aws-sdk/client-sts'; -import { - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + expectedCustomMetadataKey, + expectedCustomMetadataValue, + expectedCustomResponseValue, expectedCustomErrorMessage, } from '../e2e/constants'; import { - invokeFunction, TestRuntimesKey, TEST_RUNTIMES, + invokeFunction, + TestRuntimesKey, + TEST_RUNTIMES, } from '../../../commons/tests/utils/e2eUtils'; -import { - FunctionSegmentNotDefinedError -} from './FunctionSegmentNotDefinedError'; +import { FunctionSegmentNotDefinedError } from './FunctionSegmentNotDefinedError'; import type { ParsedDocument, ParsedSegment, @@ -32,66 +30,114 @@ import type { TracerTestFunctionParams, } from './traceUtils.types'; -const getTraces = async (xrayClient: XRayClient, startTime: Date, resourceArn: string, expectedTraces: number, expectedSegments: number): Promise => { - const retryOptions = { retries: 20, minTimeout: 5_000, maxTimeout: 10_000, factor: 1.25 }; - - return promiseRetry(async(retry: (err?: Error) => never , _: number) => { +const getTraces = async ( + xrayClient: XRayClient, + startTime: Date, + resourceArn: string, + expectedTraces: number, + expectedSegments: number +): Promise => { + const retryOptions = { + retries: 20, + minTimeout: 5_000, + maxTimeout: 10_000, + factor: 1.25, + }; + return promiseRetry(async (retry: (err?: Error) => never, _: number) => { 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 = "${resourceArn}"'`); - const traces = await xrayClient - .send(new GetTraceSummariesCommand({ + 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 = "${resourceArn}"'` + ); + const traces = await xrayClient.send( + new GetTraceSummariesCommand({ StartTime: startTime, EndTime: endTime, FilterExpression: `resource.arn = "${resourceArn}"`, - })); + }) + ); if (traces.TraceSummaries?.length !== expectedTraces) { - retry(new Error(`Expected ${expectedTraces} traces, got ${traces.TraceSummaries?.length} for ${resourceArn}`)); + retry( + new Error( + `Expected ${expectedTraces} traces, got ${traces.TraceSummaries?.length} for ${resourceArn}` + ) + ); } - const traceIds = traces.TraceSummaries?.map((traceSummary) => traceSummary.Id); + 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 ${resourceArn}`)); + retry( + new Error( + `Expected all trace summaries to have an ID, got ${traceIds} for ${resourceArn}` + ) + ); } - const traceDetails = await xrayClient.send(new BatchGetTracesCommand({ - TraceIds: traceIds as string[], - })); + const traceDetails = await xrayClient.send( + new BatchGetTracesCommand({ + TraceIds: traceIds as string[], + }) + ); if (traceDetails.Traces?.length !== expectedTraces) { - retry(new Error(`Expected ${expectedTraces} trace summaries, got ${traceDetails.Traces?.length} for ${resourceArn}`)); + retry( + new Error( + `Expected ${expectedTraces} trace summaries, got ${traceDetails.Traces?.length} for ${resourceArn}` + ) + ); } - 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); + 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; try { invocationSubsegment = getInvocationSubsegment(trace); } catch (error) { - if (error instanceof FunctionSegmentNotDefinedError){ - retry(new Error(`There is no Function subsegment (AWS::Lambda::Function) yet. Retry.`)); + 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); + 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.`)); + 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.` + ) + ); } } @@ -100,12 +146,18 @@ const getTraces = async (xrayClient: XRayClient, startTime: Date, resourceArn: s } if (sortedTraces.length !== expectedTraces) { - throw new Error(`Expected ${expectedTraces} sorted traces, but got ${sortedTraces.length} for ${resourceArn}`); + throw new Error( + `Expected ${expectedTraces} sorted traces, but got ${sortedTraces.length} for ${resourceArn}` + ); } sortedTraces.forEach((trace) => { if (trace.Segments?.length != expectedSegments) { - retry(new Error(`Expected ${expectedSegments} segments, got ${trace.Segments?.length} for trace id ${trace.Id}`)); + retry( + new Error( + `Expected ${expectedSegments} segments, got ${trace.Segments?.length} for trace id ${trace.Id}` + ) + ); } }); @@ -114,10 +166,14 @@ const getTraces = async (xrayClient: XRayClient, startTime: Date, resourceArn: s }; const getFunctionSegment = (trace: ParsedTrace): ParsedSegment => { - const functionSegment = trace.Segments.find((segment) => segment.Document.origin === 'AWS::Lambda::Function'); + 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'); + throw new FunctionSegmentNotDefinedError( + 'Function segment is undefined. This can be either due to eventual consistency or a bug in Tracer' + ); } return functionSegment; @@ -134,8 +190,9 @@ const getFirstSubsegment = (segment: ParsedDocument): ParsedDocument => { const getInvocationSubsegment = (trace: ParsedTrace): ParsedDocument => { const functionSegment = getFunctionSegment(trace); - const invocationSubsegment = functionSegment.Document?.subsegments - ?.find((subsegment) => subsegment.name === 'Invocation'); + const invocationSubsegment = functionSegment.Document?.subsegments?.find( + (subsegment) => subsegment.name === 'Invocation' + ); if (invocationSubsegment === undefined) { throw new Error('Invocation subsegment is undefined'); @@ -144,44 +201,59 @@ const getInvocationSubsegment = (trace: ParsedTrace): ParsedDocument => { return invocationSubsegment; }; -const splitSegmentsByName = (subsegments: ParsedDocument[], expectedNames: string[]): Map => { - const splitSegments: Map = new Map([ ...expectedNames, 'other' ].map(name => [ name, [] ])); - subsegments.forEach(subsegment => { - const name = expectedNames.indexOf(subsegment.name) !== -1 ? subsegment.name : 'other'; +const splitSegmentsByName = ( + subsegments: ParsedDocument[], + expectedNames: string[] +): Map => { + const splitSegments: Map = new Map( + [...expectedNames, 'other'].map((name) => [name, []]) + ); + subsegments.forEach((subsegment) => { + 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): Promise => { - await invokeFunction(functionName, 1, 'SEQUENTIAL', { + await invokeFunction(functionName, 1, 'SEQUENTIAL', { invocation: 1, throw: false, }); - await invokeFunction(functionName, 1, 'SEQUENTIAL', { + await invokeFunction(functionName, 1, 'SEQUENTIAL', { invocation: 2, throw: false, }); - await invokeFunction(functionName, 1, 'SEQUENTIAL', { + await invokeFunction(functionName, 1, 'SEQUENTIAL', { invocation: 3, throw: true, // only last invocation should throw }); }; -const createTracerTestFunction = (params: TracerTestFunctionParams): NodejsFunction => { - const { stack, functionName, entry, expectedServiceName, environmentParams, runtime } = params; +const createTracerTestFunction = ( + params: TracerTestFunctionParams +): NodejsFunction => { + const { + stack, + functionName, + entry, + expectedServiceName, + environmentParams, + runtime, + } = params; const func = new NodejsFunction(stack, functionName, { entry: entry, functionName: functionName, @@ -194,8 +266,12 @@ const createTracerTestFunction = (params: TracerTestFunctionParams): NodejsFunct EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, - EXPECTED_CUSTOM_METADATA_VALUE: JSON.stringify(expectedCustomMetadataValue), - EXPECTED_CUSTOM_RESPONSE_VALUE: JSON.stringify(expectedCustomResponseValue), + EXPECTED_CUSTOM_METADATA_VALUE: JSON.stringify( + expectedCustomMetadataValue + ), + EXPECTED_CUSTOM_RESPONSE_VALUE: JSON.stringify( + expectedCustomResponseValue + ), EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage, ...environmentParams, }, @@ -207,13 +283,16 @@ const createTracerTestFunction = (params: TracerTestFunctionParams): NodejsFunct }; let account: string | undefined; -const getFunctionArn = async (stsClient: STSClient, functionName: string): Promise => { +const getFunctionArn = async ( + stsClient: STSClient, + functionName: string +): Promise => { const region = process.env.AWS_REGION; if (!account) { const identity = await stsClient.send(new GetCallerIdentityCommand({})); account = identity.Account; } - + return `arn:aws:lambda:${region}:${account}:function:${functionName}`; }; diff --git a/packages/tracer/tests/unit/ProviderService.test.ts b/packages/tracer/tests/unit/ProviderService.test.ts index 7335b70fd2..c44b491059 100644 --- a/packages/tracer/tests/unit/ProviderService.test.ts +++ b/packages/tracer/tests/unit/ProviderService.test.ts @@ -19,7 +19,7 @@ import { setLogger, setSegment, Subsegment, - Segment + Segment, } from 'aws-xray-sdk-core'; import http from 'http'; import https from 'https'; @@ -41,250 +41,198 @@ jest.mock('aws-xray-sdk-core', () => ({ })); describe('Class: ProviderService', () => { - beforeEach(() => { jest.clearAllMocks(); }); describe('Method: captureAWS', () => { - test('when called, it forwards the correct parameter, and call the correct function', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.captureAWS({}); - + // Assess expect(captureAWS).toHaveBeenCalledTimes(1); expect(captureAWS).toHaveBeenCalledWith({}); - }); - }); describe('Method: captureAWSClient', () => { - test('when called, it forwards the correct parameter, and call the correct function', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.captureAWSClient({}); - + // Assess expect(captureAWSClient).toHaveBeenCalledTimes(1); expect(captureAWSClient).toHaveBeenCalledWith({}); - }); - }); describe('Method: captureAWSv3Client', () => { - test('when called, it forwards the correct parameter, and call the correct function', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.captureAWSv3Client({}); - + // Assess expect(captureAWSv3Client).toHaveBeenCalledTimes(1); expect(captureAWSv3Client).toHaveBeenCalledWith({}); - }); - }); describe('Method: captureAsyncFunc', () => { - test('when called, it forwards the correct parameter, and call the correct function', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.captureAsyncFunc('my-func', () => true); - + // Assess expect(captureAsyncFunc).toHaveBeenCalledTimes(1); - expect(captureAsyncFunc).toHaveBeenCalledWith('my-func', expect.anything()); - + expect(captureAsyncFunc).toHaveBeenCalledWith( + 'my-func', + expect.anything() + ); }); - }); describe('Method: captureHTTPsGlobal', () => { - test('when called, it forwards the correct parameter and calls the correct function, twice', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.captureHTTPsGlobal(); - + // Assess expect(captureHTTPsGlobal).toHaveBeenCalledTimes(2); expect(captureHTTPsGlobal).toHaveBeenNthCalledWith(1, http); expect(captureHTTPsGlobal).toHaveBeenNthCalledWith(2, https); - }); - }); describe('Method: captureFunc', () => { - test('when called, it forwards the correct parameter, and call the correct function', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.captureFunc('my-func', () => true); - + // Assess expect(captureFunc).toHaveBeenCalledTimes(1); expect(captureFunc).toHaveBeenCalledWith('my-func', expect.anything()); - }); - }); describe('Method: captureFunc', () => { - test('when called, it forwards the correct parameter, and call the correct function', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.captureFunc('my-func', () => true); - + // Assess expect(captureFunc).toHaveBeenCalledTimes(1); expect(captureFunc).toHaveBeenCalledWith('my-func', expect.anything()); - }); - }); describe('Method: getNamespace', () => { - test('when called, it forwards the correct parameter, and call the correct function', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.getNamespace(); - + // Assess expect(getNamespace).toHaveBeenCalledTimes(1); expect(getNamespace).toHaveBeenCalledWith(); - }); - }); describe('Method: getSegment', () => { - test('when called, it forwards the correct parameter, and call the correct function', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.getSegment(); - + // Assess expect(getSegment).toHaveBeenCalledTimes(1); expect(getSegment).toHaveBeenCalledWith(); - }); - }); describe('Method: setContextMissingStrategy', () => { - test('when called, it forwards the correct parameter, and call the correct function', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.setContextMissingStrategy('LOG_ERROR'); - + // Assess expect(setContextMissingStrategy).toHaveBeenCalledTimes(1); expect(setContextMissingStrategy).toHaveBeenCalledWith('LOG_ERROR'); - }); - }); describe('Method: setDaemonAddress', () => { - test('when called, it forwards the correct parameter, and call the correct function', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.setDaemonAddress('http://localhost:8000'); - + // Assess expect(setDaemonAddress).toHaveBeenCalledTimes(1); expect(setDaemonAddress).toHaveBeenCalledWith('http://localhost:8000'); - }); - }); describe('Method: setLogger', () => { - test('when called, it forwards the correct parameter, and call the correct function', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.setLogger({}); - + // Assess expect(setLogger).toHaveBeenCalledTimes(1); expect(setLogger).toHaveBeenCalledWith({}); - }); - }); describe('Method: setSegment', () => { - test('when called, it forwards the correct parameter, and call the correct function', () => { - // Prepare const provider: ProviderService = new ProviderService(); - + // Act provider.setSegment({ name: '## foo-bar' } as unknown as Subsegment); - + // Assess expect(setSegment).toHaveBeenCalledTimes(1); expect(setSegment).toHaveBeenCalledWith({ name: '## foo-bar' }); - }); - }); describe('Method: putAnnotation', () => { - test('when called and there is no segment, it logs a warning and does not throw', () => { - // Prepare const provider: ProviderService = new ProviderService(); const logSpy = jest.spyOn(console, 'warn').mockImplementation(); @@ -294,17 +242,18 @@ describe('Class: ProviderService', () => { // Assess expect(logSpy).toHaveBeenCalledTimes(1); - expect(logSpy).toHaveBeenCalledWith('No active segment or subsegment found, skipping annotation'); - + expect(logSpy).toHaveBeenCalledWith( + 'No active segment or subsegment found, skipping annotation' + ); }); test('when called and the current segment is not a subsegment, it logs a warning and does not annotate the segment', () => { - // Prepare const provider: ProviderService = new ProviderService(); const facade = new Segment('facade'); const logWarningSpy = jest.spyOn(console, 'warn').mockImplementation(); - jest.spyOn(provider, 'getSegment') + jest + .spyOn(provider, 'getSegment') .mockImplementation(() => new Segment('facade')); const addAnnotationSpy = jest.spyOn(facade, 'addAnnotation'); @@ -317,16 +266,13 @@ describe('Class: ProviderService', () => { 'You cannot annotate the main segment in a Lambda execution environment' ); expect(addAnnotationSpy).toHaveBeenCalledTimes(0); - }); test('when called and the current segment is a subsegment, it annotates it', () => { - // Prepare const provider: ProviderService = new ProviderService(); const segment = new Subsegment('## dummySegment'); - jest.spyOn(provider, 'getSegment') - .mockImplementation(() => segment); + jest.spyOn(provider, 'getSegment').mockImplementation(() => segment); const segmentSpy = jest.spyOn(segment, 'addAnnotation'); // Act @@ -335,15 +281,11 @@ describe('Class: ProviderService', () => { // Assess expect(segmentSpy).toHaveBeenCalledTimes(1); expect(segmentSpy).toHaveBeenCalledWith('foo', 'bar'); - }); - }); - - describe('Method: putMetadata', () => { + describe('Method: putMetadata', () => { test('when called and there is no segment, it logs a warning and does not throw', () => { - // Prepare const provider: ProviderService = new ProviderService(); const logWarningSpy = jest.spyOn(console, 'warn').mockImplementation(); @@ -356,16 +298,15 @@ describe('Class: ProviderService', () => { expect(logWarningSpy).toHaveBeenCalledWith( 'No active segment or subsegment found, skipping metadata addition' ); - }); test('when called and the current segment is not a subsegment, it logs a warning and does not annotate the segment', () => { - // Prepare const provider: ProviderService = new ProviderService(); const facade = new Segment('facade'); const logSpy = jest.spyOn(console, 'warn').mockImplementation(); - jest.spyOn(provider, 'getSegment') + jest + .spyOn(provider, 'getSegment') .mockImplementation(() => new Segment('facade')); const facadeSpy = jest.spyOn(facade, 'addMetadata'); @@ -378,16 +319,13 @@ describe('Class: ProviderService', () => { 'You cannot add metadata to the main segment in a Lambda execution environment' ); expect(facadeSpy).toHaveBeenCalledTimes(0); - }); test('when called and the current segment is a subsegment, it adds the metadata', () => { - // Prepare const provider: ProviderService = new ProviderService(); const segment = new Subsegment('## dummySegment'); - jest.spyOn(provider, 'getSegment') - .mockImplementation(() => segment); + jest.spyOn(provider, 'getSegment').mockImplementation(() => segment); const segmentSpy = jest.spyOn(segment, 'addMetadata'); // Act @@ -396,9 +334,6 @@ describe('Class: ProviderService', () => { // Assess expect(segmentSpy).toHaveBeenCalledTimes(1); expect(segmentSpy).toHaveBeenCalledWith('foo', 'bar', 'baz'); - }); - }); - -}); \ No newline at end of file +}); diff --git a/packages/tracer/tests/unit/Tracer.test.ts b/packages/tracer/tests/unit/Tracer.test.ts index 28ae7ffa4d..5cbcc14b06 100644 --- a/packages/tracer/tests/unit/Tracer.test.ts +++ b/packages/tracer/tests/unit/Tracer.test.ts @@ -4,15 +4,34 @@ * @group unit/tracer/all */ -import { ContextExamples as dummyContext, Events as dummyEvent, LambdaInterface } from '@aws-lambda-powertools/commons'; +import { + ContextExamples as dummyContext, + Events as dummyEvent, + LambdaInterface, +} from '@aws-lambda-powertools/commons'; import { Tracer } from '../../src'; import { Callback, Context } from 'aws-lambda/handler'; -import { Segment, setContextMissingStrategy, Subsegment } from 'aws-xray-sdk-core'; +import { + Segment, + setContextMissingStrategy, + Subsegment, +} from 'aws-xray-sdk-core'; import { ProviderServiceInterface } from '../../src/provider'; -type CaptureAsyncFuncMock = jest.SpyInstance unknown, parent?: Segment | Subsegment]>; -const createCaptureAsyncFuncMock = function(provider: ProviderServiceInterface, subsegment?: Subsegment): CaptureAsyncFuncMock { - return jest.spyOn(provider, 'captureAsyncFunc') +type CaptureAsyncFuncMock = jest.SpyInstance< + unknown, + [ + name: string, + fcn: (subsegment?: Subsegment) => unknown, + parent?: Segment | Subsegment + ] +>; +const createCaptureAsyncFuncMock = function ( + provider: ProviderServiceInterface, + subsegment?: Subsegment +): CaptureAsyncFuncMock { + return jest + .spyOn(provider, 'captureAsyncFunc') .mockImplementation(async (methodName, callBackFn) => { if (!subsegment) { subsegment = new Subsegment(`### ${methodName}`); @@ -42,9 +61,7 @@ describe('Class: Tracer', () => { }); describe('Method: annotateColdStart', () => { - test('when called while tracing is disabled, it does nothing', () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); const putAnnotationSpy = jest.spyOn(tracer, 'putAnnotation'); @@ -54,14 +71,14 @@ describe('Class: Tracer', () => { // Assess expect(putAnnotationSpy).toBeCalledTimes(0); - }); test('when called multiple times, it annotates true the first time and then false afterwards', () => { - // Prepare const tracer: Tracer = new Tracer(); - const putAnnotationSpy = jest.spyOn(tracer, 'putAnnotation').mockImplementation(() => null); + const putAnnotationSpy = jest + .spyOn(tracer, 'putAnnotation') + .mockImplementation(() => null); // Act tracer.annotateColdStart(); @@ -72,20 +89,16 @@ describe('Class: Tracer', () => { // Assess expect(putAnnotationSpy).toBeCalledTimes(4); expect(putAnnotationSpy.mock.calls).toEqual([ - [ 'ColdStart', true ], - [ 'ColdStart', false ], - [ 'ColdStart', false ], - [ 'ColdStart', false ], + ['ColdStart', true], + ['ColdStart', false], + ['ColdStart', false], + ['ColdStart', false], ]); - }); - }); describe('Method: addServiceNameAnnotation', () => { - test('when called while tracing is disabled, it does nothing', () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); const putAnnotation = jest.spyOn(tracer, 'putAnnotation'); @@ -95,14 +108,14 @@ describe('Class: Tracer', () => { // Assess expect(putAnnotation).toBeCalledTimes(0); - }); test('when called while a serviceName has been set, it adds it as annotation', () => { - // Prepare const tracer: Tracer = new Tracer({ serviceName: 'foo' }); - const putAnnotation = jest.spyOn(tracer, 'putAnnotation').mockImplementation(() => null); + const putAnnotation = jest + .spyOn(tracer, 'putAnnotation') + .mockImplementation(() => null); // Act tracer.addServiceNameAnnotation(); @@ -110,15 +123,15 @@ describe('Class: Tracer', () => { // Assess expect(putAnnotation).toBeCalledTimes(1); expect(putAnnotation).toBeCalledWith('Service', 'foo'); - }); test('when called when a serviceName has not been set in the constructor or environment variables, it adds the default service name as an annotation', () => { - // Prepare delete process.env.POWERTOOLS_SERVICE_NAME; const tracer: Tracer = new Tracer(); - const putAnnotation = jest.spyOn(tracer, 'putAnnotation').mockImplementation(() => null); + const putAnnotation = jest + .spyOn(tracer, 'putAnnotation') + .mockImplementation(() => null); // Act tracer.addServiceNameAnnotation(); @@ -126,15 +139,11 @@ describe('Class: Tracer', () => { // Assess expect(putAnnotation).toBeCalledTimes(1); expect(putAnnotation).toBeCalledWith('Service', 'service_undefined'); - }); - }); describe('Method: addResponseAsMetadata', () => { - test('when called while tracing is disabled, it does nothing', () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); const putMetadataSpy = jest.spyOn(tracer, 'putMetadata'); @@ -144,11 +153,9 @@ describe('Class: Tracer', () => { // Assess expect(putMetadataSpy).toBeCalledTimes(0); - }); test('when called while POWERTOOLS_TRACER_CAPTURE_RESPONSE is set to false, it does nothing', () => { - // Prepare process.env.POWERTOOLS_TRACER_CAPTURE_RESPONSE = 'false'; const tracer: Tracer = new Tracer(); @@ -160,11 +167,9 @@ describe('Class: Tracer', () => { // Assess expect(putMetadataSpy).toBeCalledTimes(0); delete process.env.POWERTOOLS_TRACER_CAPTURE_RESPONSE; - }); test('when called with data equal to undefined, it does nothing', () => { - // Prepare const tracer: Tracer = new Tracer(); const putMetadataSpy = jest.spyOn(tracer, 'putMetadata'); @@ -174,30 +179,29 @@ describe('Class: Tracer', () => { // Assess expect(putMetadataSpy).toBeCalledTimes(0); - }); test('when called with default config, it calls tracer.putMetadata correctly', () => { - // Prepare const tracer: Tracer = new Tracer(); - const putMetadataSpy = jest.spyOn(tracer, 'putMetadata').mockImplementation(() => null); + const putMetadataSpy = jest + .spyOn(tracer, 'putMetadata') + .mockImplementation(() => null); // Act tracer.addResponseAsMetadata({ foo: 'bar' }, context.functionName); // Assess expect(putMetadataSpy).toBeCalledTimes(1); - expect(putMetadataSpy).toBeCalledWith(`${context.functionName} response`, expect.objectContaining({ foo: 'bar' })); - + expect(putMetadataSpy).toBeCalledWith( + `${context.functionName} response`, + expect.objectContaining({ foo: 'bar' }) + ); }); - }); describe('Method: addErrorAsMetadata', () => { - test('when called while tracing is disabled, it does nothing', () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); const getSegmentSpy = jest.spyOn(tracer, 'getSegment'); @@ -207,11 +211,9 @@ describe('Class: Tracer', () => { // Assess expect(getSegmentSpy).toBeCalledTimes(0); - }); test('when called while POWERTOOLS_TRACER_CAPTURE_ERROR is set to false, it does not capture the error', () => { - // Prepare process.env.POWERTOOLS_TRACER_CAPTURE_ERROR = 'false'; const tracer: Tracer = new Tracer(); @@ -227,11 +229,9 @@ describe('Class: Tracer', () => { expect(addErrorFlagSpy).toBeCalledTimes(1); expect(addErrorSpy).toBeCalledTimes(0); delete process.env.POWERTOOLS_TRACER_CAPTURE_ERROR; - }); test('when called with default config, it calls subsegment.addError correctly', () => { - // Prepare const tracer: Tracer = new Tracer(); const subsegment = new Subsegment(`## ${context.functionName}`); @@ -246,26 +246,20 @@ describe('Class: Tracer', () => { expect(addErrorFlagSpy).toBeCalledTimes(0); expect(addErrorSpy).toBeCalledTimes(1); expect(addErrorSpy).toBeCalledWith(new Error('foo'), false); - }); test('when called and the segment is not found, it returns instead of throwing', () => { - // Prepare const tracer: Tracer = new Tracer(); jest.spyOn(tracer, 'getSegment').mockImplementation(() => undefined); // Act & Assess expect(() => tracer.addErrorAsMetadata(new Error('foo'))).not.toThrow(); - }); - }); describe('Method: getRootXrayTraceId', () => { - test('when called, it returns the X-Ray trace ID', () => { - // Prepare const tracer: Tracer = new Tracer(); @@ -274,11 +268,9 @@ describe('Class: Tracer', () => { // Assess expect(xRayTraceId).toBe('1-abcdef12-3456abcdef123456abcdef12'); - }); test('when called and Tracer is disabled, it returns undefined', () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); @@ -288,13 +280,10 @@ describe('Class: Tracer', () => { // Assess expect(xRayTraceId).toBe(undefined); }); - }); describe('Method: isTraceSampled', () => { - test('when called, it returns true if the Sampled flag is set', () => { - // Prepare const tracer: Tracer = new Tracer(); @@ -303,11 +292,9 @@ describe('Class: Tracer', () => { // Assess expect(xRayTraceSampled).toBe(false); - }); test('when called and Trace is disabled, it returns false', () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); @@ -316,32 +303,28 @@ describe('Class: Tracer', () => { // Assess expect(xRayTraceSampled).toBe(false); - }); - }); describe('Method: getSegment', () => { - test('when called and no segment is returned, it logs a warning', () => { - // Prepare const tracer: Tracer = new Tracer(); - jest.spyOn(tracer.provider, 'getSegment').mockImplementation(() => undefined); + jest + .spyOn(tracer.provider, 'getSegment') + .mockImplementation(() => undefined); jest.spyOn(console, 'warn').mockImplementation(() => null); - + // Act tracer.getSegment(); - + // Assess expect(console.warn).toHaveBeenCalledWith( 'Failed to get the current sub/segment from the context, this is likely because you are not using the Tracer in a Lambda function.' ); - }); test('when called outside of a namespace or without parent segment, and tracing is disabled, it returns a dummy subsegment', () => { - // Prepare delete process.env.AWS_EXECUTION_ENV; // This will disable the tracer, simulating local execution const tracer: Tracer = new Tracer(); @@ -352,463 +335,541 @@ describe('Class: Tracer', () => { // Assess expect(segment).toBeInstanceOf(Subsegment); expect((segment as Subsegment).name).toBe('## Dummy segment'); - }); test('when called within a namespace, it returns the parent segment', () => { - // Prepare const tracer: Tracer = new Tracer(); - jest.spyOn(tracer.provider, 'getSegment').mockImplementation(() => new Segment('facade', process.env._X_AMZN_TRACE_ID || null)); - + jest + .spyOn(tracer.provider, 'getSegment') + .mockImplementation( + () => new Segment('facade', process.env._X_AMZN_TRACE_ID || null) + ); + // Act const segment = tracer.getSegment(); - + // Assess expect(segment).toBeInstanceOf(Segment); - expect(segment).toEqual(expect.objectContaining({ - 'name': 'facade', - 'trace_id': process.env._X_AMZN_TRACE_ID - })); - + expect(segment).toEqual( + expect.objectContaining({ + name: 'facade', + trace_id: process.env._X_AMZN_TRACE_ID, + }) + ); }); - }); describe('Method: setSegment', () => { test('when called outside of a namespace or without parent segment, and Tracer is enabled, it throws an error', () => { - // Prepare const tracer: Tracer = new Tracer(); - + // Act / Assess expect(() => { const newSubsegment = new Subsegment('## foo.bar'); tracer.setSegment(newSubsegment); - }).toThrow('No context available. ns.run() or ns.bind() must be called first.'); + }).toThrow( + 'No context available. ns.run() or ns.bind() must be called first.' + ); }); test('when called outside of a namespace or without parent segment, and tracing is disabled, it does nothing', () => { - // Prepare delete process.env.AWS_EXECUTION_ENV; // This will disable the tracer, simulating local execution const tracer: Tracer = new Tracer(); const setSegmentSpy = jest.spyOn(tracer.provider, 'setSegment'); - + // Act const newSubsegment = new Subsegment('## foo.bar'); tracer.setSegment(newSubsegment); - + // Assess expect(setSegmentSpy).toBeCalledTimes(0); - }); test('when called within a namespace, it sets the segment', () => { - // Prepare const tracer: Tracer = new Tracer(); - jest.spyOn(tracer.provider, 'getSegment').mockImplementation(() => new Segment('facade', process.env._X_AMZN_TRACE_ID || null)); - const providerSetSegmentSpy = jest.spyOn(tracer.provider, 'setSegment').mockImplementation(() => ({})); - + jest + .spyOn(tracer.provider, 'getSegment') + .mockImplementation( + () => new Segment('facade', process.env._X_AMZN_TRACE_ID || null) + ); + const providerSetSegmentSpy = jest + .spyOn(tracer.provider, 'setSegment') + .mockImplementation(() => ({})); + // Act - const newSubsegment: Segment | Subsegment | undefined = new Subsegment('## foo.bar'); + const newSubsegment: Segment | Subsegment | undefined = new Subsegment( + '## foo.bar' + ); tracer.setSegment(newSubsegment); // Assess expect(providerSetSegmentSpy).toBeCalledTimes(1); - expect(providerSetSegmentSpy).toBeCalledWith(expect.objectContaining({ - 'id': newSubsegment.id, - 'name': newSubsegment.name - })); - + expect(providerSetSegmentSpy).toBeCalledWith( + expect.objectContaining({ + id: newSubsegment.id, + name: newSubsegment.name, + }) + ); }); - }); describe('Method: putAnnotation', () => { - test('when called while tracing is disabled, it does nothing', () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); const putAnnotationSpy = jest.spyOn(tracer.provider, 'putAnnotation'); // Act tracer.putAnnotation('foo', 'bar'); - + // Assess expect(putAnnotationSpy).toBeCalledTimes(0); - }); - + test('it calls the provider method with the correct arguments', () => { - // Prepare const tracer: Tracer = new Tracer(); const putAnnotationSpy = jest.spyOn(tracer.provider, 'putAnnotation'); // Act tracer.putAnnotation('foo', 'bar'); - + // Assess expect(putAnnotationSpy).toBeCalledTimes(1); expect(putAnnotationSpy).toBeCalledWith('foo', 'bar'); - }); - }); describe('Method: putMetadata', () => { - test('when tracing is disabled, it does nothing', () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); const putMetadataSpy = jest.spyOn(tracer.provider, 'putMetadata'); // Act tracer.putMetadata('foo', 'bar'); - + // Assess expect(putMetadataSpy).toBeCalledTimes(0); - }); - + test('it calls the provider method with the correct arguments', () => { - // Prepare const tracer: Tracer = new Tracer(); const putMetadataSpy = jest.spyOn(tracer.provider, 'putMetadata'); // Act tracer.putMetadata('foo', 'bar'); - + // Assess expect(putMetadataSpy).toBeCalledTimes(1); // The default namespace is 'hello-world' and it comes from the service name environment variable expect(putMetadataSpy).toBeCalledWith('foo', 'bar', 'hello-world'); - }); - + test('when passed a custom namespace, it calls the provider method with the correct arguments', () => { - // Prepare const tracer: Tracer = new Tracer(); const putMetadataSpy = jest.spyOn(tracer.provider, 'putMetadata'); // Act tracer.putMetadata('foo', 'bar', 'baz'); - + // Assess expect(putMetadataSpy).toBeCalledTimes(1); expect(putMetadataSpy).toBeCalledWith('foo', 'bar', 'baz'); - }); - + test('when a custom namespace was set in the constructor, it calls the provider method with the correct arguments', () => { - // Prepare const tracer: Tracer = new Tracer({ serviceName: 'baz' }); const putMetadataSpy = jest.spyOn(tracer.provider, 'putMetadata'); // Act tracer.putMetadata('foo', 'bar'); - + // Assess expect(putMetadataSpy).toBeCalledTimes(1); expect(putMetadataSpy).toBeCalledWith('foo', 'bar', 'baz'); - }); - }); describe('Method: captureLambdaHandler', () => { - test('when used as decorator while tracing is disabled, it does nothing', async () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); - jest.spyOn(tracer.provider, 'getSegment').mockImplementation(() => new Segment('facade', process.env._X_AMZN_TRACE_ID || null)); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); + jest + .spyOn(tracer.provider, 'getSegment') + .mockImplementation( + () => new Segment('facade', process.env._X_AMZN_TRACE_ID || null) + ); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); class Lambda implements LambdaInterface { - @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - return new Promise((resolve, _reject) => resolve({ - foo: 'bar' - } as unknown as TResult)); + public handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { + return new Promise((resolve, _reject) => + resolve({ + foo: 'bar', + } as unknown as TResult) + ); } - } - + // Act - await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + await new Lambda().handler(event, context, () => + console.log('Lambda invoked!') + ); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(0); - }); test('when used as decorator while POWERTOOLS_TRACER_CAPTURE_RESPONSE is set to false, it does not capture the response as metadata', async () => { - // Prepare process.env.POWERTOOLS_TRACER_CAPTURE_RESPONSE = 'false'; const tracer: Tracer = new Tracer(); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); const putMetadataSpy = jest.spyOn(tracer, 'putMetadata'); - - class Lambda implements LambdaInterface { + class Lambda implements LambdaInterface { @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - return new Promise((resolve, _reject) => resolve({ - foo: 'bar' - } as unknown as TResult)); + public handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { + return new Promise((resolve, _reject) => + resolve({ + foo: 'bar', + } as unknown as TResult) + ); } - } - + // Act - await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + await new Lambda().handler(event, context, () => + console.log('Lambda invoked!') + ); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); expect(putMetadataSpy).toHaveBeenCalledTimes(0); delete process.env.POWERTOOLS_TRACER_CAPTURE_RESPONSE; - }); test('when used as decorator while captureResponse is set to false, it does not capture the response as metadata', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); - const addResponseAsMetadataSpy = jest.spyOn(tracer, 'addResponseAsMetadata'); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); + const addResponseAsMetadataSpy = jest.spyOn( + tracer, + 'addResponseAsMetadata' + ); class Lambda implements LambdaInterface { - @tracer.captureLambdaHandler({ captureResponse: false }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - return new Promise((resolve, _reject) => resolve({ - foo: 'bar' - } as unknown as TResult)); + public handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { + return new Promise((resolve, _reject) => + resolve({ + foo: 'bar', + } as unknown as TResult) + ); } - } // Act - await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + await new Lambda().handler(event, context, () => + console.log('Lambda invoked!') + ); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); expect(addResponseAsMetadataSpy).toHaveBeenCalledTimes(0); - }); test('when used as decorator while captureResponse is set to true, it captures the response as metadata', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); - const addResponseAsMetadataSpy = jest.spyOn(tracer, 'addResponseAsMetadata'); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); + const addResponseAsMetadataSpy = jest.spyOn( + tracer, + 'addResponseAsMetadata' + ); class Lambda implements LambdaInterface { - @tracer.captureLambdaHandler({ captureResponse: true }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - return new Promise((resolve, _reject) => resolve({ - foo: 'bar' - } as unknown as TResult)); + public handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { + return new Promise((resolve, _reject) => + resolve({ + foo: 'bar', + } as unknown as TResult) + ); } - } - + // Act - await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + await new Lambda().handler(event, context, () => + console.log('Lambda invoked!') + ); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); - expect(captureAsyncFuncSpy).toHaveBeenCalledWith('## index.handler', expect.anything()); + expect(captureAsyncFuncSpy).toHaveBeenCalledWith( + '## index.handler', + expect.anything() + ); expect(addResponseAsMetadataSpy).toHaveBeenCalledTimes(1); - expect(addResponseAsMetadataSpy).toHaveBeenCalledWith({ foo: 'bar' }, 'index.handler'); - + expect(addResponseAsMetadataSpy).toHaveBeenCalledWith( + { foo: 'bar' }, + 'index.handler' + ); }); test('when used as decorator and with standard config, it captures the response as metadata', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); - const addResponseAsMetadataSpy = jest.spyOn(tracer, 'addResponseAsMetadata'); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); + const addResponseAsMetadataSpy = jest.spyOn( + tracer, + 'addResponseAsMetadata' + ); class Lambda implements LambdaInterface { - @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - return new Promise((resolve, _reject) => resolve({ - foo: 'bar' - } as unknown as TResult)); + public handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { + return new Promise((resolve, _reject) => + resolve({ + foo: 'bar', + } as unknown as TResult) + ); } - } - + // Act - await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + await new Lambda().handler(event, context, () => + console.log('Lambda invoked!') + ); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); - expect(captureAsyncFuncSpy).toHaveBeenCalledWith('## index.handler', expect.anything()); + expect(captureAsyncFuncSpy).toHaveBeenCalledWith( + '## index.handler', + expect.anything() + ); expect(addResponseAsMetadataSpy).toHaveBeenCalledTimes(1); - expect(addResponseAsMetadataSpy).toHaveBeenCalledWith({ foo: 'bar' }, 'index.handler'); - + expect(addResponseAsMetadataSpy).toHaveBeenCalledWith( + { foo: 'bar' }, + 'index.handler' + ); }); test('when used as decorator while POWERTOOLS_TRACER_CAPTURE_ERROR is set to false, it does not capture the exceptions', async () => { - // Prepare process.env.POWERTOOLS_TRACER_CAPTURE_ERROR = 'false'; const tracer: Tracer = new Tracer(); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); const newSubsegment = new Subsegment('### dummyMethod'); - jest.spyOn(tracer, 'getSegment') - .mockImplementation(() => newSubsegment); + jest.spyOn(tracer, 'getSegment').mockImplementation(() => newSubsegment); const addErrorFlagSpy = jest.spyOn(newSubsegment, 'addErrorFlag'); const addErrorSpy = jest.spyOn(newSubsegment, 'addError'); class Lambda implements LambdaInterface { - @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + public handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { throw new Error('Exception thrown!'); } - } - + // Act & Assess - await expect(new Lambda().handler({}, context, () => console.log('Lambda invoked!'))).rejects.toThrowError(Error); + await expect( + new Lambda().handler({}, context, () => console.log('Lambda invoked!')) + ).rejects.toThrowError(Error); expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); expect(addErrorFlagSpy).toHaveBeenCalledTimes(1); expect(addErrorSpy).toHaveBeenCalledTimes(0); expect.assertions(4); delete process.env.POWERTOOLS_TRACER_CAPTURE_ERROR; - }); test('when used as decorator and with standard config, it captures the exception', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); const addErrorAsMetadataSpy = jest.spyOn(tracer, 'addErrorAsMetadata'); const newSubsegment = new Subsegment('### dummyMethod'); - jest.spyOn(tracer, 'getSegment') - .mockImplementation(() => newSubsegment); + jest.spyOn(tracer, 'getSegment').mockImplementation(() => newSubsegment); const addErrorFlagSpy = jest.spyOn(newSubsegment, 'addErrorFlag'); const addErrorSpy = jest.spyOn(newSubsegment, 'addError'); class Lambda implements LambdaInterface { - @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + public handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { throw new Error('Exception thrown!'); } - } - + // Act & Assess - await expect(new Lambda().handler({}, context, () => console.log('Lambda invoked!'))).rejects.toThrowError(Error); + await expect( + new Lambda().handler({}, context, () => console.log('Lambda invoked!')) + ).rejects.toThrowError(Error); expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); expect(addErrorAsMetadataSpy).toHaveBeenCalledTimes(1); expect(addErrorAsMetadataSpy).toHaveBeenCalledWith(expect.any(Error)); expect(addErrorFlagSpy).toHaveBeenCalledTimes(0); expect(addErrorSpy).toHaveBeenCalledTimes(1); expect.assertions(6); - }); test('when used as decorator and with standard config, it annotates ColdStart', async () => { - // Prepare const tracer: Tracer = new Tracer(); const captureAsyncFuncSpy = createCaptureAsyncFuncMock(tracer.provider); const annotateColdStartSpy = jest.spyOn(tracer, 'annotateColdStart'); class Lambda implements LambdaInterface { - @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - return new Promise((resolve, _reject) => resolve({ - foo: 'bar' - } as unknown as TResult)); + public handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { + return new Promise((resolve, _reject) => + resolve({ + foo: 'bar', + } as unknown as TResult) + ); } - } - + // Act - await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + await new Lambda().handler(event, context, () => + console.log('Lambda invoked!') + ); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); - expect(captureAsyncFuncSpy).toHaveBeenCalledWith('## index.handler', expect.anything()); + expect(captureAsyncFuncSpy).toHaveBeenCalledWith( + '## index.handler', + expect.anything() + ); expect(annotateColdStartSpy).toHaveBeenCalledTimes(1); - }); test('when used as decorator and with standard config, it adds the Service annotation', async () => { - // Prepare const tracer: Tracer = new Tracer(); const captureAsyncFuncSpy = createCaptureAsyncFuncMock(tracer.provider); - const addServiceNameAnnotationSpy = jest.spyOn(tracer, 'addServiceNameAnnotation'); + const addServiceNameAnnotationSpy = jest.spyOn( + tracer, + 'addServiceNameAnnotation' + ); class Lambda implements LambdaInterface { - @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - return new Promise((resolve, _reject) => resolve({ - foo: 'bar' - } as unknown as TResult)); + public handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { + return new Promise((resolve, _reject) => + resolve({ + foo: 'bar', + } as unknown as TResult) + ); } - } - + // Act - await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + await new Lambda().handler(event, context, () => + console.log('Lambda invoked!') + ); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); - expect(captureAsyncFuncSpy).toHaveBeenCalledWith('## index.handler', expect.anything()); + expect(captureAsyncFuncSpy).toHaveBeenCalledWith( + '## index.handler', + expect.anything() + ); // The first call is for the Cold Start annotation expect(addServiceNameAnnotationSpy).toHaveBeenCalledTimes(1); - }); test('when used as decorator and when calling the handler, it has access to member variables', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod'); - jest.spyOn(tracer.provider, 'getSegment') + const newSubsegment: Segment | Subsegment | undefined = new Subsegment( + '### dummyMethod' + ); + jest + .spyOn(tracer.provider, 'getSegment') .mockImplementation(() => newSubsegment); setContextMissingStrategy(() => null); @@ -822,29 +883,37 @@ describe('Class: Tracer', () => { @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public async handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + public async handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { return (`memberVariable:${this.memberVariable}` as unknown); } - } // Act / Assess const lambda = new Lambda('someValue'); const handler = lambda.handler.bind(lambda); - expect(await handler({}, context, () => console.log('Lambda invoked!'))).toEqual('memberVariable:someValue'); - + expect( + await handler({}, context, () => console.log('Lambda invoked!')) + ).toEqual('memberVariable:someValue'); }); test('when used as decorator on an async method, the method is awaited correctly', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod'); - - jest.spyOn(tracer.provider, 'getSegment') + const newSubsegment: Segment | Subsegment | undefined = new Subsegment( + '### dummyMethod' + ); + + jest + .spyOn(tracer.provider, 'getSegment') .mockImplementation(() => newSubsegment); setContextMissingStrategy(() => null); - const subsegmentCloseSpy = jest.spyOn(newSubsegment, 'close').mockImplementation(); + const subsegmentCloseSpy = jest + .spyOn(newSubsegment, 'close') + .mockImplementation(); createCaptureAsyncFuncMock(tracer.provider, newSubsegment); class Lambda implements LambdaInterface { @@ -855,7 +924,11 @@ describe('Class: Tracer', () => { @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public async handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + public async handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { await this.dummyMethod(); this.otherDummyMethod(); @@ -865,12 +938,13 @@ describe('Class: Tracer', () => { public otherDummyMethod(): void { return; } - } - + // Act const lambda = new Lambda(); - const otherDummyMethodSpy = jest.spyOn(lambda, 'otherDummyMethod').mockImplementation(); + const otherDummyMethodSpy = jest + .spyOn(lambda, 'otherDummyMethod') + .mockImplementation(); const handler = lambda.handler.bind(lambda); await handler({}, context, () => console.log('Lambda invoked!')); @@ -879,22 +953,21 @@ describe('Class: Tracer', () => { // that should always be called after the handler has returned. If otherDummyMethodSpy is called after it means the // decorator is NOT awaiting the handler which would cause the test to fail. const dummyCallOrder = subsegmentCloseSpy.mock.invocationCallOrder[0]; - const otherDummyCallOrder = otherDummyMethodSpy.mock.invocationCallOrder[0]; + const otherDummyCallOrder = + otherDummyMethodSpy.mock.invocationCallOrder[0]; expect(otherDummyCallOrder).toBeLessThan(dummyCallOrder); - }); - }); describe('Method: captureMethod', () => { - test('when called while tracing is disabled, it does nothing', async () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); class Lambda implements LambdaInterface { - @tracer.captureMethod() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -903,137 +976,192 @@ describe('Class: Tracer', () => { return new Promise((resolve, _reject) => resolve(some)); } - public async handler(_event: TEvent, _context: Context, _callback: Callback): Promise { + public async handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): Promise { const result = await this.dummyMethod('foo bar'); - - return new Promise((resolve, _reject) => resolve(result as unknown as TResult)); - } + return new Promise((resolve, _reject) => + resolve(result as unknown as TResult) + ); + } } // Act - await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + await new Lambda().handler(event, context, () => + console.log('Lambda invoked!') + ); // Assess expect(captureAsyncFuncSpy).toBeCalledTimes(0); - }); test('when used as decorator and with standard config, it captures the response as metadata', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); - const addResponseAsMetadataSpy = jest.spyOn(tracer, 'addResponseAsMetadata'); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); + const addResponseAsMetadataSpy = jest.spyOn( + tracer, + 'addResponseAsMetadata' + ); class Lambda implements LambdaInterface { - @tracer.captureMethod() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore public async dummyMethod(some: string): Promise { - return new Promise((resolve, _reject) => setTimeout(() => resolve(some), 3000)); + return new Promise((resolve, _reject) => + setTimeout(() => resolve(some), 3000) + ); } - public async handler(_event: TEvent, _context: Context, _callback: Callback): Promise { + public async handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): Promise { const result = await this.dummyMethod('foo bar'); - - return new Promise((resolve, _reject) => resolve(result as unknown as TResult)); - } + return new Promise((resolve, _reject) => + resolve(result as unknown as TResult) + ); + } } // Act - await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + await new Lambda().handler(event, context, () => + console.log('Lambda invoked!') + ); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); - expect(captureAsyncFuncSpy).toHaveBeenCalledWith('### dummyMethod', expect.anything()); + expect(captureAsyncFuncSpy).toHaveBeenCalledWith( + '### dummyMethod', + expect.anything() + ); expect(addResponseAsMetadataSpy).toHaveBeenCalledTimes(1); - expect(addResponseAsMetadataSpy).toHaveBeenCalledWith('foo bar', 'dummyMethod'); - + expect(addResponseAsMetadataSpy).toHaveBeenCalledWith( + 'foo bar', + 'dummyMethod' + ); }); test('when used as decorator and with captureResponse set to false, it does not capture the response as metadata', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); - const addResponseAsMetadataSpy = jest.spyOn(tracer, 'addResponseAsMetadata'); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); + const addResponseAsMetadataSpy = jest.spyOn( + tracer, + 'addResponseAsMetadata' + ); class Lambda implements LambdaInterface { - @tracer.captureMethod({ captureResponse: false }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore public async dummyMethod(some: string): Promise { - return new Promise((resolve, _reject) => setTimeout(() => resolve(some), 3000)); + return new Promise((resolve, _reject) => + setTimeout(() => resolve(some), 3000) + ); } - public async handler(_event: TEvent, _context: Context, _callback: Callback): Promise { + public async handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): Promise { const result = await this.dummyMethod('foo bar'); - return new Promise((resolve, _reject) => resolve(result as unknown as TResult)); + return new Promise((resolve, _reject) => + resolve(result as unknown as TResult) + ); } - } // Act - await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + await new Lambda().handler(event, context, () => + console.log('Lambda invoked!') + ); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); - expect(captureAsyncFuncSpy).toHaveBeenCalledWith('### dummyMethod', expect.anything()); + expect(captureAsyncFuncSpy).toHaveBeenCalledWith( + '### dummyMethod', + expect.anything() + ); expect(addResponseAsMetadataSpy).toHaveBeenCalledTimes(0); - }); test('when used as decorator and with captureResponse set to true, it does captures the response as metadata', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); - const addResponseAsMetadataSpy = jest.spyOn(tracer, 'addResponseAsMetadata'); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); + const addResponseAsMetadataSpy = jest.spyOn( + tracer, + 'addResponseAsMetadata' + ); class Lambda implements LambdaInterface { - @tracer.captureMethod() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore public async dummyMethod(some: string): Promise { - return new Promise((resolve, _reject) => setTimeout(() => resolve(some), 3000)); + return new Promise((resolve, _reject) => + setTimeout(() => resolve(some), 3000) + ); } - public async handler(_event: TEvent, _context: Context, _callback: Callback): Promise { + public async handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): Promise { const result = await this.dummyMethod('foo bar'); - - return new Promise((resolve, _reject) => resolve(result as unknown as TResult)); - } + return new Promise((resolve, _reject) => + resolve(result as unknown as TResult) + ); + } } // Act - await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + await new Lambda().handler(event, context, () => + console.log('Lambda invoked!') + ); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); - expect(captureAsyncFuncSpy).toHaveBeenCalledWith('### dummyMethod', expect.anything()); + expect(captureAsyncFuncSpy).toHaveBeenCalledWith( + '### dummyMethod', + expect.anything() + ); expect(addResponseAsMetadataSpy).toHaveBeenCalledTimes(1); - }); test('when used as decorator and with standard config, it captures the exception correctly', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod'); - jest.spyOn(tracer.provider, 'getSegment') + const newSubsegment: Segment | Subsegment | undefined = new Subsegment( + '### dummyMethod' + ); + jest + .spyOn(tracer.provider, 'getSegment') .mockImplementation(() => newSubsegment); setContextMissingStrategy(() => null); const captureAsyncFuncSpy = createCaptureAsyncFuncMock(tracer.provider); const addErrorSpy = jest.spyOn(newSubsegment, 'addError'); class Lambda implements LambdaInterface { - @tracer.captureMethod() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -1041,38 +1169,50 @@ describe('Class: Tracer', () => { throw new Error('Exception thrown!'); } - public async handler(_event: TEvent, _context: Context, _callback: Callback): Promise { + public async handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): Promise { const result = await this.dummyMethod('foo bar'); - - return new Promise((resolve, _reject) => resolve(result as unknown as TResult)); - } + return new Promise((resolve, _reject) => + resolve(result as unknown as TResult) + ); + } } // Act / Assess - await expect(new Lambda().handler({}, context, () => console.log('Lambda invoked!'))).rejects.toThrowError(Error); + await expect( + new Lambda().handler({}, context, () => console.log('Lambda invoked!')) + ).rejects.toThrowError(Error); expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); - expect(newSubsegment).toEqual(expect.objectContaining({ - name: '### dummyMethod', - })); + expect(newSubsegment).toEqual( + expect.objectContaining({ + name: '### dummyMethod', + }) + ); expect('cause' in newSubsegment).toBe(true); expect(addErrorSpy).toHaveBeenCalledTimes(1); - expect(addErrorSpy).toHaveBeenCalledWith(new Error('Exception thrown!'), false); + expect(addErrorSpy).toHaveBeenCalledWith( + new Error('Exception thrown!'), + false + ); expect.assertions(6); - }); test('when used as decorator and when calling other methods/props in the class they are called in the orginal scope', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod'); - jest.spyOn(tracer.provider, 'getSegment') + const newSubsegment: Segment | Subsegment | undefined = new Subsegment( + '### dummyMethod' + ); + jest + .spyOn(tracer.provider, 'getSegment') .mockImplementation(() => newSubsegment); setContextMissingStrategy(() => null); class Lambda implements LambdaInterface { - @tracer.captureMethod() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -1083,27 +1223,35 @@ describe('Class: Tracer', () => { @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public async handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - return (await this.dummyMethod() as unknown); + public async handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { + return ((await this.dummyMethod()) as unknown); } public otherMethod(): string { return 'otherMethod'; } - } // Act / Assess - expect(await (new Lambda()).handler({}, context, () => console.log('Lambda invoked!'))).toEqual('otherMethod:otherMethod'); - + expect( + await new Lambda().handler({}, context, () => + console.log('Lambda invoked!') + ) + ).toEqual('otherMethod:otherMethod'); }); test('when used as decorator and when calling a method in the class, it has access to member variables', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod'); - jest.spyOn(tracer.provider, 'getSegment') + const newSubsegment: Segment | Subsegment | undefined = new Subsegment( + '### dummyMethod' + ); + jest + .spyOn(tracer.provider, 'getSegment') .mockImplementation(() => newSubsegment); setContextMissingStrategy(() => null); @@ -1124,28 +1272,34 @@ describe('Class: Tracer', () => { @tracer.captureLambdaHandler() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public async handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - return (await this.dummyMethod() as unknown); + public async handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): void | Promise { + return ((await this.dummyMethod()) as unknown); } - } // Act / Assess const lambda = new Lambda('someValue'); expect(await lambda.dummyMethod()).toEqual('memberVariable:someValue'); - }); test('when used as decorator on an async method, the method is awaited correctly', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod'); + const newSubsegment: Segment | Subsegment | undefined = new Subsegment( + '### dummyMethod' + ); - jest.spyOn(tracer.provider, 'getSegment') + jest + .spyOn(tracer.provider, 'getSegment') .mockImplementation(() => newSubsegment); setContextMissingStrategy(() => null); - const subsegmentCloseSpy = jest.spyOn(newSubsegment, 'close').mockImplementation(); + const subsegmentCloseSpy = jest + .spyOn(newSubsegment, 'close') + .mockImplementation(); createCaptureAsyncFuncMock(tracer.provider, newSubsegment); class Lambda implements LambdaInterface { @@ -1156,7 +1310,11 @@ describe('Class: Tracer', () => { return; } - public async handler(_event: TEvent, _context: Context, _callback: Callback): Promise { + public async handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): Promise { await this.dummyMethod(); this.otherDummyMethod(); @@ -1170,24 +1328,28 @@ describe('Class: Tracer', () => { // Act const lambda = new Lambda(); - const otherDummyMethodSpy = jest.spyOn(lambda, 'otherDummyMethod').mockImplementation(); + const otherDummyMethodSpy = jest + .spyOn(lambda, 'otherDummyMethod') + .mockImplementation(); const handler = lambda.handler.bind(lambda); await handler({}, context, () => console.log('Lambda invoked!')); - + // Here we assert that the subsegment.close() (inside the finally of decorator) is called before the other otherDummyMethodSpy method // that should always be called after the handler has returned. If subsegment.close() is called after it means the // decorator is NOT awaiting the method which would cause the test to fail. const dummyCallOrder = subsegmentCloseSpy.mock.invocationCallOrder[0]; - const otherDummyCallOrder = otherDummyMethodSpy.mock.invocationCallOrder[0]; + const otherDummyCallOrder = + otherDummyMethodSpy.mock.invocationCallOrder[0]; expect(dummyCallOrder).toBeLessThan(otherDummyCallOrder); - }); test('when used as decorator together with another external decorator, the method name is detected properly', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); // Creating custom external decorator // eslint-disable-next-line func-style @@ -1215,12 +1377,15 @@ describe('Class: Tracer', () => { return `foo`; } - public async handler(_event: TEvent, _context: Context, _callback: Callback): Promise { + public async handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): Promise { await this.dummyMethod(); return; } - } // Act / Assess @@ -1229,55 +1394,70 @@ describe('Class: Tracer', () => { await handler({}, context, () => console.log('Lambda invoked!')); // Assess - expect(captureAsyncFuncSpy).toHaveBeenCalledWith('### dummyMethod', expect.any(Function)); - + expect(captureAsyncFuncSpy).toHaveBeenCalledWith( + '### dummyMethod', + expect.any(Function) + ); }); test('when used as decorator and with a custom subSegmentName, it sets the correct name for the subsegment', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod'); + const newSubsegment: Segment | Subsegment | undefined = new Subsegment( + '### dummyMethod' + ); jest.spyOn(newSubsegment, 'flush').mockImplementation(() => null); - jest.spyOn(tracer.provider, 'getSegment') + jest + .spyOn(tracer.provider, 'getSegment') .mockImplementation(() => newSubsegment); setContextMissingStrategy(() => null); - const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); + const captureAsyncFuncSpy = jest.spyOn( + tracer.provider, + 'captureAsyncFunc' + ); class Lambda implements LambdaInterface { - @tracer.captureMethod({ subSegmentName: '#### myCustomMethod' }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore public async dummyMethod(some: string): Promise { - return new Promise((resolve, _reject) => setTimeout(() => resolve(some), 3000)); + return new Promise((resolve, _reject) => + setTimeout(() => resolve(some), 3000) + ); } - public async handler(_event: TEvent, _context: Context, _callback: Callback): Promise { + public async handler( + _event: TEvent, + _context: Context, + _callback: Callback + ): Promise { const result = await this.dummyMethod('foo bar'); - - return new Promise((resolve, _reject) => resolve(result as unknown as TResult)); - } + return new Promise((resolve, _reject) => + resolve(result as unknown as TResult) + ); + } } // Act - await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + await new Lambda().handler(event, context, () => + console.log('Lambda invoked!') + ); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); - expect(captureAsyncFuncSpy).toHaveBeenCalledWith('#### myCustomMethod', expect.anything()); - + expect(captureAsyncFuncSpy).toHaveBeenCalledWith( + '#### myCustomMethod', + expect.anything() + ); }); - }); describe('Method: captureAWS', () => { - test('when called while tracing is disabled, it does nothing', () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); - const captureAWSSpy = jest.spyOn(tracer.provider, 'captureAWS') + const captureAWSSpy = jest + .spyOn(tracer.provider, 'captureAWS') .mockImplementation(() => null); // Act @@ -1285,14 +1465,13 @@ describe('Class: Tracer', () => { // Assess expect(captureAWSSpy).toBeCalledTimes(0); - }); test('when called it returns the decorated object that was passed to it', () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAWSSpy = jest.spyOn(tracer.provider, 'captureAWS') + const captureAWSSpy = jest + .spyOn(tracer.provider, 'captureAWS') .mockImplementation(() => null); // Act @@ -1301,17 +1480,15 @@ describe('Class: Tracer', () => { // Assess expect(captureAWSSpy).toBeCalledTimes(1); expect(captureAWSSpy).toBeCalledWith({}); - }); - }); describe('Method: captureAWSv3Client', () => { - test('when called while tracing is disabled, it does nothing', () => { // Prepare const tracer: Tracer = new Tracer({ enabled: false }); - const captureAWSv3ClientSpy = jest.spyOn(tracer.provider, 'captureAWSv3Client') + const captureAWSv3ClientSpy = jest + .spyOn(tracer.provider, 'captureAWSv3Client') .mockImplementation(() => null); // Act @@ -1319,14 +1496,13 @@ describe('Class: Tracer', () => { // Assess expect(captureAWSv3ClientSpy).toBeCalledTimes(0); - }); test('when called it returns the decorated object that was passed to it', () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAWSv3ClientSpy = jest.spyOn(tracer.provider, 'captureAWSv3Client') + const captureAWSv3ClientSpy = jest + .spyOn(tracer.provider, 'captureAWSv3Client') .mockImplementation(() => null); // Act @@ -1335,32 +1511,30 @@ describe('Class: Tracer', () => { // Assess expect(captureAWSv3ClientSpy).toBeCalledTimes(1); expect(captureAWSv3ClientSpy).toBeCalledWith({}); - }); - }); describe('Method: captureAWSClient', () => { - test('when called while tracing is disabled, it does nothing', () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); - const captureAWSClientSpy = jest.spyOn(tracer.provider, 'captureAWSClient'); + const captureAWSClientSpy = jest.spyOn( + tracer.provider, + 'captureAWSClient' + ); // Act tracer.captureAWSClient({}); // Assess expect(captureAWSClientSpy).toBeCalledTimes(0); - }); test('when called with a base AWS SDK v2 client, it calls the provider method to patch it', () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAWSClientSpy = jest.spyOn(tracer.provider, 'captureAWSClient') + const captureAWSClientSpy = jest + .spyOn(tracer.provider, 'captureAWSClient') .mockImplementation(() => null); // Act @@ -1369,17 +1543,16 @@ describe('Class: Tracer', () => { // Assess expect(captureAWSClientSpy).toBeCalledTimes(1); expect(captureAWSClientSpy).toBeCalledWith({}); - }); test('when called with a complex AWS SDK v2 client, it calls the provider method to patch it', () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAWSClientSpy = jest.spyOn(tracer.provider, 'captureAWSClient') - .mockImplementationOnce( - () => { throw new Error('service.customizeRequests is not a function'); } - ) + const captureAWSClientSpy = jest + .spyOn(tracer.provider, 'captureAWSClient') + .mockImplementationOnce(() => { + throw new Error('service.customizeRequests is not a function'); + }) .mockImplementation(() => null); // Act @@ -1390,14 +1563,15 @@ describe('Class: Tracer', () => { expect(captureAWSClientSpy).toBeCalledTimes(2); expect(captureAWSClientSpy).toHaveBeenNthCalledWith(1, { service: {} }); expect(captureAWSClientSpy).toHaveBeenNthCalledWith(2, {}); - }); test('when called with an uncompatible object, it throws an error', () => { - // Prepare const tracer: Tracer = new Tracer(); - const captureAWSClientSpy = jest.spyOn(tracer.provider, 'captureAWSClient'); + const captureAWSClientSpy = jest.spyOn( + tracer.provider, + 'captureAWSClient' + ); // Act / Assess expect(() => { @@ -1407,9 +1581,6 @@ describe('Class: Tracer', () => { expect(captureAWSClientSpy).toHaveBeenNthCalledWith(1, {}); expect(captureAWSClientSpy).toHaveBeenNthCalledWith(2, undefined); expect.assertions(4); - }); - }); - -}); \ No newline at end of file +}); diff --git a/packages/tracer/tests/unit/config/EnvironmentVariablesService.test.ts b/packages/tracer/tests/unit/config/EnvironmentVariablesService.test.ts index 478944ce31..9e2adf9eb2 100644 --- a/packages/tracer/tests/unit/config/EnvironmentVariablesService.test.ts +++ b/packages/tracer/tests/unit/config/EnvironmentVariablesService.test.ts @@ -7,7 +7,6 @@ import { EnvironmentVariablesService } from '../../../src/config'; describe('Class: EnvironmentVariablesService', () => { - const ENVIRONMENT_VARIABLES = process.env; beforeEach(() => { @@ -20,9 +19,7 @@ describe('Class: EnvironmentVariablesService', () => { }); describe('Method: getTracingEnabled', () => { - test('It returns the value of the environment variable POWERTOOLS_TRACE_ENABLED', () => { - // Prepare process.env.POWERTOOLS_TRACE_ENABLED = 'false'; const service = new EnvironmentVariablesService(); @@ -33,13 +30,10 @@ describe('Class: EnvironmentVariablesService', () => { // Assess expect(value).toEqual('false'); }); - }); describe('Method: getTracingCaptureResponse', () => { - test('It returns the value of the environment variable POWERTOOLS_TRACER_CAPTURE_RESPONSE', () => { - // Prepare process.env.POWERTOOLS_TRACER_CAPTURE_RESPONSE = 'false'; const service = new EnvironmentVariablesService(); @@ -50,13 +44,10 @@ describe('Class: EnvironmentVariablesService', () => { // Assess expect(value).toEqual('false'); }); - }); describe('Method: getTracingCaptureError', () => { - test('It returns the value of the environment variable POWERTOOLS_TRACER_CAPTURE_ERROR', () => { - // Prepare process.env.POWERTOOLS_TRACER_CAPTURE_ERROR = 'false'; const service = new EnvironmentVariablesService(); @@ -67,13 +58,10 @@ describe('Class: EnvironmentVariablesService', () => { // Assess expect(value).toEqual('false'); }); - }); describe('Method: getCaptureHTTPsRequests', () => { - test('It returns the value of the environment variable POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS', () => { - // Prepare process.env.POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = 'false'; const service = new EnvironmentVariablesService(); @@ -84,13 +72,10 @@ describe('Class: EnvironmentVariablesService', () => { // Assess expect(value).toEqual('false'); }); - }); describe('Method: getSamLocal', () => { - test('It returns the value of the environment variable AWS_SAM_LOCAL', () => { - // Prepare process.env.AWS_SAM_LOCAL = 'true'; const service = new EnvironmentVariablesService(); @@ -101,13 +86,10 @@ describe('Class: EnvironmentVariablesService', () => { // Assess expect(value).toEqual('true'); }); - }); describe('Method: getAwsExecutionEnv', () => { - test('It returns the value of the environment variable AWS_EXECUTION_ENV', () => { - // Prepare process.env.AWS_EXECUTION_ENV = 'nodejs16.x'; const service = new EnvironmentVariablesService(); @@ -118,7 +100,5 @@ describe('Class: EnvironmentVariablesService', () => { // Assess expect(value).toEqual('nodejs16.x'); }); - }); - -}); \ No newline at end of file +}); diff --git a/packages/tracer/tests/unit/helpers.test.ts b/packages/tracer/tests/unit/helpers.test.ts index c127d79279..cb2394a875 100644 --- a/packages/tracer/tests/unit/helpers.test.ts +++ b/packages/tracer/tests/unit/helpers.test.ts @@ -30,16 +30,16 @@ describe('Helper: createTracer function', () => { // Assess expect(tracer).toBeInstanceOf(Tracer); - expect(tracer).toEqual(expect.objectContaining({ - tracingEnabled: true, - serviceName: 'hello-world', - captureHTTPsRequests: true - })); - + expect(tracer).toEqual( + expect.objectContaining({ + tracingEnabled: true, + serviceName: 'hello-world', + captureHTTPsRequests: true, + }) + ); }); test('when all tracer options are passed, returns a Tracer instance with the correct properties', () => { - // Prepare const tracerOptions = { enabled: false, @@ -52,19 +52,19 @@ describe('Helper: createTracer function', () => { // Assess expect(tracer).toBeInstanceOf(Tracer); - expect(tracer).toEqual(expect.objectContaining({ - tracingEnabled: false, - serviceName: 'my-lambda-service', - captureHTTPsRequests: false, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + tracingEnabled: false, + serviceName: 'my-lambda-service', + captureHTTPsRequests: false, + }) + ); }); test('when a custom serviceName is passed, returns a Tracer instance with the correct properties', () => { - // Prepare const tracerOptions = { - serviceName: 'my-lambda-service' + serviceName: 'my-lambda-service', }; // Act @@ -72,19 +72,19 @@ describe('Helper: createTracer function', () => { // Assess expect(tracer).toBeInstanceOf(Tracer); - expect(tracer).toEqual(expect.objectContaining({ - tracingEnabled: true, - serviceName: 'my-lambda-service', - captureHTTPsRequests: true, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + tracingEnabled: true, + serviceName: 'my-lambda-service', + captureHTTPsRequests: true, + }) + ); }); test('when a custom, but invalid, serviceName is passed, returns a Tracer instance with the correct properties', () => { - // Prepare const tracerOptions = { - serviceName: '' + serviceName: '', }; // Act @@ -92,19 +92,19 @@ describe('Helper: createTracer function', () => { // Assess expect(tracer).toBeInstanceOf(Tracer); - expect(tracer).toEqual(expect.objectContaining({ - tracingEnabled: true, - serviceName: 'hello-world', - captureHTTPsRequests: true, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + tracingEnabled: true, + serviceName: 'hello-world', + captureHTTPsRequests: true, + }) + ); }); test('when (tracing) disabled is passed, returns a Tracer instance with the correct properties', () => { - // Prepare const tracerOptions = { - enabled: false + enabled: false, }; // Act @@ -112,16 +112,16 @@ describe('Helper: createTracer function', () => { // Assess expect(tracer).toBeInstanceOf(Tracer); - expect(tracer).toEqual(expect.objectContaining({ - tracingEnabled: false, - serviceName: 'hello-world', - captureHTTPsRequests: true, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + tracingEnabled: false, + serviceName: 'hello-world', + captureHTTPsRequests: true, + }) + ); }); test('when a custom customConfigService is passed, returns a Logger instance with the correct proprieties', () => { - const configService: ConfigServiceInterface = { get(name: string): string { return `a-string-from-${name}`; @@ -140,70 +140,69 @@ describe('Helper: createTracer function', () => { }, getServiceName(): string { return 'my-backend-service'; - } + }, }; // Prepare const tracerOptions: TracerOptions = { - customConfigService: configService + customConfigService: configService, }; - + // Act const tracer = createTracer(tracerOptions); - + // Assess expect(tracer).toBeInstanceOf(Tracer); - expect(tracer).toEqual(expect.objectContaining({ - customConfigService: configService, - tracingEnabled: false, - serviceName: 'my-backend-service', - captureHTTPsRequests: false, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + customConfigService: configService, + tracingEnabled: false, + serviceName: 'my-backend-service', + captureHTTPsRequests: false, + }) + ); }); test('when tracing is enabled and patching of http requests is opted-out, captureHTTPsRequests is false', () => { - // Prepare const tracerOptions = { enabled: true, - captureHTTPsRequests: false + captureHTTPsRequests: false, }; - + // Act const tracer = createTracer(tracerOptions); // Assess expect(tracer).toBeInstanceOf(Tracer); - expect(tracer).toEqual(expect.objectContaining({ - tracingEnabled: true, - captureHTTPsRequests: false, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + tracingEnabled: true, + captureHTTPsRequests: false, + }) + ); }); test('when tracing is enabled captureHTTPsGlobal is true', () => { - // Prepare const tracerOptions = { enabled: true, }; - + // Act const tracer = createTracer(tracerOptions); // Assess expect(tracer).toBeInstanceOf(Tracer); - expect(tracer).toEqual(expect.objectContaining({ - tracingEnabled: true, - captureHTTPsRequests: true, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + tracingEnabled: true, + captureHTTPsRequests: true, + }) + ); }); - }); describe('Environment Variables configs', () => { - test('when AWS_EXECUTION_ENV environment variable is equal to AWS_Lambda_amplify-mock, tracing is disabled', () => { // Prepare process.env.AWS_EXECUTION_ENV = 'AWS_Lambda_amplify-mock'; @@ -212,10 +211,11 @@ describe('Helper: createTracer function', () => { const tracer = createTracer(); // Assess - expect(tracer).toEqual(expect.objectContaining({ - tracingEnabled: false, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + tracingEnabled: false, + }) + ); }); test('when AWS_SAM_LOCAL environment variable is set, tracing is disabled', () => { @@ -226,10 +226,11 @@ describe('Helper: createTracer function', () => { const tracer = createTracer(); // Assess - expect(tracer).toEqual(expect.objectContaining({ - tracingEnabled: false, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + tracingEnabled: false, + }) + ); }); test('when AWS_EXECUTION_ENV environment variable is set, tracing is enabled', () => { @@ -240,10 +241,11 @@ describe('Helper: createTracer function', () => { const tracer = createTracer(); // Assess - expect(tracer).toEqual(expect.objectContaining({ - tracingEnabled: true, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + tracingEnabled: true, + }) + ); }); test('when AWS_EXECUTION_ENV environment variable is NOT set, tracing is disabled', () => { @@ -254,9 +256,11 @@ describe('Helper: createTracer function', () => { const tracer = createTracer(); // Assess - expect(tracer).toEqual(expect.objectContaining({ - tracingEnabled: false, - })); + expect(tracer).toEqual( + expect.objectContaining({ + tracingEnabled: false, + }) + ); }); test('when POWERTOOLS_TRACE_ENABLED environment variable is set, a tracer with tracing disabled is returned', () => { @@ -267,10 +271,11 @@ describe('Helper: createTracer function', () => { const tracer = createTracer(); // Assess - expect(tracer).toEqual(expect.objectContaining({ - tracingEnabled: false, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + tracingEnabled: false, + }) + ); }); test('when POWERTOOLS_SERVICE_NAME environment variable is set, a tracer with the correct serviceName is returned', () => { @@ -281,10 +286,11 @@ describe('Helper: createTracer function', () => { const tracer = createTracer(); // Assess - expect(tracer).toEqual(expect.objectContaining({ - serviceName: 'my-backend-service' - })); - + expect(tracer).toEqual( + expect.objectContaining({ + serviceName: 'my-backend-service', + }) + ); }); test('when POWERTOOLS_TRACER_CAPTURE_RESPONSE environment variable is set, a tracer with captureResponse disabled is returned', () => { @@ -295,10 +301,11 @@ describe('Helper: createTracer function', () => { const tracer = createTracer(); // Assess - expect(tracer).toEqual(expect.objectContaining({ - captureResponse: false - })); - + expect(tracer).toEqual( + expect.objectContaining({ + captureResponse: false, + }) + ); }); test('when POWERTOOLS_TRACER_CAPTURE_RESPONSE environment variable is set to invalid value, a tracer with captureResponse enabled is returned', () => { @@ -309,10 +316,11 @@ describe('Helper: createTracer function', () => { const tracer = createTracer(); // Assess - expect(tracer).toEqual(expect.objectContaining({ - captureResponse: true - })); - + expect(tracer).toEqual( + expect.objectContaining({ + captureResponse: true, + }) + ); }); test('when POWERTOOLS_TRACER_CAPTURE_ERROR environment variable is set, a tracer with captureError disabled is returned', () => { @@ -323,10 +331,11 @@ describe('Helper: createTracer function', () => { const tracer = createTracer(); // Assess - expect(tracer).toEqual(expect.objectContaining({ - captureError: false - })); - + expect(tracer).toEqual( + expect.objectContaining({ + captureError: false, + }) + ); }); test('when POWERTOOLS_TRACER_CAPTURE_ERROR environment variable is set to invalid value, a tracer with captureError enabled is returned', () => { @@ -337,10 +346,11 @@ describe('Helper: createTracer function', () => { const tracer = createTracer(); // Assess - expect(tracer).toEqual(expect.objectContaining({ - captureError: true - })); - + expect(tracer).toEqual( + expect.objectContaining({ + captureError: true, + }) + ); }); test('when POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS environment variable is set, captureHTTPsGlobal is called', () => { @@ -351,10 +361,11 @@ describe('Helper: createTracer function', () => { const tracer = createTracer(); // Assess - expect(tracer).toEqual(expect.objectContaining({ - captureHTTPsRequests: false, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + captureHTTPsRequests: false, + }) + ); }); test('when POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS environment variable is set to invalid value, captureHTTPsGlobal is called', () => { @@ -365,11 +376,11 @@ describe('Helper: createTracer function', () => { const tracer = createTracer(); // Assess - expect(tracer).toEqual(expect.objectContaining({ - captureHTTPsRequests: true, - })); - + expect(tracer).toEqual( + expect.objectContaining({ + captureHTTPsRequests: true, + }) + ); }); - }); -}); \ No newline at end of file +}); diff --git a/packages/tracer/tests/unit/middy.test.ts b/packages/tracer/tests/unit/middy.test.ts index 6b8a2ca7b3..f3fc79e1c3 100644 --- a/packages/tracer/tests/unit/middy.test.ts +++ b/packages/tracer/tests/unit/middy.test.ts @@ -8,7 +8,11 @@ import { captureLambdaHandler } from '../../src/middleware/middy'; import middy from '@middy/core'; import { Tracer } from './../../src'; import type { Context, Handler } from 'aws-lambda/handler'; -import { Segment, setContextMissingStrategy, Subsegment } from 'aws-xray-sdk-core'; +import { + Segment, + setContextMissingStrategy, + Subsegment, +} from 'aws-xray-sdk-core'; jest.spyOn(console, 'debug').mockImplementation(() => null); jest.spyOn(console, 'warn').mockImplementation(() => null); @@ -23,7 +27,8 @@ describe('Middy middleware', () => { memoryLimitInMB: '128', logGroupName: '/aws/lambda/foo-bar-function-123456abcdef', logStreamName: '2021/03/09/[$LATEST]abcdef123456abcdef123456abcdef123456', - invokedFunctionArn: 'arn:aws:lambda:eu-west-1:123456789012:function:Example', + invokedFunctionArn: + 'arn:aws:lambda:eu-west-1:123456789012:function:Example', awsRequestId: 'c6af9ac6-7b61-11e6-9a41-93e8deadbeef', getRemainingTimeInMillis: () => 1234, done: () => console.log('Done!'), @@ -41,17 +46,23 @@ describe('Middy middleware', () => { process.env = ENVIRONMENT_VARIABLES; }); describe('Middleware: captureLambdaHandler', () => { - test('when used while tracing is disabled, it does nothing', async () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); - const setSegmentSpy = jest.spyOn(tracer.provider, 'setSegment').mockImplementation(); - const getSegmentSpy = jest.spyOn(tracer.provider, 'getSegment') - .mockImplementationOnce(() => new Segment('facade', process.env._X_AMZN_TRACE_ID || null)) + const setSegmentSpy = jest + .spyOn(tracer.provider, 'setSegment') + .mockImplementation(); + const getSegmentSpy = jest + .spyOn(tracer.provider, 'getSegment') + .mockImplementationOnce( + () => new Segment('facade', process.env._X_AMZN_TRACE_ID || null) + ) .mockImplementationOnce(() => new Subsegment('## index.handler')); - const lambdaHandler: Handler = async (_event: unknown, _context: Context) => ({ - foo: 'bar' + const lambdaHandler: Handler = async ( + _event: unknown, + _context: Context + ) => ({ + foo: 'bar', }); const handler = middy(lambdaHandler).use(captureLambdaHandler(tracer)); @@ -61,32 +72,38 @@ describe('Middy middleware', () => { // Assess expect(setSegmentSpy).toHaveBeenCalledTimes(0); expect(getSegmentSpy).toHaveBeenCalledTimes(0); - }); test('when used while tracing is disabled, even if the handler throws an error, it does nothing', async () => { - // Prepare const tracer: Tracer = new Tracer({ enabled: false }); - const setSegmentSpy = jest.spyOn(tracer.provider, 'setSegment').mockImplementation(); - const getSegmentSpy = jest.spyOn(tracer.provider, 'getSegment') - .mockImplementationOnce(() => new Segment('facade', process.env._X_AMZN_TRACE_ID || null)) + const setSegmentSpy = jest + .spyOn(tracer.provider, 'setSegment') + .mockImplementation(); + const getSegmentSpy = jest + .spyOn(tracer.provider, 'getSegment') + .mockImplementationOnce( + () => new Segment('facade', process.env._X_AMZN_TRACE_ID || null) + ) .mockImplementationOnce(() => new Subsegment('## index.handler')); - const lambdaHandler: Handler = async (_event: unknown, _context: Context) => { + const lambdaHandler: Handler = async ( + _event: unknown, + _context: Context + ) => { throw new Error('Exception thrown!'); }; const handler = middy(lambdaHandler).use(captureLambdaHandler(tracer)); // Act & Assess - await expect(handler({}, context, () => console.log('Lambda invoked!'))).rejects.toThrowError(Error); + await expect( + handler({}, context, () => console.log('Lambda invoked!')) + ).rejects.toThrowError(Error); expect(setSegmentSpy).toHaveBeenCalledTimes(0); expect(getSegmentSpy).toHaveBeenCalledTimes(0); expect.assertions(3); - }); test('when used while POWERTOOLS_TRACER_CAPTURE_RESPONSE is set to false, it does not capture the response as metadata', async () => { - // Prepare process.env.POWERTOOLS_TRACER_CAPTURE_RESPONSE = 'false'; const tracer: Tracer = new Tracer(); @@ -94,7 +111,7 @@ describe('Middy middleware', () => { const putMetadataSpy = jest.spyOn(tracer, 'putMetadata'); const handler = middy(async (_event: unknown, _context: Context) => ({ - foo: 'bar' + foo: 'bar', })).use(captureLambdaHandler(tracer)); // Act @@ -103,18 +120,16 @@ describe('Middy middleware', () => { // Assess expect(putMetadataSpy).toHaveBeenCalledTimes(0); delete process.env.POWERTOOLS_TRACER_CAPTURE_RESPONSE; - }); test('when used while captureResponse set to false, it does not capture the response as metadata', async () => { - // Prepare const tracer: Tracer = new Tracer(); jest.spyOn(tracer.provider, 'setSegment').mockImplementation(); const putMetadataSpy = jest.spyOn(tracer, 'putMetadata'); const handler = middy(async (_event: unknown, _context: Context) => ({ - foo: 'bar' + foo: 'bar', })).use(captureLambdaHandler(tracer, { captureResponse: false })); // Act @@ -122,18 +137,16 @@ describe('Middy middleware', () => { // Assess expect(putMetadataSpy).toHaveBeenCalledTimes(0); - }); test('when used while captureResponse set to true, it captures the response as metadata', async () => { - // Prepare const tracer: Tracer = new Tracer(); jest.spyOn(tracer.provider, 'setSegment').mockImplementation(); const putMetadataSpy = jest.spyOn(tracer, 'putMetadata'); const handler = middy(async (_event: unknown, _context: Context) => ({ - foo: 'bar' + foo: 'bar', })).use(captureLambdaHandler(tracer, { captureResponse: true })); // Act @@ -141,19 +154,19 @@ describe('Middy middleware', () => { // Assess expect(putMetadataSpy).toHaveBeenCalledTimes(1); - expect(putMetadataSpy).toHaveBeenCalledWith('index.handler response', { foo: 'bar' }); - + expect(putMetadataSpy).toHaveBeenCalledWith('index.handler response', { + foo: 'bar', + }); }); test('when used with standard config, it captures the response as metadata', async () => { - // Prepare const tracer: Tracer = new Tracer(); jest.spyOn(tracer.provider, 'setSegment').mockImplementation(); const putMetadataSpy = jest.spyOn(tracer, 'putMetadata'); const handler = middy(async (_event: unknown, _context: Context) => ({ - foo: 'bar' + foo: 'bar', })).use(captureLambdaHandler(tracer)); // Act @@ -161,28 +174,39 @@ describe('Middy middleware', () => { // Assess expect(putMetadataSpy).toHaveBeenCalledTimes(1); - expect(putMetadataSpy).toHaveBeenCalledWith('index.handler response', { foo: 'bar' }); - + expect(putMetadataSpy).toHaveBeenCalledWith('index.handler response', { + foo: 'bar', + }); }); test('when used while POWERTOOLS_TRACER_CAPTURE_ERROR is set to false, it does not capture the exceptions', async () => { - // Prepare process.env.POWERTOOLS_TRACER_CAPTURE_ERROR = 'false'; const tracer: Tracer = new Tracer(); - const newSubsegment: Segment | Subsegment | undefined = new Subsegment('## index.handler'); - const setSegmentSpy = jest.spyOn(tracer.provider, 'setSegment').mockImplementation(); - jest.spyOn(tracer.provider, 'getSegment').mockImplementation(() => newSubsegment); + const newSubsegment: Segment | Subsegment | undefined = new Subsegment( + '## index.handler' + ); + const setSegmentSpy = jest + .spyOn(tracer.provider, 'setSegment') + .mockImplementation(); + jest + .spyOn(tracer.provider, 'getSegment') + .mockImplementation(() => newSubsegment); setContextMissingStrategy(() => null); const addErrorSpy = jest.spyOn(newSubsegment, 'addError'); const addErrorFlagSpy = jest.spyOn(newSubsegment, 'addErrorFlag'); - const lambdaHandler: Handler = async (_event: unknown, _context: Context) => { + const lambdaHandler: Handler = async ( + _event: unknown, + _context: Context + ) => { throw new Error('Exception thrown!'); }; const handler = middy(lambdaHandler).use(captureLambdaHandler(tracer)); // Act & Assess - await expect(handler({}, context, () => console.log('Lambda invoked!'))).rejects.toThrowError(Error); + await expect( + handler({}, context, () => console.log('Lambda invoked!')) + ).rejects.toThrowError(Error); expect(setSegmentSpy).toHaveBeenCalledTimes(2); expect('cause' in newSubsegment).toBe(false); expect(addErrorFlagSpy).toHaveBeenCalledTimes(1); @@ -190,75 +214,84 @@ describe('Middy middleware', () => { expect.assertions(5); delete process.env.POWERTOOLS_TRACER_CAPTURE_ERROR; - }); - }); test('when used with standard config, it captures the exception correctly', async () => { - // Prepare const tracer: Tracer = new Tracer(); - const newSubsegment: Segment | Subsegment | undefined = new Subsegment('## index.handler'); - const setSegmentSpy = jest.spyOn(tracer.provider, 'setSegment').mockImplementation(); - jest.spyOn(tracer.provider, 'getSegment').mockImplementation(() => newSubsegment); + const newSubsegment: Segment | Subsegment | undefined = new Subsegment( + '## index.handler' + ); + const setSegmentSpy = jest + .spyOn(tracer.provider, 'setSegment') + .mockImplementation(); + jest + .spyOn(tracer.provider, 'getSegment') + .mockImplementation(() => newSubsegment); setContextMissingStrategy(() => null); const addErrorSpy = jest.spyOn(newSubsegment, 'addError'); - const lambdaHandler: Handler = async (_event: unknown, _context: Context) => { + const lambdaHandler: Handler = async ( + _event: unknown, + _context: Context + ) => { throw new Error('Exception thrown!'); }; const handler = middy(lambdaHandler).use(captureLambdaHandler(tracer)); // Act & Assess - await expect(handler({}, context, () => console.log('Lambda invoked!'))).rejects.toThrowError(Error); + await expect( + handler({}, context, () => console.log('Lambda invoked!')) + ).rejects.toThrowError(Error); expect(setSegmentSpy).toHaveBeenCalledTimes(2); expect('cause' in newSubsegment).toBe(true); expect(addErrorSpy).toHaveBeenCalledTimes(1); - expect(addErrorSpy).toHaveBeenCalledWith(new Error('Exception thrown!'), false); + expect(addErrorSpy).toHaveBeenCalledWith( + new Error('Exception thrown!'), + false + ); expect.assertions(5); - }); test('when used with standard config, it annotates ColdStart correctly', async () => { - // Prepare const tracer: Tracer = new Tracer(); jest.spyOn(tracer.provider, 'setSegment').mockImplementation(() => ({})); - jest.spyOn(tracer.provider, 'getSegment') + jest + .spyOn(tracer.provider, 'getSegment') .mockImplementationOnce(() => new Segment('facade')) .mockImplementationOnce(() => new Subsegment('## index.handler')) .mockImplementationOnce(() => new Segment('facade')) .mockImplementation(() => new Subsegment('## index.handler')); const putAnnotationSpy = jest.spyOn(tracer, 'putAnnotation'); - + const handler = middy(async (_event: unknown, _context: Context) => ({ - foo: 'bar' + foo: 'bar', })).use(captureLambdaHandler(tracer)); // Act await handler({}, context); await handler({}, context); - + // Assess // 2x Cold Start + 2x Service expect(putAnnotationSpy).toHaveBeenCalledTimes(4); expect(putAnnotationSpy).toHaveBeenNthCalledWith(1, 'ColdStart', true); expect(putAnnotationSpy).toHaveBeenNthCalledWith(3, 'ColdStart', false); - }); test('when used with standard config, it annotates Service correctly', async () => { - // Prepare const tracer: Tracer = new Tracer(); jest.spyOn(tracer.provider, 'setSegment').mockImplementation(() => ({})); - jest.spyOn(tracer.provider, 'getSegment') + jest + .spyOn(tracer.provider, 'getSegment') .mockImplementationOnce(() => new Segment('facade')) .mockImplementation(() => new Subsegment('## index.handler')); const putAnnotationSpy = jest.spyOn(tracer, 'putAnnotation'); const handler = middy(async (_event: unknown, _context: Context) => ({ - foo: 'bar' + foo: 'bar', })).use(captureLambdaHandler(tracer)); // Act @@ -267,8 +300,10 @@ describe('Middy middleware', () => { // Assess // The first call is for the Cold Start annotation expect(putAnnotationSpy).toHaveBeenCalledTimes(2); - expect(putAnnotationSpy).toHaveBeenNthCalledWith(2, 'Service', 'hello-world'); - + expect(putAnnotationSpy).toHaveBeenNthCalledWith( + 2, + 'Service', + 'hello-world' + ); }); - -}); \ No newline at end of file +}); From 09c68f42fe140f7fbae5975e6ddb7470b3896bd6 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 11 May 2023 20:12:43 +0200 Subject: [PATCH 2/2] style(tracer): apply standardized formatting --- packages/tracer/src/Tracer.ts | 2 - packages/tracer/tests/unit/Tracer.test.ts | 389 ++++++++-------------- 2 files changed, 138 insertions(+), 253 deletions(-) diff --git a/packages/tracer/src/Tracer.ts b/packages/tracer/src/Tracer.ts index 29f3078f2c..b741becc7e 100644 --- a/packages/tracer/src/Tracer.ts +++ b/packages/tracer/src/Tracer.ts @@ -533,8 +533,6 @@ class Tracer extends Utility implements TracerInterface { * @returns string - The root X-Ray trace id. */ public getRootXrayTraceId(): string | undefined { - if (!this.isTracingEnabled()) return undefined; - return this.envVarsService.getXrayTraceId(); } diff --git a/packages/tracer/tests/unit/Tracer.test.ts b/packages/tracer/tests/unit/Tracer.test.ts index 5cbcc14b06..7aef53a05f 100644 --- a/packages/tracer/tests/unit/Tracer.test.ts +++ b/packages/tracer/tests/unit/Tracer.test.ts @@ -41,6 +41,7 @@ const createCaptureAsyncFuncMock = function ( }); }; +jest.spyOn(console, 'log').mockImplementation(() => null); jest.spyOn(console, 'debug').mockImplementation(() => null); jest.spyOn(console, 'warn').mockImplementation(() => null); jest.spyOn(console, 'error').mockImplementation(() => null); @@ -269,41 +270,6 @@ describe('Class: Tracer', () => { // Assess expect(xRayTraceId).toBe('1-abcdef12-3456abcdef123456abcdef12'); }); - - test('when called and Tracer is disabled, it returns undefined', () => { - // Prepare - const tracer: Tracer = new Tracer({ enabled: false }); - - // Act - const xRayTraceId = tracer.getRootXrayTraceId(); - - // Assess - expect(xRayTraceId).toBe(undefined); - }); - }); - - describe('Method: isTraceSampled', () => { - test('when called, it returns true if the Sampled flag is set', () => { - // Prepare - const tracer: Tracer = new Tracer(); - - // Act - const xRayTraceSampled = tracer.isTraceSampled(); - - // Assess - expect(xRayTraceSampled).toBe(false); - }); - - test('when called and Trace is disabled, it returns false', () => { - // Prepare - const tracer: Tracer = new Tracer({ enabled: false }); - - // Act - const xRayTraceSampled = tracer.isTraceSampled(); - - // Assess - expect(xRayTraceSampled).toBe(false); - }); }); describe('Method: getSegment', () => { @@ -360,6 +326,30 @@ describe('Class: Tracer', () => { }); }); + describe('Method: isTraceSampled', () => { + test('when called, it returns true if the Sampled flag is set', () => { + // Prepare + const tracer: Tracer = new Tracer(); + + // Act + const xRayTraceSampled = tracer.isTraceSampled(); + + // Assess + expect(xRayTraceSampled).toBe(false); + }); + + test('when called and Trace is disabled, it returns false', () => { + // Prepare + const tracer: Tracer = new Tracer({ enabled: false }); + + // Act + const xRayTraceSampled = tracer.isTraceSampled(); + + // Assess + expect(xRayTraceSampled).toBe(false); + }); + }); + describe('Method: setSegment', () => { test('when called outside of a namespace or without parent segment, and Tracer is enabled, it throws an error', () => { // Prepare @@ -513,23 +503,19 @@ describe('Class: Tracer', () => { ); class Lambda implements LambdaInterface { @tracer.captureLambdaHandler() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - public handler( - _event: TEvent, + public handler( + _event: unknown, _context: Context, - _callback: Callback - ): void | Promise { - return new Promise((resolve, _reject) => - resolve({ - foo: 'bar', - } as unknown as TResult) - ); + callback: Callback + ): void { + callback(null, { + foo: 'bar', + }); } } // Act - await new Lambda().handler(event, context, () => + new Lambda().handler(event, context, () => console.log('Lambda invoked!') ); @@ -723,20 +709,19 @@ describe('Class: Tracer', () => { class Lambda implements LambdaInterface { @tracer.captureLambdaHandler() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - public handler( - _event: TEvent, + public handler( + _event: unknown, _context: Context, - _callback: Callback - ): void | Promise { + _callback: Callback + ): void { throw new Error('Exception thrown!'); } } + const lambda = new Lambda(); // Act & Assess - await expect( - new Lambda().handler({}, context, () => console.log('Lambda invoked!')) + expect( + lambda.handler({}, context, () => console.log('Lambda invoked!')) ).rejects.toThrowError(Error); expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); expect(addErrorFlagSpy).toHaveBeenCalledTimes(1); @@ -761,20 +746,19 @@ describe('Class: Tracer', () => { class Lambda implements LambdaInterface { @tracer.captureLambdaHandler() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - public handler( - _event: TEvent, + public handler( + _event: unknown, _context: Context, - _callback: Callback - ): void | Promise { - throw new Error('Exception thrown!'); + _callback: Callback + ): void { + throw new Error('Exception thrown!2'); } } // Act & Assess - await expect( - new Lambda().handler({}, context, () => console.log('Lambda invoked!')) + const lambda = new Lambda(); + expect( + lambda.handler({}, context, () => console.log('Lambda invoked!')) ).rejects.toThrowError(Error); expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); expect(addErrorAsMetadataSpy).toHaveBeenCalledTimes(1); @@ -792,23 +776,17 @@ describe('Class: Tracer', () => { class Lambda implements LambdaInterface { @tracer.captureLambdaHandler() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - public handler( - _event: TEvent, + public handler( + _event: unknown, _context: Context, - _callback: Callback - ): void | Promise { - return new Promise((resolve, _reject) => - resolve({ - foo: 'bar', - } as unknown as TResult) - ); + callback: Callback<{ foo: string }> + ): void { + callback(null, { foo: 'bar' }); } } // Act - await new Lambda().handler(event, context, () => + new Lambda().handler(event, context, () => console.log('Lambda invoked!') ); @@ -832,23 +810,19 @@ describe('Class: Tracer', () => { class Lambda implements LambdaInterface { @tracer.captureLambdaHandler() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - public handler( - _event: TEvent, + public handler( + _event: unknown, _context: Context, - _callback: Callback - ): void | Promise { - return new Promise((resolve, _reject) => - resolve({ - foo: 'bar', - } as unknown as TResult) - ); + callback: Callback<{ foo: string }> + ): void { + callback(null, { + foo: 'bar', + }); } } // Act - await new Lambda().handler(event, context, () => + new Lambda().handler(event, context, () => console.log('Lambda invoked!') ); @@ -881,23 +855,18 @@ describe('Class: Tracer', () => { } @tracer.captureLambdaHandler() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - public async handler( - _event: TEvent, - _context: Context, - _callback: Callback - ): void | Promise { - return (`memberVariable:${this.memberVariable}` as unknown); + public async handler( + _event: unknown, + _context: Context + ): Promise { + return `memberVariable:${this.memberVariable}`; } } // Act / Assess const lambda = new Lambda('someValue'); const handler = lambda.handler.bind(lambda); - expect( - await handler({}, context, () => console.log('Lambda invoked!')) - ).toEqual('memberVariable:someValue'); + expect(await handler({}, context)).toEqual('memberVariable:someValue'); }); test('when used as decorator on an async method, the method is awaited correctly', async () => { @@ -922,13 +891,10 @@ describe('Class: Tracer', () => { } @tracer.captureLambdaHandler() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - public async handler( - _event: TEvent, - _context: Context, - _callback: Callback - ): void | Promise { + public async handler( + _event: unknown, + _context: Context + ): Promise { await this.dummyMethod(); this.otherDummyMethod(); @@ -946,7 +912,7 @@ describe('Class: Tracer', () => { .spyOn(lambda, 'otherDummyMethod') .mockImplementation(); const handler = lambda.handler.bind(lambda); - await handler({}, context, () => console.log('Lambda invoked!')); + await handler({}, context); // Assess // Here we assert that the otherDummyMethodSpy method is called before the cleanup logic (inside the finally of decorator) @@ -969,30 +935,20 @@ describe('Class: Tracer', () => { ); class Lambda implements LambdaInterface { @tracer.captureMethod() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public async dummyMethod(some: string): Promise { - return new Promise((resolve, _reject) => resolve(some)); + public async dummyMethod(some: string): Promise { + return some; } - public async handler( - _event: TEvent, - _context: Context, - _callback: Callback - ): Promise { - const result = await this.dummyMethod('foo bar'); - - return new Promise((resolve, _reject) => - resolve(result as unknown as TResult) - ); + public async handler( + _event: unknown, + _context: Context + ): Promise { + return await this.dummyMethod('foo bar'); } } // Act - await new Lambda().handler(event, context, () => - console.log('Lambda invoked!') - ); + await new Lambda().handler(event, context); // Assess expect(captureAsyncFuncSpy).toBeCalledTimes(0); @@ -1012,31 +968,20 @@ describe('Class: Tracer', () => { class Lambda implements LambdaInterface { @tracer.captureMethod() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public async dummyMethod(some: string): Promise { - return new Promise((resolve, _reject) => - setTimeout(() => resolve(some), 3000) - ); + return some; } - public async handler( - _event: TEvent, - _context: Context, - _callback: Callback - ): Promise { - const result = await this.dummyMethod('foo bar'); - - return new Promise((resolve, _reject) => - resolve(result as unknown as TResult) - ); + public async handler( + _event: unknown, + _context: Context + ): Promise { + return await this.dummyMethod('foo bar'); } } // Act - await new Lambda().handler(event, context, () => - console.log('Lambda invoked!') - ); + await new Lambda().handler(event, context); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); @@ -1065,31 +1010,20 @@ describe('Class: Tracer', () => { class Lambda implements LambdaInterface { @tracer.captureMethod({ captureResponse: false }) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public async dummyMethod(some: string): Promise { - return new Promise((resolve, _reject) => - setTimeout(() => resolve(some), 3000) - ); + return some; } - public async handler( - _event: TEvent, - _context: Context, - _callback: Callback - ): Promise { - const result = await this.dummyMethod('foo bar'); - - return new Promise((resolve, _reject) => - resolve(result as unknown as TResult) - ); + public async handler( + _event: unknown, + _context: Context + ): Promise { + return await this.dummyMethod('foo bar'); } } // Act - await new Lambda().handler(event, context, () => - console.log('Lambda invoked!') - ); + await new Lambda().handler(event, context); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); @@ -1114,31 +1048,20 @@ describe('Class: Tracer', () => { class Lambda implements LambdaInterface { @tracer.captureMethod() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public async dummyMethod(some: string): Promise { - return new Promise((resolve, _reject) => - setTimeout(() => resolve(some), 3000) - ); + return some; } - public async handler( - _event: TEvent, - _context: Context, - _callback: Callback - ): Promise { - const result = await this.dummyMethod('foo bar'); - - return new Promise((resolve, _reject) => - resolve(result as unknown as TResult) - ); + public async handler( + _event: unknown, + _context: Context + ): Promise { + return await this.dummyMethod('foo bar'); } } // Act - await new Lambda().handler(event, context, () => - console.log('Lambda invoked!') - ); + await new Lambda().handler(event, context); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); @@ -1163,29 +1086,22 @@ describe('Class: Tracer', () => { const addErrorSpy = jest.spyOn(newSubsegment, 'addError'); class Lambda implements LambdaInterface { @tracer.captureMethod() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public async dummyMethod(_some: string): Promise { throw new Error('Exception thrown!'); } - public async handler( - _event: TEvent, - _context: Context, - _callback: Callback - ): Promise { - const result = await this.dummyMethod('foo bar'); - - return new Promise((resolve, _reject) => - resolve(result as unknown as TResult) - ); + public async handler( + _event: unknown, + _context: Context + ): Promise { + return await this.dummyMethod('foo bar'); } } // Act / Assess - await expect( - new Lambda().handler({}, context, () => console.log('Lambda invoked!')) - ).rejects.toThrowError(Error); + await expect(new Lambda().handler({}, context)).rejects.toThrowError( + Error + ); expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); expect(newSubsegment).toEqual( expect.objectContaining({ @@ -1214,21 +1130,16 @@ describe('Class: Tracer', () => { class Lambda implements LambdaInterface { @tracer.captureMethod() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public async dummyMethod(): Promise { return `otherMethod:${this.otherMethod()}`; } @tracer.captureLambdaHandler() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - public async handler( - _event: TEvent, - _context: Context, - _callback: Callback - ): void | Promise { - return ((await this.dummyMethod()) as unknown); + public async handler( + _event: unknown, + _context: Context + ): Promise { + return await this.dummyMethod(); } public otherMethod(): string { @@ -1237,11 +1148,9 @@ describe('Class: Tracer', () => { } // Act / Assess - expect( - await new Lambda().handler({}, context, () => - console.log('Lambda invoked!') - ) - ).toEqual('otherMethod:otherMethod'); + expect(await new Lambda().handler({}, context)).toEqual( + 'otherMethod:otherMethod' + ); }); test('when used as decorator and when calling a method in the class, it has access to member variables', async () => { @@ -1263,21 +1172,16 @@ describe('Class: Tracer', () => { } @tracer.captureMethod() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public async dummyMethod(): Promise { return `memberVariable:${this.memberVariable}`; } @tracer.captureLambdaHandler() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - public async handler( - _event: TEvent, - _context: Context, - _callback: Callback - ): void | Promise { - return ((await this.dummyMethod()) as unknown); + public async handler( + _event: unknown, + _context: Context + ): Promise { + return await this.dummyMethod(); } } @@ -1304,16 +1208,13 @@ describe('Class: Tracer', () => { class Lambda implements LambdaInterface { @tracer.captureMethod() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public async dummyMethod(): Promise { return; } - public async handler( - _event: TEvent, - _context: Context, - _callback: Callback + public async handler( + _event: unknown, + _context: Context ): Promise { await this.dummyMethod(); this.otherDummyMethod(); @@ -1332,7 +1233,7 @@ describe('Class: Tracer', () => { .spyOn(lambda, 'otherDummyMethod') .mockImplementation(); const handler = lambda.handler.bind(lambda); - await handler({}, context, () => console.log('Lambda invoked!')); + await handler({}, context); // Here we assert that the subsegment.close() (inside the finally of decorator) is called before the other otherDummyMethodSpy method // that should always be called after the handler has returned. If subsegment.close() is called after it means the @@ -1371,16 +1272,13 @@ describe('Class: Tracer', () => { class Lambda implements LambdaInterface { @tracer.captureMethod() @passThrough() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public async dummyMethod(): Promise { return `foo`; } - public async handler( - _event: TEvent, - _context: Context, - _callback: Callback + public async handler( + _event: unknown, + _context: Context ): Promise { await this.dummyMethod(); @@ -1391,7 +1289,7 @@ describe('Class: Tracer', () => { // Act / Assess const lambda = new Lambda(); const handler = lambda.handler.bind(lambda); - await handler({}, context, () => console.log('Lambda invoked!')); + await handler({}, context); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledWith( @@ -1417,31 +1315,20 @@ describe('Class: Tracer', () => { ); class Lambda implements LambdaInterface { @tracer.captureMethod({ subSegmentName: '#### myCustomMethod' }) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public async dummyMethod(some: string): Promise { - return new Promise((resolve, _reject) => - setTimeout(() => resolve(some), 3000) - ); + return some; } - public async handler( - _event: TEvent, - _context: Context, - _callback: Callback - ): Promise { - const result = await this.dummyMethod('foo bar'); - - return new Promise((resolve, _reject) => - resolve(result as unknown as TResult) - ); + public async handler( + _event: unknown, + _context: Context + ): Promise { + return await this.dummyMethod('foo bar'); } } // Act - await new Lambda().handler(event, context, () => - console.log('Lambda invoked!') - ); + await new Lambda().handler(event, context); // Assess expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1);