Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(instr-express): keep hidden properties in layer handlers #2137

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

david-luna
Copy link
Contributor

Which problem is this PR solving?

  • The original handle function of an express layer may contain certain metadata, specially in the router layer. When instrumentation does the "patch" the patched function does not have the same properties so any code relying on those props will find nothing. That may not be the case in express itself but 3rd party tools that look into that metadata are failing.

Fixes: #1950

Short description of the changes

  • The patched function now exposes same properties proxying the ones from the original handle function.
  • A test has been added to check the stack property is available for the router layer.

@david-luna david-luna requested a review from a team as a code owner April 22, 2024 17:19
@@ -165,8 +165,7 @@ export class ExpressInstrumentation extends InstrumentationBase<
) {
const route = original.apply(this, args);
const layer = this._router.stack[this._router.stack.length - 1];
instrumentation._applyPatch.call(
instrumentation,
instrumentation._applyPatch(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for reviewer: this usage of Function.protptype.call comes from this commit 0723d06 as a new addition.

I did not dig further to get the original implementation but I think there is no need to use .call(...) since we have access to the container object and therefore calling _applyPatch with the right context.

Copy link

codecov bot commented Apr 22, 2024

Codecov Report

Attention: Patch coverage is 80.00000% with 2 lines in your changes missing coverage. Please review.

Project coverage is 90.38%. Comparing base (dfb2dff) to head (686c669).
Report is 153 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2137      +/-   ##
==========================================
- Coverage   90.97%   90.38%   -0.60%     
==========================================
  Files         146      149       +3     
  Lines        7492     7521      +29     
  Branches     1502     1573      +71     
==========================================
- Hits         6816     6798      -18     
- Misses        676      723      +47     
Files Coverage Δ
...etry-instrumentation-express/src/internal-types.ts 100.00% <ø> (ø)
...try-instrumentation-express/src/instrumentation.ts 98.61% <80.00%> (-1.39%) ⬇️

... and 51 files with indirect coverage changes

@david-luna
Copy link
Contributor Author

... will decrease coverage by 0.65%.

This is due to no tests are manipulating the hidden properties of the handle function of the layers. so the setter is not covered in the tests.

Screenshot 2024-04-23 at 10 18 27

I can remove the setter since until now not having the property at all was not breaking apps. But I think is a matter of time to get a new issue because of a lib trying to update those rops

Copy link
Member

@JamieDanielson JamieDanielson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @david-luna ! I will review this week. In the meantime can you run npm run lint:fix to update the lint errors?

Object.keys(original).forEach(key => {
Object.defineProperty(patched, key, {
get() {
// @ts-expect-error -- the original function has hidden props indexed by strings
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I understand the purpose of what we're trying to achieve, the error that comes up here feels very relevant and makes me wonder if there's a better way to do this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JamieDanielson

Thanks for your review :)

...makes me wonder if there's a better way to do this.

For "better" you meant at the type level or you think the usage of Object.defineProperty is not a good solution? I'll put my thoughts on both.

Type error
The type error we get is:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Function'.
  No index signature with a parameter of type 'string' was found on type 'Function'.ts(7053)

The internal typings define the handle property of the layer as a Function. I've also checked @types/express to see if we can get the type from there but the router stack (where the layers are stored) is defines as any[]. So at the time of writing it adding comment to highlight express is "leveraging" the Object nature of functions to hide metadata felt the right thing to do. Note: the type error also arises with @blumamir's proposal since its a type level error.

We may get rid of the error by doing some improvements in the internal types so the compiler will know the function being patched is also used as a record. Something like the type below should work.

type HandlerType = Function & Record <string,any>

Object.defineProperty vs copy

Doing the shallow copy proposed by @blumamir fixes this issue as well but those properties represents the state of the layer. If that state is modified for the handler to change it's behaviour the original one will not notice that change and keep behaving according the original state (before the copy). With defineProperty we keep patched and original in sync (sharing state) so the instrumentation will not interfere with any code accessing or changing the state.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, that sounds like a good idea to use that new HandlerType. Would you be open to adding that into internal-types, and using that instead of ignoring the TypeScript error?

Also thank you for the detailed explanation, that helps me understand much better!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, that sounds like a good idea to use that new HandlerType. Would you be open to adding that into internal-types, and using that instead of ignoring the TypeScript error?

Also thank you for the detailed explanation, that helps me understand much better!

I support doing this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update the types :)

@david-luna
Copy link
Contributor Author

Note to self: this issue will probably get fixed by this PR. Check it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Node - TypeError: Cannot read properties of undefined (reading 'forEach')
4 participants