8000 feat(redux): Add 'attachReduxState' option (#8953) · jorrit/sentry-javascript@595e4e2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 595e4e2

Browse files
malay44AbhiPrasad
andauthored
feat(redux): Add 'attachReduxState' option (getsentry#8953)
Co-authored-by: Abhijeet Prasad <devabhiprasad@gmail.com>
1 parent ce84fb3 commit 595e4e2

File tree

3 files changed

+203
-1
lines changed

3 files changed

+203
-1
lines changed

packages/react/src/redux.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { configureScope, getCurrentHub } from '@sentry/browser';
2+
import { addGlobalEventProcessor, configureScope, getCurrentHub } from '@sentry/browser';
33
import type { Scope } from '@sentry/types';
44
import { addNonEnumerableProperty } from '@sentry/utils';
55

@@ -49,6 +49,12 @@ type StoreEnhancerStoreCreator<Ext = Record<string, unknown>, StateExt = never>
4949
) => Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext;
5050

5151
export interface SentryEnhancerOptions<S = any> {
52+
/**
53+
* Redux state in attachments or not.
54+
* @default true
55+
*/
56+
attachReduxState?: boolean;
57+
5258
/**
5359
* Transforms the state before attaching it to an event.
5460
* Use this to remove any private data before sending it to Sentry.
@@ -71,6 +77,7 @@ const ACTION_BREADCRUMB_CATEGORY = 'redux.action';
7177
const ACTION_BREADCRUMB_TYPE = 'info';
7278

7379
const defaultOptions: SentryEnhancerOptions = {
80+
attachReduxState: true,
7481
actionTransformer: action => action,
7582
stateTransformer: state => state || null,
7683
};
@@ -89,6 +96,23 @@ function createReduxEnhancer(enhancerOptions?: Partial<SentryEnhancerOptions>):
8996

9097
return (next: StoreEnhancerStoreCreator): StoreEnhancerStoreCreator =>
9198
<S = any, A extends Action = AnyAction>(reducer: Reducer<S, A>, initialState?: PreloadedState<S>) => {
99+
options.attachReduxState &&
100+
addGlobalEventProcessor((event, hint) => {
101+
try {
102+
// @ts-expect-error try catch to reduce bundle size
103+
if (event.type === undefined && event.contexts.state.state.type === 'redux') {
104+
hint.attachments = [
105+
...(hint.attachments || []),
106+
// @ts-expect-error try catch to reduce bundle size
107+
{ filename: 'redux_state.json', data: JSON.stringify(event.contexts.state.state.value) },
108+
];
109+
}
110+
} catch (_) {
111+
// empty
112+
}
113+
return event;
114+
});
115+
92116
const sentryReducer: Reducer<S, A> = (state, action): S => {
93117
const newState = reducer(state, action);
94118

packages/react/test/redux.test.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@ jest.mock('@sentry/browser', () => ({
1414
addBreadcrumb: mockAddBreadcrumb,
1515
setContext: mockSetContext,
1616
}),
17+
addGlobalEventProcessor: jest.fn(),
1718
}));
1819

20+
const mockAddGlobalEventProcessor = Sentry.addGlobalEventProcessor as jest.Mock;
21+
1922
afterEach(() => {
2023
mockAddBreadcrumb.mockReset();
2124
mockSetContext.mockReset();
25+
mockAddGlobalEventProcessor.mockReset();
2226
});
2327

2428
describe('createReduxEnhancer', () => {
@@ -243,4 +247,170 @@ describe('createReduxEnhancer', () => {
243247
value: 'latest',
244248
});
245249
});
250+
251+
describe('Redux State Attachments', () => {
252+
it('attaches Redux state to Sentry scope', () => {
253+
const enhancer = createReduxEnhancer();
254+
255+
const initialState = {
256+
value: 'initial',
257+
};
258+
259+
Redux.createStore((state = initialState) => state, enhancer);
260+
261+
expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(1);
262+
263+
const callbackFunction = mockAddGlobalEventProcessor.mock.calls[0][0];
264+
265+
const mockEvent = {
266+
contexts: {
267+
state: {
268+
state: {
269+
type: 'redux',
270+
value: 'UPDATED_VALUE',
271+
},
272+
},
273+
},
274+
};
275+
276+
const mockHint = {
277+
attachments: [],
278+
};
279+
280+
const result = callbackFunction(mockEvent, mockHint);
281+
282+
expect(result).toEqual({
283+
...mockEvent,
284+
contexts: {
285+
state: {
286+
state: {
287+
type: 'redux',
288+
value: 'UPDATED_VALUE',
289+
},
290+
},
291+
},
292+
});
293+
294+
expect(mockHint.attachments).toHaveLength(1);
295+
expect(mockHint.attachments[0]).toEqual({
296+
filename: 'redux_state.json',
297+
data: JSON.stringify('UPDATED_VALUE'),
298+
});
299+
});
300+
301+
it('does not attach when attachReduxState is false', () => {
302+
const enhancer = createReduxEnhancer({ attachReduxState: false });
303+
304+
const initialState = {
305+
value: 'initial',
306+
};
307+
308+
Redux.createStore((state = initialState) => state, enhancer);
309+
310+
expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(0);
311+
});
312+
313+
it('does not attach when state.type is not redux', () => {
314+
const enhancer = createReduxEnhancer();
315+
316+
const initialState = {
317+
value: 'initial',
318+
};
319+
320+
Redux.createStore((state = initialState) => state, enhancer);
321+
322+
expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(1);
323+
324+
const callbackFunction = mockAddGlobalEventProcessor.mock.calls[0][0];
325+
326+
const mockEvent = {
327+
contexts: {
328+
state: {
329+
state: {
330+
type: 'not_redux',
331+
value: 'UPDATED_VALUE',
332+
},
333+
},
334+
},
335+
};
336+
337+
const mockHint = {
338+
attachments: [],
339+
};
340+
341+
const result = callbackFunction(mockEvent, mockHint);
342+
343+
expect(result).toEqual(mockEvent);
344+
345+
expect(mockHint.attachments).toHaveLength(0);
346+
});
347+
348+
it('does not attach when state is undefined', () => {
349+
const enhancer = createReduxEnhancer();
350+
351+
const initialState = {
352+
value: 'initial',
353+
};
354+
355+
Redux.createStore((state = initialState) => state, enhancer);
356+
357+
expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(1);
358+
359+
const callbackFunction = mockAddGlobalEventProcessor.mock.calls[0][0];
360+
361+
const mockEvent = {
362+
contexts: {
363+
state: {
364+
state: undefined,
365+
},
366+
},
367+
};
368+
369+
const mockHint = {
370+
attachments: [],
371+
};
372+
373+
const result = callbackFunction(mockEvent, mockHint);
374+
375+
expect(result).toEqual(mockEvent);
376+
377+
expect(mockHint.attachments).toHaveLength(0);
378+
});
379+
380+
it('does not attach when event type is not undefined', () => {
381+
const enhancer = createReduxEnhancer();
382+
383+
const initialState = {
384+
value: 'initial',
385+
};
386+
387+
Redux.createStore((state = initialState) => state, enhancer);
388+
389+
expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(1);
390+
391+
const callbackFunction = mockAddGlobalEventProcessor.mock.calls[0][0];
392+
393+
const mockEvent = {
394+
type: 'not_redux',
395+
contexts: {
396+
state: {
397+
state: {
398+
type: 'redux',
399+
value: 'UPDATED_VALUE',
400+
},
401+
},
402+
},
403+
};
404+
405+
const mockHint = {
406+
attachments: [],
407+
};
408+
409+
const result = callbackFunction(mockEvent, mockHint);
410+
411+
expect(result).toEqual(mockEvent);
412+
413+
expect(mockHint.attachments).toHaveLength(0);
414+
});
415+
});
246416
});

packages/types/src/context.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ export interface Contexts extends Record<string, Context | undefined> {
1010
response?: ResponseContext;
1111
trace?: TraceContext;
1212
cloud_resource?: CloudResourceContext;
13+
state?: StateContext;
14+
}
15+
16+
export interface StateContext extends Record<string, unknown> {
17+
state: {
18+
type: string;
19+
value: Record<string, unknown>;
20+
};
1321
}
1422

1523
export interface AppContext extends Record<string, unknown> {

0 commit comments

Comments
 (0)
0