You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
All three existing utilities (Logger, Tracer, Metrics) provide method decorators that customers can use to decorate a Lambda class in AWS Lambda. Decorators are an experimental feature in TypeScript and at the time of writing the only way of using them is to use Classes.
Here's a generic example of how customers are expected to use the Powertools decorators:
import{Tracer}from'@aws-lambda-powertools/tracer';import{LambdaInterface}from'@aws-lambda-powertools/commons';consttracer=newTracer({serviceName: 'serverlessAirline'});classLambdaimplementsLambdaInterface{// Decorate your handler class method
@tracer.captureLambdaHandler()publicasynchandler(_event: any,_context: any): Promise<void>{/* ... */}}exportconsthandlerClass=newLambda();exportconsthandler=handlerClass.handler;
Every time the function is invoked, the Lambda service will call the exported handler, which is now decorated. While the example above shows a decorator that belongs to Tracer, what discussed below affects also the other two utilities as they all use a similar implementation.
According to the TypeScript documentation, a method decorator has the following signature/type annotation:
target: can be either the constructor function of the class for a static member, or the prototype of the class for an instance member (in most of the cases that interest this discussion & the usage that we recommend, the latter applies).
propertyKey: name of the property (e.g. handler in the example above)
Let us now take a simplified & stripped down version of the current implementation seen across Powertools:
classDecoratorUtilClass{publicdecorate(){returnfunction(target: any,propertyKey: string,descriptor: PropertyDescriptor){constoriginalMethod=descriptor.value;descriptor.value=(...args: any[])=>{// Do something beforeconstresult=originalMethod.call(target,args);// Do something afterreturnresult;};returndescriptor;};}}
The above would be used as follows:
constmyUtilClass=newDecoratorUtilClass();classLambdaimplementsLambdaInterface{// Decorate your handler class method
@ myUtilClass.decorate()publicasynchandler(_event: any,_context: any): Promise<void>{/* ... */}}exportconsthandlerClass=newLambda();exportconsthandler=handlerClass.handler;
The interesting bits of the implementation above are:
We save a reference of the original method being decorated const originalMethod = descriptor.value; [1]
We then reassign a new value to the descriptor to a custom function that:
Performs some actions before decorated method
Calls the decorated method by passing back the target as first parameter, hence returning the initial this (aka the decorated Class) (more on this in the "Possible solution" section)
Performs some actions after the decorated method has returned
Returns the descriptor with a new value (thus decorating it)
The issue with this implementation is that the current implementation prevents customers from accessing all attributes from the decorated class. This is because the implementation is using an () => {} arrow function and the incorrect value of this is being passed back down.
Additionally, there's another caveat that was ignored until now which is that when exporting the handler at the end of the file:
[1] This is mostly harmless but the line const originalMethod = descriptor.value; is potentially useless. The reason why I say this is that description.value evaluates to undefined all the time. This happens because we most probably misunderstood the order of evaluation of a decorator (see "Order of Evaluation" section of this page). That section of the code is always evaluated when the utility is instantiated for the first time. In the context of Lambda, this means during the execution environment's initialisation (aka during cold start).
Customers should potentially be able to decorate a Class method and then still be able to access both methods and attributes of the decorated class after the decoration has happened.
Current Behavior
Depending on the implementation & execution environment the behaviour varies (see possible solutions below).
Possible Solution
There are a few changes to the decorator implementation that essentially boil down to switching to a function() {} (instead of an () => {} arrow fn) and then passing the correct value for this:
classDecoratorUtilClass{publicdecorate(){returnfunction(target: any,propertyKey: string,descriptor: PropertyDescriptor){constoriginalMethod=descriptor.value;// We save this reference so we can call other methods on the DecoratorUtilClassconsttracerRef=this;// We change the function to a regular function instead of an arrow fndescriptor.value=(function(this: Handler, ...args: any[]){// We save the reference to the decorated fn/handler (in the actual implementation this is needed)consthandlerRef: Handler=this;// Do something before// We pass the handlerRef when calling/applyingconstresult=originalMethod.call(handlerRef,args);// Do something afterreturnresult;};returndescriptor;};}}
Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.
dreamorosi
changed the title
Bug (tracer): decorated class methods cannot access this
Bug: decorated class methods cannot access this in Tracer
Nov 14, 2022
Bug description
All three existing utilities (Logger, Tracer, Metrics) provide method decorators that customers can use to decorate a
Lambda
class in AWS Lambda. Decorators are an experimental feature in TypeScript and at the time of writing the only way of using them is to use Classes.Here's a generic example of how customers are expected to use the Powertools decorators:
Every time the function is invoked, the Lambda service will call the exported
handler
, which is now decorated. While the example above shows a decorator that belongs to Tracer, what discussed below affects also the other two utilities as they all use a similar implementation.According to the TypeScript documentation, a method decorator has the following signature/type annotation:
The parameters are:
target
: can be either the constructor function of the class for a static member, or the prototype of the class for an instance member (in most of the cases that interest this discussion & the usage that we recommend, the latter applies).propertyKey
: name of the property (e.g.handler
in the example above)descriptor
: property descriptor for the memberLet us now take a simplified & stripped down version of the current implementation seen across Powertools:
The above would be used as follows:
The interesting bits of the implementation above are:
const originalMethod = descriptor.value;
[1]target
as first parameter, hence returning the initialthis
(aka the decorated Class) (more on this in the "Possible solution" section)The issue with this implementation is that the current implementation prevents customers from accessing all attributes from the decorated class. This is because the implementation is using an
() => {}
arrow function and the incorrect value ofthis
is being passed back down.Additionally, there's another caveat that was ignored until now which is that when exporting the handler at the end of the file:
Customers actually should
bind()
the class back to the handler so that when used and called later it will have access to the correct value ofthis
.[1] This is mostly harmless but the line
const originalMethod = descriptor.value;
is potentially useless. The reason why I say this is thatdescription.value
evaluates toundefined
all the time. This happens because we most probably misunderstood the order of evaluation of a decorator (see "Order of Evaluation" section of this page). That section of the code is always evaluated when the utility is instantiated for the first time. In the context of Lambda, this means during the execution environment's initialisation (aka during cold start).Further readings
Expected Behavior
Customers should potentially be able to decorate a Class method and then still be able to access both methods and attributes of the decorated class after the decoration has happened.
Current Behavior
Depending on the implementation & execution environment the behaviour varies (see possible solutions below).
Possible Solution
There are a few changes to the decorator implementation that essentially boil down to switching to a function() {} (instead of an () => {} arrow fn) and then passing the correct value for
this
:This work is being done in #1055
Steps to Reproduce
Environment
Related issues, RFCs
#1026
The text was updated successfully, but these errors were encountered: