Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/core/src/integrations/requestdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { parseCookie } from '../utils/cookie';
import { SENSITIVE_COOKIE_NAME_SNIPPETS } from '../utils/data-collection/filtering-snippets';
import { filterKeyValueData } from '../utils/data-collection/filterKeyValueData';
import { filterQueryParams } from '../utils/data-collection/filterQueryParams';
import { filterUrlQuery } from '../utils/data-collection/filterUrlQuery';
Comment thread
sentry-warden[bot] marked this conversation as resolved.
import { httpHeadersToSpanAttributes } from '../utils/request';
import { getUrlQuery } from '../utils/url';
import { getClientIPAddress, ipHeaderNames } from '../vendor/getIpAddress';
Expand Down Expand Up @@ -138,6 +139,9 @@ function addNormalizedRequestDataToEvent(
if (requestData.query_string) {
requestData.query_string = normalizeAndFilterQueryString(requestData.query_string, dataCollection.urlQueryParams);
}
if (requestData.url) {
requestData.url = filterUrlQuery(requestData.url, dataCollection.urlQueryParams);
}

event.request = {
...event.request,
Expand Down Expand Up @@ -166,7 +170,7 @@ function addNormalizedRequestDataToSpan(
const attributes: Record<string, unknown> = {};

if (requestData.url) {
attributes[URL_FULL] = requestData.url;
attributes[URL_FULL] = filterUrlQuery(requestData.url, dataCollection.urlQueryParams);
}

if (requestData.method) {
Expand Down
54 changes: 54 additions & 0 deletions packages/core/test/lib/integrations/requestdata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,42 @@ function richNormalizedRequest() {
}

describe('requestDataIntegration', () => {
// `event.request.url` carries the same query string as `request.query_string`, so it has to respect
// `dataCollection.urlQueryParams` too.
describe('event.request.url query params', () => {
function processWith(urlQueryParams?: DataCollection['urlQueryParams']): Event {
const integration = requestDataIntegration();
const event = baseEvent({
sdkProcessingMetadata: {
normalizedRequest: {
method: 'GET',
url: 'https://example.com/reset?token=secret&id=1',
query_string: 'token=secret&id=1',
},
},
});

integration.processEvent?.(event, {}, mockClient({ userInfo: false, urlQueryParams }));

return event;
}

it('filters sensitive params by default', () => {
expect(processWith().request?.url).toBe('https://example.com/reset?token=[Filtered]&id=1');
});

it('strips the query entirely when collection is off', () => {
const event = processWith(false);

expect(event.request?.url).toBe('https://example.com/reset');
expect(event.request?.query_string).toBeUndefined();
});

it('honors allowList mode', () => {
expect(processWith({ allow: ['id'] }).request?.url).toBe('https://example.com/reset?token=[Filtered]&id=1');
});
});

describe('IP-related headers on event.request', () => {
it('removes known IP headers from event.request.headers when userInfo is false', () => {
const integration = requestDataIntegration();
Expand Down Expand Up @@ -974,6 +1010,24 @@ describe('requestDataIntegration processSegmentSpan', () => {
});
});

it('filters sensitive query params in `url.full` on the segment span', () => {
const integration = requestDataIntegration();
const span = makeSpan();

mockIsolationScope({
url: 'https://example.com/api/users?token=secret&page=1',
method: 'GET',
query_string: 'token=secret&page=1',
});

integration.processSegmentSpan!(span, mockClient({ userInfo: false }));

expect(span.attributes).toMatchObject({
'url.full': 'https://example.com/api/users?token=[Filtered]&page=1',
'url.query': 'token=[Filtered]&page=1',
});
});

it('handles query_string in object format', () => {
const integration = requestDataIntegration();
const span = makeSpan();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export function createSentryClientInstrumentation(
navigationSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
}
captureInstrumentationError(result, captureErrors, 'react_router.navigate', {
[URL_FULL]: toPath,
[URL_FULL]: filterCollectedUrl(toPath),
Comment thread
chargome marked this conversation as resolved.
});
}
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,34 @@ describe('createSentryClientInstrumentation', () => {
);
});

// `navigate('/x?token=y')` is app-supplied, so the query has to go through `dataCollection.urlQueryParams`.
it('filters sensitive query params in the `url.full` reported for a failed navigate', async () => {
const mockError = new Error('Navigate failed');
const mockCallNavigate = vi.fn().mockResolvedValue({ status: 'error', error: mockError });
const mockInstrument = vi.fn();

(core.getClient as any).mockReturnValue({});
(globalThis as any).location = {
href: 'https://example.com/home',
origin: 'https://example.com',
pathname: '/home',
};

const instrumentation = createSentryClientInstrumentation();
instrumentation.router?.({ instrument: mockInstrument });
const hooks = mockInstrument.mock.calls[0]![0];

await hooks.navigate(mockCallNavigate, { currentUrl: '/home', to: '/search?token=secret&page=1' });

expect(core.captureException).toHaveBeenCalledWith(mockError, {
mechanism: {
type: 'react_router.navigate',
handled: false,
data: { 'url.full': '/search?token=[Filtered]&page=1' },
},
});
});

it('should capture errors when captureErrors is true (default)', async () => {
const mockError = new Error('Test error');
// React Router returns an error result, not a rejection
Expand Down
Loading