Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trace stack #418

Merged
merged 41 commits into from
Dec 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8e869c8
Get stack trace using an outside `trace` function
zalmoxisus Dec 10, 2018
6efcc78
Exclude instrumentation calls from stack trace
zalmoxisus Dec 10, 2018
faea737
Limit stack trace frames
zalmoxisus Dec 10, 2018
5a2c1ee
Move StackTraceTab and `react-error-overlay`
zalmoxisus Dec 11, 2018
57ce2ab
Remove unused code referred to `error-stack-parser`
zalmoxisus Dec 12, 2018
ba265e4
Use `lerna run` to test only what was changed since master
zalmoxisus Dec 10, 2018
b4070fe
Lerna publish commands
zalmoxisus Dec 11, 2018
39f9151
Create initial root README
zalmoxisus Dec 12, 2018
52f1a4e
Update links
zalmoxisus Dec 12, 2018
672c17d
Fix linting and dependences
zalmoxisus Dec 12, 2018
801ae0f
Transpile to commonjs for all environments
zalmoxisus Dec 12, 2018
bc32342
Fix throwing when no source maps on calling `forEach` of null
zalmoxisus Dec 12, 2018
6e117be
Remove console logs in production
zalmoxisus Dec 12, 2018
d026421
Add tests for `redux-devtools-trace-monitor`
zalmoxisus Dec 12, 2018
1ab28c7
Fix babel transpiling
zalmoxisus Dec 13, 2018
73ea322
Run build on install as yarn workspaces don't handle install lifecycle
zalmoxisus Dec 13, 2018
ee439fa
Get stack trace using an outside `trace` function
zalmoxisus Dec 10, 2018
d94c034
Exclude instrumentation calls from stack trace
zalmoxisus Dec 10, 2018
671b261
Limit stack trace frames
zalmoxisus Dec 10, 2018
d6a0e13
Move StackTraceTab and `react-error-overlay`
zalmoxisus Dec 11, 2018
f9e0208
Remove unused code referred to `error-stack-parser`
zalmoxisus Dec 12, 2018
00863e8
Fix linting and dependences
zalmoxisus Dec 12, 2018
1721069
Transpile to commonjs for all environments
zalmoxisus Dec 12, 2018
58593c3
Fix throwing when no source maps on calling `forEach` of null
zalmoxisus Dec 12, 2018
5e3e308
Remove console logs in production
zalmoxisus Dec 12, 2018
1806f37
Add tests for `redux-devtools-trace-monitor`
zalmoxisus Dec 12, 2018
d187904
Fix babel transpiling
zalmoxisus Dec 13, 2018
f64db6c
Merge remote-tracking branch 'origin/trace-stack' into trace-stack
zalmoxisus Dec 13, 2018
4acf9ee
Pass instrumentation's function to exclude from trace stack
zalmoxisus Dec 13, 2018
651df1a
Take into account that there's an extra 1st frame in Chrome only
zalmoxisus Dec 13, 2018
d338dd9
Force `traceLimit` value modifying and restoring `Error.stackTraceLimit`
zalmoxisus Dec 13, 2018
7f2979b
Implement `redux-devtools-themes` and fix styles
zalmoxisus Dec 14, 2018
983aeb7
Auto expand frames group when no CodeBlock shown
zalmoxisus Dec 14, 2018
2960ac6
Implement the ability to exclude certain file names (extensions for now)
zalmoxisus Dec 14, 2018
292e5f6
Include 3 extra frames when Error.captureStackTrace not supported
zalmoxisus Dec 14, 2018
44a1a46
Open in editor
zalmoxisus Dec 15, 2018
8a58a2b
Fix support for Firefox
zalmoxisus Dec 15, 2018
fa2cec5
Handle `node_modules` paths
zalmoxisus Dec 15, 2018
0c40677
Better handling for opening the editor
zalmoxisus Dec 17, 2018
2ac0e90
Bump `redux-devtools-instrument`
zalmoxisus Dec 18, 2018
29ae303
Show a hint with link to docs when no stack provided
zalmoxisus Dec 18, 2018
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"private": true,
"devDependencies": {
"babel-eslint": "^10.0.0",
"lerna": "3.4.2"
},
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion packages/redux-devtools-instrument/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export default function configureStore(initialState) {
- **pauseActionType** *string* - if specified, whenever `pauseRecording(false)` lifted action is dispatched and there are actions in the history log, will add this action type. If not specified, will commit when paused.
- **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`.
- **shouldIncludeCallstack** *boolean* - if set to `true`, will include callstack for every dispatched action. 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`. 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')` (`+1` is needed for Chrome where's an extra 1st frame for `Error\n`).

### License

Expand Down
2 changes: 1 addition & 1 deletion packages/redux-devtools-instrument/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux-devtools-instrument",
"version": "1.9.3",
"version": "1.9.4",
"description": "Redux DevTools instrumentation",
"main": "lib/instrument.js",
"scripts": {
Expand Down
54 changes: 41 additions & 13 deletions packages/redux-devtools-instrument/src/instrument.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const ActionTypes = {
* Action creators to change the History state.
*/
export const ActionCreators = {
performAction(action, shouldIncludeCallstack) {
performAction(action, trace, traceLimit, toExcludeFromTrace) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
Expand All @@ -38,10 +38,35 @@ export const ActionCreators = {
);
}

return {
type: ActionTypes.PERFORM_ACTION, action, timestamp: Date.now(),
stack: shouldIncludeCallstack ? Error().stack : undefined
};
let stack;
if (trace) {
let extraFrames = 0;
if (typeof trace === 'function') {
stack = trace(action);
} else {
const error = Error();
let prevStackTraceLimit;
if (Error.captureStackTrace) {
if (Error.stackTraceLimit < traceLimit) {
prevStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = traceLimit;
}
Error.captureStackTrace(error, toExcludeFromTrace);
} else {
extraFrames = 3;
}
stack = error.stack;
if (prevStackTraceLimit) Error.stackTraceLimit = prevStackTraceLimit;
if (extraFrames || typeof Error.stackTraceLimit !== 'number' || Error.stackTraceLimit > traceLimit) {
const frames = stack.split('\n');
if (frames.length > traceLimit) {
stack = frames.slice(0, traceLimit + extraFrames + (frames[0] === 'Error' ? 1 : 0)).join('\n');
}
}
}
}

return { type: ActionTypes.PERFORM_ACTION, action, timestamp: Date.now(), stack };
},

reset() {
Expand Down Expand Up @@ -188,8 +213,8 @@ function recomputeStates(
/**
* Lifts an app's action into an action on the lifted store.
*/
export function liftAction(action, shouldIncludeCallstack) {
return ActionCreators.performAction(action, shouldIncludeCallstack);
export function liftAction(action, trace, traceLimit, toExcludeFromTrace) {
return ActionCreators.performAction(action, trace, traceLimit, toExcludeFromTrace);
}

/**
Expand Down Expand Up @@ -502,7 +527,7 @@ export function liftReducerWith(reducer, initialCommittedState, monitorReducer,
minInvalidatedStateIndex = 0;
// iterate through actions
liftedAction.nextLiftedState.forEach(action => {
actionsById[nextActionId] = liftAction(action, options.shouldIncludeCallstack);
actionsById[nextActionId] = liftAction(action, options.trace || options.shouldIncludeCallstack);
stagedActionIds.push(nextActionId);
nextActionId++;
});
Expand Down Expand Up @@ -595,7 +620,8 @@ export function unliftState(liftedState) {
*/
export function unliftStore(liftedStore, liftReducer, options) {
let lastDefinedState;
const { shouldIncludeCallstack } = options;
const trace = options.trace || options.shouldIncludeCallstack;
const traceLimit = options.traceLimit || 10;

function getState() {
const state = unliftState(liftedStore.getState());
Expand All @@ -605,15 +631,17 @@ export function unliftStore(liftedStore, liftReducer, options) {
return lastDefinedState;
}

function dispatch(action) {
liftedStore.dispatch(liftAction(action, trace, traceLimit, dispatch));
return action;
}

return {
...liftedStore,

liftedStore,

dispatch(action) {
liftedStore.dispatch(liftAction(action, shouldIncludeCallstack));
return action;
},
dispatch,

getState,

Expand Down
180 changes: 176 additions & 4 deletions packages/redux-devtools-instrument/test/instrument.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,178 @@ describe('instrument', () => {
});
});

describe('trace option', () => {
let monitoredStore;
let monitoredLiftedStore;
let exportedState;

it('should not include stack trace', () => {
monitoredStore = createStore(counter, instrument());
monitoredLiftedStore = monitoredStore.liftedStore;
monitoredStore.dispatch({ type: 'INCREMENT' });

exportedState = monitoredLiftedStore.getState();
expect(exportedState.actionsById[0].stack).toBe(undefined);
expect(exportedState.actionsById[1].stack).toBe(undefined);
});

it('should include stack trace', () => {
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(/^Error/);
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 force traceLimit value of 3 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 force traceLimit value of 5 even when Error.stackTraceLimit is 2', () => {
const stackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 2;
monitoredStore = createStore(counter, instrument(undefined, { trace: true, traceLimit: 5 }));
monitoredLiftedStore = monitoredStore.liftedStore;
monitoredStore.dispatch({ type: 'INCREMENT' });
Error.stackTraceLimit = stackTraceLimit;

exportedState = monitoredLiftedStore.getState();
expect(exportedState.actionsById[0].stack).toBe(undefined);
expect(exportedState.actionsById[1].stack).toBeA('string');
expect(exportedState.actionsById[1].stack).toMatch(/^Error/);
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(5 + 1);
});

it('should force default limit of 10 even 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' });
Error.stackTraceLimit = stackTraceLimit;

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).toMatch(/at fn4 /);
expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js');
expect(exportedState.actionsById[1].stack.split('\n').length).toBe(10 + 1);
}
function fn2() { return fn1(); }
function fn3() { return fn2(); }
function fn4() { return fn3(); }
fn4();
});

it('should include 3 extra frames when Error.captureStackTrace not suported', () => {
const captureStackTrace = Error.captureStackTrace;
Error.captureStackTrace = undefined;
monitoredStore = createStore(counter, instrument(undefined, { trace: true, traceLimit: 5 }));
monitoredLiftedStore = monitoredStore.liftedStore;
monitoredStore.dispatch({ type: 'INCREMENT' });
Error.captureStackTrace = captureStackTrace;

exportedState = monitoredLiftedStore.getState();
expect(exportedState.actionsById[0].stack).toBe(undefined);
expect(exportedState.actionsById[1].stack).toBeA('string');
expect(exportedState.actionsById[1].stack).toMatch(/^Error/);
expect(exportedState.actionsById[1].stack).toContain('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(5 + 3 + 1);
});

it('should get stack trace from a function', () => {
const traceFn = () => new Error().stack;
monitoredStore = createStore(counter, instrument(undefined, { trace: traceFn }));
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).toContain('at Object.performAction');
expect(exportedState.actionsById[1].stack).toContain('instrument.js');
expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js');
expect(exportedState.actionsById[1].stack).toContain('/mocha/');
});

it('should get stack trace inside setTimeout using a function', (done) => {
const stack = new Error().stack;
setTimeout(() => {
const traceFn = () => stack + new Error().stack;
monitoredStore = createStore(counter, instrument(undefined, { trace: traceFn }));
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).toContain('at Object.performAction');
expect(exportedState.actionsById[1].stack).toContain('instrument.js');
expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js');
expect(exportedState.actionsById[1].stack).toContain('/mocha/');
done();
});
});
});

describe('Import State', () => {
let monitoredStore;
let monitoredLiftedStore;
Expand Down Expand Up @@ -736,8 +908,8 @@ describe('instrument', () => {
expect(importMonitoredLiftedStore.getState()).toEqual(expectedImportedState);
});

it('should include callstack', () => {
let importMonitoredStore = createStore(counter, instrument(undefined, { shouldIncludeCallstack: true }));
it('should include stack trace', () => {
let importMonitoredStore = createStore(counter, instrument(undefined, { trace: true }));
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;

importMonitoredStore.dispatch({ type: 'DECREMENT' });
Expand Down Expand Up @@ -801,8 +973,8 @@ describe('instrument', () => {
expect(filterStackAndTimestamps(importMonitoredLiftedStore.getState())).toEqual(exportedState);
});

it('should include callstack', () => {
let importMonitoredStore = createStore(counter, instrument(undefined, { shouldIncludeCallstack: true }));
it('should include stack trace', () => {
let importMonitoredStore = createStore(counter, instrument(undefined, { trace: true }));
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;

importMonitoredStore.dispatch({ type: 'DECREMENT' });
Expand Down
13 changes: 13 additions & 0 deletions packages/redux-devtools-trace-monitor/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"presets": [ ["env", { "modules": "commonjs" }], "react", "flow" ],
"plugins": [
["transform-runtime", {
"polyfill": false,
"regenerator": true
}],
"transform-class-properties",
"transform-object-rest-spread",
"add-module-exports",
"transform-decorators-legacy"
]
}
5 changes: 5 additions & 0 deletions packages/redux-devtools-trace-monitor/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
build
dev
dist
lib
34 changes: 34 additions & 0 deletions packages/redux-devtools-trace-monitor/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"extends": "plugin:flowtype/recommended",
"globals": {
"chrome": true
},
"env": {
"jest": true,
"browser": true,
"node": true
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"parser": "babel-eslint",
"rules": {
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/react-in-jsx-scope": 2,
"react/sort-comp": 0,
"react/jsx-quotes": 0,
"eol-last": 0,
"no-unused-vars": 0,
"no-console": 1,
"comma-dangle": 0
},
"plugins": [
"react",
"flowtype"
]
}
Loading