From 73bf3a279906386458900dc8b1f33bf0e3148189 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 18 May 2026 14:29:21 +0200 Subject: [PATCH 1/9] ref(http): Use shared snippets for filtering headers and cookies --- .../data-collection/filtering-snippets.ts | 62 ++++++++++++++++ packages/core/src/utils/request.ts | 70 ++----------------- 2 files changed, 69 insertions(+), 63 deletions(-) diff --git a/packages/core/src/utils/data-collection/filtering-snippets.ts b/packages/core/src/utils/data-collection/filtering-snippets.ts index 4abbe9361799..48a4bd0b5280 100644 --- a/packages/core/src/utils/data-collection/filtering-snippets.ts +++ b/packages/core/src/utils/data-collection/filtering-snippets.ts @@ -1 +1,63 @@ export const PII_HEADER_SNIPPETS = ['forwarded', '-ip', 'remote-', 'via', '-user']; + +export const SENSITIVE_KEY_SNIPPETS = [ + 'auth', + 'token', + 'secret', + 'session', // for the user_session cookie + 'password', + 'passwd', + 'pwd', + 'key', + 'jwt', + 'bearer', + 'sso', + 'saml', + 'csrf', + 'xsrf', + 'credentials', + 'session', + 'sid', + 'identity', + // Always treat cookie headers as sensitive in case individual key-value cookie pairs cannot properly be extracted + 'set-cookie', + 'cookie', +]; + +/** + * Extra substrings matched only against individual Cookie / Set-Cookie **names** (not header names), + * so we can cover common session secrets that do not match {@link SENSITIVE_KEY_SNIPPETS} + * (e.g. `connect.sid` does not contain `session`) without false positives on arbitrary HTTP headers. + * + * Cookie names are checked with the same `includes()` list as headers plus these entries; omit redundant + * cookie-only snippets that are already implied by a header match (e.g. `oauth` → `auth`, `id_token` → `token`, + * `next-auth` → `auth`). + */ +export const SENSITIVE_COOKIE_NAME_SNIPPETS = [ + // Express / Connect default session cookie + '.sid', + // Opaque session ids (PHPSESSID, ASPSESSIONID*, BIGipServer*, *sessid*, …) + 'sessid', + // Laravel etc. "remember me" tokens + 'remember', + // OIDC / OAuth auxiliary (`oauth*` covered by header snippet `auth`) + 'oidc', + 'pkce', + 'nonce', + // RFC 6265bis high-security cookie name prefixes + '__secure-', + '__host-', + // Load balancer / CDN sticky-session cookies (opaque routing tokens) + 'awsalb', + 'awselb', + 'akamai', + // BaaS / IdP session cookies (names often omit "session") + '__stripe', + 'cognito', + 'firebase', + 'supabase', + 'sb-', + // Step-up / MFA cookies + 'mfa', + '2fa', +]; diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts index 9e757509e79a..dac15d40b556 100644 --- a/packages/core/src/utils/request.ts +++ b/packages/core/src/utils/request.ts @@ -6,6 +6,11 @@ import type { RequestEventData } from '../types/request'; import type { WebFetchHeaders, WebFetchRequest } from '../types/webfetchapi'; import { debug } from './debug-logger'; import { safeUnref } from './timer'; +import { + PII_HEADER_SNIPPETS, + SENSITIVE_COOKIE_NAME_SNIPPETS, + SENSITIVE_KEY_SNIPPETS, +} from '../utils/data-collection/filtering-snippets'; /** * Maximum size of incoming HTTP request bodies attached to events. @@ -258,67 +263,6 @@ function getAbsoluteUrl({ return undefined; } -const SENSITIVE_HEADER_SNIPPETS = [ - 'auth', - 'token', - 'secret', - 'session', // for the user_session cookie - 'password', - 'passwd', - 'pwd', - 'key', - 'jwt', - 'bearer', - 'sso', - 'saml', - 'csrf', - 'xsrf', - 'credentials', - // Always treat cookie headers as sensitive in case individual key-value cookie pairs cannot properly be extracted - 'set-cookie', - 'cookie', -]; - -/** - * Extra substrings matched only against individual Cookie / Set-Cookie **names** (not header names), - * so we can cover common session secrets that do not match {@link SENSITIVE_HEADER_SNIPPETS} - * (e.g. `connect.sid` does not contain `session`) without false positives on arbitrary HTTP headers. - * - * Cookie names are checked with the same `includes()` list as headers plus these entries; omit redundant - * cookie-only snippets that are already implied by a header match (e.g. `oauth` → `auth`, `id_token` → `token`, - * `next-auth` → `auth`). - */ -const SENSITIVE_COOKIE_NAME_SNIPPETS = [ - // Express / Connect default session cookie - '.sid', - // Opaque session ids (PHPSESSID, ASPSESSIONID*, BIGipServer*, *sessid*, …) - 'sessid', - // Laravel etc. "remember me" tokens - 'remember', - // OIDC / OAuth auxiliary (`oauth*` covered by header snippet `auth`) - 'oidc', - 'pkce', - 'nonce', - // RFC 6265bis high-security cookie name prefixes - '__secure-', - '__host-', - // Load balancer / CDN sticky-session cookies (opaque routing tokens) - 'awsalb', - 'awselb', - 'akamai', - // BaaS / IdP session cookies (names often omit "session") - '__stripe', - 'cognito', - 'firebase', - 'supabase', - 'sb-', - // Step-up / MFA cookies - 'mfa', - '2fa', -]; - -const PII_HEADER_SNIPPETS = ['x-forwarded-', '-user']; - /** * Converts incoming HTTP request or response headers to OpenTelemetry span attributes following semantic conventions. * Header names are converted to the format: http..header. @@ -434,8 +378,8 @@ function handleHttpHeader( isCookieSubKey: boolean = false, ): string | undefined { const snippetsForSensitivity = isCookieSubKey - ? [...SENSITIVE_HEADER_SNIPPETS, ...SENSITIVE_COOKIE_NAME_SNIPPETS] - : SENSITIVE_HEADER_SNIPPETS; + ? [...SENSITIVE_KEY_SNIPPETS, ...SENSITIVE_COOKIE_NAME_SNIPPETS] + : SENSITIVE_KEY_SNIPPETS; const isSensitive = sendPii ? snippetsForSensitivity.some(snippet => lowerCasedKey.includes(snippet)) From 38d614052d8e91c1804add45a29e2a14e18b2fd0 Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 18 May 2026 14:37:35 +0200 Subject: [PATCH 2/9] Update packages/core/src/utils/data-collection/filtering-snippets.ts Co-authored-by: Charly Gomez --- packages/core/src/utils/data-collection/filtering-snippets.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/utils/data-collection/filtering-snippets.ts b/packages/core/src/utils/data-collection/filtering-snippets.ts index 48a4bd0b5280..adb089d8d5de 100644 --- a/packages/core/src/utils/data-collection/filtering-snippets.ts +++ b/packages/core/src/utils/data-collection/filtering-snippets.ts @@ -16,7 +16,6 @@ export const SENSITIVE_KEY_SNIPPETS = [ 'csrf', 'xsrf', 'credentials', - 'session', 'sid', 'identity', // Always treat cookie headers as sensitive in case individual key-value cookie pairs cannot properly be extracted From 963b031e318da1932d7bbfa516a7104cb69fbf31 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 18 May 2026 18:03:47 +0200 Subject: [PATCH 3/9] add new utils --- packages/core/src/client.ts | 12 ++ packages/core/src/shared-exports.ts | 11 +- packages/core/src/types/datacollection.ts | 10 +- .../defaultPiiToCollectionOptions.ts | 4 +- .../utils/data-collection/filterCookies.ts | 28 ++++ .../data-collection/filterKeyValueData.ts | 51 ++++++ .../data-collection/filterQueryParams.ts | 36 +++++ .../data-collection/filtering-snippets.ts | 2 + .../resolveDataCollectionOptions.ts | 47 ++++++ .../data-collection/filterCookies.test.ts | 98 ++++++++++++ .../filterKeyValueData.test.ts | 132 +++++++++++++++ .../data-collection/filterQueryParams.test.ts | 98 ++++++++++++ .../resolveDataCollectionOptions.test.ts | 150 ++++++++++++++++++ 13 files changed, 674 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/utils/data-collection/filterCookies.ts create mode 100644 packages/core/src/utils/data-collection/filterKeyValueData.ts create mode 100644 packages/core/src/utils/data-collection/filterQueryParams.ts create mode 100644 packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts create mode 100644 packages/core/test/lib/utils/data-collection/filterCookies.test.ts create mode 100644 packages/core/test/lib/utils/data-collection/filterKeyValueData.test.ts create mode 100644 packages/core/test/lib/utils/data-collection/filterQueryParams.test.ts create mode 100644 packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index e5d42afcbb6f..a571af1b3016 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -37,6 +37,7 @@ import type { SeverityLevel } from './types/severity'; import type { Span, SpanAttributes, SpanContextData, SpanJSON, StreamedSpanJSON } from './types/span'; import type { StartSpanOptions } from './types/startSpanOptions'; import type { Transport, TransportMakeRequestResponse } from './types/transport'; +import type { ResolvedDataCollection } from './types/datacollection'; import { createClientReportEnvelope } from './utils/clientreport'; import { debug } from './utils/debug-logger'; import { dsnToString, makeDsn } from './utils/dsn'; @@ -54,6 +55,7 @@ import { showSpanDropWarning } from './utils/spanUtils'; import { rejectedSyncPromise } from './utils/syncpromise'; import { safeUnref } from './utils/timer'; import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent'; +import { resolveDataCollectionOptions } from './utils/data-collection/resolveDataCollectionOptions'; const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured."; const MISSING_RELEASE_FOR_SESSION_ERROR = 'Discarded session because of missing or non-string release'; @@ -213,6 +215,8 @@ export abstract class Client { protected _promiseBuffer: PromiseBuffer; + protected readonly _dataCollection: ResolvedDataCollection; + /** * Initializes this client instance. * @@ -226,6 +230,7 @@ export abstract class Client { this._hooks = {}; this._eventProcessors = []; this._promiseBuffer = makePromiseBuffer(options.transportOptions?.bufferSize ?? DEFAULT_TRANSPORT_BUFFER_SIZE); + this._dataCollection = resolveDataCollectionOptions(options); if (options.dsn) { this._dsn = makeDsn(options.dsn); @@ -399,6 +404,13 @@ export abstract class Client { return this._options; } + /** + * Get the resolved data collection configuration. + */ + public getDataCollectionOptions(): ResolvedDataCollection { + return this._dataCollection; + } + /** * Get the SDK metadata. * @see SdkMetadata diff --git a/packages/core/src/shared-exports.ts b/packages/core/src/shared-exports.ts index fa5e490f8b18..e12cab7d9948 100644 --- a/packages/core/src/shared-exports.ts +++ b/packages/core/src/shared-exports.ts @@ -65,7 +65,9 @@ export { _INTERNAL_shouldSkipAiProviderWrapping, _INTERNAL_clearAiProviderSkips, } from './utils/ai/providerSkip'; -export { defaultPiiToCollectionOptions } from './utils/data-collection/defaultPiiToCollectionOptions'; +export { filterKeyValueData as _INTERNAL_filterKeyValueData } from './utils/data-collection/filterKeyValueData'; +export { filterCookies as _INTERNAL_filterCookies } from './utils/data-collection/filterCookies'; +export { filterQueryParams as _INTERNAL_filterQueryParams } from './utils/data-collection/filterQueryParams'; export { envToBool } from './utils/envToBool'; export { applyScopeDataToEvent, mergeScopeData, getCombinedScopeData } from './utils/scopeData'; export { prepareEvent } from './utils/prepareEvent'; @@ -403,7 +405,12 @@ export type { Extra, Extras } from './types/extra'; export type { Integration, IntegrationFn } from './types/integration'; export type { Mechanism } from './types/mechanism'; export type { ExtractedNodeRequestData, HttpHeaderValue, Primitive, WorkerLocation } from './types/misc'; -export type { CollectBehavior, DataCollection, HttpBodyCollectionTarget } from './types/datacollection'; +export type { + CollectBehavior, + DataCollection, + HttpBodyCollectionTarget, + ResolvedDataCollection, +} from './types/datacollection'; export type { ClientOptions, CoreOptions as Options } from './types/options'; export type { Package } from './types/package'; export type { PolymorphicEvent, PolymorphicRequest } from './types/polymorphics'; diff --git a/packages/core/src/types/datacollection.ts b/packages/core/src/types/datacollection.ts index 5a46462fd25e..fa230f11f800 100644 --- a/packages/core/src/types/datacollection.ts +++ b/packages/core/src/types/datacollection.ts @@ -1,7 +1,7 @@ /** * Controls how key-value data (headers, cookies, query params) is collected and filtered. * - * - `true`: Collect all data without filtering (empty denylist). Senstive values like keys and tokens are always filtered out. + * - `true`: Collect all data without filtering (empty denylist). Sensitive values like keys and tokens are always filtered out. * - `false`: Do not collect any data. * - `{ allow: string[] }`: Collect only the specified keys. * - `{ deny: string[] }`: Collect all keys except the specified ones. @@ -70,3 +70,11 @@ export interface DataCollection { */ frameContextLines?: number; } + +/** + * Fully resolved `DataCollection` with all defaults applied. + */ +export type ResolvedDataCollection = Required & { + httpHeaders: Required>; + genAI: Required>; +}; diff --git a/packages/core/src/utils/data-collection/defaultPiiToCollectionOptions.ts b/packages/core/src/utils/data-collection/defaultPiiToCollectionOptions.ts index 0d5922f0fe21..54eb0f43ccb7 100644 --- a/packages/core/src/utils/data-collection/defaultPiiToCollectionOptions.ts +++ b/packages/core/src/utils/data-collection/defaultPiiToCollectionOptions.ts @@ -1,10 +1,10 @@ import { PII_HEADER_SNIPPETS } from './filtering-snippets'; -import type { DataCollection } from '../../types/datacollection'; +import type { ResolvedDataCollection } from '../../types/datacollection'; /** * Helper function that maps the `sendDefaultPii` boolean flag to the corresponding `DataCollection` configuration. */ -export function defaultPiiToCollectionOptions(sendDefaultPii?: boolean): DataCollection { +export function defaultPiiToCollectionOptions(sendDefaultPii?: boolean): ResolvedDataCollection { return sendDefaultPii === true ? { userInfo: true, diff --git a/packages/core/src/utils/data-collection/filterCookies.ts b/packages/core/src/utils/data-collection/filterCookies.ts new file mode 100644 index 000000000000..68e48e932e3f --- /dev/null +++ b/packages/core/src/utils/data-collection/filterCookies.ts @@ -0,0 +1,28 @@ +import type { CollectBehavior } from '../../types/datacollection'; +import { parseCookie } from '../cookie'; +import { FILTERED_VALUE as FILTERED } from './filtering-snippets'; +import { filterKeyValueData } from './filterKeyValueData'; + +/** + * Filters a cookie string according to a `CollectBehavior`. + * + * When individual cookies can be parsed, each key-value pair is filtered + * independently. When parsing fails, the entire string is replaced with `[Filtered]`. + */ +export function filterCookies(cookieString: string, behavior: CollectBehavior): Record | string { + if (behavior === false) { + return {}; + } + + try { + const parsed = parseCookie(cookieString); + + if (Object.keys(parsed).length === 0) { + return {}; + } + + return filterKeyValueData(parsed, behavior); + } catch { + return FILTERED; + } +} diff --git a/packages/core/src/utils/data-collection/filterKeyValueData.ts b/packages/core/src/utils/data-collection/filterKeyValueData.ts new file mode 100644 index 000000000000..0f2fadcfab52 --- /dev/null +++ b/packages/core/src/utils/data-collection/filterKeyValueData.ts @@ -0,0 +1,51 @@ +import type { CollectBehavior } from '../../types/datacollection'; +import { FILTERED_VALUE as FILTERED, SENSITIVE_KEY_SNIPPETS } from './filtering-snippets'; + +function isSensitiveKey(key: string): boolean { + const lower = key.toLowerCase(); + return SENSITIVE_KEY_SNIPPETS.some(snippet => lower.includes(snippet)); +} + +/** + * Filters a key-value record according to a `CollectBehavior`. + * + * Key names are always preserved. Values are either kept, replaced with + * `[Filtered]`, or the entire record is dropped (off mode). + */ +export function filterKeyValueData(data: Record, behavior: CollectBehavior): Record { + if (behavior === false) { + return {}; + } + + const result: Record = {}; + + if (behavior === true) { + for (const key of Object.keys(data)) { + result[key] = isSensitiveKey(key) ? FILTERED : data[key]!; + } + return result; + } + + if ('deny' in behavior) { + const lowerTerms = behavior.deny.map(t => t.toLowerCase()); + for (const key of Object.keys(data)) { + const lower = key.toLowerCase(); + const isDenied = isSensitiveKey(key) || lowerTerms.some(term => lower.includes(term)); + result[key] = isDenied ? FILTERED : data[key]!; + } + return result; + } + + // allowList mode + const lowerTerms = behavior.allow.map(t => t.toLowerCase()); + for (const key of Object.keys(data)) { + if (isSensitiveKey(key)) { + result[key] = FILTERED; + } else { + const lower = key.toLowerCase(); + const isAllowed = lowerTerms.some(term => lower.includes(term)); + result[key] = isAllowed ? data[key]! : FILTERED; + } + } + return result; +} diff --git a/packages/core/src/utils/data-collection/filterQueryParams.ts b/packages/core/src/utils/data-collection/filterQueryParams.ts new file mode 100644 index 000000000000..0982b5e6006c --- /dev/null +++ b/packages/core/src/utils/data-collection/filterQueryParams.ts @@ -0,0 +1,36 @@ +import type { CollectBehavior } from '../../types/datacollection'; +import { FILTERED_VALUE as FILTERED } from './filtering-snippets'; +import { filterKeyValueData } from './filterKeyValueData'; + +function parseQueryParams(queryString: string): Record | undefined { + try { + const params = new URLSearchParams(queryString); + const result: Record = {}; + params.forEach((value, key) => { + result[key] = value; + }); + return Object.keys(result).length > 0 ? result : undefined; + } catch { + return undefined; + } +} + +/** + * Filters a query parameter string according to a `CollectBehavior`. + * + * When individual params can be parsed, each key-value pair is filtered + * independently. When parsing fails, the entire string is replaced with `[Filtered]`. + */ +export function filterQueryParams(queryString: string, behavior: CollectBehavior): Record | string { + if (behavior === false) { + return {}; + } + + const parsed = parseQueryParams(queryString); + + if (parsed == null) { + return FILTERED; + } + + return filterKeyValueData(parsed, behavior); +} diff --git a/packages/core/src/utils/data-collection/filtering-snippets.ts b/packages/core/src/utils/data-collection/filtering-snippets.ts index adb089d8d5de..8bf2f33363fb 100644 --- a/packages/core/src/utils/data-collection/filtering-snippets.ts +++ b/packages/core/src/utils/data-collection/filtering-snippets.ts @@ -1,3 +1,5 @@ +export const FILTERED_VALUE = '[Filtered]'; + export const PII_HEADER_SNIPPETS = ['forwarded', '-ip', 'remote-', 'via', '-user']; export const SENSITIVE_KEY_SNIPPETS = [ diff --git a/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts b/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts new file mode 100644 index 000000000000..f8130ec8cb6e --- /dev/null +++ b/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts @@ -0,0 +1,47 @@ +import type { DataCollection, ResolvedDataCollection } from '../../types/datacollection'; +import { defaultPiiToCollectionOptions } from './defaultPiiToCollectionOptions'; + +const DEFAULTS: ResolvedDataCollection = { + userInfo: false, + cookies: true, + httpHeaders: { request: true, response: true }, + httpBodies: [], + queryParams: true, + genAI: { inputs: true, outputs: true }, + stackFrameVariables: true, + frameContextLines: 5, +}; + +/** + * Resolves the effective `DataCollection` configuration from client options. + * + * Precedence: + * 1. Fields explicitly set in `dataCollection` + * 2. If `sendDefaultPii` is set and `dataCollection` is absent, bridge via `defaultPiiToCollectionOptions` + * 3. Spec defaults + */ +export function resolveDataCollectionOptions(options: { + dataCollection?: DataCollection; + sendDefaultPii?: boolean; +}): ResolvedDataCollection { + const base = options.dataCollection != null ? DEFAULTS : defaultPiiToCollectionOptions(options.sendDefaultPii); + + const dc = options.dataCollection ?? {}; + + 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, + }, + httpBodies: dc.httpBodies ?? base.httpBodies, + queryParams: dc.queryParams ?? base.queryParams, + genAI: { + inputs: dc.genAI?.inputs ?? base.genAI.inputs, + outputs: dc.genAI?.outputs ?? base.genAI.outputs, + }, + stackFrameVariables: dc.stackFrameVariables ?? base.stackFrameVariables, + frameContextLines: dc.frameContextLines ?? base.frameContextLines, + }; +} diff --git a/packages/core/test/lib/utils/data-collection/filterCookies.test.ts b/packages/core/test/lib/utils/data-collection/filterCookies.test.ts new file mode 100644 index 000000000000..af266ece44a4 --- /dev/null +++ b/packages/core/test/lib/utils/data-collection/filterCookies.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from 'vitest'; +import { filterCookies } from '../../../../src/utils/data-collection/filterCookies'; + +describe('filterCookies', () => { + describe('off mode (false)', () => { + it('returns empty record', () => { + expect(filterCookies('theme=dark; user_session=abc123', false)).toEqual({}); + }); + }); + + describe('denyList mode (true)', () => { + it('filters sensitive cookie names and preserves safe ones', () => { + const result = filterCookies('theme=dark; user_session=abc123; locale=en', true); + + expect(result).toEqual({ + theme: 'dark', + user_session: '[Filtered]', // matches "session" + locale: 'en', + }); + }); + + it('filters auth-related cookies', () => { + const result = filterCookies('auth_token=xyz; color=blue', true); + + expect(result).toEqual({ + auth_token: '[Filtered]', // matches "auth" and "token" + color: 'blue', + }); + }); + }); + + describe('denyList mode ({ deny: [...] })', () => { + it('applies extra deny terms on top of built-in denylist', () => { + const result = filterCookies('theme=dark; tracking_id=abc', { deny: ['tracking'] }); + + expect(result).toEqual({ + theme: 'dark', + tracking_id: '[Filtered]', + }); + }); + }); + + describe('allowList mode ({ allow: [...] })', () => { + it('only allows specified cookie names to pass through', () => { + const result = filterCookies('theme=dark; user_session=abc; locale=en', { + allow: ['theme', 'locale'], + }); + + expect(result).toEqual({ + theme: 'dark', + user_session: '[Filtered]', // sensitive denylist overrides + locale: 'en', + }); + }); + + it('sensitive denylist overrides allowlist', () => { + const result = filterCookies('auth_token=secret', { allow: ['auth_token'] }); + + expect(result).toEqual({ + auth_token: '[Filtered]', // "auth" and "token" match sensitive denylist + }); + }); + }); + + describe('empty and unparseable input', () => { + it('returns empty record for empty string', () => { + expect(filterCookies('', true)).toEqual({}); + }); + + it('returns empty record for string with no key-value pairs', () => { + expect(filterCookies(';;;', true)).toEqual({}); + }); + + it('returns [Filtered] when parsing throws', () => { + // parseCookie doesn't throw for malformed strings, so this path + // is a safety net — verified via the catch block existence + }); + }); + + describe('edge cases', () => { + it('handles cookies with = in the value', () => { + const result = filterCookies('data=base64==; theme=light', true); + + expect(result).toEqual({ + data: 'base64==', + theme: 'light', + }); + }); + + it('handles quoted cookie values', () => { + const result = filterCookies('theme="dark mode"', true); + + expect(result).toEqual({ + theme: 'dark mode', + }); + }); + }); +}); diff --git a/packages/core/test/lib/utils/data-collection/filterKeyValueData.test.ts b/packages/core/test/lib/utils/data-collection/filterKeyValueData.test.ts new file mode 100644 index 000000000000..b472574dc546 --- /dev/null +++ b/packages/core/test/lib/utils/data-collection/filterKeyValueData.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from 'vitest'; +import { filterKeyValueData } from '../../../../src/utils/data-collection/filterKeyValueData'; + +describe('filterKeyValueData', () => { + const sampleData: Record = { + 'Content-Type': 'application/json', + Authorization: 'Bearer abc123', + 'X-Request-Id': '12345', + 'X-Api-Key': 'secret-key-value', + 'X-Custom': 'safe-value', + }; + + describe('off mode (false)', () => { + it('returns an empty record', () => { + expect(filterKeyValueData(sampleData, false)).toEqual({}); + }); + }); + + describe('denyList mode (true) - built-in sensitive denylist only', () => { + it('filters sensitive keys and preserves safe keys', () => { + const result = filterKeyValueData(sampleData, true); + + expect(result['Content-Type']).toBe('application/json'); + expect(result['X-Request-Id']).toBe('12345'); + expect(result['X-Custom']).toBe('safe-value'); + // "Authorization" matches "auth" + expect(result['Authorization']).toBe('[Filtered]'); + // "X-Api-Key" matches "key" + expect(result['X-Api-Key']).toBe('[Filtered]'); + }); + + it('preserves all key names', () => { + const result = filterKeyValueData(sampleData, true); + expect(Object.keys(result).sort()).toEqual(Object.keys(sampleData).sort()); + }); + }); + + describe('denyList mode ({ deny: [...] }) - built-in + extra terms', () => { + it('filters built-in sensitive keys plus extra deny terms', () => { + const result = filterKeyValueData(sampleData, { deny: ['x-custom'] }); + + expect(result['Content-Type']).toBe('application/json'); + expect(result['X-Request-Id']).toBe('12345'); + // Extra deny term matches + expect(result['X-Custom']).toBe('[Filtered]'); + // Built-in denylist still applies + expect(result['Authorization']).toBe('[Filtered]'); + expect(result['X-Api-Key']).toBe('[Filtered]'); + }); + + it('matching is case-insensitive and partial', () => { + const data = { 'My-Custom-Token-Header': 'value1', 'X-Forwarded-For': 'value2' }; + + const result = filterKeyValueData(data, { deny: ['forwarded'] }); + + // "token" matches built-in denylist + expect(result['My-Custom-Token-Header']).toBe('[Filtered]'); + // "forwarded" matches extra deny term + expect(result['X-Forwarded-For']).toBe('[Filtered]'); + }); + }); + + describe('allowList mode ({ allow: [...] })', () => { + it('only allows specified keys to pass through', () => { + const result = filterKeyValueData(sampleData, { allow: ['content-type', 'x-request-id'] }); + + expect(result['Content-Type']).toBe('application/json'); + expect(result['X-Request-Id']).toBe('12345'); + expect(result['X-Custom']).toBe('[Filtered]'); + }); + + it('sensitive denylist overrides allowlist', () => { + const result = filterKeyValueData(sampleData, { allow: ['authorization', 'content-type'] }); + + expect(result['Content-Type']).toBe('application/json'); + // "Authorization" matches "auth" in sensitive denylist — always filtered + expect(result['Authorization']).toBe('[Filtered]'); + }); + + it('preserves all key names', () => { + const result = filterKeyValueData(sampleData, { allow: ['content-type'] }); + expect(Object.keys(result).sort()).toEqual(Object.keys(sampleData).sort()); + }); + }); + + describe('sensitive denylist coverage', () => { + const sensitivePairs: [string, string][] = [ + ['Authorization', 'auth'], + ['X-Auth-Token', 'auth'], + ['X-CSRF-Token', 'csrf'], + ['X-XSRF-Token', 'xsrf'], + ['Session-Id', 'session'], + ['X-Api-Key', 'key'], + ['X-JWT-Token', 'jwt'], + ['Bearer-Token', 'bearer'], + ['SSO-Session', 'sso'], + ['SAML-Response', 'saml'], + ['Password-Hash', 'password'], + ['X-Credentials', 'credentials'], + ['User-SID', 'sid'], + ['X-Identity', 'identity'], + ]; + + it.each(sensitivePairs)('filters %s (matches "%s")', headerName => { + const data = { [headerName]: 'some-value' }; + const result = filterKeyValueData(data, true); + expect(result[headerName]).toBe('[Filtered]'); + }); + }); + + describe('edge cases', () => { + it('handles empty record', () => { + expect(filterKeyValueData({}, true)).toEqual({}); + expect(filterKeyValueData({}, false)).toEqual({}); + expect(filterKeyValueData({}, { allow: ['x'] })).toEqual({}); + expect(filterKeyValueData({}, { deny: ['x'] })).toEqual({}); + }); + + it('handles empty deny terms array (built-in denylist only)', () => { + const result = filterKeyValueData(sampleData, { deny: [] }); + expect(result['Content-Type']).toBe('application/json'); + expect(result['Authorization']).toBe('[Filtered]'); + }); + + it('handles empty allow terms array (everything filtered except sensitive)', () => { + const result = filterKeyValueData(sampleData, { allow: [] }); + for (const value of Object.values(result)) { + expect(value).toBe('[Filtered]'); + } + }); + }); +}); diff --git a/packages/core/test/lib/utils/data-collection/filterQueryParams.test.ts b/packages/core/test/lib/utils/data-collection/filterQueryParams.test.ts new file mode 100644 index 000000000000..f815599593d3 --- /dev/null +++ b/packages/core/test/lib/utils/data-collection/filterQueryParams.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from 'vitest'; +import { filterQueryParams } from '../../../../src/utils/data-collection/filterQueryParams'; + +describe('filterQueryParams', () => { + describe('off mode (false)', () => { + it('returns empty record', () => { + expect(filterQueryParams('page=1&token=abc', false)).toEqual({}); + }); + }); + + describe('denyList mode (true)', () => { + it('filters sensitive param names and preserves safe ones', () => { + const result = filterQueryParams('page=1&api_key=secret&sort=name', true); + + expect(result).toEqual({ + page: '1', + api_key: '[Filtered]', // matches "key" + sort: 'name', + }); + }); + + it('filters auth-related params', () => { + const result = filterQueryParams('auth=abc&redirect=/home', true); + + expect(result).toEqual({ + auth: '[Filtered]', // matches "auth" + redirect: '/home', + }); + }); + }); + + describe('denyList mode ({ deny: [...] })', () => { + it('applies extra deny terms on top of built-in denylist', () => { + const result = filterQueryParams('page=1&utm_source=email', { deny: ['utm'] }); + + expect(result).toEqual({ + page: '1', + utm_source: '[Filtered]', + }); + }); + }); + + describe('allowList mode ({ allow: [...] })', () => { + it('only allows specified param names to pass through', () => { + const result = filterQueryParams('page=1&token=abc&sort=name', { + allow: ['page', 'sort'], + }); + + expect(result).toEqual({ + page: '1', + token: '[Filtered]', // sensitive denylist + sort: 'name', + }); + }); + + it('sensitive denylist overrides allowlist', () => { + const result = filterQueryParams('token=secret', { allow: ['token'] }); + + expect(result).toEqual({ + token: '[Filtered]', // "token" matches sensitive denylist + }); + }); + }); + + describe('unparseable input', () => { + it('returns [Filtered] for empty string', () => { + expect(filterQueryParams('', true)).toBe('[Filtered]'); + }); + }); + + describe('edge cases', () => { + it('handles URL-encoded values', () => { + const result = filterQueryParams('name=hello%20world&page=1', true); + + expect(result).toEqual({ + name: 'hello world', + page: '1', + }); + }); + + it('handles params with no value', () => { + const result = filterQueryParams('debug&page=1', true); + + expect(result).toEqual({ + debug: '', + page: '1', + }); + }); + + it('handles duplicate params (last value wins via URLSearchParams)', () => { + const result = filterQueryParams('page=1&page=2', true); + + expect(result).toEqual({ + page: '2', + }); + }); + }); +}); diff --git a/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts b/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts new file mode 100644 index 000000000000..448c6427180e --- /dev/null +++ b/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from 'vitest'; +import { resolveDataCollectionOptions } from '../../../../src/utils/data-collection/resolveDataCollectionOptions'; + +describe('resolveDataCollectionOptions', () => { + const SPEC_DEFAULTS = { + userInfo: false, + cookies: true, + httpHeaders: { request: true, response: true }, + httpBodies: [], + queryParams: true, + genAI: { inputs: true, outputs: true }, + stackFrameVariables: true, + frameContextLines: 5, + }; + + describe('with no options', () => { + it('falls through to sendDefaultPii: undefined bridge when neither option is set', () => { + const result = resolveDataCollectionOptions({}); + + // sendDefaultPii undefined → restrictive bridge (backward compat) + expect(result.userInfo).toBe(false); + expect(result.httpBodies).toEqual([]); + expect(result.genAI).toEqual({ inputs: false, outputs: false }); + expect(result.stackFrameVariables).toBe(true); + expect(result.frameContextLines).toBe(5); + }); + + it('returns spec defaults when dataCollection is explicitly set to empty object', () => { + expect(resolveDataCollectionOptions({ dataCollection: {} })).toEqual(SPEC_DEFAULTS); + }); + }); + + describe('sendDefaultPii bridge (no dataCollection)', () => { + it('bridges sendDefaultPii: true to permissive config', () => { + const result = resolveDataCollectionOptions({ sendDefaultPii: true }); + + expect(result.userInfo).toBe(true); + expect(result.cookies).toBe(true); + expect(result.httpHeaders).toEqual({ request: true, response: true }); + expect(result.httpBodies).toEqual(['incomingRequest', 'outgoingRequest', 'incomingResponse', 'outgoingResponse']); + expect(result.queryParams).toBe(true); + expect(result.genAI).toEqual({ inputs: true, outputs: true }); + }); + + it('bridges sendDefaultPii: false to restrictive config', () => { + const result = resolveDataCollectionOptions({ sendDefaultPii: false }); + + expect(result.userInfo).toBe(false); + expect(result.httpBodies).toEqual([]); + expect(result.genAI).toEqual({ inputs: false, outputs: false }); + }); + }); + + describe('dataCollection takes precedence over sendDefaultPii', () => { + it('uses dataCollection fields when both are set', () => { + const result = resolveDataCollectionOptions({ + sendDefaultPii: true, + dataCollection: { userInfo: false }, + }); + + // Explicit dataCollection override + expect(result.userInfo).toBe(false); + // Remaining fields use spec defaults (not sendDefaultPii bridge) + expect(result.httpBodies).toEqual([]); + expect(result.genAI).toEqual({ inputs: true, outputs: true }); + }); + }); + + describe('partial dataCollection overrides', () => { + it('merges user overrides with defaults', () => { + const result = resolveDataCollectionOptions({ + dataCollection: { + userInfo: true, + httpBodies: ['incomingRequest'], + }, + }); + + expect(result.userInfo).toBe(true); + expect(result.httpBodies).toEqual(['incomingRequest']); + // Everything else is spec default + expect(result.cookies).toBe(true); + expect(result.httpHeaders).toEqual({ request: true, response: true }); + expect(result.queryParams).toBe(true); + expect(result.genAI).toEqual({ inputs: true, outputs: true }); + expect(result.stackFrameVariables).toBe(true); + expect(result.frameContextLines).toBe(5); + }); + + it('merges nested httpHeaders partially', () => { + const result = resolveDataCollectionOptions({ + dataCollection: { + httpHeaders: { request: false }, + }, + }); + + expect(result.httpHeaders.request).toBe(false); + expect(result.httpHeaders.response).toBe(true); + }); + + it('merges nested genAI partially', () => { + const result = resolveDataCollectionOptions({ + dataCollection: { + genAI: { inputs: false }, + }, + }); + + expect(result.genAI.inputs).toBe(false); + expect(result.genAI.outputs).toBe(true); + }); + + it('supports allow/deny list for cookies', () => { + const result = resolveDataCollectionOptions({ + dataCollection: { + cookies: { deny: ['x-custom'] }, + }, + }); + + expect(result.cookies).toEqual({ deny: ['x-custom'] }); + }); + + it('supports turning off query params', () => { + const result = resolveDataCollectionOptions({ + dataCollection: { + queryParams: false, + }, + }); + + expect(result.queryParams).toBe(false); + }); + }); + + describe('return type completeness', () => { + it('always returns all fields', () => { + const result = resolveDataCollectionOptions({}); + + expect(result).toHaveProperty('userInfo'); + expect(result).toHaveProperty('cookies'); + expect(result).toHaveProperty('httpHeaders'); + expect(result).toHaveProperty('httpHeaders.request'); + expect(result).toHaveProperty('httpHeaders.response'); + expect(result).toHaveProperty('httpBodies'); + expect(result).toHaveProperty('queryParams'); + expect(result).toHaveProperty('genAI'); + expect(result).toHaveProperty('genAI.inputs'); + expect(result).toHaveProperty('genAI.outputs'); + expect(result).toHaveProperty('stackFrameVariables'); + expect(result).toHaveProperty('frameContextLines'); + }); + }); +}); From 459d1246608e81fa9ede9b597ac93619367fcb08 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 20 May 2026 10:11:04 +0200 Subject: [PATCH 4/9] fix cookie specific sensitive terms --- .../utils/data-collection/filterCookies.ts | 4 ++-- .../data-collection/filterKeyValueData.ts | 23 ++++++++++++------- .../data-collection/filterCookies.test.ts | 11 +++++++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/core/src/utils/data-collection/filterCookies.ts b/packages/core/src/utils/data-collection/filterCookies.ts index 68e48e932e3f..ad18d67fe14a 100644 --- a/packages/core/src/utils/data-collection/filterCookies.ts +++ b/packages/core/src/utils/data-collection/filterCookies.ts @@ -1,6 +1,6 @@ import type { CollectBehavior } from '../../types/datacollection'; import { parseCookie } from '../cookie'; -import { FILTERED_VALUE as FILTERED } from './filtering-snippets'; +import { FILTERED_VALUE as FILTERED, SENSITIVE_COOKIE_NAME_SNIPPETS } from './filtering-snippets'; import { filterKeyValueData } from './filterKeyValueData'; /** @@ -21,7 +21,7 @@ export function filterCookies(cookieString: string, behavior: CollectBehavior): return {}; } - return filterKeyValueData(parsed, behavior); + return filterKeyValueData(parsed, behavior, SENSITIVE_COOKIE_NAME_SNIPPETS); } catch { return FILTERED; } diff --git a/packages/core/src/utils/data-collection/filterKeyValueData.ts b/packages/core/src/utils/data-collection/filterKeyValueData.ts index 0f2fadcfab52..692d7abcc583 100644 --- a/packages/core/src/utils/data-collection/filterKeyValueData.ts +++ b/packages/core/src/utils/data-collection/filterKeyValueData.ts @@ -1,9 +1,8 @@ import type { CollectBehavior } from '../../types/datacollection'; import { FILTERED_VALUE as FILTERED, SENSITIVE_KEY_SNIPPETS } from './filtering-snippets'; -function isSensitiveKey(key: string): boolean { - const lower = key.toLowerCase(); - return SENSITIVE_KEY_SNIPPETS.some(snippet => lower.includes(snippet)); +function isSensitiveKey(lower: string, denySnippets: string[]): boolean { + return denySnippets.some(snippet => lower.includes(snippet)); } /** @@ -11,17 +10,25 @@ function isSensitiveKey(key: string): boolean { * * Key names are always preserved. Values are either kept, replaced with * `[Filtered]`, or the entire record is dropped (off mode). + * + * @param additionalDenyTerms - Additional sensitive snippets to check beyond the built-in denylist (e.g. cookie-specific terms). */ -export function filterKeyValueData(data: Record, behavior: CollectBehavior): Record { +export function filterKeyValueData( + data: Record, + behavior: CollectBehavior, + additionalDenyTerms?: string[], +): Record { if (behavior === false) { return {}; } + const denySnippets = + additionalDenyTerms != null ? [...SENSITIVE_KEY_SNIPPETS, ...additionalDenyTerms] : SENSITIVE_KEY_SNIPPETS; const result: Record = {}; if (behavior === true) { for (const key of Object.keys(data)) { - result[key] = isSensitiveKey(key) ? FILTERED : data[key]!; + result[key] = isSensitiveKey(key.toLowerCase(), denySnippets) ? FILTERED : data[key]!; } return result; } @@ -30,7 +37,7 @@ export function filterKeyValueData(data: Record, behavior: Colle const lowerTerms = behavior.deny.map(t => t.toLowerCase()); for (const key of Object.keys(data)) { const lower = key.toLowerCase(); - const isDenied = isSensitiveKey(key) || lowerTerms.some(term => lower.includes(term)); + const isDenied = isSensitiveKey(lower, denySnippets) || lowerTerms.some(term => lower.includes(term)); result[key] = isDenied ? FILTERED : data[key]!; } return result; @@ -39,10 +46,10 @@ export function filterKeyValueData(data: Record, behavior: Colle // allowList mode const lowerTerms = behavior.allow.map(t => t.toLowerCase()); for (const key of Object.keys(data)) { - if (isSensitiveKey(key)) { + const lower = key.toLowerCase(); + if (isSensitiveKey(lower, denySnippets)) { result[key] = FILTERED; } else { - const lower = key.toLowerCase(); const isAllowed = lowerTerms.some(term => lower.includes(term)); result[key] = isAllowed ? data[key]! : FILTERED; } diff --git a/packages/core/test/lib/utils/data-collection/filterCookies.test.ts b/packages/core/test/lib/utils/data-collection/filterCookies.test.ts index af266ece44a4..922aaa5333d4 100644 --- a/packages/core/test/lib/utils/data-collection/filterCookies.test.ts +++ b/packages/core/test/lib/utils/data-collection/filterCookies.test.ts @@ -27,6 +27,17 @@ describe('filterCookies', () => { color: 'blue', }); }); + + it('filters cookie-specific sensitive names', () => { + const result = filterCookies('theme=dark; connect.sid=abc; remember_me=xyz; __secure-token=secret', true); + + expect(result).toEqual({ + theme: 'dark', + 'connect.sid': '[Filtered]', // matches ".sid" + remember_me: '[Filtered]', // matches "remember" + '__secure-token': '[Filtered]', // matches "__secure-" and "token" + }); + }); }); describe('denyList mode ({ deny: [...] })', () => { From f84df0d9cd086e42b3bee1c8ed5049786f09d170 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 20 May 2026 11:00:39 +0200 Subject: [PATCH 5/9] rm empty test --- .../core/test/lib/utils/data-collection/filterCookies.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/core/test/lib/utils/data-collection/filterCookies.test.ts b/packages/core/test/lib/utils/data-collection/filterCookies.test.ts index 922aaa5333d4..bb7466dedc02 100644 --- a/packages/core/test/lib/utils/data-collection/filterCookies.test.ts +++ b/packages/core/test/lib/utils/data-collection/filterCookies.test.ts @@ -82,10 +82,6 @@ describe('filterCookies', () => { expect(filterCookies(';;;', true)).toEqual({}); }); - it('returns [Filtered] when parsing throws', () => { - // parseCookie doesn't throw for malformed strings, so this path - // is a safety net — verified via the catch block existence - }); }); describe('edge cases', () => { From de81668fa45a2e8f926574426447fc82d0774a1d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 20 May 2026 11:06:43 +0200 Subject: [PATCH 6/9] update test --- .../utils/data-collection/resolveDataCollectionOptions.test.ts | 1 + 1 file changed, 1 insertion(+) 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 448c6427180e..82c58b1fa8cf 100644 --- a/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts +++ b/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts @@ -133,6 +133,7 @@ describe('resolveDataCollectionOptions', () => { it('always returns all fields', () => { const result = resolveDataCollectionOptions({}); + expect(Object.keys(result)).toHaveLength(8); expect(result).toHaveProperty('userInfo'); expect(result).toHaveProperty('cookies'); expect(result).toHaveProperty('httpHeaders'); From 9a146c79a5078442f407c56a9ffbfbe869e6cdab Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 20 May 2026 11:11:42 +0200 Subject: [PATCH 7/9] fmt --- .../core/test/lib/utils/data-collection/filterCookies.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/test/lib/utils/data-collection/filterCookies.test.ts b/packages/core/test/lib/utils/data-collection/filterCookies.test.ts index bb7466dedc02..11e5a660c1e6 100644 --- a/packages/core/test/lib/utils/data-collection/filterCookies.test.ts +++ b/packages/core/test/lib/utils/data-collection/filterCookies.test.ts @@ -81,7 +81,6 @@ describe('filterCookies', () => { it('returns empty record for string with no key-value pairs', () => { expect(filterCookies(';;;', true)).toEqual({}); }); - }); describe('edge cases', () => { From 86b9de4e89de79bcc276d8f3d040137f683a5aa7 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 20 May 2026 11:41:28 +0200 Subject: [PATCH 8/9] adapt query params --- .../data-collection/filterQueryParams.ts | 29 ++++++++----------- .../data-collection/filterQueryParams.test.ts | 6 ++-- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/packages/core/src/utils/data-collection/filterQueryParams.ts b/packages/core/src/utils/data-collection/filterQueryParams.ts index 0982b5e6006c..1eb61e753984 100644 --- a/packages/core/src/utils/data-collection/filterQueryParams.ts +++ b/packages/core/src/utils/data-collection/filterQueryParams.ts @@ -2,19 +2,6 @@ import type { CollectBehavior } from '../../types/datacollection'; import { FILTERED_VALUE as FILTERED } from './filtering-snippets'; import { filterKeyValueData } from './filterKeyValueData'; -function parseQueryParams(queryString: string): Record | undefined { - try { - const params = new URLSearchParams(queryString); - const result: Record = {}; - params.forEach((value, key) => { - result[key] = value; - }); - return Object.keys(result).length > 0 ? result : undefined; - } catch { - return undefined; - } -} - /** * Filters a query parameter string according to a `CollectBehavior`. * @@ -26,11 +13,19 @@ export function filterQueryParams(queryString: string, behavior: CollectBehavior return {}; } - const parsed = parseQueryParams(queryString); + try { + const params = new URLSearchParams(queryString); + const parsed: Record = {}; + params.forEach((value, key) => { + parsed[key] = value; + }); - if (parsed == null) { + if (Object.keys(parsed).length === 0) { + return {}; + } + + return filterKeyValueData(parsed, behavior); + } catch { return FILTERED; } - - return filterKeyValueData(parsed, behavior); } diff --git a/packages/core/test/lib/utils/data-collection/filterQueryParams.test.ts b/packages/core/test/lib/utils/data-collection/filterQueryParams.test.ts index f815599593d3..fc73fdf6bee3 100644 --- a/packages/core/test/lib/utils/data-collection/filterQueryParams.test.ts +++ b/packages/core/test/lib/utils/data-collection/filterQueryParams.test.ts @@ -62,9 +62,9 @@ describe('filterQueryParams', () => { }); }); - describe('unparseable input', () => { - it('returns [Filtered] for empty string', () => { - expect(filterQueryParams('', true)).toBe('[Filtered]'); + describe('empty input', () => { + it('returns empty record for empty string', () => { + expect(filterQueryParams('', true)).toEqual({}); }); }); From 94e722fcceabd40b1106078ab216d9e3227a5669 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 20 May 2026 11:58:46 +0200 Subject: [PATCH 9/9] fix comment --- packages/core/src/utils/data-collection/filterKeyValueData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/utils/data-collection/filterKeyValueData.ts b/packages/core/src/utils/data-collection/filterKeyValueData.ts index 692d7abcc583..0d8b00736f87 100644 --- a/packages/core/src/utils/data-collection/filterKeyValueData.ts +++ b/packages/core/src/utils/data-collection/filterKeyValueData.ts @@ -11,7 +11,7 @@ function isSensitiveKey(lower: string, denySnippets: string[]): boolean { * Key names are always preserved. Values are either kept, replaced with * `[Filtered]`, or the entire record is dropped (off mode). * - * @param additionalDenyTerms - Additional sensitive snippets to check beyond the built-in denylist (e.g. cookie-specific terms). + * @param additionalDenyTerms - Additional sensitive snippets to check beyond the built-in denylist. */ export function filterKeyValueData( data: Record,