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
11 changes: 10 additions & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,17 @@ Sentry.init({
Each key-value field (`cookies`, `urlQueryParams`, `httpHeaders.request`, `httpHeaders.response`) accepts
`true`, `false`, `{ allow: string[] }`, or `{ deny: string[] }` for fine-grained control.

#### RequestData

The `requestDataIntegration`'s `include` options remain an integration-level override. An explicit `false`
prevents that category from being attached, while an explicit `true` enables it even when the corresponding
`dataCollection` category is disabled. For cookies, headers, and query parameters, any configured `allow` or
`deny` filtering continues to apply: When `include` enables a category which `dataCollection` disabled, the
default sensitive-value denylist is applied.

User IP address inference, which was previously gated on `sendDefaultPii`, is now controlled by
`dataCollection.userInfo`.
`dataCollection.userInfo`. An explicit `requestDataIntegration({ include: { ip: true } })` overrides
`dataCollection.userInfo: false` for data collected by that integration.

### Browser sessions use `unhandled` instead of `crashed`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,4 @@ Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [httpClientIntegration()],
tracesSampleRate: 1,
// todo(v11): remove together with the sendDefaultPii guard in httpclient.ts (JS-2580)
sendDefaultPii: true,
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,48 @@ import type { Event } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';

sentryTest(
'should filter sensitive header and cookie values with sendDefaultPii',
async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('**/foo', route => {
return route.fulfill({
status: 500,
body: JSON.stringify({
error: {
message: 'Internal Server Error',
},
}),
headers: {
'Content-Type': 'text/html',
'X-Auth-Token': 'secret-response-token',
'X-Request-Id': 'abc-123',
sentryTest('filters sensitive header and cookie values by default', async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('**/foo', route => {
return route.fulfill({
status: 500,
body: JSON.stringify({
error: {
message: 'Internal Server Error',
},
});
}),
headers: {
'Content-Type': 'text/html',
'X-Auth-Token': 'secret-response-token',
'X-Request-Id': 'abc-123',
},
});
});

const req = await Promise.all([waitForErrorRequest(page), page.goto(url)]).then(([r]) => r);
const eventData = envelopeRequestParser<Event>(req);
const req = await Promise.all([waitForErrorRequest(page), page.goto(url)]).then(([r]) => r);
const eventData = envelopeRequestParser<Event>(req);

expect(eventData.exception?.values).toHaveLength(1);
expect(eventData.exception?.values).toHaveLength(1);

const reqHeaders = eventData.request?.headers || {};
const resHeaders = (eventData.contexts?.response?.headers as Record<string, string>) || {};
const reqHeaders = eventData.request?.headers || {};
const resHeaders = (eventData.contexts?.response?.headers as Record<string, string>) || {};

// Non-sensitive request headers should be present with their values
expect(reqHeaders['accept']).toBe('application/json');
expect(reqHeaders['content-type']).toBe('application/json');
expect(reqHeaders['x-custom-header']).toBe('safe-value');
// Non-sensitive request headers should be present with their values
expect(reqHeaders['accept']).toBe('application/json');
expect(reqHeaders['content-type']).toBe('application/json');
expect(reqHeaders['x-custom-header']).toBe('safe-value');

// Sensitive request headers should have their values filtered
// 'authorization' matches the 'auth' snippet
expect(reqHeaders['authorization']).toBe('[Filtered]');
// 'x-api-key' matches the 'key' snippet
expect(reqHeaders['x-api-key']).toBe('[Filtered]');
// Sensitive request headers should have their values filtered
// 'authorization' matches the 'auth' snippet
expect(reqHeaders['authorization']).toBe('[Filtered]');
// 'x-api-key' matches the 'key' snippet
expect(reqHeaders['x-api-key']).toBe('[Filtered]');

// Non-sensitive response headers should be present with their values
expect(resHeaders['x-request-id']).toBe('abc-123');
// Non-sensitive response headers should be present with their values
expect(resHeaders['x-request-id']).toBe('abc-123');

// Sensitive response headers should have their values filtered
// 'x-auth-token' matches 'auth' and 'token' snippets
expect(resHeaders['x-auth-token']).toBe('[Filtered]');
},
);
// Sensitive response headers should have their values filtered
// 'x-auth-token' matches 'auth' and 'token' snippets
expect(resHeaders['x-auth-token']).toBe('[Filtered]');
});

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,4 @@ Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [httpClientIntegration()],
tracesSampleRate: 1,
// todo(v11): remove together with the sendDefaultPii guard in httpclient.ts (JS-2580)
sendDefaultPii: true,
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,21 @@ describe('express tracing with error', () => {
runner.makeRequest('get', '/test/123/abc?q=1');
await runner.completed();
});

test('preserves encoded query parameters while filtering sensitive values on events', async () => {
const runner = createRunner()
.ignore('transaction')
.expect({
event: {
request: {
query_string: 'q=hello%20world&token=[Filtered]',
},
},
})
.start();

await runner.makeRequest('get', '/test/123/abc?q=hello%20world&token=secret');
await runner.completed();
});
});
});
11 changes: 0 additions & 11 deletions packages/browser/src/integrations/httpclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,17 +427,6 @@ function _getDataCollectionSettings() {
return { cookies: false, requestHeaders: false, responseHeaders: false };
}

// todo(v11): Always use granular dataCollection settings and remove this legacy guard.
// Currently, when dataCollection is not explicitly set, we gate all collection on
// sendDefaultPii to avoid sending more data than before (the spec defaults would
// collect headers/cookies with deny-list filtering even without sendDefaultPii).
const options = client.getOptions();
if (options.dataCollection == null) {
// eslint-disable-next-line typescript/no-deprecated
const enabled = Boolean(options.sendDefaultPii);
return { cookies: enabled, requestHeaders: enabled, responseHeaders: enabled };
}

const { cookies, httpHeaders } = client.getDataCollectionOptions();
return { cookies, requestHeaders: httpHeaders.request, responseHeaders: httpHeaders.response };
}
27 changes: 15 additions & 12 deletions packages/browser/test/integrations/httpclient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,21 +142,24 @@ describe('httpClientIntegration', () => {
});
});

// TODO(v11): also collect safe headers by default (but no PII headers) to align with server behavior
it('does not collect headers or cookies without sendDefaultPii or dataCollection', () => {
it('collects safe headers and filters sensitive headers by default', () => {
const { fetchHandler, captureEventSpy } = setup();

triggerFetch(fetchHandler, {
requestHeaders: { Authorization: 'Bearer x', Accept: 'application/json' },
responseHeaders: { 'Content-Type': 'text/html' },
requestHeaders: { Authorization: 'Bearer x', Accept: 'application/json', Cookie: 'theme=dark; session=secret' },
responseHeaders: { 'Content-Type': 'text/html', 'Set-Cookie': 'locale=en; session=secret' },
});

expect(captureEventSpy).toHaveBeenCalledTimes(1);
const event = getEvent(captureEventSpy);
expect(event.request?.headers).toBeUndefined();
expect(event.request?.cookies).toBeUndefined();
expect(event.contexts?.response?.headers).toBeUndefined();
expect(event.contexts?.response?.cookies).toBeUndefined();
expect(event.request?.headers).toEqual({
accept: 'application/json',
authorization: '[Filtered]',
cookie: '[Filtered]',
});
expect(event.request?.cookies).toEqual({ theme: 'dark', session: '[Filtered]' });
expect(event.contexts?.response?.headers).toEqual({ 'content-type': 'text/html', 'set-cookie': '[Filtered]' });
expect(event.contexts?.response?.cookies).toEqual({ locale: 'en', session: '[Filtered]' });
});

it('filters PII headers when an explicit deny list is configured', () => {
Expand Down Expand Up @@ -252,7 +255,7 @@ describe('httpClientIntegration', () => {
});
});

it('does not collect response headers or cookies without sendDefaultPii or dataCollection', () => {
it('collects response headers and filters response cookies by default', () => {
const { xhrHandler, captureEventSpy } = setup();

triggerXhr(xhrHandler, {
Expand All @@ -263,9 +266,9 @@ describe('httpClientIntegration', () => {

expect(captureEventSpy).toHaveBeenCalledTimes(1);
const event = getEvent(captureEventSpy);
expect(event.request?.headers).toBeUndefined();
expect(event.contexts?.response?.headers).toBeUndefined();
expect(event.contexts?.response?.cookies).toBeUndefined();
expect(event.request?.headers).toEqual({ Authorization: '[Filtered]' });
expect(event.contexts?.response?.headers).toEqual({ 'content-type': 'text/html' });
expect(event.contexts?.response?.cookies).toEqual({ session: '[Filtered]', theme: 'dark' });
});
});
});
Loading
Loading