From 962995a16fe36fa4cc0dd686498ffae86e28bb68 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 7 Aug 2026 12:03:19 +0200 Subject: [PATCH 1/2] fix(core): Filter query params in `url.full` on request data and navigate errors Two sites set `url.full` from a raw URL, so query params reached Sentry even though `dataCollection.urlQueryParams` was configured to filter them. `requestDataIntegration` filtered `url.query` on the segment span but left `url.full` untouched, so the same span carried both the filtered and the unfiltered query string. On the react-router client, the `url.full` reported for a failed navigate came from the app's own navigate target, which can include a query. Co-Authored-By: Claude Opus 5 (1M context) --- packages/core/src/integrations/requestdata.ts | 3 +- .../test/lib/integrations/requestdata.test.ts | 18 ++++++++++++ .../src/client/createClientInstrumentation.ts | 2 +- .../createClientInstrumentation.test.ts | 28 +++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index 89aa5abe3411..e0e048cc8a08 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -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'; import { httpHeadersToSpanAttributes } from '../utils/request'; import { getUrlQuery } from '../utils/url'; import { getClientIPAddress, ipHeaderNames } from '../vendor/getIpAddress'; @@ -166,7 +167,7 @@ function addNormalizedRequestDataToSpan( const attributes: Record = {}; if (requestData.url) { - attributes[URL_FULL] = requestData.url; + attributes[URL_FULL] = filterUrlQuery(requestData.url, dataCollection.urlQueryParams); } if (requestData.method) { diff --git a/packages/core/test/lib/integrations/requestdata.test.ts b/packages/core/test/lib/integrations/requestdata.test.ts index 2810277fe90f..5e231cd1c7db 100644 --- a/packages/core/test/lib/integrations/requestdata.test.ts +++ b/packages/core/test/lib/integrations/requestdata.test.ts @@ -974,6 +974,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(); diff --git a/packages/react-router/src/client/createClientInstrumentation.ts b/packages/react-router/src/client/createClientInstrumentation.ts index b7edc7624651..635ddac3ec03 100644 --- a/packages/react-router/src/client/createClientInstrumentation.ts +++ b/packages/react-router/src/client/createClientInstrumentation.ts @@ -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), }); } return; diff --git a/packages/react-router/test/client/createClientInstrumentation.test.ts b/packages/react-router/test/client/createClientInstrumentation.test.ts index 284f66a72555..f74d180a1a63 100644 --- a/packages/react-router/test/client/createClientInstrumentation.test.ts +++ b/packages/react-router/test/client/createClientInstrumentation.test.ts @@ -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 From 716749ace6f130316df618de72b0781aeb79176e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 7 Aug 2026 14:38:34 +0200 Subject: [PATCH 2/2] fix(core): Filter query params in `event.request.url` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `addNormalizedRequestDataToEvent` filtered cookies, headers and `query_string` but spread the raw URL into `event.request.url`, so an error event carried the query string even with `dataCollection.urlQueryParams: false` — the same secret the SDK had just dropped from `query_string`. Co-Authored-By: Claude Opus 5 (1M context) --- packages/core/src/integrations/requestdata.ts | 3 ++ .../test/lib/integrations/requestdata.test.ts | 36 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index e0e048cc8a08..eda1603c809d 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -139,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, diff --git a/packages/core/test/lib/integrations/requestdata.test.ts b/packages/core/test/lib/integrations/requestdata.test.ts index 5e231cd1c7db..63282281abf8 100644 --- a/packages/core/test/lib/integrations/requestdata.test.ts +++ b/packages/core/test/lib/integrations/requestdata.test.ts @@ -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();