Skip to content

Commit c1d9c66

Browse files
authored
ref(node): Small RequestData integration tweaks (#5979)
This makes a few small revisions to the new `RequestData` integration: - Switch to using booleans rather than an array of keys when specifying what user data to include. This makes it match all of the other options, and is something I should have just done from the get-go. Given that the integration is new and thus far entirely undocumented, IMHO it feels safe to make what is technically a breaking change here. - Rename the integration's internal `RequestDataOptions` type to `RequestDataIntegrationOptions`, to help distinguish it from the many other flavors of request data functions and types floating around. - Make all properties in `RequestDataIntegrationOptions` optional, rather than using the `Partial` helper type. - Switch the callback which actually does the data adding from being an option to being a protected property, in order to make it less public but still leave open the option of subclassing and setting it to a different value if we ever get around to using this in browser-based SDKs. Because I also made the property's type slightly more generic and used an index signature to do it, I also had to switch `AddRequestDataToEventOptions` from being an interface to being a type. See microsoft/TypeScript#15300. - Rename the helper function which formats the `include` option for use in `addRequestDataToEvent` to more specifically indicate that it's converting from integration-style options to `addRequestDataToEvent`-style options. - Refactor the aforementioned helper function to act upon and return an entire options object rather than just the `include` property, in order to have access to the `transactionNamingScheme` option. - Add missing `transaction` property in helper function's output. - Add tests for the helper function.
1 parent 95f6030 commit c1d9c66

File tree

3 files changed

+173
-35
lines changed

3 files changed

+173
-35
lines changed

packages/node/src/integrations/requestdata.ts

+68-33
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,48 @@
11
// TODO (v8 or v9): Whenever this becomes a default integration for `@sentry/browser`, move this to `@sentry/core`. For
22
// now, we leave it in `@sentry/integrations` so that it doesn't contribute bytes to our CDN bundles.
33

4-
import { EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
4+
import { Event, EventProcessor, Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/types';
55
import { extractPathForTransaction } from '@sentry/utils';
66

7-
import {
8-
addRequestDataToEvent,
9-
AddRequestDataToEventOptions,
10-
DEFAULT_USER_INCLUDES,
11-
TransactionNamingScheme,
12-
} from '../requestdata';
7+
import { addRequestDataToEvent, AddRequestDataToEventOptions, TransactionNamingScheme } from '../requestdata';
138

14-
type RequestDataOptions = {
9+
export type RequestDataIntegrationOptions = {
1510
/**
1611
* Controls what data is pulled from the request and added to the event
1712
*/
18-
include: {
13+
include?: {
1914
cookies?: boolean;
2015
data?: boolean;
2116
headers?: boolean;
2217
ip?: boolean;
2318
query_string?: boolean;
2419
url?: boolean;
25-
user?: boolean | Array<typeof DEFAULT_USER_INCLUDES[number]>;
20+
user?:
21+
| boolean
22+
| {
23+
id?: boolean;
24+
username?: boolean;
25+
email?: boolean;
26+
};
2627
};
2728

2829
/** Whether to identify transactions by parameterized path, parameterized path with method, or handler name */
29-
transactionNamingScheme: TransactionNamingScheme;
30-
31-
/**
32-
* Function for adding request data to event. Defaults to `addRequestDataToEvent` from `@sentry/node` for now, but
33-
* left injectable so this integration can be moved to `@sentry/core` and used in browser-based SDKs in the future.
34-
*
35-
* @hidden
36-
*/
37-
addRequestData: typeof addRequestDataToEvent;
30+
transactionNamingScheme?: TransactionNamingScheme;
3831
};
3932

4033
const DEFAULT_OPTIONS = {
41-
addRequestData: addRequestDataToEvent,
4234
include: {
4335
cookies: true,
4436
data: true,
4537
headers: true,
4638
ip: false,
4739
query_string: true,
4840
url: true,
49-
user: DEFAULT_USER_INCLUDES,
41+
user: {
42+
id: true,
43+
username: true,
44+
email: true,
45+
},
5046
},
5147
transactionNamingScheme: 'methodpath',
5248
};
@@ -64,12 +60,20 @@ export class RequestData implements Integration {
6460
*/
6561
public name: string = RequestData.id;
6662

67-
private _options: RequestDataOptions;
63+
/**
64+
* Function for adding request data to event. Defaults to `addRequestDataToEvent` from `@sentry/node` for now, but
65+
* left as a property so this integration can be moved to `@sentry/core` as a base class in case we decide to use
66+
* something similar in browser-based SDKs in the future.
67+
*/
68+
protected _addRequestData: (event: Event, req: PolymorphicRequest, options?: { [key: string]: unknown }) => Event;
69+
70+
private _options: Required<RequestDataIntegrationOptions>;
6871

6972
/**
7073
* @inheritDoc
7174
*/
72-
public constructor(options: Partial<RequestDataOptions> = {}) {
75+
public constructor(options: RequestDataIntegrationOptions = {}) {
76+
this._addRequestData = addRequestDataToEvent;
7377
this._options = {
7478
...DEFAULT_OPTIONS,
7579
...options,
@@ -79,6 +83,14 @@ export class RequestData implements Integration {
7983
method: true,
8084
...DEFAULT_OPTIONS.include,
8185
...options.include,
86+
user:
87+
options.include && typeof options.include.user === 'boolean'
88+
? options.include.user
89+
: {
90+
...DEFAULT_OPTIONS.include.user,
91+
// Unclear why TS still thinks `options.include.user` could be a boolean at this point
92+
...((options.include || {}).user as Record<string, boolean>),
93+
},
8294
},
8395
};
8496
}
@@ -91,7 +103,7 @@ export class RequestData implements Integration {
91103
// the moment it lives here, though, until https://github.com/getsentry/sentry-javascript/issues/5718 is addressed.
92104
// (TL;DR: Those functions touch many parts of the repo in many different ways, and need to be clened up. Once
93105
// that's happened, it will be easier to add this logic in without worrying about unexpected side effects.)
94-
const { include, addRequestData, transactionNamingScheme } = this._options;
106+
const { transactionNamingScheme } = this._options;
95107

96108
addGlobalEventProcessor(event => {
97109
const hub = getCurrentHub();
@@ -104,7 +116,9 @@ export class RequestData implements Integration {
104116
return event;
105117
}
106118

107-
const processedEvent = addRequestData(event, req, { include: formatIncludeOption(include) });
119+
const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(this._options);
120+
121+
const processedEvent = this._addRequestData(event, req, addRequestDataOptions);
108122

109123
// Transaction events already have the right `transaction` value
110124
if (event.type === 'transaction' || transactionNamingScheme === 'handler') {
@@ -138,12 +152,15 @@ export class RequestData implements Integration {
138152
}
139153
}
140154

141-
/** Convert `include` option to match what `addRequestDataToEvent` expects */
155+
/** Convert this integration's options to match what `addRequestDataToEvent` expects */
142156
/** TODO: Can possibly be deleted once https://github.com/getsentry/sentry-javascript/issues/5718 is fixed */
143-
function formatIncludeOption(
144-
integrationInclude: RequestDataOptions['include'] = {},
145-
): AddRequestDataToEventOptions['include'] {
146-
const { ip, user, ...requestOptions } = integrationInclude;
157+
function convertReqDataIntegrationOptsToAddReqDataOpts(
158+
integrationOptions: Required<RequestDataIntegrationOptions>,
159+
): AddRequestDataToEventOptions {
160+
const {
161+
transactionNamingScheme,
162+
include: { ip, user, ...requestOptions },
163+
} = integrationOptions;
147164

148165
const requestIncludeKeys: string[] = [];
149166
for (const [key, value] of Object.entries(requestOptions)) {
@@ -152,10 +169,28 @@ function formatIncludeOption(
152169
}
153170
}
154171

172+
let addReqDataUserOpt;
173+
if (user === undefined) {
174+
addReqDataUserOpt = true;
175+
} else if (typeof user === 'boolean') {
176+
addReqDataUserOpt = user;
177+
} else {
178+
const userIncludeKeys: string[] = [];
179+
for (const [key, value] of Object.entries(user)) {
180+
if (value) {
181+
userIncludeKeys.push(key);
182+
}
183+
}
184+
addReqDataUserOpt = userIncludeKeys;
185+
}
186+
155187
return {
156-
ip,
157-
user,
158-
request: requestIncludeKeys.length !== 0 ? requestIncludeKeys : undefined,
188+
include: {
189+
ip,
190+
user: addReqDataUserOpt,
191+
request: requestIncludeKeys.length !== 0 ? requestIncludeKeys : undefined,
192+
transaction: transactionNamingScheme,
193+
},
159194
};
160195
}
161196

packages/node/src/requestdata.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ export const DEFAULT_USER_INCLUDES = ['id', 'username', 'email'];
1515
/**
1616
* Options deciding what parts of the request to use when enhancing an event
1717
*/
18-
export interface AddRequestDataToEventOptions {
18+
export type AddRequestDataToEventOptions = {
1919
/** Flags controlling whether each type of data should be added to the event */
2020
include?: {
2121
ip?: boolean;
2222
request?: boolean | Array<typeof DEFAULT_REQUEST_INCLUDES[number]>;
2323
transaction?: boolean | TransactionNamingScheme;
2424
user?: boolean | Array<typeof DEFAULT_USER_INCLUDES[number]>;
2525
};
26-
}
26+
};
2727

2828
export type TransactionNamingScheme = 'path' | 'methodPath' | 'handler';
2929

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { getCurrentHub, Hub, makeMain } from '@sentry/core';
2+
import { Event, EventProcessor } from '@sentry/types';
3+
import * as http from 'http';
4+
5+
import { NodeClient } from '../../src/client';
6+
import { RequestData, RequestDataIntegrationOptions } from '../../src/integrations/requestdata';
7+
import * as requestDataModule from '../../src/requestdata';
8+
import { getDefaultNodeClientOptions } from '../helper/node-client-options';
9+
10+
const addRequestDataToEventSpy = jest.spyOn(requestDataModule, 'addRequestDataToEvent');
11+
const requestDataEventProcessor = jest.fn();
12+
13+
const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' };
14+
const method = 'wagging';
15+
const protocol = 'mutualsniffing';
16+
const hostname = 'the.dog.park';
17+
const path = '/by/the/trees/';
18+
const queryString = 'chase=me&please=thankyou';
19+
20+
function initWithRequestDataIntegrationOptions(integrationOptions: RequestDataIntegrationOptions): void {
21+
const setMockEventProcessor = (eventProcessor: EventProcessor) =>
22+
requestDataEventProcessor.mockImplementationOnce(eventProcessor);
23+
24+
const requestDataIntegration = new RequestData({
25+
...integrationOptions,
26+
});
27+
28+
const client = new NodeClient(
29+
getDefaultNodeClientOptions({
30+
dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012',
31+
integrations: [requestDataIntegration],
32+
}),
33+
);
34+
client.setupIntegrations = () => requestDataIntegration.setupOnce(setMockEventProcessor, getCurrentHub);
35+
client.getIntegration = () => requestDataIntegration as any;
36+
37+
const hub = new Hub(client);
38+
39+
makeMain(hub);
40+
}
41+
42+
describe('`RequestData` integration', () => {
43+
let req: http.IncomingMessage, event: Event;
44+
45+
beforeEach(() => {
46+
req = {
47+
headers,
48+
method,
49+
protocol,
50+
hostname,
51+
originalUrl: `${path}?${queryString}`,
52+
} as unknown as http.IncomingMessage;
53+
event = { sdkProcessingMetadata: { request: req } };
54+
});
55+
56+
afterEach(() => {
57+
jest.clearAllMocks();
58+
});
59+
60+
describe('option conversion', () => {
61+
it('leaves `ip` and `user` at top level of `include`', () => {
62+
initWithRequestDataIntegrationOptions({ include: { ip: false, user: true } });
63+
64+
requestDataEventProcessor(event);
65+
66+
const passedOptions = addRequestDataToEventSpy.mock.calls[0][2];
67+
68+
expect(passedOptions?.include).toEqual(expect.objectContaining({ ip: false, user: true }));
69+
});
70+
71+
it('moves `transactionNamingScheme` to `transaction` include', () => {
72+
initWithRequestDataIntegrationOptions({ transactionNamingScheme: 'path' });
73+
74+
requestDataEventProcessor(event);
75+
76+
const passedOptions = addRequestDataToEventSpy.mock.calls[0][2];
77+
78+
expect(passedOptions?.include).toEqual(expect.objectContaining({ transaction: 'path' }));
79+
});
80+
81+
it('moves `true` request keys into `request` include, but omits `false` ones', async () => {
82+
initWithRequestDataIntegrationOptions({ include: { data: true, cookies: false } });
83+
84+
requestDataEventProcessor(event);
85+
86+
const passedOptions = addRequestDataToEventSpy.mock.calls[0][2];
87+
88+
expect(passedOptions?.include?.request).toEqual(expect.arrayContaining(['data']));
89+
expect(passedOptions?.include?.request).not.toEqual(expect.arrayContaining(['cookies']));
90+
});
91+
92+
it('moves `true` user keys into `user` include, but omits `false` ones', async () => {
93+
initWithRequestDataIntegrationOptions({ include: { user: { id: true, email: false } } });
94+
95+
requestDataEventProcessor(event);
96+
97+
const passedOptions = addRequestDataToEventSpy.mock.calls[0][2];
98+
99+
expect(passedOptions?.include?.user).toEqual(expect.arrayContaining(['id']));
100+
expect(passedOptions?.include?.user).not.toEqual(expect.arrayContaining(['email']));
101+
});
102+
});
103+
});

0 commit comments

Comments
 (0)