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

feat(node): Ensure request bodies are reliably captured for http requests #13746

Merged
merged 16 commits into from
Nov 13, 2024

Conversation

mydea
Copy link
Member

@mydea mydea commented Sep 23, 2024

This PR started out as trying to fix capturing request bodies for Koa.

Some investigation later, we found out that the fundamental problem was that we relied on the request body being on request.body, which is non-standard and thus does not necessarily works. It seems that in express this works because it under the hood writes the body there, but this is non-standard and rather undefined behavior. For other frameworks (e.g. Koa and probably more) this did not work, the body was not on the request and thus never captured. We also had no test coverage for this overall.

This PR ended up doing a few things:

  • Add tests for this for express and koa
  • Streamline types for sdkProcessingMetadata - this used to be any, which lead to any usage of this not really being typed at all. I added proper types for this now.
  • Generic extraction of the http request body in the http instrumentation - this should now work for any node framework

Most importantly, I opted to not force this into the existing, rather complicated and hard to follow request data integration flow. This used to take an IsomorphicRequest and then did a bunch of conversion etc.

Since now in Node, we always have the same, proper http request (for any framework, because this always goes through http instrumentation), we can actually streamline this and normalize this properly at the time where we set this.

So with this PR, we set a normalizedRequest which already has the url, headers etc. set in a way that we need it/it makes sense.
Additionally, the parsed & stringified request body will be set on this too.

If this normalized request is set in sdkProcessingMetadata, we will use it as source of truth instead of the plain request. (Note that we still need the plain request for some auxiliary data that is non-standard, e.g. request.user).

For the body parsing itself, we monkey-patch req.on('data'). this way, we ensure to not add more handlers than a user has, and we only extract the body if the user is extracting it anyhow, ensuring we do not alter behavior.

Closes #13722

@mydea mydea self-assigned this Sep 23, 2024
Copy link
Contributor

github-actions bot commented Sep 23, 2024

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 22.77 KB - -
@sentry/browser - with treeshaking flags 21.53 KB - -
@sentry/browser (incl. Tracing) 35.26 KB - -
@sentry/browser (incl. Tracing, Replay) 71.98 KB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 62.35 KB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 76.31 KB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 89.14 KB - -
@sentry/browser (incl. Feedback) 39.93 KB - -
@sentry/browser (incl. sendFeedback) 27.42 KB - -
@sentry/browser (incl. FeedbackAsync) 32.23 KB - -
@sentry/react 25.52 KB - -
@sentry/react (incl. Tracing) 38.22 KB - -
@sentry/vue 26.91 KB - -
@sentry/vue (incl. Tracing) 37.1 KB - -
@sentry/svelte 22.91 KB - -
CDN Bundle 24.13 KB - -
CDN Bundle (incl. Tracing) 37.04 KB - -
CDN Bundle (incl. Tracing, Replay) 71.7 KB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 77.05 KB - -
CDN Bundle - uncompressed 70.73 KB - -
CDN Bundle (incl. Tracing) - uncompressed 109.9 KB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 222.42 KB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 235.64 KB - -
@sentry/nextjs (client) 38.33 KB - -
@sentry/sveltekit (client) 35.84 KB - -
@sentry/node 134.28 KB +0.59% +798 B 🔺
@sentry/node - without tracing 96.46 KB +0.83% +807 B 🔺
@sentry/aws-serverless 106.72 KB +0.74% +802 B 🔺

View base workflow run

Copy link

codecov bot commented Sep 23, 2024

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
637 1 636 33
View the top 1 failed tests by shortest run time
transactions.test.ts Captures request metadata
Stack Traces | 0.034s run time
transactions.test.ts:139:5 Captures request metadata

To view individual test run time comparison to the main branch, go to the Test Analytics Dashboard

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.

Really cool change. I think we can clean up the flow a bit and I also hope that the patching doesn't blow up in any unexpected ways.

// This is non-standard, but may be set on e.g. Next.js or Express requests
const cookies = (req as PolymorphicRequest).cookies;

const normalizedRequest: Request = {
Copy link
Member

Choose a reason for hiding this comment

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

l/m: I have been burned in the past because Request is a native type. I suggest we rename this to smth like NormalizedRequest or ProcessingMetadataRequest.

Copy link
Member Author

Choose a reason for hiding this comment

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

this type already exists, it is not new 😬 but I do agree, but we probably need to alias it then and keep the old name around (deprecated)...?

@@ -148,7 +149,27 @@ export const instrumentHttp = Object.assign(
const isolationScope = (scopes.isolationScope || getIsolationScope()).clone();
const scope = scopes.scope || getCurrentScope();

const headers = req.headers;
const host = headers.host || '<no host>';
Copy link
Member

Choose a reason for hiding this comment

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

l: I haven't put too much thought into this but is <no host> a good default?

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah no idea, just kept this around ^^

packages/node/src/integrations/http.ts Outdated Show resolved Hide resolved
@@ -101,7 +101,7 @@ export type ProfileItem = BaseEnvelopeItem<ProfileItemHeaders, Profile>;
export type ProfileChunkItem = BaseEnvelopeItem<ProfileChunkItemHeaders, ProfileChunk>;
export type SpanItem = BaseEnvelopeItem<SpanItemHeaders, Partial<SpanJSON>>;

export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext };
export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: Partial<DynamicSamplingContext> };
Copy link
Member

Choose a reason for hiding this comment

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

h: What is this about?

Copy link
Member Author

Choose a reason for hiding this comment

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

sorry, forgot to mention this here: this was just not correctly annotated before, really. Since we did not type dynamicSamplingContext on the sdkProcessingMetadata before, this worked by accident. but because this is really Partial<DSC> (based on what we set on it, which is also partial), it makes this necessary to be correct.

We could also choose to leave this and cast it in the place where we set this, but really that means lying to ourselves because we don't know it is complete. 🤷 no strong feelings though, if we feel this is a bad type change I can also cast it.

packages/utils/src/requestdata.ts Outdated Show resolved Hide resolved
Comment on lines 76 to 96
const { sdkProcessingMetadata = {} } = event;
const req = sdkProcessingMetadata.request;
const { request, normalizedRequest } = sdkProcessingMetadata;

if (!req) {
return event;
const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(_options);

// If this is set, it takes precedence over the plain request object
if (normalizedRequest) {
// Some other data is not available in standard HTTP requests, but can sometimes be augmented by e.g. Express or Next.js
const ipAddress = request ? request.ip || (request.socket && request.socket.remoteAddress) : undefined;
const user = request ? request.user : undefined;

return addNormalizedRequestDataToEvent(event, normalizedRequest, { ipAddress, user }, addRequestDataOptions);
}

const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(_options);
if (!request) {
return event;
}

return addRequestDataToEvent(event, req, addRequestDataOptions);
return addRequestDataToEvent(event, request, addRequestDataOptions);
Copy link
Member

Choose a reason for hiding this comment

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

I would mayyyybee do it like this?:

Suggested change
const { sdkProcessingMetadata = {} } = event;
const req = sdkProcessingMetadata.request;
const { request, normalizedRequest } = sdkProcessingMetadata;
if (!req) {
return event;
const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(_options);
// If this is set, it takes precedence over the plain request object
if (normalizedRequest) {
// Some other data is not available in standard HTTP requests, but can sometimes be augmented by e.g. Express or Next.js
const ipAddress = request ? request.ip || (request.socket && request.socket.remoteAddress) : undefined;
const user = request ? request.user : undefined;
return addNormalizedRequestDataToEvent(event, normalizedRequest, { ipAddress, user }, addRequestDataOptions);
}
const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(_options);
if (!request) {
return event;
}
return addRequestDataToEvent(event, req, addRequestDataOptions);
return addRequestDataToEvent(event, request, addRequestDataOptions);
const { sdkProcessingMetadata = {} } = event;
const { request, normalizedRequest } = sdkProcessingMetadata;
const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(_options);
if (request) {
addRequestDataToEvent(event, request, addRequestDataOptions);
}
// If this is set, it takes precedence over the plain request object
if (normalizedRequest) {
addNormalizedRequestDataToEvent(event, normalizedRequest, addRequestDataOptions);
}
return event;

So that

  • We always use all of the data.
  • For now we can just use the old logic from addRequestDataToEvent to extract the "non-standard" fields on the request. Removing the need for the additionalData arg which I admittedly find a bit offputting.

Copy link
Member Author

Choose a reason for hiding this comment

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

(Sorry, took some time to come back to this 😅 )

I guess my main problem with this is that we do all the work of parsing the "old" request etc. even though we don't need it? I do feel like it would be easier to understand to have a single function be responsible for it - otherwise, we end up in the state of "why is this field set here? let's look through all of the old code to find if it sets it somewhere, then look over the new code, ...".

By not calling the old method anymore it makes it easier to follow the "new, happy" path imho 🤔 though I do agree that the additionalData part is not great...

Choose a reason for hiding this comment

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

@lforst ? 🙃

Copy link
Member

Choose a reason for hiding this comment

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

@maximedupre we will get to it. Please understand that we are bound to other obligations, absences, deadlines... Pinging usually doesn't influence in what order we do things so I would generally just not do it - in any open source project. Thanks!

packages/node/src/integrations/http.ts Outdated Show resolved Hide resolved
@maximedupre
Copy link

🥹

@mydea mydea force-pushed the fn/node-request-data branch from 183be5b to a5821ce Compare October 8, 2024 11:47
@mydea mydea force-pushed the fn/node-request-data branch from a5821ce to 3ad5459 Compare October 28, 2024 09:14
@mydea mydea force-pushed the fn/node-request-data branch 2 times, most recently from edab56c to ae94190 Compare November 12, 2024 09:59
/**
* Request data included in an event as sent to Sentry.
* TODO(v9): Rename this to avoid confusion, because Request is also a native type.
*/
export interface Request {
Copy link
Member

Choose a reason for hiding this comment

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

l: Can't we change this now? What exposed to users relies on this?

Copy link
Member Author

Choose a reason for hiding this comment

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

it is exported from @sentry/types, probably does not affect anybody, but 🤷 IMHO I would just clean this up and rename these things in v9, WDYT?

@mydea mydea force-pushed the fn/node-request-data branch 2 times, most recently from 383e4f2 to 28e6dfe Compare November 13, 2024 10:53
@mydea mydea force-pushed the fn/node-request-data branch from 28e6dfe to db8c688 Compare November 13, 2024 11:24
@mydea mydea merged commit a55e2b0 into develop Nov 13, 2024
148 checks passed
@mydea mydea deleted the fn/node-request-data branch November 13, 2024 13:43
alexandresoro pushed a commit to alexandresoro/ouca that referenced this pull request Nov 22, 2024
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@sentry/node](https://github.com/getsentry/sentry-javascript/tree/master/packages/node) ([source](https://github.com/getsentry/sentry-javascript)) | dependencies | minor | [`8.37.1` -> `8.40.0`](https://renovatebot.com/diffs/npm/@sentry%2fnode/8.37.1/8.40.0) |
| [@sentry/react](https://github.com/getsentry/sentry-javascript/tree/master/packages/react) ([source](https://github.com/getsentry/sentry-javascript)) | dependencies | minor | [`8.37.1` -> `8.40.0`](https://renovatebot.com/diffs/npm/@sentry%2freact/8.37.1/8.40.0) |

---

### Release Notes

<details>
<summary>getsentry/sentry-javascript (@&#8203;sentry/node)</summary>

### [`v8.40.0`](https://github.com/getsentry/sentry-javascript/blob/HEAD/CHANGELOG.md#8400)

[Compare Source](getsentry/sentry-javascript@8.39.0...8.40.0)

##### Important Changes

-   **feat(angular): Support Angular 19 ([#&#8203;14398](getsentry/sentry-javascript#14398

    The `@sentry/angular` SDK can now be used with Angular 19. If you're upgrading to the new Angular version, you might want to migrate from the now deprecated `APP_INITIALIZER` token to `provideAppInitializer`.
    In this case, change the Sentry `TraceService` initialization in `app.config.ts`:

    ```ts
    // Angular 18
    export const appConfig: ApplicationConfig = {
      providers: [
        // other providers
        {
          provide: TraceService,
          deps: [Router],
        },
        {
          provide: APP_INITIALIZER,
          useFactory: () => () => {},
          deps: [TraceService],
          multi: true,
        },
      ],
    };

    // Angular 19
    export const appConfig: ApplicationConfig = {
      providers: [
        // other providers
        {
          provide: TraceService,
          deps: [Router],
        },
        provideAppInitializer(() => {
          inject(TraceService);
        }),
      ],
    };
    ```

-   **feat(core): Deprecate `debugIntegration` and `sessionTimingIntegration` ([#&#8203;14363](getsentry/sentry-javascript#14363

    The `debugIntegration` was deprecated and will be removed in the next major version of the SDK.
    To log outgoing events, use [Hook Options](https://docs.sentry.io/platforms/javascript/configuration/options/#hooks) (`beforeSend`, `beforeSendTransaction`, ...).

    The `sessionTimingIntegration` was deprecated and will be removed in the next major version of the SDK.
    To capture session durations alongside events, use [Context](https://docs.sentry.io/platforms/javascript/enriching-events/context/) (`Sentry.setContext()`).

-   **feat(nestjs): Deprecate `@WithSentry` in favor of `@SentryExceptionCaptured` ([#&#8203;14323](getsentry/sentry-javascript#14323

    The `@WithSentry` decorator was deprecated. Use `@SentryExceptionCaptured` instead. This is a simple renaming and functionality stays identical.

-   **feat(nestjs): Deprecate `SentryTracingInterceptor`, `SentryService`, `SentryGlobalGenericFilter`, `SentryGlobalGraphQLFilter` ([#&#8203;14371](getsentry/sentry-javascript#14371

    The `SentryTracingInterceptor` was deprecated. If you are using `@sentry/nestjs` you can safely remove any references to the `SentryTracingInterceptor`. If you are using another package migrate to `@sentry/nestjs` and remove the `SentryTracingInterceptor` afterwards.

    The `SentryService` was deprecated and its functionality was added to `Sentry.init`. If you are using `@sentry/nestjs` you can safely remove any references to the `SentryService`. If you are using another package migrate to `@sentry/nestjs` and remove the `SentryService` afterwards.

    The `SentryGlobalGenericFilter` was deprecated. Use the `SentryGlobalFilter` instead which is a drop-in replacement.

    The `SentryGlobalGraphQLFilter` was deprecated. Use the `SentryGlobalFilter` instead which is a drop-in replacement.

-   **feat(node): Deprecate `nestIntegration` and `setupNestErrorHandler` in favor of using `@sentry/nestjs` ([#&#8203;14374](getsentry/sentry-javascript#14374

    The `nestIntegration` and `setupNestErrorHandler` functions from `@sentry/node` were deprecated and will be removed in the next major version of the SDK. If you're using `@sentry/node` in a NestJS application, we recommend switching to our new dedicated `@sentry/nestjs` package.

##### Other Changes

-   feat(browser): Send additional LCP timing info ([#&#8203;14372](getsentry/sentry-javascript#14372))
-   feat(replay): Clear event buffer when full and in buffer mode ([#&#8203;14078](getsentry/sentry-javascript#14078))
-   feat(core): Ensure `normalizedRequest` on `sdkProcessingMetadata` is merged ([#&#8203;14315](getsentry/sentry-javascript#14315))
-   feat(core): Hoist everything from `@sentry/utils` into `@sentry/core` ([#&#8203;14382](getsentry/sentry-javascript#14382))
-   fix(core): Do not throw when trying to fill readonly properties ([#&#8203;14402](getsentry/sentry-javascript#14402))
-   fix(feedback): Fix `__self` and `__source` attributes on feedback nodes ([#&#8203;14356](getsentry/sentry-javascript#14356))
-   fix(feedback): Fix non-wrapping form title ([#&#8203;14355](getsentry/sentry-javascript#14355))
-   fix(nextjs): Update check for not found navigation error ([#&#8203;14378](getsentry/sentry-javascript#14378))

### [`v8.39.0`](https://github.com/getsentry/sentry-javascript/blob/HEAD/CHANGELOG.md#8390)

[Compare Source](getsentry/sentry-javascript@8.38.0...8.39.0)

##### Important Changes

-   **feat(nestjs): Instrument event handlers ([#&#8203;14307](getsentry/sentry-javascript#14307

The `@sentry/nestjs` SDK will now capture performance data for [NestJS Events (`@nestjs/event-emitter`)](https://docs.nestjs.com/techniques/events)

##### Other Changes

-   feat(nestjs): Add alias `@SentryExceptionCaptured` for `@WithSentry` ([#&#8203;14322](getsentry/sentry-javascript#14322))
-   feat(nestjs): Duplicate `SentryService` behaviour into `@sentry/nestjs` SDK `init()` ([#&#8203;14321](getsentry/sentry-javascript#14321))
-   feat(nestjs): Handle GraphQL contexts in `SentryGlobalFilter` ([#&#8203;14320](getsentry/sentry-javascript#14320))
-   feat(node): Add alias `childProcessIntegration` for `processThreadBreadcrumbIntegration` and deprecate it ([#&#8203;14334](getsentry/sentry-javascript#14334))
-   feat(node): Ensure request bodies are reliably captured for http requests ([#&#8203;13746](getsentry/sentry-javascript#13746))
-   feat(replay): Upgrade rrweb packages to 2.29.0 ([#&#8203;14160](getsentry/sentry-javascript#14160))
-   fix(cdn): Ensure `_sentryModuleMetadata` is not mangled ([#&#8203;14344](getsentry/sentry-javascript#14344))
-   fix(core): Set `sentry.source` attribute to `custom` when calling `span.updateName` on `SentrySpan` ([#&#8203;14251](getsentry/sentry-javascript#14251))
-   fix(mongo): rewrite Buffer as ? during serialization ([#&#8203;14071](getsentry/sentry-javascript#14071))
-   fix(replay): Remove replay id from DSC on expired sessions ([#&#8203;14342](getsentry/sentry-javascript#14342))
-   ref(profiling) Fix electron crash ([#&#8203;14216](getsentry/sentry-javascript#14216))
-   ref(types): Deprecate `Request` type in favor of `RequestEventData` ([#&#8203;14317](getsentry/sentry-javascript#14317))
-   ref(utils): Stop setting `transaction` in `requestDataIntegration` ([#&#8203;14306](getsentry/sentry-javascript#14306))
-   ref(vue): Reduce bundle size for starting application render span ([#&#8203;14275](getsentry/sentry-javascript#14275))

### [`v8.38.0`](https://github.com/getsentry/sentry-javascript/blob/HEAD/CHANGELOG.md#8380)

[Compare Source](getsentry/sentry-javascript@8.37.1...8.38.0)

-   docs: Improve docstrings for node otel integrations ([#&#8203;14217](getsentry/sentry-javascript#14217))
-   feat(browser): Add moduleMetadataIntegration lazy loading support ([#&#8203;13817](getsentry/sentry-javascript#13817))
-   feat(core): Add trpc path to context in trpcMiddleware ([#&#8203;14218](getsentry/sentry-javascript#14218))
-   feat(deps): Bump [@&#8203;opentelemetry/instrumentation-amqplib](https://github.com/opentelemetry/instrumentation-amqplib) from 0.42.0 to 0.43.0 ([#&#8203;14230](getsentry/sentry-javascript#14230))
-   feat(deps): Bump [@&#8203;sentry/cli](https://github.com/sentry/cli) from 2.37.0 to 2.38.2 ([#&#8203;14232](getsentry/sentry-javascript#14232))
-   feat(node): Add `knex` integration ([#&#8203;13526](getsentry/sentry-javascript#13526))
-   feat(node): Add `tedious` integration ([#&#8203;13486](getsentry/sentry-javascript#13486))
-   feat(utils): Single implementation to fetch debug ids ([#&#8203;14199](getsentry/sentry-javascript#14199))
-   fix(browser): Avoid recording long animation frame spans starting before their parent span ([#&#8203;14186](getsentry/sentry-javascript#14186))
-   fix(node): Include `debug_meta` with ANR events ([#&#8203;14203](getsentry/sentry-javascript#14203))
-   fix(nuxt): Fix dynamic import rollup plugin to work with latest nitro ([#&#8203;14243](getsentry/sentry-javascript#14243))
-   fix(react): Support wildcard routes on React Router 6 ([#&#8203;14205](getsentry/sentry-javascript#14205))
-   fix(spotlight): Export spotlightBrowserIntegration from the main browser package ([#&#8203;14208](getsentry/sentry-javascript#14208))
-   ref(browser): Ensure start time of interaction root and child span is aligned ([#&#8203;14188](getsentry/sentry-javascript#14188))
-   ref(nextjs): Make build-time value injection turbopack compatible ([#&#8203;14081](getsentry/sentry-javascript#14081))

Work in this release was contributed by [@&#8203;grahamhency](https://github.com/grahamhency), [@&#8203;Zen-cronic](https://github.com/Zen-cronic), [@&#8203;gilisho](https://github.com/gilisho) and [@&#8203;phuctm97](https://github.com/phuctm97). Thank you for your contributions!

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xNDIuNyIsInVwZGF0ZWRJblZlciI6IjM4LjE0Mi43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: https://git.tristess.app/alexandresoro/ouca/pulls/317
Reviewed-by: Alexandre Soro <code@soro.dev>
Co-authored-by: renovate <renovate@git.tristess.app>
Co-committed-by: renovate <renovate@git.tristess.app>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Koa request body is not captured for events
5 participants