Skip to content

Commit f8c5a6f

Browse files
committed
feat(pii) Sanitize URLs in Span descriptions and breadcrumbs
1 parent 1cf8988 commit f8c5a6f

File tree

3 files changed

+60
-6
lines changed

3 files changed

+60
-6
lines changed

packages/node/src/integrations/http.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,13 +192,28 @@ function _createWrappedRequestMethodFactory(
192192

193193
const scope = getCurrentHub().getScope();
194194

195+
const spanData: Record<string, string> = {
196+
url: requestUrl,
197+
method: requestOptions.method || 'GET',
198+
};
199+
if (requestOptions.hash) {
200+
// strip leading "#"
201+
spanData['http.fragment'] = requestOptions.hash.substring(1);
202+
}
203+
if (requestOptions.search) {
204+
// strip leading "?"
205+
spanData['http.query'] = requestOptions.search.substring(1);
206+
}
207+
208+
// TODO potential breaking change... shouldCreateSpanForRequest no longer has access to query + fragment, is that a problem?
195209
if (scope && tracingOptions && shouldCreateSpan(requestUrl)) {
196210
parentSpan = scope.getSpan();
197211

198212
if (parentSpan) {
199213
requestSpan = parentSpan.startChild({
200-
description: `${requestOptions.method || 'GET'} ${requestUrl}`,
214+
description: `${spanData.method} ${spanData.url}`,
201215
op: 'http.client',
216+
data: spanData,
202217
});
203218

204219
if (shouldAttachTraceData(requestUrl)) {
@@ -253,7 +268,7 @@ function _createWrappedRequestMethodFactory(
253268
// eslint-disable-next-line @typescript-eslint/no-this-alias
254269
const req = this;
255270
if (breadcrumbsEnabled) {
256-
addRequestBreadcrumb('response', requestUrl, req, res);
271+
addRequestBreadcrumb('response', spanData, req, res);
257272
}
258273
if (requestSpan) {
259274
if (res.statusCode) {
@@ -268,7 +283,7 @@ function _createWrappedRequestMethodFactory(
268283
const req = this;
269284

270285
if (breadcrumbsEnabled) {
271-
addRequestBreadcrumb('error', requestUrl, req);
286+
addRequestBreadcrumb('error', spanData, req);
272287
}
273288
if (requestSpan) {
274289
requestSpan.setHttpStatus(500);
@@ -283,7 +298,12 @@ function _createWrappedRequestMethodFactory(
283298
/**
284299
* Captures Breadcrumb based on provided request/response pair
285300
*/
286-
function addRequestBreadcrumb(event: string, url: string, req: http.ClientRequest, res?: http.IncomingMessage): void {
301+
function addRequestBreadcrumb(
302+
event: string,
303+
spanData: Record<string, string>,
304+
req: http.ClientRequest,
305+
res?: http.IncomingMessage,
306+
): void {
287307
if (!getCurrentHub().getIntegration(Http)) {
288308
return;
289309
}
@@ -294,7 +314,7 @@ function addRequestBreadcrumb(event: string, url: string, req: http.ClientReques
294314
data: {
295315
method: req.method,
296316
status_code: res && res.statusCode,
297-
url,
317+
...spanData,
298318
},
299319
type: 'http',
300320
},

packages/node/src/integrations/utils/http.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export function extractUrl(requestOptions: RequestOptions): string {
2727
// Don't log standard :80 (http) and :443 (https) ports to reduce the noise
2828
const port =
2929
!requestOptions.port || requestOptions.port === 80 || requestOptions.port === 443 ? '' : `:${requestOptions.port}`;
30-
const path = requestOptions.path ? requestOptions.path : '/';
30+
// do not include search or hash in span descriptions, per https://github.com/getsentry/develop/pull/773
31+
const path = requestOptions.pathname || '/';
3132

3233
return `${protocol}//${hostname}${port}${path}`;
3334
}

packages/node/test/integrations/http.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,39 @@ describe('tracing', () => {
197197
expect(loggerLogSpy).toBeCalledWith('HTTP Integration is skipped because of instrumenter configuration.');
198198
});
199199

200+
it('omits query and fragment from description and adds to span data instead', () => {
201+
nock('http://dogs.are.great').get('/spaniel?tail=wag&cute=true#learn-more').reply(200);
202+
203+
const transaction = createTransactionOnScope();
204+
const spans = (transaction as unknown as Span).spanRecorder?.spans as Span[];
205+
206+
http.get('http://dogs.are.great/spaniel?tail=wag&cute=true#learn-more');
207+
208+
expect(spans.length).toEqual(2);
209+
210+
// our span is at index 1 because the transaction itself is at index 0
211+
expect(spans[1].description).toEqual('GET http://dogs.are.great/spaniel');
212+
expect(spans[1].op).toEqual('http.client');
213+
expect(spans[1].data.method).toEqual('GET');
214+
expect(spans[1].data.url).toEqual('http://dogs.are.great/spaniel');
215+
expect(spans[1].data['http.query']).toEqual('tail=wag&cute=true');
216+
expect(spans[1].data['http.fragment']).toEqual('learn-more');
217+
});
218+
219+
it('omits username and password entirely from span', () => {
220+
nock('http://username:[email protected]').get('/').reply(200);
221+
222+
const transaction = createTransactionOnScope();
223+
const spans = (transaction as unknown as Span).spanRecorder?.spans as Span[];
224+
225+
http.get('http://username:[email protected]/');
226+
227+
expect(spans.length).toEqual(2);
228+
229+
// our span is at index 1 because the transaction itself is at index 0
230+
expect(spans[1].description).toEqual('GET http://dogs.are.great/');
231+
});
232+
200233
describe('Tracing options', () => {
201234
beforeEach(() => {
202235
// hacky way of restoring monkey patched functions

0 commit comments

Comments
 (0)