diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index 5ef64a3d2522..5bac99dbe511 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { configureScope } from '@sentry/browser'; +import { configureScope, getCurrentHub } from '@sentry/browser'; import type { Scope } from '@sentry/types'; +import { addNonEnumerableProperty } from '@sentry/utils'; interface Action { type: T; @@ -105,7 +106,20 @@ function createReduxEnhancer(enhancerOptions?: Partial): /* Set latest state to scope */ const transformedState = options.stateTransformer(newState); if (typeof transformedState !== 'undefined' && transformedState !== null) { - scope.setContext('state', { state: { type: 'redux', value: transformedState } }); + const client = getCurrentHub().getClient(); + const options = client && client.getOptions(); + const normalizationDepth = (options && options.normalizeDepth) || 3; // default state normalization depth to 3 + + // Set the normalization depth of the redux state to the configured `normalizeDepth` option or a sane number as a fallback + const newStateContext = { state: { type: 'redux', value: transformedState } }; + addNonEnumerableProperty( + newStateContext, + '__sentry_override_normalization_depth__', + 3 + // 3 layers for `state.value.transformedState` + normalizationDepth, // rest for the actual state + ); + + scope.setContext('state', newStateContext); } else { scope.setContext('state', null); } diff --git a/packages/utils/src/normalize.ts b/packages/utils/src/normalize.ts index b10f78f39e98..508442c2d14e 100644 --- a/packages/utils/src/normalize.ts +++ b/packages/utils/src/normalize.ts @@ -100,8 +100,17 @@ function visit( return value as ObjOrArray; } + // Do not normalize objects that we know have already been normalized. As a general rule, the + // "__sentry_skip_normalization__" property should only be used sparingly and only should only be set on objects that + // have already been normalized. + let overriddenDepth = depth; + + if (typeof (value as ObjOrArray)['__sentry_override_normalization_depth__'] === 'number') { + overriddenDepth = (value as ObjOrArray)['__sentry_override_normalization_depth__'] as number; + } + // We're also done if we've reached the max depth - if (depth === 0) { + if (overriddenDepth === 0) { // At this point we know `serialized` is a string of the form `"[object XXXX]"`. Clean it up so it's just `"[XXXX]"`. return stringified.replace('object ', ''); } @@ -117,7 +126,7 @@ function visit( try { const jsonValue = valueWithToJSON.toJSON(); // We need to normalize the return value of `.toJSON()` in case it has circular references - return visit('', jsonValue, depth - 1, maxProperties, memo); + return visit('', jsonValue, overriddenDepth - 1, maxProperties, memo); } catch (err) { // pass (The built-in `toJSON` failed, but we can still try to do it ourselves) } @@ -146,7 +155,7 @@ function visit( // Recursively visit all the child nodes const visitValue = visitable[visitKey]; - normalized[visitKey] = visit(visitKey, visitValue, depth - 1, maxProperties, memo); + normalized[visitKey] = visit(visitKey, visitValue, overriddenDepth - 1, maxProperties, memo); numAdded++; } diff --git a/packages/utils/test/normalize.test.ts b/packages/utils/test/normalize.test.ts index 1319805e9488..94676c1449da 100644 --- a/packages/utils/test/normalize.test.ts +++ b/packages/utils/test/normalize.test.ts @@ -582,4 +582,58 @@ describe('normalize()', () => { expect(result?.foo?.bar?.boo?.bam?.pow).not.toBe('poof'); }); }); + + describe('overrides normalization depth with a non-enumerable property __sentry_override_normalization_depth__', () => { + test('by increasing depth if it is higher', () => { + const normalizationTarget = { + foo: 'bar', + baz: 42, + obj: { + obj: { + obj: { + bestSmashCharacter: 'Cpt. Falcon', + }, + }, + }, + }; + + addNonEnumerableProperty(normalizationTarget, '__sentry_override_normalization_depth__', 3); + + const result = normalize(normalizationTarget, 1); + + expect(result).toEqual({ + baz: 42, + foo: 'bar', + obj: { + obj: { + obj: '[Object]', + }, + }, + }); + }); + + test('by decreasing depth if it is lower', () => { + const normalizationTarget = { + foo: 'bar', + baz: 42, + obj: { + obj: { + obj: { + bestSmashCharacter: 'Cpt. Falcon', + }, + }, + }, + }; + + addNonEnumerableProperty(normalizationTarget, '__sentry_override_normalization_depth__', 1); + + const result = normalize(normalizationTarget, 3); + + expect(result).toEqual({ + baz: 42, + foo: 'bar', + obj: '[Object]', + }); + }); + }); });