From ecd341ad65dd306ee495860a1b3db1d51d600417 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 11 Sep 2026 14:11:29 +0200 Subject: [PATCH] feat(v10/core): Accept `CollectBehavior` shorthand for `dataCollection.httpHeaders` Backport of: #24336 --- CHANGELOG.md | 2 + packages/core/src/types/datacollection.ts | 21 +++++--- .../resolveDataCollectionOptions.ts | 34 ++++++++++-- .../resolveDataCollectionOptions.test.ts | 53 +++++++++++++++++++ 4 files changed, 98 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff8882cabf3..5e2233aa18a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +- feat(core): Accept a `CollectBehavior` shorthand for `dataCollection.httpHeaders` ([#24336](https://github.com/getsentry/sentry-javascript/pull/24336)). Passing `true`, `false`, `{ allow: [...] }` or `{ deny: [...] }` now applies to both request and response headers; `{ request, response }` still controls each direction independently. + ## 10.74.0 - feat(v10): Streamline isolation scope handling & reset in isolation scopes ([#24152](https://github.com/getsentry/sentry-javascript/pull/24152)) diff --git a/packages/core/src/types/datacollection.ts b/packages/core/src/types/datacollection.ts index 8d87b07f9577..379266ba9cac 100644 --- a/packages/core/src/types/datacollection.ts +++ b/packages/core/src/types/datacollection.ts @@ -10,6 +10,14 @@ export type CollectBehavior = boolean | { allow: string[] } | { deny: string[] } export type HttpBodyCollectionTarget = 'incomingRequest' | 'outgoingRequest' | 'incomingResponse' | 'outgoingResponse'; +/** + * Controls HTTP header collection per direction. + */ +export interface HttpHeadersCollection { + request?: CollectBehavior; + response?: CollectBehavior; +} + /** * Controls what data the SDK collects and sends to Sentry. * @@ -30,12 +38,11 @@ export interface DataCollection { /** * Controls HTTP header collection for requests and responses. + * + * Accepts a `CollectBehavior` applied to both directions, or `{ request, response }` to control each independently. * @default { request: true, response: true } */ - httpHeaders?: { - request?: CollectBehavior; - response?: CollectBehavior; - }; + httpHeaders?: CollectBehavior | HttpHeadersCollection; /** * Which HTTP body types to collect. An omitted value collects all body types valid for the @@ -112,9 +119,9 @@ export interface DataCollection { /** * Fully resolved `DataCollection` with all defaults applied. */ -// todo(v11): change `Omit` to just `DataCollection` -export type ResolvedDataCollection = Required> & { - httpHeaders: Required>; +// todo(v11): change `Omit` to `Omit` +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 4c558737903e..ffcfa70a7f96 100644 --- a/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts +++ b/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts @@ -1,4 +1,9 @@ -import type { DataCollection, ResolvedDataCollection } from '../../types/datacollection'; +import type { + CollectBehavior, + DataCollection, + HttpHeadersCollection, + ResolvedDataCollection, +} from '../../types/datacollection'; import { defaultPiiToCollectionOptions } from './defaultPiiToCollectionOptions'; const DEFAULTS: ResolvedDataCollection = { @@ -14,6 +19,28 @@ const DEFAULTS: ResolvedDataCollection = { frameContextLines: 5, }; +function isCollectBehavior(value: CollectBehavior | HttpHeadersCollection): value is CollectBehavior { + return typeof value === 'boolean' || 'allow' in value || 'deny' in value; +} + +function resolveHttpHeaders( + httpHeaders: DataCollection['httpHeaders'], + base: ResolvedDataCollection, +): ResolvedDataCollection['httpHeaders'] { + if (httpHeaders === undefined) { + return { ...base.httpHeaders }; + } + + if (isCollectBehavior(httpHeaders)) { + return { request: httpHeaders, response: httpHeaders }; + } + + return { + request: httpHeaders.request ?? base.httpHeaders.request, + response: httpHeaders.response ?? base.httpHeaders.response, + }; +} + /** * Resolves the effective `DataCollection` configuration from client options. * @@ -39,10 +66,7 @@ export function resolveDataCollectionOptions(options: { return { userInfo: dc.userInfo ?? base.userInfo, cookies: dc.cookies ?? base.cookies, - httpHeaders: { - request: dc.httpHeaders?.request ?? base.httpHeaders.request, - response: dc.httpHeaders?.response ?? base.httpHeaders.response, - }, + httpHeaders: resolveHttpHeaders(dc.httpHeaders, base), httpBodies: dc.httpBodies ?? base.httpBodies, // oxlint-disable-next-line typescript/no-deprecated urlQueryParams: dc.urlQueryParams ?? dc.queryParams ?? base.urlQueryParams, 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 8d6ec21a0ab6..d12515be1c2d 100644 --- a/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts +++ b/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts @@ -115,6 +115,59 @@ describe('resolveDataCollectionOptions', () => { expect(result.httpHeaders.response).toBe(true); }); + it('merges nested httpHeaders partially for the response direction', () => { + const result = resolveDataCollectionOptions({ + dataCollection: { + httpHeaders: { response: { allow: ['content-type'] } }, + }, + }); + + expect(result.httpHeaders).toEqual({ request: true, response: { allow: ['content-type'] } }); + }); + + it('resolves independent request and response header settings', () => { + const result = resolveDataCollectionOptions({ + dataCollection: { + httpHeaders: { request: { allow: ['x-request-id'] }, response: false }, + }, + }); + + expect(result.httpHeaders).toEqual({ request: { allow: ['x-request-id'] }, response: false }); + }); + + it('treats an empty httpHeaders object as directional config with defaults', () => { + expect(resolveDataCollectionOptions({ dataCollection: { httpHeaders: {} } }).httpHeaders).toEqual({ + request: true, + response: true, + }); + }); + + it('applies boolean httpHeaders shorthand to both directions', () => { + expect(resolveDataCollectionOptions({ dataCollection: { httpHeaders: false } }).httpHeaders).toEqual({ + request: false, + response: false, + }); + + expect(resolveDataCollectionOptions({ dataCollection: { httpHeaders: true } }).httpHeaders).toEqual({ + request: true, + response: true, + }); + }); + + it('applies allow/deny httpHeaders shorthand to both directions', () => { + const deny = { deny: ['forwarded', '-ip'] }; + expect(resolveDataCollectionOptions({ dataCollection: { httpHeaders: deny } }).httpHeaders).toEqual({ + request: deny, + response: deny, + }); + + const allow = { allow: ['content-type'] }; + expect(resolveDataCollectionOptions({ dataCollection: { httpHeaders: allow } }).httpHeaders).toEqual({ + request: allow, + response: allow, + }); + }); + it('merges nested genAI partially', () => { const result = resolveDataCollectionOptions({ dataCollection: {