Skip to content

Commit 107fa04

Browse files
authored
fix(tracer): decorated class methods cannot access this (aws-powertools#1055)
* fix(tracer): use instance scope in capture decorators * docs: add more inline comments * docs: small grammatical inline docs correction * refactor: apply review changes * docs: .bind changes * test: use member variable in e2e tests * fix: bind the handler in the e2e tests
1 parent 9ce180a commit 107fa04

File tree

5 files changed

+126
-28
lines changed

5 files changed

+126
-28
lines changed

docs/core/tracer.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,11 @@ You can quickly start by importing the `Tracer` class, initialize it outside the
143143
}
144144
145145
export const handlerClass = new Lambda();
146-
export const handler = handlerClass.handler;
146+
export const handler = handlerClass.handler.bind(handlerClass); // (1)
147147
```
148+
149+
1. Binding your handler method allows your handler to access `this`.
150+
148151
=== "Manual"
149152

150153
```typescript hl_lines="6 8-9 12-13 19 22 26 28"
@@ -253,8 +256,11 @@ You can trace other Class methods using the `captureMethod` decorator or any arb
253256
}
254257
255258
export const myFunction = new Lambda();
256-
export const handler = myFunction.handler;
259+
export const handler = myFunction.handler.bind(myFunction); // (1)
257260
```
261+
262+
1. Binding your handler method allows your handler to access `this`.
263+
258264
=== "Manual"
259265

260266
```typescript hl_lines="6 8-9 15 18 23 25"

packages/tracer/src/Tracer.ts

+33-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Handler } from 'aws-lambda';
2-
import { Utility } from '@aws-lambda-powertools/commons';
2+
import { AsyncHandler, SyncHandler, Utility } from '@aws-lambda-powertools/commons';
33
import { TracerInterface } from '.';
44
import { ConfigServiceInterface, EnvironmentVariablesService } from './config';
55
import { HandlerMethodDecorator, TracerOptions, MethodDecorator } from './types';
@@ -67,7 +67,7 @@ import { Segment, Subsegment } from 'aws-xray-sdk-core';
6767
* }
6868
*
6969
* export const handlerClass = new Lambda();
70-
* export const handler = handlerClass.handler;
70+
* export const handler = handlerClass.handler.bind(handlerClass);
7171
* ```
7272
*
7373
* ### Functions usage with manual instrumentation
@@ -334,33 +334,40 @@ class Tracer extends Utility implements TracerInterface {
334334
* }
335335
*
336336
* export const handlerClass = new Lambda();
337-
* export const handler = handlerClass.handler;
337+
* export const handler = handlerClass.handler.bind(handlerClass);
338338
* ```
339339
*
340340
* @decorator Class
341341
*/
342342
public captureLambdaHandler(): HandlerMethodDecorator {
343-
return (target, _propertyKey, descriptor) => {
343+
return (_target, _propertyKey, descriptor) => {
344344
/**
345345
* The descriptor.value is the method this decorator decorates, it cannot be undefined.
346346
*/
347347
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
348348
const originalMethod = descriptor.value!;
349349

350-
descriptor.value = ((event, context, callback) => {
351-
if (!this.isTracingEnabled()) {
352-
return originalMethod.apply(target, [ event, context, callback ]);
350+
// eslint-disable-next-line @typescript-eslint/no-this-alias
351+
const tracerRef = this;
352+
// Use a function() {} instead of an () => {} arrow function so that we can
353+
// access `myClass` as `this` in a decorated `myClass.myMethod()`.
354+
descriptor.value = (function (this: Handler, event, context, callback) {
355+
// eslint-disable-next-line @typescript-eslint/no-this-alias
356+
const handlerRef: Handler = this;
357+
358+
if (!tracerRef.isTracingEnabled()) {
359+
return originalMethod.apply(handlerRef, [ event, context, callback ]);
353360
}
354361

355-
return this.provider.captureAsyncFunc(`## ${process.env._HANDLER}`, async subsegment => {
356-
this.annotateColdStart();
357-
this.addServiceNameAnnotation();
362+
return tracerRef.provider.captureAsyncFunc(`## ${process.env._HANDLER}`, async subsegment => {
363+
tracerRef.annotateColdStart();
364+
tracerRef.addServiceNameAnnotation();
358365
let result: unknown;
359366
try {
360-
result = await originalMethod.apply(target, [ event, context, callback ]);
361-
this.addResponseAsMetadata(result, process.env._HANDLER);
367+
result = await originalMethod.apply(handlerRef, [ event, context, callback ]);
368+
tracerRef.addResponseAsMetadata(result, process.env._HANDLER);
362369
} catch (error) {
363-
this.addErrorAsMetadata(error as Error);
370+
tracerRef.addErrorAsMetadata(error as Error);
364371
throw error;
365372
} finally {
366373
subsegment?.close();
@@ -369,7 +376,7 @@ class Tracer extends Utility implements TracerInterface {
369376

370377
return result;
371378
});
372-
}) as Handler;
379+
}) as SyncHandler<Handler> | AsyncHandler<Handler>;
373380

374381
return descriptor;
375382
};
@@ -411,23 +418,27 @@ class Tracer extends Utility implements TracerInterface {
411418
* @decorator Class
412419
*/
413420
public captureMethod(): MethodDecorator {
414-
return (target, _propertyKey, descriptor) => {
421+
return (_target, _propertyKey, descriptor) => {
415422
// The descriptor.value is the method this decorator decorates, it cannot be undefined.
416423
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
417424
const originalMethod = descriptor.value!;
418425

419-
descriptor.value = (...args: unknown[]) => {
420-
if (!this.isTracingEnabled()) {
421-
return originalMethod.apply(target, [...args]);
426+
// eslint-disable-next-line @typescript-eslint/no-this-alias
427+
const tracerRef = this;
428+
// Use a function() {} instead of an () => {} arrow function so that we can
429+
// access `myClass` as `this` in a decorated `myClass.myMethod()`.
430+
descriptor.value = function (...args: unknown[]) {
431+
if (!tracerRef.isTracingEnabled()) {
432+
return originalMethod.apply(this, [...args]);
422433
}
423434

424-
return this.provider.captureAsyncFunc(`### ${originalMethod.name}`, async subsegment => {
435+
return tracerRef.provider.captureAsyncFunc(`### ${originalMethod.name}`, async subsegment => {
425436
let result;
426437
try {
427-
result = await originalMethod.apply(target, [...args]);
428-
this.addResponseAsMetadata(result, originalMethod.name);
438+
result = await originalMethod.apply(this, [...args]);
439+
tracerRef.addResponseAsMetadata(result, originalMethod.name);
429440
} catch (error) {
430-
this.addErrorAsMetadata(error as Error);
441+
tracerRef.addErrorAsMetadata(error as Error);
431442

432443
throw error;
433444
} finally {

packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ const tracer = new Tracer({ serviceName: serviceName });
3636
const dynamoDBv3 = tracer.captureAWSv3Client(new DynamoDBClient({}));
3737

3838
export class MyFunctionWithDecorator {
39+
private readonly returnValue: string;
40+
41+
public constructor() {
42+
this.returnValue = customResponseValue;
43+
}
44+
3945
@tracer.captureLambdaHandler()
4046
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4147
// @ts-ignore
@@ -77,9 +83,9 @@ export class MyFunctionWithDecorator {
7783
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
7884
// @ts-ignore
7985
public myMethod(): string {
80-
return customResponseValue;
86+
return this.returnValue;
8187
}
8288
}
8389

8490
export const handlerClass = new MyFunctionWithDecorator();
85-
export const handler = handlerClass.handler;
91+
export const handler = handlerClass.handler.bind(handlerClass);

packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ const tracer = new Tracer({ serviceName: serviceName });
3636
const dynamoDBv3 = tracer.captureAWSv3Client(new DynamoDBClient({}));
3737

3838
export class MyFunctionWithDecorator {
39+
private readonly returnValue: string;
40+
41+
public constructor() {
42+
this.returnValue = customResponseValue;
43+
}
44+
3945
@tracer.captureLambdaHandler()
4046
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4147
// @ts-ignore
@@ -72,9 +78,9 @@ export class MyFunctionWithDecorator {
7278
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
7379
// @ts-ignore
7480
public myMethod(): string {
75-
return customResponseValue;
81+
return this.returnValue;
7682
}
7783
}
7884

7985
export const handlerClass = new MyFunctionWithDecorator();
80-
export const handler = handlerClass.handler;
86+
export const handler = handlerClass.handler.bind(handlerClass);

packages/tracer/tests/unit/Tracer.test.ts

+69
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,37 @@ describe('Class: Tracer', () => {
854854

855855
});
856856

857+
test('when used as decorator and when calling the handler, it has access to member variables', async () => {
858+
859+
// Prepare
860+
const tracer: Tracer = new Tracer();
861+
const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod');
862+
jest.spyOn(tracer.provider, 'getSegment')
863+
.mockImplementation(() => newSubsegment);
864+
setContextMissingStrategy(() => null);
865+
866+
class Lambda implements LambdaInterface {
867+
private readonly memberVariable: string;
868+
869+
public constructor(memberVariable: string) {
870+
this.memberVariable = memberVariable;
871+
}
872+
873+
@tracer.captureLambdaHandler()
874+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
875+
// @ts-ignore
876+
public async handler<TEvent, TResult>(_event: TEvent, _context: Context, _callback: Callback<TResult>): void | Promise<TResult> {
877+
return <TResult>(`memberVariable:${this.memberVariable}` as unknown);
878+
}
879+
880+
}
881+
882+
// Act / Assess
883+
const lambda = new Lambda('someValue');
884+
const handler = lambda.handler.bind(lambda);
885+
expect(await handler({}, context, () => console.log('Lambda invoked!'))).toEqual('memberVariable:someValue');
886+
887+
});
857888
});
858889

859890
describe('Method: captureMethod', () => {
@@ -1009,6 +1040,44 @@ describe('Class: Tracer', () => {
10091040

10101041
});
10111042

1043+
test('when used as decorator and when calling a method in the class, it has access to member variables', async () => {
1044+
1045+
// Prepare
1046+
const tracer: Tracer = new Tracer();
1047+
const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod');
1048+
jest.spyOn(tracer.provider, 'getSegment')
1049+
.mockImplementation(() => newSubsegment);
1050+
setContextMissingStrategy(() => null);
1051+
1052+
class Lambda implements LambdaInterface {
1053+
private readonly memberVariable: string;
1054+
1055+
public constructor(memberVariable: string) {
1056+
this.memberVariable = memberVariable;
1057+
}
1058+
1059+
@tracer.captureMethod()
1060+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
1061+
// @ts-ignore
1062+
public async dummyMethod(): Promise<string> {
1063+
return `memberVariable:${this.memberVariable}`;
1064+
}
1065+
1066+
@tracer.captureLambdaHandler()
1067+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
1068+
// @ts-ignore
1069+
public async handler<TEvent, TResult>(_event: TEvent, _context: Context, _callback: Callback<TResult>): void | Promise<TResult> {
1070+
return <TResult>(await this.dummyMethod() as unknown);
1071+
}
1072+
1073+
}
1074+
1075+
// Act / Assess
1076+
const lambda = new Lambda('someValue');
1077+
expect(await lambda.dummyMethod()).toEqual('memberVariable:someValue');
1078+
1079+
});
1080+
10121081
});
10131082

10141083
describe('Method: captureAWS', () => {

0 commit comments

Comments
 (0)