diff --git a/packages/redux-devtools-instrument/README.md b/packages/redux-devtools-instrument/README.md index cc347e35..f83fb965 100644 --- a/packages/redux-devtools-instrument/README.md +++ b/packages/redux-devtools-instrument/README.md @@ -51,6 +51,7 @@ export default function configureStore(initialState) { - **shouldStartLocked** *boolean* - if specified as `true`, it will not allow any non-monitor actions to be dispatched till `lockChanges(false)` is dispatched. Default is `false`. - **shouldHotReload** *boolean* - if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Default to `true`. - **trace** *boolean* or *function* - if set to `true`, will include stack trace for every dispatched action. You can use a function (with action object as argument) which should return `new Error().stack` string, getting the stack outside of reducers. Default to `false`. + - **traceLimit** *number* - maximum stack trace frames to be stored (in case `trace` option was provided as `true`). By default it's `10`. Note that for Chrome there's a global limit to `10`, so you should also override the global `Error.stackTraceLimit` for more. If `trace` option is a function, `traceLimit` will have no effect, that should be handled there like so: `trace: () => new Error().stack.split('\n').slice(0, limit+1).join('\n')`. There's `+1` for `Error\n`. ### License diff --git a/packages/redux-devtools-instrument/src/instrument.js b/packages/redux-devtools-instrument/src/instrument.js index ea9a38c9..2d596b08 100644 --- a/packages/redux-devtools-instrument/src/instrument.js +++ b/packages/redux-devtools-instrument/src/instrument.js @@ -23,7 +23,7 @@ export const ActionTypes = { * Action creators to change the History state. */ export const ActionCreators = { - performAction(action, trace, toExcludeFromTrace) { + performAction(action, trace, traceLimit, toExcludeFromTrace) { if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + @@ -40,6 +40,7 @@ export const ActionCreators = { let stack; let error; + let frames; if (trace) { if (typeof trace === 'function') stack = trace(action); else { @@ -47,6 +48,10 @@ export const ActionCreators = { // https://v8.dev/docs/stack-trace-api#stack-trace-collection-for-custom-exceptions if (Error.captureStackTrace) Error.captureStackTrace(error, toExcludeFromTrace); stack = error.stack; + if (typeof Error.stackTraceLimit !== 'number' || Error.stackTraceLimit > traceLimit) { + frames = stack.split('\n'); + if (frames.length > traceLimit) stack = frames.slice(0, traceLimit + 1).join('\n'); // +1 for `Error\n` + } } } @@ -197,8 +202,8 @@ function recomputeStates( /** * Lifts an app's action into an action on the lifted store. */ -export function liftAction(action, trace, toExcludeFromTrace) { - return ActionCreators.performAction(action, trace, toExcludeFromTrace); +export function liftAction(action, trace, traceLimit, toExcludeFromTrace) { + return ActionCreators.performAction(action, trace, traceLimit, toExcludeFromTrace); } /** @@ -605,6 +610,7 @@ export function unliftState(liftedState) { export function unliftStore(liftedStore, liftReducer, options) { let lastDefinedState; const trace = options.trace || options.shouldIncludeCallstack; + const traceLimit = options.traceLimit || 10; function getState() { const state = unliftState(liftedStore.getState()); @@ -620,7 +626,7 @@ export function unliftStore(liftedStore, liftReducer, options) { liftedStore, dispatch(action) { - liftedStore.dispatch(liftAction(action, trace, this.dispatch)); + liftedStore.dispatch(liftAction(action, trace, traceLimit, this.dispatch)); return action; }, diff --git a/packages/redux-devtools-instrument/test/instrument.spec.js b/packages/redux-devtools-instrument/test/instrument.spec.js index 428c6308..6a8d4d6d 100644 --- a/packages/redux-devtools-instrument/test/instrument.spec.js +++ b/packages/redux-devtools-instrument/test/instrument.spec.js @@ -713,6 +713,79 @@ describe('instrument', () => { expect(exportedState.actionsById[1].stack).toNotMatch(/instrument.js/); expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js'); expect(exportedState.actionsById[1].stack).toContain('/mocha/'); + expect(exportedState.actionsById[1].stack.split('\n').length).toBe(10 + 1); // +1 is for `Error\n` + }); + + it('should include only 3 frames for stack trace', () => { + function fn1() { + monitoredStore = createStore(counter, instrument(undefined, { trace: true, traceLimit: 3 })); + monitoredLiftedStore = monitoredStore.liftedStore; + monitoredStore.dispatch({ type: 'INCREMENT' }); + + exportedState = monitoredLiftedStore.getState(); + expect(exportedState.actionsById[0].stack).toBe(undefined); + expect(exportedState.actionsById[1].stack).toBeA('string'); + expect(exportedState.actionsById[1].stack).toMatch(/at fn1 /); + expect(exportedState.actionsById[1].stack).toMatch(/at fn2 /); + expect(exportedState.actionsById[1].stack).toMatch(/at fn3 /); + expect(exportedState.actionsById[1].stack).toNotMatch(/at fn4 /); + expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js'); + expect(exportedState.actionsById[1].stack.split('\n').length).toBe(3 + 1); + } + function fn2() { return fn1(); } + function fn3() { return fn2(); } + function fn4() { return fn3(); } + fn4(); + }); + + it('should include only 3 frames for stack trace when Error.stackTraceLimit is 3', () => { + const stackTraceLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 3; + function fn1() { + monitoredStore = createStore(counter, instrument(undefined, { trace: true })); + monitoredLiftedStore = monitoredStore.liftedStore; + monitoredStore.dispatch({ type: 'INCREMENT' }); + + exportedState = monitoredLiftedStore.getState(); + expect(exportedState.actionsById[0].stack).toBe(undefined); + expect(exportedState.actionsById[1].stack).toBeA('string'); + expect(exportedState.actionsById[1].stack).toMatch(/at fn1 /); + expect(exportedState.actionsById[1].stack).toMatch(/at fn2 /); + expect(exportedState.actionsById[1].stack).toMatch(/at fn3 /); + expect(exportedState.actionsById[1].stack).toNotMatch(/at fn4 /); + expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js'); + expect(exportedState.actionsById[1].stack.split('\n').length).toBe(3 + 1); + } + function fn2() { return fn1(); } + function fn3() { return fn2(); } + function fn4() { return fn3(); } + fn4(); + Error.stackTraceLimit = stackTraceLimit; + }); + + it('should include only 3 frames for stack trace when Error.stackTraceLimit is 10', () => { + const stackTraceLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 10; + function fn1() { + monitoredStore = createStore(counter, instrument(undefined, { trace: true, traceLimit: 3 })); + monitoredLiftedStore = monitoredStore.liftedStore; + monitoredStore.dispatch({ type: 'INCREMENT' }); + + exportedState = monitoredLiftedStore.getState(); + expect(exportedState.actionsById[0].stack).toBe(undefined); + expect(exportedState.actionsById[1].stack).toBeA('string'); + expect(exportedState.actionsById[1].stack).toMatch(/at fn1 /); + expect(exportedState.actionsById[1].stack).toMatch(/at fn2 /); + expect(exportedState.actionsById[1].stack).toMatch(/at fn3 /); + expect(exportedState.actionsById[1].stack).toNotMatch(/at fn4 /); + expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js'); + expect(exportedState.actionsById[1].stack.split('\n').length).toBe(3 + 1); + } + function fn2() { return fn1(); } + function fn3() { return fn2(); } + function fn4() { return fn3(); } + fn4(); + Error.stackTraceLimit = stackTraceLimit; }); it('should get stack trace from a function', () => {