Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
102 changes: 56 additions & 46 deletions packages/react/src/redux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,52 +112,62 @@ 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);
return {
...store,
replaceReducer(nextReducer: Reducer<S, A>) {
store.replaceReducer(sentryWrapReducer(nextReducer))
},
}

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