Skip to content

Commit cc59160

Browse files
committed
feat(tracer): use decorator options to disable exception and result capture
1 parent bafed02 commit cc59160

File tree

4 files changed

+281
-7
lines changed

4 files changed

+281
-7
lines changed

Diff for: docs/core/tracer.md

+68
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,40 @@ Use **`POWERTOOLS_TRACER_CAPTURE_RESPONSE=false`** environment variable to instr
412412
2. You might manipulate **streaming objects that can be read only once**; this prevents subsequent calls from being empty
413413
3. You might return **more than 64K** of data _e.g., `message too long` error_
414414

415+
### Disabling response capture for targeted methods and handlers
416+
417+
Use the `captureResponse: false` option in both `tracer.captureLambdaHandler()` and `tracer.captureMethod()` decorators to instruct Tracer **not** to serialize function responses as metadata.
418+
419+
=== "method.ts"
420+
421+
```typescript hl_lines="5"
422+
import { Tracer } from '@aws-lambda-powertools/tracer';
423+
424+
const tracer = new Tracer({ serviceName: 'serverlessAirline' });
425+
class MyThing {
426+
@tracer.captureMethod({ captureResponse: false })
427+
myMethod(): string {
428+
/* ... */
429+
return 'foo bar';
430+
}
431+
}
432+
```
433+
434+
=== "handler.ts"
435+
436+
```typescript hl_lines="6"
437+
import { Tracer } from '@aws-lambda-powertools/tracer';
438+
import { LambdaInterface } from '@aws-lambda-powertools/commons';
439+
440+
const tracer = new Tracer({ serviceName: 'serverlessAirline' });
441+
class MyHandler implements LambdaInterface {
442+
@tracer.captureLambdaHandler({ captureResponse: false })
443+
async handler(_event: any, _context: any): Promise<void> {
444+
/* ... */
445+
}
446+
}
447+
```
448+
415449
### Disabling exception auto-capture
416450

417451
Use **`POWERTOOLS_TRACER_CAPTURE_ERROR=false`** environment variable to instruct Tracer **not** to serialize exceptions as metadata.
@@ -420,6 +454,40 @@ Use **`POWERTOOLS_TRACER_CAPTURE_ERROR=false`** environment variable to instruct
420454

421455
1. You might **return sensitive** information from exceptions, stack traces you might not control
422456

457+
### Disabling exception capture for targeted methods and handlers
458+
459+
Use the `captureError: false` option in both `tracer.captureLambdaHandler()` and `tracer.captureMethod()` decorators to instruct Tracer **not** to serialize exceptions as metadata.
460+
461+
=== "method.ts"
462+
463+
```typescript hl_lines="5"
464+
import { Tracer } from '@aws-lambda-powertools/tracer';
465+
466+
const tracer = new Tracer({ serviceName: 'serverlessAirline' });
467+
class MyThing {
468+
@tracer.captureMethod({ captureError: false })
469+
myMethod(): string {
470+
/* ... */
471+
return 'foo bar';
472+
}
473+
}
474+
```
475+
476+
=== "handler.ts"
477+
478+
```typescript hl_lines="6"
479+
import { Tracer } from '@aws-lambda-powertools/tracer';
480+
import { LambdaInterface } from '@aws-lambda-powertools/commons';
481+
482+
const tracer = new Tracer({ serviceName: 'serverlessAirline' });
483+
class MyHandler implements LambdaInterface {
484+
@tracer.captureLambdaHandler({ captureError: false })
485+
async handler(_event: any, _context: any): Promise<void> {
486+
/* ... */
487+
}
488+
}
489+
```
490+
423491
### Escape hatch mechanism
424492

425493
You can use `tracer.provider` attribute to access all methods provided by the [AWS X-Ray SDK](https://docs.aws.amazon.com/xray-sdk-for-nodejs/latest/reference/AWSXRay.html).

Diff for: packages/tracer/src/Tracer.ts

+21-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Handler } from 'aws-lambda';
22
import { AsyncHandler, SyncHandler, Utility } from '@aws-lambda-powertools/commons';
33
import { TracerInterface } from '.';
44
import { ConfigServiceInterface, EnvironmentVariablesService } from './config';
5-
import { HandlerMethodDecorator, TracerOptions, MethodDecorator } from './types';
5+
import { HandlerMethodDecorator, TracerOptions, TracerCaptureMethodOptions, TracerCaptureLambdaHandlerOptions, MethodDecorator } from './types';
66
import { ProviderService, ProviderServiceInterface } from './provider';
77
import { Segment, Subsegment } from 'aws-xray-sdk-core';
88

@@ -339,7 +339,7 @@ class Tracer extends Utility implements TracerInterface {
339339
*
340340
* @decorator Class
341341
*/
342-
public captureLambdaHandler(): HandlerMethodDecorator {
342+
public captureLambdaHandler(options?: TracerCaptureLambdaHandlerOptions): HandlerMethodDecorator {
343343
return (_target, _propertyKey, descriptor) => {
344344
/**
345345
* The descriptor.value is the method this decorator decorates, it cannot be undefined.
@@ -365,9 +365,16 @@ class Tracer extends Utility implements TracerInterface {
365365
let result: unknown;
366366
try {
367367
result = await originalMethod.apply(handlerRef, [ event, context, callback ]);
368-
tracerRef.addResponseAsMetadata(result, process.env._HANDLER);
368+
if (options?.captureResponse ?? true) {
369+
tracerRef.addResponseAsMetadata(result, process.env._HANDLER);
370+
}
371+
369372
} catch (error) {
370-
tracerRef.addErrorAsMetadata(error as Error);
373+
if (options?.captureError ?? true) {
374+
tracerRef.addErrorAsMetadata(error as Error);
375+
} else {
376+
tracerRef.getSegment().addErrorFlag();
377+
}
371378
throw error;
372379
} finally {
373380
subsegment?.close();
@@ -416,7 +423,7 @@ class Tracer extends Utility implements TracerInterface {
416423
*
417424
* @decorator Class
418425
*/
419-
public captureMethod(): MethodDecorator {
426+
public captureMethod(options?: TracerCaptureMethodOptions): MethodDecorator {
420427
return (_target, _propertyKey, descriptor) => {
421428
// The descriptor.value is the method this decorator decorates, it cannot be undefined.
422429
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -435,9 +442,16 @@ class Tracer extends Utility implements TracerInterface {
435442
let result;
436443
try {
437444
result = await originalMethod.apply(this, [...args]);
438-
tracerRef.addResponseAsMetadata(result, originalMethod.name);
445+
if (options?.captureResponse ?? true) {
446+
tracerRef.addResponseAsMetadata(result, originalMethod.name);
447+
}
448+
439449
} catch (error) {
440-
tracerRef.addErrorAsMetadata(error as Error);
450+
if (options?.captureError ?? true) {
451+
tracerRef.addErrorAsMetadata(error as Error);
452+
} else {
453+
tracerRef.getSegment().addErrorFlag();
454+
}
441455

442456
throw error;
443457
} finally {

Diff for: packages/tracer/src/types/Tracer.ts

+40
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,44 @@ type TracerOptions = {
2626
customConfigService?: ConfigServiceInterface
2727
};
2828

29+
/**
30+
* Options for the captureMethod decorator to be used when decorating a method.
31+
*
32+
* Usage:
33+
* @example
34+
* ```typescript
35+
* const tracer = new Tracer();
36+
*
37+
* class MyThing {
38+
* @tracer.captureMethod({ captureResponse: false, captureError: false })
39+
* async myMethod() { ... }
40+
* }
41+
* ```
42+
*/
43+
type TracerCaptureMethodOptions = {
44+
captureResponse?: boolean
45+
captureError?: boolean
46+
};
47+
48+
/**
49+
* Options for the captureLambdaHandler decorator to be used when decorating a method.
50+
*
51+
* Usage:
52+
* @example
53+
* ```typescript
54+
* const tracer = new Tracer();
55+
*
56+
* class MyThing implements LambdaInterface {
57+
* @tracer.captureLambdaHandler({ captureResponse: false, captureError: false })
58+
* async handler() { ... }
59+
* }
60+
* ```
61+
*/
62+
type TracerCaptureLambdaHandlerOptions = {
63+
captureResponse?: boolean
64+
captureError?: boolean
65+
};
66+
2967
type HandlerMethodDecorator = (
3068
target: LambdaInterface,
3169
propertyKey: string | symbol,
@@ -38,6 +76,8 @@ type MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: T
3876

3977
export {
4078
TracerOptions,
79+
TracerCaptureLambdaHandlerOptions,
80+
TracerCaptureMethodOptions,
4181
HandlerMethodDecorator,
4282
MethodDecorator
4383
};

Diff for: packages/tracer/tests/unit/Tracer.test.ts

+152
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,36 @@ describe('Class: Tracer', () => {
648648

649649
});
650650

651+
test('when used as decorator while captureResponse is set to false, it does not capture the response as metadata', async () => {
652+
653+
// Prepare
654+
const tracer: Tracer = new Tracer();
655+
const newSubsegment: Segment | Subsegment | undefined = new Subsegment('## index.handler');
656+
jest.spyOn(tracer.provider, 'getSegment').mockImplementation(() => newSubsegment);
657+
setContextMissingStrategy(() => null);
658+
const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc');
659+
class Lambda implements LambdaInterface {
660+
661+
@tracer.captureLambdaHandler({ captureResponse: false })
662+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
663+
// @ts-ignore
664+
public handler<TEvent, TResult>(_event: TEvent, _context: Context, _callback: Callback<TResult>): void | Promise<TResult> {
665+
return new Promise((resolve, _reject) => resolve({
666+
foo: 'bar'
667+
} as unknown as TResult));
668+
}
669+
670+
}
671+
672+
// Act
673+
await new Lambda().handler(event, context, () => console.log('Lambda invoked!'));
674+
675+
// Assess
676+
expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1);
677+
expect('metadata' in newSubsegment).toBe(false);
678+
679+
});
680+
651681
test('when used as decorator and with standard config, it captures the response as metadata', async () => {
652682

653683
// Prepare
@@ -727,6 +757,41 @@ describe('Class: Tracer', () => {
727757

728758
});
729759

760+
test('when used as decorator while captureError is set to false, it does not capture the exceptions', async () => {
761+
762+
// Prepare
763+
const tracer: Tracer = new Tracer();
764+
const newSubsegment: Segment | Subsegment | undefined = new Subsegment('## index.handler');
765+
jest.spyOn(tracer.provider, 'getSegment')
766+
.mockImplementation(() => newSubsegment);
767+
setContextMissingStrategy(() => null);
768+
const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc');
769+
const addErrorSpy = jest.spyOn(newSubsegment, 'addError');
770+
const addErrorFlagSpy = jest.spyOn(newSubsegment, 'addErrorFlag');
771+
class Lambda implements LambdaInterface {
772+
773+
@tracer.captureLambdaHandler({ captureError: false })
774+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
775+
// @ts-ignore
776+
public handler<TEvent, TResult>(_event: TEvent, _context: Context, _callback: Callback<TResult>): void | Promise<TResult> {
777+
throw new Error('Exception thrown!');
778+
}
779+
780+
}
781+
782+
// Act & Assess
783+
await expect(new Lambda().handler({}, context, () => console.log('Lambda invoked!'))).rejects.toThrowError(Error);
784+
expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1);
785+
expect(newSubsegment).toEqual(expect.objectContaining({
786+
name: '## index.handler',
787+
}));
788+
expect('cause' in newSubsegment).toBe(false);
789+
expect(addErrorFlagSpy).toHaveBeenCalledTimes(1);
790+
expect(addErrorSpy).toHaveBeenCalledTimes(0);
791+
expect.assertions(6);
792+
793+
});
794+
730795
test('when used as decorator and with standard config, it captures the exception correctly', async () => {
731796

732797
// Prepare
@@ -964,6 +1029,52 @@ describe('Class: Tracer', () => {
9641029

9651030
});
9661031

1032+
test('when used as decorator and with captureResponse set to false, it does not capture the response as metadata', async () => {
1033+
1034+
// Prepare
1035+
const tracer: Tracer = new Tracer();
1036+
const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod');
1037+
jest.spyOn(newSubsegment, 'flush').mockImplementation(() => null);
1038+
jest.spyOn(tracer.provider, 'getSegment')
1039+
.mockImplementation(() => newSubsegment);
1040+
setContextMissingStrategy(() => null);
1041+
const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc');
1042+
class Lambda implements LambdaInterface {
1043+
1044+
@tracer.captureMethod({ captureResponse: false })
1045+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
1046+
// @ts-ignore
1047+
public async dummyMethod(some: string): Promise<string> {
1048+
return new Promise((resolve, _reject) => setTimeout(() => resolve(some), 3000));
1049+
}
1050+
1051+
public async handler<TEvent, TResult>(_event: TEvent, _context: Context, _callback: Callback<TResult>): Promise<TResult> {
1052+
const result = await this.dummyMethod('foo bar');
1053+
1054+
return new Promise((resolve, _reject) => resolve(result as unknown as TResult));
1055+
}
1056+
1057+
}
1058+
1059+
// Act
1060+
await new Lambda().handler(event, context, () => console.log('Lambda invoked!'));
1061+
1062+
// Assess
1063+
expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1);
1064+
expect(captureAsyncFuncSpy).toHaveBeenCalledWith('### dummyMethod', expect.anything());
1065+
expect(newSubsegment).toEqual(expect.objectContaining({
1066+
name: '### dummyMethod',
1067+
}));
1068+
expect(newSubsegment).not.toEqual(expect.objectContaining({
1069+
metadata: {
1070+
'hello-world': {
1071+
'dummyMethod response': 'foo bar',
1072+
},
1073+
}
1074+
}));
1075+
1076+
});
1077+
9671078
test('when used as decorator and with standard config, it captures the exception correctly', async () => {
9681079

9691080
// Prepare
@@ -1004,6 +1115,47 @@ describe('Class: Tracer', () => {
10041115

10051116
});
10061117

1118+
test('when used as decorator and with captureError set to false, it does not capture the exception', async () => {
1119+
1120+
// Prepare
1121+
const tracer: Tracer = new Tracer();
1122+
const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod');
1123+
jest.spyOn(tracer.provider, 'getSegment')
1124+
.mockImplementation(() => newSubsegment);
1125+
setContextMissingStrategy(() => null);
1126+
const captureAsyncFuncSpy = createCaptureAsyncFuncMock(tracer.provider);
1127+
const addErrorSpy = jest.spyOn(newSubsegment, 'addError');
1128+
const addErrorFlagSpy = jest.spyOn(newSubsegment, 'addErrorFlag');
1129+
class Lambda implements LambdaInterface {
1130+
1131+
@tracer.captureMethod({ captureError: false })
1132+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
1133+
// @ts-ignore
1134+
public async dummyMethod(_some: string): Promise<string> {
1135+
throw new Error('Exception thrown!');
1136+
}
1137+
1138+
public async handler<TEvent, TResult>(_event: TEvent, _context: Context, _callback: Callback<TResult>): Promise<TResult> {
1139+
const result = await this.dummyMethod('foo bar');
1140+
1141+
return new Promise((resolve, _reject) => resolve(result as unknown as TResult));
1142+
}
1143+
1144+
}
1145+
1146+
// Act / Assess
1147+
await expect(new Lambda().handler({}, context, () => console.log('Lambda invoked!'))).rejects.toThrowError(Error);
1148+
expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1);
1149+
expect(newSubsegment).toEqual(expect.objectContaining({
1150+
name: '### dummyMethod',
1151+
}));
1152+
expect('cause' in newSubsegment).toBe(false);
1153+
expect(addErrorFlagSpy).toHaveBeenCalledTimes(1);
1154+
expect(addErrorSpy).toHaveBeenCalledTimes(0);
1155+
expect.assertions(6);
1156+
1157+
});
1158+
10071159
test('when used as decorator and when calling other methods/props in the class they are called in the orginal scope', async () => {
10081160

10091161
// Prepare

0 commit comments

Comments
 (0)