Skip to content

Commit 2094b45

Browse files
authored
Stack trace options (#612)
* Implement tracing in the examples * External editor settings for trace stacks * Implement opening options page for Firefox * Implement "trace" feature * Support tracing non-redux actions * Add docs for trace actions calls * Update webpack and use terser instead of uglify * Fix tests
1 parent a8489fd commit 2094b45

File tree

22 files changed

+296
-28
lines changed

22 files changed

+296
-28
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ See [integrations](docs/Integrations.md) and [the blog post](https://medium.com/
178178
- [Methods (advanced API)](docs/API/Methods.md)
179179
- [Create Redux store for debugging](docs/API/CreateStore.md)
180180
- [FAQ](docs/FAQ.md)
181+
- Features
182+
- [Trace actions calls](/docs/Features/Trace.md)
181183
- [Troubleshooting](docs/Troubleshooting.md)
182184
- [Articles](docs/Articles.md)
183185
- [Videos](docs/Videos.md)

docs/API/Arguments.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ The `options` object is optional, and can include any of the following.
2929
### `maxAge`
3030
*number* (>1) - maximum allowed actions to be stored in the history tree. The oldest actions are removed once maxAge is reached. It's critical for performance. Default is `50`.
3131

32+
### `trace`
33+
*boolean* or *function* - if set to `true`, will include stack trace for every dispatched action, so you can see it in trace tab jumping directly to that part of code ([more details](../Features/Trace.md)). 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`.
34+
35+
### `traceLimit`
36+
*number* - maximum stack trace frames to be stored (in case `trace` option was provided as `true`). By default it's `10`. Note that, because extension's calls are excluded, the resulted frames could be 1 less. If `trace` option is a function, `traceLimit` will have no effect, as it's supposed to be handled there.
37+
3238
### `serialize`
3339
*boolean* or *object* which contains:
3440

docs/Features/Trace.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Trace actions calls
2+
3+
![trace-demo](https://user-images.githubusercontent.com/7957859/50161148-a1639300-02e3-11e9-80e7-18d3215a0bf8.gif)
4+
5+
One of the features of Redux DevTools is to select an action in the history and see the callstack that triggered it. It aims to solve the problem of finding the source of events in the event list.
6+
7+
By default it's disabled as, depending of the use case, generating and serializing stack traces for every action can impact the performance. To enable it, set `trace` option to `true` as in [examples](https://github.com/zalmoxisus/redux-devtools-extension/commit/64717bb9b3534ff616d9db56c2be680627c7b09d). See [the API](../API/Arguments.md#trace) for more details.
8+
9+
For some edge cases where stack trace cannot be obtained with just `Error().stack`, you can pass a function as `trace` with your implementation. It's useful for cases where the stack is broken, like, for example, [when calling `setTimeout`](https://github.com/zalmoxisus/redux-devtools-instrument/blob/e7c05c98e7e9654cb7db92a2f56c6b5f3ff2452b/test/instrument.spec.js#L735-L737). It takes `action` object as argument and should return `stack` string. This way it can be also used to provide stack conditionally only for certain actions.
10+
11+
There's also an optional `traceLimit` parameter, which is `10` by default, to prevent consuming too much memory and serializing large stacks. It prevents consuming too much memory and serializing large stacks, also allows you to get larger stacks than limited by the browser (it will overpass default limit of `10` imposed by Chrome in `Error.stackTraceLimit`). 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`).
12+
13+
Apart from opening resources in Chrome DevTools, as seen in the demo above, it can open the file (and jump to the line-column) right in your editor. Pretty useful for debugging, and also as an alternative when it's not possible to use openResource (for Firefox or when using the extension from window or for remote debugging). You can click Settings button and enable that, also adding the path to your project root directory to use. It works out of the box for VSCode, Atom, Webstorm/Phpstorm/IntelliJ, Sublime, Emacs, MacVim, Textmate on Mac and Windows. For Linux you can use [`atom-url-handler`](https://github.com/eclemens/atom-url-handler).

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* [Options (arguments)](/docs/API/Arguments.md)
99
* [Methods (advanced API)](/docs/API/Methods.md)
1010
* [Create Redux store for debugging](/docs/API/CreateStore.md)
11+
* Features
12+
* [Trace actions calls](/docs/Features/Trace.md)
1113
* [Integrations](/docs/Integrations.md)
1214
* [FAQ](/docs/FAQ.md)
1315
* [Troubleshooting](/docs/Troubleshooting.md)

examples/counter/store/configureStore.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as actionCreators from '../actions/counter';
77

88
export let isMonitorAction;
99
export default function configureStore(preloadedState) {
10-
const composeEnhancers = composeWithDevTools({ actionCreators });
10+
const composeEnhancers = composeWithDevTools({ actionCreators, trace: true, traceLimit: 25 });
1111
const store = createStore(reducer, preloadedState, composeEnhancers(
1212
applyMiddleware(invariant(), thunk)
1313
));

examples/counter/webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ var path = require('path');
22
var webpack = require('webpack');
33

44
module.exports = {
5-
devtool: 'cheap-module-eval-source-map',
5+
devtool: 'source-map',
66
entry: [
77
'webpack-hot-middleware/client',
88
'./index'

examples/saga-counter/src/main.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import rootSaga from './sagas'
1313

1414

1515
const sagaMiddleware = createSagaMiddleware(/* {sagaMonitor} */)
16-
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
16+
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
17+
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true, traceLimit: 25 }) || compose;
1718
const store = createStore(
1819
reducer,
1920
composeEnhancers(applyMiddleware(sagaMiddleware))

examples/saga-counter/webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ var path = require('path')
22
var webpack = require('webpack')
33

44
module.exports = {
5-
devtool: 'cheap-module-eval-source-map',
5+
devtool: 'source-map',
66
entry: [
77
'webpack-hot-middleware/client',
88
path.join(__dirname, 'src', 'main')

examples/todomvc/store/configureStore.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as actionCreators from '../actions';
44

55
export default function configureStore(preloadedState) {
66
const enhancer = window.__REDUX_DEVTOOLS_EXTENSION__ &&
7-
window.__REDUX_DEVTOOLS_EXTENSION__({ actionCreators, serialize: true });
7+
window.__REDUX_DEVTOOLS_EXTENSION__({ actionCreators, serialize: true, trace: true });
88
if (!enhancer) {
99
console.warn('Install Redux DevTools Extension to inspect the app state: ' +
1010
'https://github.com/zalmoxisus/redux-devtools-extension#installation')

examples/todomvc/webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ var path = require('path');
22
var webpack = require('webpack');
33

44
module.exports = {
5-
devtool: 'cheap-module-eval-source-map',
5+
devtool: 'source-map',
66
entry: [
77
'webpack-hot-middleware/client',
88
'./index'

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@
8181
"react-redux": "^4.4.5",
8282
"redux": "^3.5.2",
8383
"redux-devtools": "^3.4.1",
84-
"redux-devtools-instrument": "^1.9.3",
85-
"remotedev-app": "^0.10.12",
84+
"redux-devtools-instrument": "^1.9.4",
85+
"remotedev-app": "^0.10.13-beta",
8686
"remotedev-monitor-components": "^0.0.5",
8787
"remotedev-serialize": "^0.1.7",
8888
"remotedev-slider": "^1.1.1",

src/app/api/index.js

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,42 @@ function post(message) {
7272
window.postMessage(message, '*');
7373
}
7474

75-
function amendActionType(action) {
76-
if (typeof action === 'string') return { action: { type: action }, timestamp: Date.now() };
77-
if (!action.type) return { action: { type: 'update' }, timestamp: Date.now() };
78-
if (action.action) return action;
79-
return { action, timestamp: Date.now() };
75+
function getStackTrace(config, toExcludeFromTrace) {
76+
if (!config.trace) return undefined;
77+
if (typeof config.trace === 'function') return config.trace();
78+
79+
let stack;
80+
let extraFrames = 0;
81+
let prevStackTraceLimit;
82+
const traceLimit = config.traceLimit;
83+
const error = Error();
84+
if (Error.captureStackTrace) {
85+
if (Error.stackTraceLimit < traceLimit) {
86+
prevStackTraceLimit = Error.stackTraceLimit;
87+
Error.stackTraceLimit = traceLimit;
88+
}
89+
Error.captureStackTrace(error, toExcludeFromTrace);
90+
} else {
91+
extraFrames = 3;
92+
}
93+
stack = error.stack;
94+
if (prevStackTraceLimit) Error.stackTraceLimit = prevStackTraceLimit;
95+
if (extraFrames || typeof Error.stackTraceLimit !== 'number' || Error.stackTraceLimit > traceLimit) {
96+
const frames = stack.split('\n');
97+
if (frames.length > traceLimit) {
98+
stack = frames.slice(0, traceLimit + extraFrames + (frames[0] === 'Error' ? 1 : 0)).join('\n');
99+
}
100+
}
101+
return stack;
102+
}
103+
104+
function amendActionType(action, config, toExcludeFromTrace) {
105+
let timestamp = Date.now();
106+
let stack = getStackTrace(config, toExcludeFromTrace);
107+
if (typeof action === 'string') return { action: { type: action }, timestamp, stack };
108+
if (!action.type) return { action: { type: 'update' }, timestamp, stack };
109+
if (action.action) return stack ? { stack, ...action } : action;
110+
return { action, timestamp, stack };
80111
}
81112

82113
export function toContentScript(message, serializeState, serializeAction) {
@@ -103,7 +134,7 @@ export function sendMessage(action, state, config, instanceId, name) {
103134
if (typeof config !== 'object') {
104135
// Legacy: sending actions not from connected part
105136
config = {}; // eslint-disable-line no-param-reassign
106-
if (action) amendedAction = amendActionType(action);
137+
if (action) amendedAction = amendActionType(action, config, sendMessage);
107138
}
108139
const message = {
109140
type: action ? 'ACTION' : 'STATE',
@@ -224,7 +255,7 @@ export function connect(preConfig) {
224255
}
225256
}
226257
else if (config.actionSanitizer) amendedAction = config.actionSanitizer(action);
227-
amendedAction = amendActionType(amendedAction);
258+
amendedAction = amendActionType(amendedAction, config, send);
228259
if (latency) {
229260
delayedActions.push(amendedAction);
230261
delayedStates.push(amendedState);

src/app/containers/App.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ class App extends Component {
3131
openWindow = (position) => {
3232
chrome.runtime.sendMessage({ type: 'OPEN', position });
3333
};
34+
openOptionsPage = () => {
35+
if (navigator.userAgent.indexOf('Firefox') !== -1) {
36+
chrome.runtime.sendMessage({ type: 'OPEN_OPTIONS' });
37+
} else {
38+
chrome.runtime.openOptionsPage();
39+
}
40+
};
3441

3542
render() {
3643
const {
@@ -126,10 +133,10 @@ class App extends Component {
126133
onClick={() => { this.openWindow('remote'); }}
127134
>Remote</Button>
128135
}
129-
{chrome.runtime.openOptionsPage &&
136+
{(chrome.runtime.openOptionsPage || navigator.userAgent.indexOf('Firefox') !== -1) &&
130137
<Button
131138
Icon={SettingsIcon}
132-
onClick={() => { chrome.runtime.openOptionsPage(); }}
139+
onClick={this.openOptionsPage}
133140
>Settings</Button>
134141
}
135142
</div>

src/app/middlewares/api.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ function messaging(request, sender, sendResponse) {
8787
}
8888
return;
8989
}
90+
if (request.type === 'OPEN_OPTIONS') {
91+
chrome.runtime.openOptionsPage();
92+
return;
93+
}
9094
if (request.type === 'GET_OPTIONS') {
9195
window.syncOptions.get(options => {
9296
sendResponse({ options });

src/app/stores/enhancerStore.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export default function configureStore(next, monitorReducer, config) {
1313
monitorReducer,
1414
{
1515
maxAge: config.maxAge,
16+
trace: config.trace,
17+
traceLimit: config.traceLimit,
1618
shouldCatchErrors: config.shouldCatchErrors || window.shouldCatchErrors,
1719
shouldHotReload: config.shouldHotReload,
1820
shouldRecordChanges: config.shouldRecordChanges,

src/browser/extension/chromeAPIMock.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
window.isElectron = window.navigator &&
44
window.navigator.userAgent.indexOf('Electron') !== -1;
55

6+
const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
7+
68
// Background page only
79
if (
810
window.isElectron &&
911
location.pathname === '/_generated_background_page.html' ||
10-
navigator.userAgent.indexOf('Firefox') !== -1
12+
isFirefox
1113
) {
1214
chrome.runtime.onConnectExternal = {
1315
addListener() {}
@@ -84,3 +86,7 @@ if (window.isElectron) {
8486
return originSendMessage(...arguments);
8587
};
8688
}
89+
90+
if (isFirefox) {
91+
chrome.storage.sync = chrome.storage.local;
92+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from 'react';
2+
3+
export default ({ options, saveOption }) => {
4+
const EditorState = {
5+
BROWSER: 0,
6+
EXTERNAL: 1
7+
};
8+
9+
return (
10+
<fieldset className="option-group">
11+
<legend className="option-group__title">Editor for stack traces</legend>
12+
13+
<div className="option option_type_radio">
14+
<input className="option__element"
15+
id="editor-browser"
16+
name="useEditor"
17+
type="radio"
18+
checked={options.useEditor === EditorState.BROWSER}
19+
onChange={() => saveOption('useEditor', EditorState.BROWSER)}/>
20+
<label className="option__label" htmlFor="editor-browser">{
21+
navigator.userAgent.indexOf('Firefox') !== -1 ?
22+
'Don\'t open in external editor' :
23+
'Use browser\'s debugger (from Chrome devpanel only)'
24+
}</label>
25+
</div>
26+
27+
<div className="option option_type_radio" style={{ display: 'flex', alignItems: 'center' }}>
28+
<input className="option__element"
29+
id="editor-external"
30+
name="useEditor"
31+
type="radio"
32+
checked={options.useEditor === EditorState.EXTERNAL}
33+
onChange={() => saveOption('useEditor', EditorState.EXTERNAL)}/>
34+
<label className="option__label" htmlFor="editor-external">External editor:&nbsp;</label>
35+
<input className="option__element"
36+
id="editor"
37+
type="text"
38+
size="33"
39+
maxLength={30}
40+
placeholder="vscode, atom, webstorm, sublime..."
41+
value={options.editor}
42+
disabled={options.useEditor !== EditorState.EXTERNAL}
43+
onChange={(e) => saveOption('editor', e.target.value.replace(/\W/g, ''))}/>
44+
</div>
45+
<div className="option option_type_radio">
46+
<label className="option__label" htmlFor="editor-external" style={{marginLeft: '20px'}}>
47+
Absolute path to the project directory to open:
48+
</label>
49+
<br/>
50+
<textarea className="option__textarea"
51+
placeholder="/home/user/my-awesome-app"
52+
value={options.projectPath}
53+
disabled={options.useEditor !== EditorState.EXTERNAL}
54+
onChange={(e) => saveOption('projectPath', e.target.value.replace('\n', ''))} />
55+
<div className="option__hint">Run `pwd` in your project root directory to get it</div>
56+
</div>
57+
</fieldset>
58+
);
59+
};
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import React from 'react';
2+
import EditorGroup from './EditorGroup';
23
import FilterGroup from './FilterGroup';
34
import AllowToRunGroup from './AllowToRunGroup';
45
import MiscellaneousGroup from './MiscellaneousGroup';
56
import ContextMenuGroup from './ContextMenuGroup';
67

78
export default (props) => (
89
<div>
9-
<div style={{ color: 'red' }}>Setting options here is discouraged, and will not be possible in the next major release. Please <a href="https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md" target="_blank" style={{ color: 'red' }}>specify them as parameters</a>. See <a href="https://github.com/zalmoxisus/redux-devtools-extension/issues/296" target="_blank" style={{ color: 'red' }}>the issue</a> for more details.<br /> <hr /></div>
10+
<EditorGroup {...props} />
1011
<FilterGroup {...props} />
1112
<AllowToRunGroup {...props} />
1213
<MiscellaneousGroup {...props} />
1314
<ContextMenuGroup {...props} />
15+
<div style={{ color: 'red' }}><br /><hr />Setting options here is discouraged, and will not be possible in the next major release. Please <a href="https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md" target="_blank" style={{ color: 'red' }}>specify them as parameters</a>. See <a href="https://github.com/zalmoxisus/redux-devtools-extension/issues/296" target="_blank" style={{ color: 'red' }}>the issue</a> for more details.</div>
1416
</div>
1517
);

src/browser/extension/options/syncOptions.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const get = callback => {
3333
if (options) callback(options);
3434
else {
3535
chrome.storage.sync.get({
36+
useEditor: 0,
37+
editor: '',
38+
projectPath: '',
3639
maxAge: 50,
3740
filter: FilterState.DO_NOT_FILTER,
3841
whitelist: '',

test/app/inject/api.spec.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import 'babel-polyfill';
21
import expect from 'expect';
32
import { insertScript, listenMessage } from '../../utils/inject';
43
import '../../../src/browser/extension/inject/pageScript';

test/app/setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require('babel-register')();
2+
require('babel-polyfill');
23
global.chrome = require('sinon-chrome');
34
var jsdom = require('jsdom').jsdom;
45

0 commit comments

Comments
 (0)