Skip to content
New issue

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

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

Already on GitHub? # to your account

feat(nestjs): Automatic instrumentation of nestjs middleware #13065

Merged
merged 28 commits into from
Jul 30, 2024

Conversation

nicohrubec
Copy link
Contributor

@nicohrubec nicohrubec commented Jul 26, 2024

Adds middleware instrumentation to the @sentry/nestjs. The implementation lives in @sentry/node so that both users using @sentry/nestjs directly as well as users still on @sentry/node benefit. The instrumentation is automatic without requiring any additional setup. The idea is to hook into the Injectable decorator (every class middleware is annotated with @Injectable and patch the use method if it is implemented.

Caveat: This implementation only works for class middleware, which implements the use method, which seems to be the standard for implementing middleware in nest. However, nest also provides functional middleware, for which this implementation does not work.

Trace from my sample app:
Screenshot 2024-07-29 at 15 49 17

Resolves #12769

@nicohrubec nicohrubec self-assigned this Jul 26, 2024
Copy link
Contributor

github-actions bot commented Jul 26, 2024

size-limit report 📦

Path Size
@sentry/browser 22.45 KB (0%)
@sentry/browser (incl. Tracing) 34.22 KB (0%)
@sentry/browser (incl. Tracing, Replay) 70.26 KB (0%)
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 63.59 KB (0%)
@sentry/browser (incl. Tracing, Replay with Canvas) 74.66 KB (0%)
@sentry/browser (incl. Tracing, Replay, Feedback) 87.24 KB (0%)
@sentry/browser (incl. Tracing, Replay, Feedback, metrics) 89.08 KB (0%)
@sentry/browser (incl. metrics) 26.75 KB (0%)
@sentry/browser (incl. Feedback) 39.37 KB (0%)
@sentry/browser (incl. sendFeedback) 27.06 KB (0%)
@sentry/browser (incl. FeedbackAsync) 31.7 KB (0%)
@sentry/react 25.22 KB (0%)
@sentry/react (incl. Tracing) 37.22 KB (0%)
@sentry/vue 26.6 KB (0%)
@sentry/vue (incl. Tracing) 36.06 KB (0%)
@sentry/svelte 22.58 KB (0%)
CDN Bundle 23.64 KB (0%)
CDN Bundle (incl. Tracing) 35.88 KB (0%)
CDN Bundle (incl. Tracing, Replay) 70.26 KB (0%)
CDN Bundle (incl. Tracing, Replay, Feedback) 75.53 KB (0%)
CDN Bundle - uncompressed 69.37 KB (0%)
CDN Bundle (incl. Tracing) - uncompressed 106.31 KB (0%)
CDN Bundle (incl. Tracing, Replay) - uncompressed 217.95 KB (0%)
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 230.78 KB (0%)
@sentry/nextjs (client) 37.08 KB (0%)
@sentry/sveltekit (client) 34.81 KB (0%)
@sentry/node 114.55 KB (+2.36% 🔺)
@sentry/node - without tracing 89.33 KB (0%)
@sentry/aws-serverless 98.5 KB (0%)

@lforst lforst force-pushed the nh/nestjs-middleware-instrumentation branch from 4f394c4 to 6e35eb1 Compare July 26, 2024 12:09
@nicohrubec nicohrubec marked this pull request as ready for review July 29, 2024 15:01
Copy link
Member

@lforst lforst left a comment

Choose a reason for hiding this comment

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

Great job! I left a few comments that we need to address.

if (typeof target.prototype.use === 'function') {
const originalUse = target.prototype.use;

target.prototype.use = function (req: unknown, res: unknown, next: (error?: unknown) => void): void {
Copy link
Member

Choose a reason for hiding this comment

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

m: We should make very sure that the this context stays intact. Also we should forward all arguments!

Maybe let's add a unit test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

replaced this also with a proxy and pass all arguments along now.

*/
private _getInjectableFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile {
return new InstrumentationNodeModuleFile(
'@nestjs/common/decorators/core/injectable.decorator.js',
Copy link
Member

Choose a reason for hiding this comment

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

m: Since Injectable is exported from the top-level of @nestjs/common, I wonder if we can just do '@nestjs/common' here 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No doesn't work (changed it locally and it breaks the e2e tests)

Copy link
Member

Choose a reason for hiding this comment

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

FWIW they also wrap specific files like this in the nestjs-core instrumentation (https://www.npmjs.com/package/@opentelemetry/instrumentation-nestjs-core)

Copy link
Member

@lforst lforst Jul 30, 2024

Choose a reason for hiding this comment

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

Kinda sucks because if they change the file structure this instantly won't work anymore :/ Stuff like this makes us need canary test otherwise we'll never catch breakages like that.

@nicohrubec can you investigate whether OTEL has some other functionality that we can use where we can point at '@nestjs/common'.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have played around with it but could not make anything else work so far. FWIW I think it's fine here. They have not moved this file in 6 years: https://github.com/nestjs/nest/commits/master/packages/common/decorators/core/injectable.decorator.ts

},
(span: Span) => {
// patch next to end span before next middleware is being called
const wrappedNext = (error?: Error | unknown): void => {
Copy link
Member

Choose a reason for hiding this comment

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

h: We should forward all arguments not just the first one. Ideally we even use a proxy like for example here:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

using a proxy now

return new SentryNestInstrumentation();
});

export const instrumentNest = Object.assign(
Copy link
Member

Choose a reason for hiding this comment

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

m: The object assign here seems unnecessary. This could just be a simple function.

Copy link
Member

Choose a reason for hiding this comment

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

we need the id for filtering out stuff in preload!

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 have updated it to use no Object.assign explicitly but still set an id, which should work too. No strong feelings either way

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Going with the original version with Object.assign to not introduce side-effects

@nicohrubec
Copy link
Contributor Author

What should we put for instrumentationVersion?

@lforst
Copy link
Member

lforst commented Jul 30, 2024

What should we put for instrumentationVersion?

We could do SDK_VERSION as exported by @sentry/utils.

@nicohrubec nicohrubec merged commit e3af1ce into develop Jul 30, 2024
111 checks passed
@nicohrubec nicohrubec deleted the nh/nestjs-middleware-instrumentation branch July 30, 2024 14:14
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Instrumentation of NestJS middleware
3 participants