Skip to content
Open
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
19 changes: 7 additions & 12 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
14 changes: 12 additions & 2 deletions packages/core/src/types/datacollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -143,7 +153,7 @@ export interface DataCollection {
/**
* Fully resolved `DataCollection` with all defaults applied.
*/
export type ResolvedDataCollection = Required<Omit<DataCollection, 'httpHeaders'>> & {
export type ResolvedDataCollection = Required<Omit<DataCollection, 'httpHeaders' | 'defaults'>> & {
httpHeaders: Required<HttpHeadersCollection>;
graphQL: Required<NonNullable<DataCollection['graphQL']>>;
genAI: Required<NonNullable<DataCollection['genAI']>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,56 +20,75 @@ 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)) {
return { request: httpHeaders, response: httpHeaders };
}

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,
};
}

/**
* 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,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({});
Expand Down
Loading