Skip to content

fix(react): Fix Redux integration failing with reducer injection #16106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 57 additions & 46 deletions packages/react/src/redux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,52 +112,63 @@ function createReduxEnhancer(enhancerOptions?: Partial<SentryEnhancerOptions>):
return event;
});

const sentryReducer: Reducer<S, A> = (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<S, A>): Reducer<S, A> {
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])]);
Copy link
Member

@s1gr1d s1gr1d Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just saw your question here: LeComptoirDesPharmacies@631f71e

I am wondering if we can do better than accessing the index [0] directly ?

Not really as we need to use the first argument :/

},
});

return store;
};
}

Expand Down
33 changes: 33 additions & 0 deletions packages/react/test/redux.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = 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',
},
},
});
});
});
Loading