-
Notifications
You must be signed in to change notification settings - Fork 571
feat(instrumentation-express): add support for Express v5, take 2 #2801
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
base: main
Are you sure you want to change the base?
Conversation
This PR starts with @timfish's open-telemetry#2437 (Tim did all the hard work) and makes it a smaller patch -- mostly by avoiding having separate test/v4 and test/v5 trees. Now that we are on JS SDK v2 and hence only support Node.js v18 as a minimum (the same as express@5) the testing story is now simpler: no need to avoid running some tests with older Node.js versions. (See open-telemetry#2722 for a general discussion of the pain when having to do that.) Obsoletes: open-telemetry#2437 Closes: open-telemetry#2435
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some dev notes.
const isExpressV5 = | ||
typeof moduleExports?.Router?.prototype?.route === 'function'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dev note: I used feature-sniffing to detect the version. We could use (moduleExports, version) => { ...
and a semver.satisfies() check of the loaded module version if there is a preference for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very brittle, but I don't know enough about your InstrumentationNodeModuleDefinition
wrapper to be sure. Is this simply a check to tell which version it is? Or do you have logic somewhere based on if .route
is a function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the main question here is whether that feature may be backported at any point, removing the reliability of this check. Considering v4.x has been out for... years... I wouldn't rule out backporting features as a possibility. I'd lean toward version if we can, similar to how we do it in MongoDB.
Alternatively if we can comment the reasoning with a link to docs somewhere that might be good enough and we can change to use version if it causes a problem later with a backport.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also just realized you used the semver.satisfies() check in testing. I like the explicitness of it anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the main question here is whether that feature may be backported at any point
No it will not be backborted, but we want to clean up some of these interfaces in the future and so understanding how users are using these api's (especially incorrectly like this for version checking) is important for us to keep in mind.
Sorry, I am not sure if I was clear before, but I am one of the TC members for express, that's why I was pinged on the other PR and so got a notification for this one. Feel free to ask me if you have questions for the express side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this simply a check to tell which version it is? Or do you have logic somewhere based on if
.route
is a function?
The instrumentation here wraps methods on the Router prototype, including .route
(just below this const isExpressV5 = ...
statement), so it feels reasonable to (somehow) check that the thing to be instrumented exists.
If express@5 internals changed such that Router.prototype.route
went away, then this instrumentation-express implementation would have to change. So, IIUC, a semver check something like semver.satisfies(version, '^5')
wouldn't really help for future-proofness either.
@wesleytodd Or am I missing a detail in which you think this is more brittle than the semver-based differentiation that the previous PR was doing: https://github.com/open-telemetry/opentelemetry-js-contrib/pull/2437/files#diff-81c3e13c3dac53e0dd6aabf6dc165305350eb522a85a2561f9ee51d9249011e7R84-R90
If the var was called isExpressWithRouterPrototype
or something like that, would that be clearer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The instrumentation here wraps methods on the Router prototype, including .route (just below this const isExpressV5 = ... statement), so it feels reasonable to (somehow) check that the thing to be instrumented exists.
Ah ok yes, with this explanation I agree it makes sense. Calling it isExpressV5
is what threw me as I thought maybe it could be used for other things related to other parts of the v5 api changes (spoilers for below...).
If express@5 internals changed such that Router.prototype.route went away,
We cannot do that as it would be breaking. So you are for sure safe on that until the next major.
So, IIUC, a semver check
And I agree I am not sure a semver check would be better since you use this for is to patch .route
.
If the var was called isExpressWithRouterPrototype or something like that, would that be clearer?
Yes, after reading your first line that is where my head went before I even read down to the end of your reply. 🎉
'arr/requiredPath/optionalPath/', | ||
'arr/requiredPath/optionalPath/lastParam', | ||
'arr/requiredpath/optionalPath/', | ||
'arr/requiredpath/optionalPath/lastParam', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dev Note:
Interesting side-bar. The following route in "test/fixtures/use-express-regex.mjs" (added in #2008) was testing whether a request path /test/arr/requiredPath/optionalPath/
matched that route. Notice the capital letter in requiredPath
. Normally this should NOT match. However it does in express@4, not in express@5 and I think it is an express@4 limitation/bug.
app.get(
[
'/test/arr/:id',
/\/test\/arr[0-9]*\/required(path)?(\/optionalPath)?\/(lastParam)?/,
],
(_req, res) => {
res.send({ response: 'response' });
}
);
Let's use a simpler example (note to self: /Users/trentm/tmp/asdf.20250424T102536/app.js):
app.get(
[
'/foo',
/^\/another-path$/,
],
(req, res) => {
res.end('pong');
}
);
- Express routes are case-insensitive by default .
- express@4 used
path-to-regexp@0.1
for its route matching. That version of the lib creates a single regexp to match for any of the entries in the route array. So how to match '/foo' case-insensitively, but '/another-path' case-sensitively? The answer is that you don't. This is the regexp used in express@4 for matching that route:/^\/foo\/?$|^\/another-path$/i
. It is all case-insensitive. In other words, by including a regexp route together with a string route in an array toapp.get(...)
results in the regexp route being made case-insensitive. - In express@5 the
router@2
module is used for route matching and it handles each of the'/foo'
and/^\/another-path$/
entries separately.
The existing test data here was unintentionally relying on that Express routing subtlety. That subtlety changed in express@5, but isn't relevant to the testing here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for clarifying this!
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2801 +/- ##
==========================================
+ Coverage 89.50% 89.53% +0.03%
==========================================
Files 180 180
Lines 8719 8727 +8
Branches 1767 1773 +6
==========================================
+ Hits 7804 7814 +10
+ Misses 915 913 -2
🚀 New features to boost your workflow:
|
^^ toggle the |
cc @raphael-theriault-swi @JamieDanielson @pkanal (component owners) |
This will likely also fix open-telemetry/opentelemetry.io#6726 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have much to include just like on the last one, but if there is some better way we can export the express version from the library I am down for landing that to help prevent you from having checks like that one.
@@ -1,5 +1,5 @@ | |||
express: | |||
- versions: | |||
include: "^4.16.2" | |||
include: ">=4.16.2 <6" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
4.16.2
is not supported anymore, ideally bump this to 4.21.2
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, although if 4.16.2 still works without issue I'm okay keeping this until it causes us problems, unless it is massively increasing testing time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just mentioned it because of these: https://github.com/expressjs/express/security/advisories?state=published
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I should have posted more context. I doubt any impact from CVEs here. I just like to point it out in case there are parts of the systems I am unaware of that if it is not done for a real reason it is best to just avoid the versions with published CVEs. Hope that helps clarify my reasoning for mentioning it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah good callout! Thanks for pointing that out. I don't think we have a strict process we follow on when to drop package versions from contrib instrumentations. It's unfortunate that we don't have a way to track how many folks are on that version, though we're not technically removing the ability to use it, just narrowing the test scope. Now is probably a good time to remove it from testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we have a strict process we follow on when to drop package versions from contrib instrumentations.
We have worked on an LTS plan which includes things like when partners should drop support for integrations. Maybe this helps? https://github.com/expressjs/discussions/blob/master/docs/adr/lts-strategy.md
In this case, I think this is the relevant line:
Users are required to follow the head/latest of each major release line for support with all packages (express, dependencies, middleware, & tools/other)
It's unfortunate that we don't have a way to track how many folks are on that version
I don't know what scope you mean here, but the downloads tab on npm gives a rather broad view of that: https://www.npmjs.com/package/express?activeTab=versions
though we're not technically removing the ability to use it, just narrowing the test scope. Now is probably a good time to remove it from testing.
All in all I think this is a good choice if it is just impacting testing. If a user comes in broken and says "well I haven't updated express in 5 years" I think they will be better off going to upgrade. Win win!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
downloads tab on npm
Ah yep, I meant specifically folks who use this instrumentation. But to your point, this version is overall pretty low in terms of downloads compared to even the next minor release. Besides, if someone is still on this old of a version of express, they are less likely to be upgrading this dependency. Let's increase minimum testing version 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree slightly and I'd like to defer changing this to a separate PR so this one can stay on "add support for express@5".
const isExpressV5 = | ||
typeof moduleExports?.Router?.prototype?.route === 'function'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very brittle, but I don't know enough about your InstrumentationNodeModuleDefinition
wrapper to be sure. Is this simply a check to tell which version it is? Or do you have logic somewhere based on if .route
is a function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR starts with @timfish's #2437 (Tim did all the hard work) and
makes it a smaller patch -- mostly by avoiding having separate
test/v4 and test/v5 trees.
Now that we are on JS SDK v2 and hence only support Node.js v18 as
a minimum (the same as express@5) the testing story is now simpler:
no need to avoid running some tests with older Node.js versions.
(See #2722 for a general discussion of the pain when having to do that.)
Obsoletes: #2437
Closes: #2435
Testing Notes
npm test
..tav.yml
so that supported express@4 versions are tested vianpm run test-all-versions
.(If there is a coverage failure due to this, I'll make the argument that we can ignore it. Given our instrumentations have version-specific code, the only reasonable way to get full coverage is to gather coverage from
npm run test-all-versions
runs and not fromnpm test
runs.)