diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index ce70c6f075b2..3a713701c676 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -112,52 +112,63 @@ function createReduxEnhancer(enhancerOptions?: Partial): return event; }); - const sentryReducer: Reducer = (state, action): S => { - const newState = reducer(state, action); - - const scope = getCurrentScope(); - - /* Action breadcrumbs */ - const transformedAction = options.actionTransformer(action); - if (typeof transformedAction !== 'undefined' && transformedAction !== null) { - addBreadcrumb({ - category: ACTION_BREADCRUMB_CATEGORY, - data: transformedAction, - type: ACTION_BREADCRUMB_TYPE, - }); - } - - /* Set latest state to scope */ - const transformedState = options.stateTransformer(newState); - if (typeof transformedState !== 'undefined' && transformedState !== null) { - const client = getClient(); - const options = client?.getOptions(); - const normalizationDepth = 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); - } - - /* Allow user to configure scope with latest state */ - const { configureScopeWithState } = options; - if (typeof configureScopeWithState === 'function') { - configureScopeWithState(scope, newState); - } - - return newState; - }; - - return next(sentryReducer, initialState); + function sentryWrapReducer(reducer: Reducer): Reducer { + return (state, action): S => { + const newState = reducer(state, action); + + const scope = getCurrentScope(); + + /* Action breadcrumbs */ + const transformedAction = options.actionTransformer(action); + if (typeof transformedAction !== 'undefined' && transformedAction !== null) { + addBreadcrumb({ + category: ACTION_BREADCRUMB_CATEGORY, + data: transformedAction, + type: ACTION_BREADCRUMB_TYPE, + }); + } + + /* Set latest state to scope */ + const transformedState = options.stateTransformer(newState); + if (typeof transformedState !== 'undefined' && transformedState !== null) { + const client = getClient(); + const options = client?.getOptions(); + const normalizationDepth = 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); + } + + /* Allow user to configure scope with latest state */ + const { configureScopeWithState } = options; + if (typeof configureScopeWithState === 'function') { + configureScopeWithState(scope, newState); + } + + return newState; + }; + } + + const store = next(sentryWrapReducer(reducer), initialState); + + // eslint-disable-next-line @typescript-eslint/unbound-method + store.replaceReducer = new Proxy(store.replaceReducer, { + apply: function (target, thisArg, args) { + target.apply(thisArg, [sentryWrapReducer(args[0])]); + }, + }); + + return store; }; } diff --git a/packages/react/test/redux.test.ts b/packages/react/test/redux.test.ts index b08e8a0061bc..3d4f8e624046 100644 --- a/packages/react/test/redux.test.ts +++ b/packages/react/test/redux.test.ts @@ -425,4 +425,37 @@ describe('createReduxEnhancer', () => { expect(mockHint.attachments).toHaveLength(0); }); }); + + it('restore itself when calling store replaceReducer', () => { + const enhancer = createReduxEnhancer(); + + const initialState = {}; + + const ACTION_TYPE = 'UPDATE_VALUE'; + const reducer = (state: Record = initialState, action: { type: string; newValue: any }) => { + if (action.type === ACTION_TYPE) { + return { + ...state, + value: action.newValue, + }; + } + return state; + }; + + const store = Redux.createStore(reducer, enhancer); + + store.replaceReducer(reducer); + + const updateAction = { type: ACTION_TYPE, newValue: 'updated' }; + store.dispatch(updateAction); + + expect(mockSetContext).toBeCalledWith('state', { + state: { + type: 'redux', + value: { + value: 'updated', + }, + }, + }); + }); });