Skip to content

Commit 1f99c3b

Browse files
authored
ref(node): Use RequestData integration in express handlers (#5990)
This switches the request, tracing, and error handlers from the Node SDK's `handlers.ts` to use the new `RequestData` integration for adding request data to events rather than the current event processor. Notes: - So that this isn't a breaking change, for the moment (read: until v8) the integration will use the options passed to the request handler in place of its own options. (The request handler now stores its options in `sdkProcessingMetadata` alongside the request so that the integration will have access to them.) - Before this change, the event processor was backwards-compatible by dint of calling `parseRequest` rather than `addRequestDataToEvent` if the request handler's options are given in the old format. Because the integration uses only `addRequestDataToEvent` under the hood, the backwards compatibility is now achieved by converting the old-style options into equivalent new-style options, using a new helper function `convertReqHandlerOptsToAddReqDataOpts`. (And yes, that function name is definitely a mouthful, but fortunately only one will has to last until v8.) - Though in theory one should never use the error or tracing middleware without also using the request middleware, people do all sorts of weird things. All three middlewares therefore add the request to `sdkProcessingMetadata`, even though just doing so in the request handler should technically be sufficient. Ref: #5756
1 parent 6e70534 commit 1f99c3b

File tree

5 files changed

+133
-17
lines changed

5 files changed

+133
-17
lines changed

packages/node/src/handlers.ts

+48-14
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import { captureException, getCurrentHub, startTransaction, withScope } from '@sentry/core';
3-
import { Event, Span } from '@sentry/types';
3+
import { Span } from '@sentry/types';
44
import {
55
AddRequestDataToEventOptions,
66
addRequestDataToTransaction,
77
baggageHeaderToDynamicSamplingContext,
8+
dropUndefinedKeys,
89
extractPathForTransaction,
910
extractTraceparentData,
1011
isString,
@@ -14,10 +15,9 @@ import * as domain from 'domain';
1415
import * as http from 'http';
1516

1617
import { NodeClient } from './client';
17-
import { addRequestDataToEvent, extractRequestData } from './requestdata';
18-
// TODO (v8 / XXX) Remove these imports
18+
import { extractRequestData } from './requestdata';
19+
// TODO (v8 / XXX) Remove this import
1920
import type { ParseRequestOptions } from './requestDataDeprecated';
20-
import { parseRequest } from './requestDataDeprecated';
2121
import { flush, isAutoSessionTrackingEnabled } from './sdk';
2222

2323
/**
@@ -66,6 +66,11 @@ export function tracingHandler(): (
6666
...traceparentData,
6767
metadata: {
6868
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
69+
// The request should already have been stored in `scope.sdkProcessingMetadata` (which will become
70+
// `event.sdkProcessingMetadata` the same way the metadata here will) by `sentryRequestMiddleware`, but on the
71+
// off chance someone is using `sentryTracingMiddleware` without `sentryRequestMiddleware`, it doesn't hurt to
72+
// be sure
73+
request: req,
6974
source,
7075
},
7176
},
@@ -104,13 +109,41 @@ export type RequestHandlerOptions =
104109
flushTimeout?: number;
105110
};
106111

112+
/**
113+
* Backwards compatibility shim which can be removed in v8. Forces the given options to follow the
114+
* `AddRequestDataToEventOptions` interface.
115+
*
116+
* TODO (v8): Get rid of this, and stop passing `requestDataOptionsFromExpressHandler` to `setSDKProcessingMetadata`.
117+
*/
118+
function convertReqHandlerOptsToAddReqDataOpts(
119+
reqHandlerOptions: RequestHandlerOptions = {},
120+
): AddRequestDataToEventOptions | undefined {
121+
let addRequestDataOptions: AddRequestDataToEventOptions | undefined;
122+
123+
if ('include' in reqHandlerOptions) {
124+
addRequestDataOptions = { include: reqHandlerOptions.include };
125+
} else {
126+
// eslint-disable-next-line deprecation/deprecation
127+
const { ip, request, transaction, user } = reqHandlerOptions as ParseRequestOptions;
128+
129+
if (ip || request || transaction || user) {
130+
addRequestDataOptions = { include: dropUndefinedKeys({ ip, request, transaction, user }) };
131+
}
132+
}
133+
134+
return addRequestDataOptions;
135+
}
136+
107137
/**
108138
* Express compatible request handler.
109139
* @see Exposed as `Handlers.requestHandler`
110140
*/
111141
export function requestHandler(
112142
options?: RequestHandlerOptions,
113143
): (req: http.IncomingMessage, res: http.ServerResponse, next: (error?: any) => void) => void {
144+
// TODO (v8): Get rid of this
145+
const requestDataOptions = convertReqHandlerOptsToAddReqDataOpts(options);
146+
114147
const currentHub = getCurrentHub();
115148
const client = currentHub.getClient<NodeClient>();
116149
// Initialise an instance of SessionFlusher on the client when `autoSessionTracking` is enabled and the
@@ -130,15 +163,6 @@ export function requestHandler(
130163
res: http.ServerResponse,
131164
next: (error?: any) => void,
132165
): void {
133-
// TODO (v8 / XXX) Remove this shim and just use `addRequestDataToEvent`
134-
let backwardsCompatibleEventProcessor: (event: Event) => Event;
135-
if (options && 'include' in options) {
136-
backwardsCompatibleEventProcessor = (event: Event) => addRequestDataToEvent(event, req, options);
137-
} else {
138-
// eslint-disable-next-line deprecation/deprecation
139-
backwardsCompatibleEventProcessor = (event: Event) => parseRequest(event, req, options as ParseRequestOptions);
140-
}
141-
142166
if (options && options.flushTimeout && options.flushTimeout > 0) {
143167
// eslint-disable-next-line @typescript-eslint/unbound-method
144168
const _end = res.end;
@@ -161,7 +185,12 @@ export function requestHandler(
161185
const currentHub = getCurrentHub();
162186

163187
currentHub.configureScope(scope => {
164-
scope.addEventProcessor(backwardsCompatibleEventProcessor);
188+
scope.setSDKProcessingMetadata({
189+
request: req,
190+
// TODO (v8): Stop passing this
191+
requestDataOptionsFromExpressHandler: requestDataOptions,
192+
});
193+
165194
const client = currentHub.getClient<NodeClient>();
166195
if (isAutoSessionTrackingEnabled(client)) {
167196
const scope = currentHub.getScope();
@@ -240,6 +269,11 @@ export function errorHandler(options?: {
240269

241270
if (shouldHandleError(error)) {
242271
withScope(_scope => {
272+
// The request should already have been stored in `scope.sdkProcessingMetadata` by `sentryRequestMiddleware`,
273+
// but on the off chance someone is using `sentryErrorMiddleware` without `sentryRequestMiddleware`, it doesn't
274+
// hurt to be sure
275+
_scope.setSDKProcessingMetadata({ request: _req });
276+
243277
// For some reason we need to set the transaction on the scope again
244278
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
245279
const transaction = (res as any).__sentry_transaction as Span;

packages/node/src/integrations/requestdata.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,22 @@ export class RequestData implements Integration {
108108
addGlobalEventProcessor(event => {
109109
const hub = getCurrentHub();
110110
const self = hub.getIntegration(RequestData);
111-
const req = event.sdkProcessingMetadata && event.sdkProcessingMetadata.request;
111+
112+
const { sdkProcessingMetadata = {} } = event;
113+
const req = sdkProcessingMetadata.request;
112114

113115
// If the globally installed instance of this integration isn't associated with the current hub, `self` will be
114116
// undefined
115117
if (!self || !req) {
116118
return event;
117119
}
118120

119-
const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(this._options);
121+
// The Express request handler takes a similar `include` option to that which can be passed to this integration.
122+
// If passed there, we store it in `sdkProcessingMetadata`. TODO(v8): Force express people to use this
123+
// integration, so that all of this passing and conversion isn't necessary
124+
const addRequestDataOptions =
125+
sdkProcessingMetadata.requestDataOptionsFromExpressHandler ||
126+
convertReqDataIntegrationOptsToAddReqDataOpts(this._options);
120127

121128
const processedEvent = this._addRequestData(event, req, addRequestDataOptions);
122129

packages/node/test/handlers.test.ts

+49-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as sentryCore from '@sentry/core';
2-
import { Hub, Scope } from '@sentry/core';
2+
import { Hub, makeMain, Scope } from '@sentry/core';
33
import { Transaction } from '@sentry/tracing';
44
import { Event } from '@sentry/types';
55
import { SentryError } from '@sentry/utils';
@@ -136,6 +136,22 @@ describe('requestHandler', () => {
136136
done();
137137
});
138138
});
139+
140+
it('stores request and request data options in `sdkProcessingMetadata`', () => {
141+
const hub = new Hub(new NodeClient(getDefaultNodeClientOptions()));
142+
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
143+
144+
const requestHandlerOptions = { include: { ip: false } };
145+
const sentryRequestMiddleware = requestHandler(requestHandlerOptions);
146+
147+
sentryRequestMiddleware(req, res, next);
148+
149+
const scope = sentryCore.getCurrentHub().getScope();
150+
expect((scope as any)._sdkProcessingMetadata).toEqual({
151+
request: req,
152+
requestDataOptionsFromExpressHandler: requestHandlerOptions,
153+
});
154+
});
139155
});
140156

141157
describe('tracingHandler', () => {
@@ -392,6 +408,19 @@ describe('tracingHandler', () => {
392408
done();
393409
});
394410
});
411+
412+
it('stores request in transaction metadata', () => {
413+
const options = getDefaultNodeClientOptions({ tracesSampleRate: 1.0 });
414+
const hub = new Hub(new NodeClient(options));
415+
416+
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
417+
418+
sentryTracingMiddleware(req, res, next);
419+
420+
const transaction = sentryCore.getCurrentHub().getScope()?.getTransaction();
421+
422+
expect(transaction?.metadata.request).toEqual(req);
423+
});
395424
});
396425

397426
describe('errorHandler()', () => {
@@ -498,4 +527,23 @@ describe('errorHandler()', () => {
498527
const requestSession = scope?.getRequestSession();
499528
expect(requestSession).toEqual(undefined);
500529
});
530+
531+
it('stores request in `sdkProcessingMetadata`', () => {
532+
const options = getDefaultNodeClientOptions({});
533+
client = new NodeClient(options);
534+
535+
const hub = new Hub(client);
536+
makeMain(hub);
537+
538+
// `sentryErrorMiddleware` uses `withScope`, and we need access to the temporary scope it creates, so monkeypatch
539+
// `captureException` in order to examine the scope as it exists inside the `withScope` callback
540+
hub.captureException = function (this: Hub, _exception: any) {
541+
const scope = this.getScope();
542+
expect((scope as any)._sdkProcessingMetadata.request).toEqual(req);
543+
} as any;
544+
545+
sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, next);
546+
547+
expect.assertions(1);
548+
});
501549
});

packages/node/test/integrations/requestdata.test.ts

+21
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Event, EventProcessor } from '@sentry/types';
33
import * as http from 'http';
44

55
import { NodeClient } from '../../src/client';
6+
import { requestHandler } from '../../src/handlers';
67
import { RequestData, RequestDataIntegrationOptions } from '../../src/integrations/requestdata';
78
import * as requestDataModule from '../../src/requestdata';
89
import { getDefaultNodeClientOptions } from '../helper/node-client-options';
@@ -100,4 +101,24 @@ describe('`RequestData` integration', () => {
100101
expect(passedOptions?.include?.user).not.toEqual(expect.arrayContaining(['email']));
101102
});
102103
});
104+
105+
describe('usage with express request handler', () => {
106+
it('uses options from request handler', async () => {
107+
const sentryRequestMiddleware = requestHandler({ include: { transaction: 'methodPath' } });
108+
const res = new http.ServerResponse(req);
109+
const next = jest.fn();
110+
111+
initWithRequestDataIntegrationOptions({ transactionNamingScheme: 'path' });
112+
113+
sentryRequestMiddleware(req, res, next);
114+
115+
await getCurrentHub().getScope()!.applyToEvent(event, {});
116+
requestDataEventProcessor(event);
117+
118+
const passedOptions = addRequestDataToEventSpy.mock.calls[0][2];
119+
120+
// `transaction` matches the request middleware's option, not the integration's option
121+
expect(passedOptions?.include).toEqual(expect.objectContaining({ transaction: 'methodPath' }));
122+
});
123+
});
103124
});

packages/types/src/transaction.ts

+6
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ export interface TransactionMetadata {
149149
/** For transactions tracing server-side request handling, the request being tracked. */
150150
request?: PolymorphicRequest;
151151

152+
/** Compatibility shim for transitioning to the `RequestData` integration. The options passed to our Express request
153+
* handler controlling what request data is added to the event.
154+
* TODO (v8): This should go away
155+
*/
156+
requestDataOptionsFromExpressHandler?: { [key: string]: unknown };
157+
152158
/** For transactions tracing server-side request handling, the path of the request being tracked. */
153159
/** TODO: If we rm -rf `instrumentServer`, this can go, too */
154160
requestPath?: string;

0 commit comments

Comments
 (0)