Limit stack trace frames

This commit is contained in:
Zalmoxisus 2018-12-10 20:16:55 +02:00
parent 6efcc788cf
commit faea737868
3 changed files with 84 additions and 4 deletions

View File

@ -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`. - **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`. - **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`. - **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 ### License

View File

@ -23,7 +23,7 @@ export const ActionTypes = {
* Action creators to change the History state. * Action creators to change the History state.
*/ */
export const ActionCreators = { export const ActionCreators = {
performAction(action, trace, toExcludeFromTrace) { performAction(action, trace, traceLimit, toExcludeFromTrace) {
if (!isPlainObject(action)) { if (!isPlainObject(action)) {
throw new Error( throw new Error(
'Actions must be plain objects. ' + 'Actions must be plain objects. ' +
@ -40,6 +40,7 @@ export const ActionCreators = {
let stack; let stack;
let error; let error;
let frames;
if (trace) { if (trace) {
if (typeof trace === 'function') stack = trace(action); if (typeof trace === 'function') stack = trace(action);
else { else {
@ -47,6 +48,10 @@ export const ActionCreators = {
// https://v8.dev/docs/stack-trace-api#stack-trace-collection-for-custom-exceptions // https://v8.dev/docs/stack-trace-api#stack-trace-collection-for-custom-exceptions
if (Error.captureStackTrace) Error.captureStackTrace(error, toExcludeFromTrace); if (Error.captureStackTrace) Error.captureStackTrace(error, toExcludeFromTrace);
stack = error.stack; 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. * Lifts an app's action into an action on the lifted store.
*/ */
export function liftAction(action, trace, toExcludeFromTrace) { export function liftAction(action, trace, traceLimit, toExcludeFromTrace) {
return ActionCreators.performAction(action, trace, toExcludeFromTrace); return ActionCreators.performAction(action, trace, traceLimit, toExcludeFromTrace);
} }
/** /**
@ -605,6 +610,7 @@ export function unliftState(liftedState) {
export function unliftStore(liftedStore, liftReducer, options) { export function unliftStore(liftedStore, liftReducer, options) {
let lastDefinedState; let lastDefinedState;
const trace = options.trace || options.shouldIncludeCallstack; const trace = options.trace || options.shouldIncludeCallstack;
const traceLimit = options.traceLimit || 10;
function getState() { function getState() {
const state = unliftState(liftedStore.getState()); const state = unliftState(liftedStore.getState());
@ -620,7 +626,7 @@ export function unliftStore(liftedStore, liftReducer, options) {
liftedStore, liftedStore,
dispatch(action) { dispatch(action) {
liftedStore.dispatch(liftAction(action, trace, this.dispatch)); liftedStore.dispatch(liftAction(action, trace, traceLimit, this.dispatch));
return action; return action;
}, },

View File

@ -713,6 +713,79 @@ describe('instrument', () => {
expect(exportedState.actionsById[1].stack).toNotMatch(/instrument.js/); expect(exportedState.actionsById[1].stack).toNotMatch(/instrument.js/);
expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js'); expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js');
expect(exportedState.actionsById[1].stack).toContain('/mocha/'); 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', () => { it('should get stack trace from a function', () => {