From b58371064d91b3b7b3e0beea6820ab6797df012d Mon Sep 17 00:00:00 2001 From: Antoine Date: Wed, 9 Apr 2025 17:11:15 +0200 Subject: [PATCH 1/4] Bugfix #16017 : Override replaceReducer functiont so any lazy loaded injection will reinsert the sentry reducer in the chain --- packages/react/src/redux.ts | 102 ++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index ce70c6f075b2..9825f1378e16 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -112,52 +112,62 @@ 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); + return { + ...store, + replaceReducer(nextReducer: Reducer) { + store.replaceReducer(sentryWrapReducer(nextReducer)) + }, + } + + return store; }; } From 9cd9056e63eecbb74dc54de128bac6e7bb396fc4 Mon Sep 17 00:00:00 2001 From: Antoine Date: Tue, 22 Apr 2025 12:36:05 +0200 Subject: [PATCH 2/4] #16017 : Add a test to verify that redux enhancer is still calling setContext with updated store avec reducer being replaced --- packages/react/test/redux.test.ts | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/react/test/redux.test.ts b/packages/react/test/redux.test.ts index b08e8a0061bc..57b4e2bdc4f6 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', + }, + }, + }); + }); }); From 631f71e61748c1b89cd7af479328ce6a478794da Mon Sep 17 00:00:00 2001 From: Antoine Date: Tue, 22 Apr 2025 13:38:24 +0200 Subject: [PATCH 3/4] #16017 : To keep correct typescript type, use "Proxy" instead of plain object. @s1gr1d I am wondering if we can do better than accessing the index [0] directly ? --- packages/react/src/redux.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index 9825f1378e16..2f51d0535e6a 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -160,12 +160,13 @@ function createReduxEnhancer(enhancerOptions?: Partial): } const store = next(sentryWrapReducer(reducer), initialState); - return { - ...store, - replaceReducer(nextReducer: Reducer) { - store.replaceReducer(sentryWrapReducer(nextReducer)) - }, - } + + // 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; }; From 16b51f453b6b01d664b0d697a27487ecd24115cf Mon Sep 17 00:00:00 2001 From: Antoine Date: Thu, 24 Apr 2025 09:04:55 +0200 Subject: [PATCH 4/4] #16017 : Run fix:prettier --- packages/react/src/redux.ts | 4 ++-- packages/react/test/redux.test.ts | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index 2f51d0535e6a..3a713701c676 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -165,8 +165,8 @@ function createReduxEnhancer(enhancerOptions?: Partial): 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 57b4e2bdc4f6..3d4f8e624046 100644 --- a/packages/react/test/redux.test.ts +++ b/packages/react/test/redux.test.ts @@ -433,14 +433,14 @@ describe('createReduxEnhancer', () => { 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; - }; + if (action.type === ACTION_TYPE) { + return { + ...state, + value: action.newValue, + }; + } + return state; + }; const store = Redux.createStore(reducer, enhancer);