From 2172be82264c8d6b21dabeb0a4a7e82ad72f1d1e Mon Sep 17 00:00:00 2001 From: malay44 Date: Wed, 6 Sep 2023 10:36:09 +0530 Subject: [PATCH 1/7] feat(redux): Add 'attachReduxState' option Introduces 'attachReduxState' (default: true) for controlling Redux state attachment to Sentry events. Fixes GH-6266 --- packages/react/src/redux.ts | 18 +++++++++++++++++- yarn.lock | 18 +++++++++--------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index 5bac99dbe511..d0f204e2d20a 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { configureScope, getCurrentHub } from '@sentry/browser'; +import { addGlobalEventProcessor, configureScope, getCurrentHub } from '@sentry/browser'; import type { Scope } from '@sentry/types'; import { addNonEnumerableProperty } from '@sentry/utils'; @@ -49,6 +49,12 @@ type StoreEnhancerStoreCreator, StateExt = never> ) => Store, A, StateExt, Ext> & Ext; export interface SentryEnhancerOptions { + /** + * Redux state in attachments or not. + * @default true + */ + attachReduxState?: boolean; + /** * Transforms the state before attaching it to an event. * Use this to remove any private data before sending it to Sentry. @@ -71,6 +77,7 @@ const ACTION_BREADCRUMB_CATEGORY = 'redux.action'; const ACTION_BREADCRUMB_TYPE = 'info'; const defaultOptions: SentryEnhancerOptions = { + attachReduxState: true, actionTransformer: action => action, stateTransformer: state => state || null, }; @@ -89,6 +96,15 @@ function createReduxEnhancer(enhancerOptions?: Partial): return (next: StoreEnhancerStoreCreator): StoreEnhancerStoreCreator => (reducer: Reducer, initialState?: PreloadedState) => { + options.attachReduxState && + addGlobalEventProcessor((event, hint) => { + hint.attachments = [ + ...(hint.attachments || []), + { filename: 'reduxState.json', data: JSON.stringify(event.contexts && event.contexts.state) || ' ' }, + ]; + return event; + }); + const sentryReducer: Reducer = (state, action): S => { const newState = reducer(state, action); diff --git a/yarn.lock b/yarn.lock index 2f4c50f00895..2059f5294cc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4414,10 +4414,10 @@ fflate "^0.4.4" mitt "^1.1.3" -"@sentry/bundler-plugin-core@0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-0.6.0.tgz#70ad3740b2f90cdca1aff5fdbcd7306566a2f51e" - integrity sha512-gDPBkFxiOkc525U9pxnGMI5B2DAG0+UCsNuiNgl9+AieDcPSYTwdzfGHytxDZrQgPMvIHEnTAp1VlNB+6UxUGQ== +"@sentry/bundler-plugin-core@0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-0.6.1.tgz#6c6a2ff3cdc98cd0ff1c30c59408cee9f067adf2" + integrity sha512-EecCJKp9ERM7J93DNDJTvkY78UiD/IfOjBdXWnaUVE0n619O7LfMVjwlXzxRJKl2x05dBE3lDraILLDGxCf6fg== dependencies: "@sentry/cli" "^2.17.0" "@sentry/node" "^7.19.0" @@ -4464,12 +4464,12 @@ proxy-from-env "^1.1.0" which "^2.0.2" -"@sentry/vite-plugin@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-0.6.0.tgz#3902a5224d52b06d753a1deeb6b722bf6523840c" - integrity sha512-3J1ESvbI5okGJaSWm+gTAOOIa96u4ZwfI/C3n+0HSStz3e4vGiGUh59iNyb1/2m5HFgR5OLaHNfAvlyP8GM/ew== +"@sentry/vite-plugin@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-0.6.1.tgz#31eb744e8d87b1528eed8d41433647727a62e7c0" + integrity sha512-qkvKaSOcNhNWcdxRXLSs+8cF3ey0XIRmEzTl8U7sTTcZwuOMHsJB+HsYij6aTGaqsKfP8w1ozVt9szBAiL4//w== dependencies: - "@sentry/bundler-plugin-core" "0.6.0" + "@sentry/bundler-plugin-core" "0.6.1" "@sentry/webpack-plugin@1.19.0": version "1.19.0" From cd754fa8e29a17f44018143338d2358bfa654a6d Mon Sep 17 00:00:00 2001 From: malay44 Date: Thu, 7 Sep 2023 03:42:59 +0530 Subject: [PATCH 2/7] fix(redux): Improve Redux state attachment handling Refactors the code responsible for attaching the Redux state to Sentry events. The change enhances efficiency by avoiding the addition of empty JSON attachments when there is no Redux state available. --- packages/react/src/redux.ts | 15 +++++++++++---- packages/types/src/context.ts | 9 +++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index d0f204e2d20a..8b003494a331 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -98,10 +98,17 @@ function createReduxEnhancer(enhancerOptions?: Partial): (reducer: Reducer, initialState?: PreloadedState) => { options.attachReduxState && addGlobalEventProcessor((event, hint) => { - hint.attachments = [ - ...(hint.attachments || []), - { filename: 'reduxState.json', data: JSON.stringify(event.contexts && event.contexts.state) || ' ' }, - ]; + if ( + event.contexts && + event.contexts.state && + event.contexts.state.state && + event.contexts.state.state.type === 'redux' + ) { + hint.attachments = [ + ...(hint.attachments || []), + { filename: 'redux_state.json', data: JSON.stringify(event.contexts.state.state.value) }, + ]; + } return event; }); diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 110267284fc0..56f7f8eb090c 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -10,6 +10,15 @@ export interface Contexts extends Record { response?: ResponseContext; trace?: TraceContext; cloud_resource?: CloudResourceContext; + state?: ReduxStateContext; +} + +export interface ReduxStateContext extends Record { + state: { + [key: string]: any; + type: string; + value: any; + }; } export interface AppContext extends Record { From 4a7e09149114d6c3efa83e058bd232cf9beedce1 Mon Sep 17 00:00:00 2001 From: malay44 Date: Thu, 7 Sep 2023 15:32:31 +0530 Subject: [PATCH 3/7] test(redux): Add test case for Redux state attachment New test cases to validate the attachment of Redux state to Sentry events. The test ensures that the attachment logic correctly adds the Redux state when available and does not add it when it's empty or undefined and when not of type redux. --- .../react/test/reduxStateAttachments.test.ts | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 packages/react/test/reduxStateAttachments.test.ts diff --git a/packages/react/test/reduxStateAttachments.test.ts b/packages/react/test/reduxStateAttachments.test.ts new file mode 100644 index 000000000000..4a4f9060be6b --- /dev/null +++ b/packages/react/test/reduxStateAttachments.test.ts @@ -0,0 +1,105 @@ +import * as Sentry from '@sentry/browser'; +import * as Redux from 'redux'; + +import { createReduxEnhancer } from '../src/redux'; + +const mockAddBreadcrumb = jest.fn(); +const mockSetContext = jest.fn(); + +jest.mock('@sentry/browser', () => ({ + ...jest.requireActual('@sentry/browser'), +})); + +afterEach(() => { + mockAddBreadcrumb.mockReset(); + mockSetContext.mockReset(); +}); + +describe('Redux State Attachments', () => { + it('attaches Redux state to Sentry scope', () => { + const enhancer = createReduxEnhancer(); + + const initialState = { + value: 'initial', + }; + + const store = Redux.createStore((state = initialState) => state, enhancer); + + const updateAction = { type: 'UPDATE_VALUE', value: 'updated' }; + + store.dispatch(updateAction); + + const error = new Error('test'); + Sentry.captureException(error); + + Sentry.configureScope(scope => { + expect(scope.getAttachments()).toContainEqual( + expect.objectContaining({ + filename: 'redux_state.json', + data: JSON.stringify({ + value: 'updated', + }), + }), + ); + }); + }); + + it('does not attach when attachReduxState is false', () => { + const enhancer = createReduxEnhancer({ attachReduxState: false }); + + const initialState = { + value: 'initial', + }; + + const store = Redux.createStore((state = initialState) => state, enhancer); + + const updateAction = { type: 'UPDATE_VALUE', value: 'updated' }; + + store.dispatch(updateAction); + + const error = new Error('test'); + Sentry.captureException(error); + + Sentry.configureScope(scope => { + expect(scope.getAttachments()).not.toContainEqual( + expect.objectContaining({ + filename: 'redux_state.json', + data: expect.anything(), + }), + ); + }); + }); + + it('does not attach when state.type is not redux', () => { + const enhancer = createReduxEnhancer(); + + const initialState = { + value: 'initial', + }; + + Redux.createStore((state = initialState) => state, enhancer); + + Sentry.configureScope(scope => { + scope.setContext('state', { + state: { + type: 'not_redux', + value: { + value: 'updated', + }, + }, + }); + }); + + const error = new Error('test'); + Sentry.captureException(error); + + Sentry.configureScope(scope => { + expect(scope.getAttachments()).not.toContainEqual( + expect.objectContaining({ + filename: 'redux_state.json', + data: expect.anything(), + }), + ); + }); + }); +}); From 6df9fb4e7c2d02eb7e6084469c6cae9c9b90c136 Mon Sep 17 00:00:00 2001 From: malay44 Date: Fri, 8 Sep 2023 14:38:54 +0530 Subject: [PATCH 4/7] test(redux): Enhance Redux state attachment tests Refined the test suite for Redux state attachment by: 1. Removing an incorrect test from`reduxStateAttachments.test.ts`. 2. Adding a new test case to `redux.ts` to verify that the attachment logic correctly attaches the Redux state when available, avoids it when it's empty or undefined, and verifies that it's of the correct type 'redux'. --- packages/react/test/redux.test.ts | 102 +++++++++++++++++ .../react/test/reduxStateAttachments.test.ts | 105 ------------------ 2 files changed, 102 insertions(+), 105 deletions(-) delete mode 100644 packages/react/test/reduxStateAttachments.test.ts diff --git a/packages/react/test/redux.test.ts b/packages/react/test/redux.test.ts index bf9fc31853c4..8ba2d5beda8c 100644 --- a/packages/react/test/redux.test.ts +++ b/packages/react/test/redux.test.ts @@ -14,11 +14,15 @@ jest.mock('@sentry/browser', () => ({ addBreadcrumb: mockAddBreadcrumb, setContext: mockSetContext, }), + addGlobalEventProcessor: jest.fn(), })); +const mockAddGlobalEventProcessor = Sentry.addGlobalEventProcessor as jest.Mock; + afterEach(() => { mockAddBreadcrumb.mockReset(); mockSetContext.mockReset(); + mockAddGlobalEventProcessor.mockReset(); }); describe('createReduxEnhancer', () => { @@ -243,4 +247,102 @@ describe('createReduxEnhancer', () => { value: 'latest', }); }); + + describe('Redux State Attachments', () => { + it('attaches Redux state to Sentry scope', () => { + const enhancer = createReduxEnhancer(); + + const initialState = { + value: 'initial', + }; + + Redux.createStore((state = initialState) => state, enhancer); + + expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(1); + + const callbackFunction = mockAddGlobalEventProcessor.mock.calls[0][0]; + + const mockEvent = { + contexts: { + state: { + state: { + type: 'redux', + value: 'UPDATED_VALUE', + }, + }, + }, + }; + + const mockHint = { + attachments: [], + }; + + const result = callbackFunction(mockEvent, mockHint); + + expect(result).toEqual({ + ...mockEvent, + contexts: { + state: { + state: { + type: 'redux', + value: 'UPDATED_VALUE', + }, + }, + }, + }); + + expect(mockHint.attachments).toHaveLength(1); + expect(mockHint.attachments[0]).toEqual({ + filename: 'redux_state.json', + data: JSON.stringify('UPDATED_VALUE'), + }); + }); + + it('does not attach when attachReduxState is false', () => { + const enhancer = createReduxEnhancer({ attachReduxState: false }); + + const initialState = { + value: 'initial', + }; + + Redux.createStore((state = initialState) => state, enhancer); + + expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(0); + }); + + it('does not attach when state.type is not redux', () => { + const enhancer = createReduxEnhancer(); + + const initialState = { + value: 'initial', + }; + + Redux.createStore((state = initialState) => state, enhancer); + + expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(1); + + const callbackFunction = mockAddGlobalEventProcessor.mock.calls[0][0]; + + const mockEvent = { + contexts: { + state: { + state: { + type: 'not_redux', + value: 'UPDATED_VALUE', + }, + }, + }, + }; + + const mockHint = { + attachments: [], + }; + + const result = callbackFunction(mockEvent, mockHint); + + expect(result).toEqual(mockEvent); + + expect(mockHint.attachments).toHaveLength(0); + }); + }); }); diff --git a/packages/react/test/reduxStateAttachments.test.ts b/packages/react/test/reduxStateAttachments.test.ts deleted file mode 100644 index 4a4f9060be6b..000000000000 --- a/packages/react/test/reduxStateAttachments.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import * as Sentry from '@sentry/browser'; -import * as Redux from 'redux'; - -import { createReduxEnhancer } from '../src/redux'; - -const mockAddBreadcrumb = jest.fn(); -const mockSetContext = jest.fn(); - -jest.mock('@sentry/browser', () => ({ - ...jest.requireActual('@sentry/browser'), -})); - -afterEach(() => { - mockAddBreadcrumb.mockReset(); - mockSetContext.mockReset(); -}); - -describe('Redux State Attachments', () => { - it('attaches Redux state to Sentry scope', () => { - const enhancer = createReduxEnhancer(); - - const initialState = { - value: 'initial', - }; - - const store = Redux.createStore((state = initialState) => state, enhancer); - - const updateAction = { type: 'UPDATE_VALUE', value: 'updated' }; - - store.dispatch(updateAction); - - const error = new Error('test'); - Sentry.captureException(error); - - Sentry.configureScope(scope => { - expect(scope.getAttachments()).toContainEqual( - expect.objectContaining({ - filename: 'redux_state.json', - data: JSON.stringify({ - value: 'updated', - }), - }), - ); - }); - }); - - it('does not attach when attachReduxState is false', () => { - const enhancer = createReduxEnhancer({ attachReduxState: false }); - - const initialState = { - value: 'initial', - }; - - const store = Redux.createStore((state = initialState) => state, enhancer); - - const updateAction = { type: 'UPDATE_VALUE', value: 'updated' }; - - store.dispatch(updateAction); - - const error = new Error('test'); - Sentry.captureException(error); - - Sentry.configureScope(scope => { - expect(scope.getAttachments()).not.toContainEqual( - expect.objectContaining({ - filename: 'redux_state.json', - data: expect.anything(), - }), - ); - }); - }); - - it('does not attach when state.type is not redux', () => { - const enhancer = createReduxEnhancer(); - - const initialState = { - value: 'initial', - }; - - Redux.createStore((state = initialState) => state, enhancer); - - Sentry.configureScope(scope => { - scope.setContext('state', { - state: { - type: 'not_redux', - value: { - value: 'updated', - }, - }, - }); - }); - - const error = new Error('test'); - Sentry.captureException(error); - - Sentry.configureScope(scope => { - expect(scope.getAttachments()).not.toContainEqual( - expect.objectContaining({ - filename: 'redux_state.json', - data: expect.anything(), - }), - ); - }); - }); -}); From 8a2526810604d6916ba7000a12c5a537e6840a2c Mon Sep 17 00:00:00 2001 From: Malay Patel <101856674+malay44@users.noreply.github.com> Date: Mon, 11 Sep 2023 21:34:57 +0530 Subject: [PATCH 5/7] Update ReduxStateContext Co-authored-by: Abhijeet Prasad --- packages/types/src/context.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 56f7f8eb090c..d52b674e2cbe 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -10,14 +10,13 @@ export interface Contexts extends Record { response?: ResponseContext; trace?: TraceContext; cloud_resource?: CloudResourceContext; - state?: ReduxStateContext; + state?: StateContext; } -export interface ReduxStateContext extends Record { +export interface StateContext extends Record { state: { - [key: string]: any; type: string; - value: any; + value: Record; }; } From dadf492e96bd1511d0d37d8934432f9ec5a96843 Mon Sep 17 00:00:00 2001 From: malay44 Date: Mon, 11 Sep 2023 22:52:58 +0530 Subject: [PATCH 6/7] perf(redux): Add error filtering and bundle size optimization. --- packages/react/src/redux.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index 8b003494a331..cb90424ba4ad 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -98,16 +98,17 @@ function createReduxEnhancer(enhancerOptions?: Partial): (reducer: Reducer, initialState?: PreloadedState) => { options.attachReduxState && addGlobalEventProcessor((event, hint) => { - if ( - event.contexts && - event.contexts.state && - event.contexts.state.state && - event.contexts.state.state.type === 'redux' - ) { - hint.attachments = [ - ...(hint.attachments || []), - { filename: 'redux_state.json', data: JSON.stringify(event.contexts.state.state.value) }, - ]; + try { + // @ts-expect-error try catch to reduce bundle size + if (event.type === undefined && event.contexts.state.state.type === 'redux') { + hint.attachments = [ + ...(hint.attachments || []), + // @ts-expect-error try catch to reduce bundle size + { filename: 'redux_state.json', data: JSON.stringify(event.contexts.state.state.value) }, + ]; + } + } catch (_) { + // empty } return event; }); From 578da07a708da30d3a1dab3a281936ff94f965ae Mon Sep 17 00:00:00 2001 From: malay44 Date: Mon, 11 Sep 2023 22:54:59 +0530 Subject: [PATCH 7/7] test(redux): Add test for undefined state and event type. --- packages/react/test/redux.test.ts | 68 +++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/react/test/redux.test.ts b/packages/react/test/redux.test.ts index 8ba2d5beda8c..f8260a1dc278 100644 --- a/packages/react/test/redux.test.ts +++ b/packages/react/test/redux.test.ts @@ -344,5 +344,73 @@ describe('createReduxEnhancer', () => { expect(mockHint.attachments).toHaveLength(0); }); + + it('does not attach when state is undefined', () => { + const enhancer = createReduxEnhancer(); + + const initialState = { + value: 'initial', + }; + + Redux.createStore((state = initialState) => state, enhancer); + + expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(1); + + const callbackFunction = mockAddGlobalEventProcessor.mock.calls[0][0]; + + const mockEvent = { + contexts: { + state: { + state: undefined, + }, + }, + }; + + const mockHint = { + attachments: [], + }; + + const result = callbackFunction(mockEvent, mockHint); + + expect(result).toEqual(mockEvent); + + expect(mockHint.attachments).toHaveLength(0); + }); + + it('does not attach when event type is not undefined', () => { + const enhancer = createReduxEnhancer(); + + const initialState = { + value: 'initial', + }; + + Redux.createStore((state = initialState) => state, enhancer); + + expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(1); + + const callbackFunction = mockAddGlobalEventProcessor.mock.calls[0][0]; + + const mockEvent = { + type: 'not_redux', + contexts: { + state: { + state: { + type: 'redux', + value: 'UPDATED_VALUE', + }, + }, + }, + }; + + const mockHint = { + attachments: [], + }; + + const result = callbackFunction(mockEvent, mockHint); + + expect(result).toEqual(mockEvent); + + expect(mockHint.attachments).toHaveLength(0); + }); }); });