Skip to content

Commit 80ba791

Browse files
fix(react): Fix Redux integration failing with reducer injection (#16106)
Before submitting a pull request, please take a look at our [Contributing](https://github.com/getsentry/sentry-javascript/blob/master/CONTRIBUTING.md) guidelines and verify: - [ X ] If you've added code that should be tested, please add tests. - [ X ] Ensure your code lints and the test suite passes (`yarn lint`) & (`yarn test`).
1 parent 453d5a0 commit 80ba791

File tree

2 files changed

+90
-46
lines changed

2 files changed

+90
-46
lines changed

packages/react/src/redux.ts

+57-46
Original file line numberDiff line numberDiff line change
@@ -112,52 +112,63 @@ function createReduxEnhancer(enhancerOptions?: Partial<SentryEnhancerOptions>):
112112
return event;
113113
});
114114

115-
const sentryReducer: Reducer<S, A> = (state, action): S => {
116-
const newState = reducer(state, action);
117-
118-
const scope = getCurrentScope();
119-
120-
/* Action breadcrumbs */
121-
const transformedAction = options.actionTransformer(action);
122-
if (typeof transformedAction !== 'undefined' && transformedAction !== null) {
123-
addBreadcrumb({
124-
category: ACTION_BREADCRUMB_CATEGORY,
125-
data: transformedAction,
126-
type: ACTION_BREADCRUMB_TYPE,
127-
});
128-
}
129-
130-
/* Set latest state to scope */
131-
const transformedState = options.stateTransformer(newState);
132-
if (typeof transformedState !== 'undefined' && transformedState !== null) {
133-
const client = getClient();
134-
const options = client?.getOptions();
135-
const normalizationDepth = options?.normalizeDepth || 3; // default state normalization depth to 3
136-
137-
// Set the normalization depth of the redux state to the configured `normalizeDepth` option or a sane number as a fallback
138-
const newStateContext = { state: { type: 'redux', value: transformedState } };
139-
addNonEnumerableProperty(
140-
newStateContext,
141-
'__sentry_override_normalization_depth__',
142-
3 + // 3 layers for `state.value.transformedState`
143-
normalizationDepth, // rest for the actual state
144-
);
145-
146-
scope.setContext('state', newStateContext);
147-
} else {
148-
scope.setContext('state', null);
149-
}
150-
151-
/* Allow user to configure scope with latest state */
152-
const { configureScopeWithState } = options;
153-
if (typeof configureScopeWithState === 'function') {
154-
configureScopeWithState(scope, newState);
155-
}
156-
157-
return newState;
158-
};
159-
160-
return next(sentryReducer, initialState);
115+
function sentryWrapReducer(reducer: Reducer<S, A>): Reducer<S, A> {
116+
return (state, action): S => {
117+
const newState = reducer(state, action);
118+
119+
const scope = getCurrentScope();
120+
121+
/* Action breadcrumbs */
122+
const transformedAction = options.actionTransformer(action);
123+
if (typeof transformedAction !== 'undefined' && transformedAction !== null) {
124+
addBreadcrumb({
125+
category: ACTION_BREADCRUMB_CATEGORY,
126+
data: transformedAction,
127+
type: ACTION_BREADCRUMB_TYPE,
128+
});
129+
}
130+
131+
/* Set latest state to scope */
132+
const transformedState = options.stateTransformer(newState);
133+
if (typeof transformedState !== 'undefined' && transformedState !== null) {
134+
const client = getClient();
135+
const options = client?.getOptions();
136+
const normalizationDepth = options?.normalizeDepth || 3; // default state normalization depth to 3
137+
138+
// Set the normalization depth of the redux state to the configured `normalizeDepth` option or a sane number as a fallback
139+
const newStateContext = { state: { type: 'redux', value: transformedState } };
140+
addNonEnumerableProperty(
141+
newStateContext,
142+
'__sentry_override_normalization_depth__',
143+
3 + // 3 layers for `state.value.transformedState`
144+
normalizationDepth, // rest for the actual state
145+
);
146+
147+
scope.setContext('state', newStateContext);
148+
} else {
149+
scope.setContext('state', null);
150+
}
151+
152+
/* Allow user to configure scope with latest state */
153+
const { configureScopeWithState } = options;
154+
if (typeof configureScopeWithState === 'function') {
155+
configureScopeWithState(scope, newState);
156+
}
157+
158+
return newState;
159+
};
160+
}
161+
162+
const store = next(sentryWrapReducer(reducer), initialState);
163+
164+
// eslint-disable-next-line @typescript-eslint/unbound-method
165+
store.replaceReducer = new Proxy(store.replaceReducer, {
166+
apply: function (target, thisArg, args) {
167+
target.apply(thisArg, [sentryWrapReducer(args[0])]);
168+
},
169+
});
170+
171+
return store;
161172
};
162173
}
163174

packages/react/test/redux.test.ts

+33
Original file line numberDiff line numberDiff line change
@@ -425,4 +425,37 @@ describe('createReduxEnhancer', () => {
425425
expect(mockHint.attachments).toHaveLength(0);
426426
});
427427
});
428+
429+
it('restore itself when calling store replaceReducer', () => {
430+
const enhancer = createReduxEnhancer();
431+
432+
const initialState = {};
433+
434+
const ACTION_TYPE = 'UPDATE_VALUE';
435+
const reducer = (state: Record<string, unknown> = initialState, action: { type: string; newValue: any }) => {
436+
if (action.type === ACTION_TYPE) {
437+
return {
438+
...state,
439+
value: action.newValue,
440+
};
441+
}
442+
return state;
443+
};
444+
445+
const store = Redux.createStore(reducer, enhancer);
446+
447+
store.replaceReducer(reducer);
448+
449+
const updateAction = { type: ACTION_TYPE, newValue: 'updated' };
450+
store.dispatch(updateAction);
451+
452+
expect(mockSetContext).toBeCalledWith('state', {
453+
state: {
454+
type: 'redux',
455+
value: {
456+
value: 'updated',
457+
},
458+
},
459+
});
460+
});
428461
});

0 commit comments

Comments
 (0)