diff --git a/MIGRATION.md b/MIGRATION.md index 9785e86fd6a8..a109bea49753 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -209,28 +209,23 @@ Sentry.init({}); #### If you want to keep the v10 default behavior -Set the baseline explicitly. **Don't leave `dataCollection` unset** — that now enables broader collection. +Set `defaults: 'off'` and turn on only what v10 collected. **Don't leave `dataCollection` unset** — that now enables broader collection. ```js // v11 — preserves the v10 default Sentry.init({ dataCollection: { - userInfo: false, - cookies: false, - httpHeaders: { - request: { deny: ['forwarded', '-ip', 'remote-', 'via', '-user'] }, - response: { deny: ['forwarded', '-ip', 'remote-', 'via', '-user'] }, - }, - httpBodies: [], + defaults: 'off', + httpHeaders: { deny: ['forwarded', '-ip', 'remote-', 'via', '-user'] }, urlQueryParams: { deny: ['forwarded', '-ip', 'remote-', 'via', '-user'] }, - genAI: { inputs: false, outputs: false }, - databaseQueryData: false, - queues: false, - graphQL: { document: false, variables: false }, + stackFrameVariables: true, + frameContextLines: 7, }, }); ``` +With `defaults: 'off'`, every field you don't set collects nothing. This includes fields added in later minor releases, so upgrading never starts collecting new data on its own. + Each key-value field (`cookies`, `urlQueryParams`, `httpHeaders.request`, `httpHeaders.response`) accepts `true`, `false`, `{ allow: string[] }`, or `{ deny: string[] }` for fine-grained control. diff --git a/packages/core/src/types/datacollection.ts b/packages/core/src/types/datacollection.ts index fb19b0909b2a..800c54f43b18 100644 --- a/packages/core/src/types/datacollection.ts +++ b/packages/core/src/types/datacollection.ts @@ -21,9 +21,19 @@ export interface HttpHeadersCollection { /** * Controls what data the SDK collects and sends to Sentry. * - * All fields are optional. Omitted fields use the documented defaults. + * All fields are optional. Omitted fields use the documented defaults, or collect nothing when `defaults` is `'off'`. */ export interface DataCollection { + /** + * The value used for every field that is not set. + * + * - `'on'`: Unset fields use their documented defaults. + * - `'off'`: Unset fields collect nothing. This also applies to fields added in future releases, so + * only the data you turn on explicitly is collected. + * @default 'on' + */ + defaults?: 'on' | 'off'; + /** * Automatically populate `user.*` fields from instrumentation sources. * @default true @@ -143,7 +153,7 @@ export interface DataCollection { /** * Fully resolved `DataCollection` with all defaults applied. */ -export type ResolvedDataCollection = Required> & { +export type ResolvedDataCollection = Required> & { httpHeaders: Required; graphQL: Required>; genAI: Required>; diff --git a/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts b/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts index 44a5f69ba3bb..e5d23ee080db 100644 --- a/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts +++ b/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts @@ -20,13 +20,31 @@ const DEFAULTS: ResolvedDataCollection = { frameContextLines: 5, }; +const OFF: ResolvedDataCollection = { + userInfo: false, + cookies: false, + httpHeaders: { request: false, response: false }, + httpBodies: [], + urlQueryParams: false, + graphQL: { document: false, variables: false }, + genAI: { inputs: false, outputs: false }, + mcp: { inputs: false, outputs: false }, + databaseQueryData: false, + queues: false, + stackFrameVariables: false, + frameContextLines: 0, +}; + function isCollectBehavior(value: CollectBehavior | HttpHeadersCollection): value is CollectBehavior { return typeof value === 'boolean' || 'allow' in value || 'deny' in value; } -function resolveHttpHeaders(httpHeaders: DataCollection['httpHeaders']): ResolvedDataCollection['httpHeaders'] { +function resolveHttpHeaders( + httpHeaders: DataCollection['httpHeaders'], + base: ResolvedDataCollection, +): ResolvedDataCollection['httpHeaders'] { if (httpHeaders === undefined) { - return { ...DEFAULTS.httpHeaders }; + return { ...base.httpHeaders }; } if (isCollectBehavior(httpHeaders)) { @@ -34,8 +52,8 @@ function resolveHttpHeaders(httpHeaders: DataCollection['httpHeaders']): Resolve } return { - request: httpHeaders.request ?? DEFAULTS.httpHeaders.request, - response: httpHeaders.response ?? DEFAULTS.httpHeaders.response, + request: httpHeaders.request ?? base.httpHeaders.request, + response: httpHeaders.response ?? base.httpHeaders.response, }; } @@ -43,33 +61,34 @@ function resolveHttpHeaders(httpHeaders: DataCollection['httpHeaders']): Resolve * Resolves the effective `DataCollection` configuration from client options. * * Precedence: - * 1. Spec defaults + * 1. Spec defaults, or all collection turned off when `dataCollection.defaults` is `'off'` * 2. Fields explicitly set in `dataCollection` */ export function resolveDataCollectionOptions(options: { dataCollection?: DataCollection }): ResolvedDataCollection { const dc = options.dataCollection ?? {}; + const base = dc.defaults === 'off' ? OFF : DEFAULTS; return { - userInfo: dc.userInfo ?? DEFAULTS.userInfo, - cookies: dc.cookies ?? DEFAULTS.cookies, - httpHeaders: resolveHttpHeaders(dc.httpHeaders), - httpBodies: dc.httpBodies ?? DEFAULTS.httpBodies, - urlQueryParams: dc.urlQueryParams ?? DEFAULTS.urlQueryParams, + userInfo: dc.userInfo ?? base.userInfo, + cookies: dc.cookies ?? base.cookies, + httpHeaders: resolveHttpHeaders(dc.httpHeaders, base), + httpBodies: dc.httpBodies ?? base.httpBodies, + urlQueryParams: dc.urlQueryParams ?? base.urlQueryParams, graphQL: { - document: dc.graphQL?.document ?? DEFAULTS.graphQL.document, - variables: dc.graphQL?.variables ?? DEFAULTS.graphQL.variables, + document: dc.graphQL?.document ?? base.graphQL.document, + variables: dc.graphQL?.variables ?? base.graphQL.variables, }, genAI: { - inputs: dc.genAI?.inputs ?? DEFAULTS.genAI.inputs, - outputs: dc.genAI?.outputs ?? DEFAULTS.genAI.outputs, + inputs: dc.genAI?.inputs ?? base.genAI.inputs, + outputs: dc.genAI?.outputs ?? base.genAI.outputs, }, mcp: { - inputs: dc.mcp?.inputs ?? DEFAULTS.mcp.inputs, - outputs: dc.mcp?.outputs ?? DEFAULTS.mcp.outputs, + inputs: dc.mcp?.inputs ?? base.mcp.inputs, + outputs: dc.mcp?.outputs ?? base.mcp.outputs, }, - databaseQueryData: dc.databaseQueryData ?? DEFAULTS.databaseQueryData, - queues: dc.queues ?? DEFAULTS.queues, - stackFrameVariables: dc.stackFrameVariables ?? DEFAULTS.stackFrameVariables, - frameContextLines: dc.frameContextLines ?? DEFAULTS.frameContextLines, + databaseQueryData: dc.databaseQueryData ?? base.databaseQueryData, + queues: dc.queues ?? base.queues, + stackFrameVariables: dc.stackFrameVariables ?? base.stackFrameVariables, + frameContextLines: dc.frameContextLines ?? base.frameContextLines, }; } diff --git a/packages/core/test/lib/integrations/mcp-server/capturePolicy.test.ts b/packages/core/test/lib/integrations/mcp-server/capturePolicy.test.ts index 1489a6846283..8f2d1b630498 100644 --- a/packages/core/test/lib/integrations/mcp-server/capturePolicy.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/capturePolicy.test.ts @@ -448,6 +448,12 @@ describe('MCP Server Capture Policy', () => { { mcp: { inputs: true }, genAI: { inputs: false, outputs: false } }, { inputs: true, outputs: false }, ], + ["records nothing with defaults: 'off'", { defaults: 'off' }, { inputs: false, outputs: false }], + [ + "falls back to genAI with defaults: 'off'", + { defaults: 'off', genAI: { inputs: true } }, + { inputs: true, outputs: false }, + ], ])('%s', async (_, dataCollection, expected) => { const sessionId = `data-collection-${JSON.stringify(dataCollection)}`; const transport = await connectWrappedServer(sessionId); diff --git a/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts b/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts index 112d73d79461..0c9fe9f3a813 100644 --- a/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts +++ b/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts @@ -224,6 +224,56 @@ describe('resolveDataCollectionOptions', () => { }); }); + describe('defaults', () => { + const ALL_OFF = { + userInfo: false, + cookies: false, + httpHeaders: { request: false, response: false }, + httpBodies: [], + urlQueryParams: false, + graphQL: { document: false, variables: false }, + genAI: { inputs: false, outputs: false }, + mcp: { inputs: false, outputs: false }, + databaseQueryData: false, + queues: false, + stackFrameVariables: false, + frameContextLines: 0, + }; + + it('turns off every field that is not set', () => { + expect(resolveDataCollectionOptions({ dataCollection: { defaults: 'off' } })).toEqual(ALL_OFF); + }); + + it('keeps fields that are set explicitly', () => { + const result = resolveDataCollectionOptions({ + dataCollection: { + defaults: 'off', + genAI: { inputs: true }, + urlQueryParams: { deny: ['token'] }, + frameContextLines: 7, + }, + }); + + expect(result).toEqual({ + ...ALL_OFF, + genAI: { inputs: true, outputs: false }, + urlQueryParams: { deny: ['token'] }, + frameContextLines: 7, + }); + }); + + it('turns off the unset httpHeaders direction', () => { + expect( + resolveDataCollectionOptions({ dataCollection: { defaults: 'off', httpHeaders: { request: true } } }) + .httpHeaders, + ).toEqual({ request: true, response: false }); + }); + + it("uses spec defaults with defaults: 'on'", () => { + expect(resolveDataCollectionOptions({ dataCollection: { defaults: 'on' } })).toEqual(SPEC_DEFAULTS); + }); + }); + describe('return type completeness', () => { it('always returns all fields', () => { const result = resolveDataCollectionOptions({});