DevTools for Sagas
Redux DevTools currently leave a lot to be desired. Mainly because they don't track async interactions at all.
It's impossible to say which component caused which async action, which in turn caused other async actions, which in turn dispatched some regular actions.
However sagas seem different. They make side effect control flow more explicit and don't execute side effects unless the caller demands so. If we wrapped top level functions such as take(), call(), put(), fork(), into tracking functions, we could potentially build a graph of saga control flow and trace action history through the sagas that generated it. This is similar to Cerebral debugger UI which unlike ours can trace which Signal was caused by which other Signal.
The developer wins in my opinion could be huge here. If this is possible to build we need to build it. At first as a proof of concept with console logging, and later with a UI, possibly as an extension to Redux DevTools.
What do you think? Am I making sense here? Have I missed something that makes this infeasible, not useful, or hard?
What do you think? Am I making sense here? Have I missed something that makes this infeasible, not useful, or hard?
If I understand it's about 2 things
- first being able to trace each effect yielded by the Saga
- second is being able to build some kind of control-flow model from the above list
The first thing seems easy to implement. However we can't do that by wrapping effect creators (take, put, ...) because they are just pure functions so they are not aware of the context that created them (in test or in real code). Instead it can be added in the proc function (the one that drives the generator).
We can for example make the middleware accepts an effectMonitor callback, the effectMonitor will be called each time an effect is executed like effectMonitor (nameOfSaga, effect) (in replay sessions the Devtools can simply replay with the recorded results). And so you can process the log data in the way you like: log into the console, render in an UI or whatever else.
The second thing: I need to more understand the desired output: this control flow graph
we could potentially build a graph of saga control flow and trace action history through the sagas that generated it.
By graph of saga control flow do you mean a call hierarchy of Sagas -> child Sagas/functions ? so we can for example log something like (* to distinguish generators from normal functions)
*watchFetch
action FETCH_POSTS
*fetchPosts
action REQUEST_POSTS
call (fetchPosts)
action RECEIVE_POSTS
At first glance this seems doable, for example we can add a field which is the current call path. So now we can call the effectMonitor(currentCalPath, effect)
so for the sample above it would give something like
// flat list of effects
[
saga: '*watchFetch', args: [], path ''
action: FETCH_POSTS, path: ['*watchFetch']
call: *fetchPosts, args: [], path: ['*watchFetch']
action REQUEST_POSTS, path: ['*watchFetch/*fetchPosts']
...
]
And the log handler can transform this list into the desired control flow
This is kind of a quick thinking. We need also to represent other kind of effects in the control flow model : paraellel effects, race, fork, join
So to recap
- The Saga driver can provides all data that it processes to a logger callback
- The main question is how to map all the kind of effects into that control flow graph
EDIT: sorry fixed the last list
Some inspiration: https://www.youtube.com/watch?v=Fo86aiBoomE
At first glance this seems doable, for example we can add a field which is the current call path. So now we can call the effectMonitor(currentCalPath, effect)
Basically I was thinking about giving each saga an ID (internally) and notifying which saga is parent to which saga by embedding "parent ID" into every parent-child effect. So this would give us the call tree.
let's see if I understood you correctly. I'll call the Sagas/effects tasks in the following
When started, each task is giving an ID, and optionally the parent's ID of an already started task
Each time a task is started I notify the monitor with something like monitor.taskcreated(id, parentId, taskDesc) and now we have a hierarchy of tasks
Each time a task yields an effect I call the monitor.effectTriggered(taskId, effectId, effectDesc) so now the monitor can locate in which place in the call tree the effect was issued
And for tasks that return results (usually promised functions) when the result is resolved I call monitor.effectResolved/Rejected(taskId, effectId, result)
I saw the cerebral video and a couple of others. It seems the fact that the control flow being described declaratively helps a lot for tracing each operation. and with the state atom and cursor like operations every single mutation can be traced. In Redux the finest you can get is at the action level (which can leads to multiple mutations). More flexibility but less control.
Hi guys! Just wanted to add an updated video on the Cerebral Debugger, which takes into account parallell requests, paths etc. Really glad it can inspire and really looking forward to see where this is going :-) https://www.youtube.com/watch?v=QhStJqngBXc
Hello,
When implementing devtools with Sagas, make sure that when you replay events the saga does not kick in and triggers new actions (replaying history should not modify that history and it could be easy to do so with sagas).
Basically I was thinking about giving each saga an ID (internally) and notifying which saga is parent to which saga by embedding "parent ID" into every parent-child effect. So this would give us the call tree.
@gaearon Can you please elaborate on this? Did I understand it correctly, that you would like to have an user interaction defined transaction boundary? Let's say onClick these actions have been dispatched:
CLICKED_FOOAPI_STARTEDAPI_FINISHED
https://github.com/yelouafi/redux-saga/issues/5#issuecomment-166503173 describes what I mean pretty well. Implementation wise I'd experiment with creating a store enhancer that would track the relevant DevTools state in a separate reducer, so those calls to monitor are just actions. See this approach in regular Redux DevTools.
@gaearon in that case isn't this slight modification of redux-thunk melted into store enhancer exactly what we need?
const storeEnhancer = storeFactory => (reducer, initialState) => {
let correlationId = 0;
const store = storeFactory(reducer, initialState);
const wrappedDispatch = action => {
correlationId++;
if (typeof action === 'function') {
// The idea is to wrap dispatch only for thunks (which defines UI interaction transaction boundary)
return action(dispatchable => store.dispatch({...dispatchable, correlationId}), store.getState);
} else {
return store.dispatch({...action, correlationId});
}
};
return {
...store,
dispatch: wrappedDispatch
};
};
Given two action creators:
const simpleActionCreator = () => ({type: 'SIMPLE'});
and
const thunkActionCreator = (dispatch, getState) => {
dispatch({type: 'THUNK_STEP_1'});
dispatch({type: 'THUNK_STEP_2'});
setTimeout(() => {
dispatch({type: 'THUNK_STEP_3'});
}, 500);
};
when called sequentially
dispatch(simpleActionCreator());
dispatch(thunkActionCreator);
will dispatch these actions:
[{type: 'SIMPLE', correlationId: 1},
{type: 'THUNK_STEP_1', correlationId: 2},
{type: 'THUNK_STEP_2', correlationId: 2},
{type: 'THUNK_STEP_3', correlationId: 2}]
Because the implementation is exclusive with thunk-middleware it must allow recursive thunk dispatching, therefore slight modification. The implementation does not break any middleware chain and all the middlewares get applied:
const storeEnhancer = storeFactory => (reducer, initialState) => {
let correlationId = 0;
const store = storeFactory(reducer, initialState);
const thunkMiddlewareWithCorrelationId = id => action => {
if (typeof action === 'function') {
return action(thunkMiddlewareWithCorrelationId(id), store.getState);
} else {
return store.dispatch({...action, correlationId: id});
}
};
const wrappedDispatch = action => {
correlationId++;
return thunkMiddlewareWithCorrelationId(correlationId)(action);
};
return {
...store,
dispatch: wrappedDispatch
};
};
EDIT: Reflecting the tree structure of correlation ids is fairly simple, you can display in devtools the exact async thunk hierarchy.
Also the cool thing about this is that it replaces redux-thunk for development, the functionality is no different except it provides some additional action metadata. Therefore we can use this enhancer for development and swap it for redux-thunk in production.
@gaearon I pushed a new branch for experimenting. This is not so big, but can serve as a starting point to refine the model
Right now, the middleware can dispatch low level actions (defined here). usage example here
This forms a kind of a DB with 2 tables: tasks and effects, with parent/child relation from tasks to effects, and a hierarchical parent/child relation on the tasks table itself. so we can construct different 'views' or 'queries' from this. Actually I can think of 2 different views
- a view per Saga: we can watch saga progression (receiving actions, firing effects, nested sagas)
- a view 'per action': means upon each UI-dispatched action, picks all sagas watching for that action and show their reactions below the triggered action; this is quite semblable to how Cerebral debugger work. But seems more challenging to implement (but not impossible)
Reworked the monitoring branch.
First, Sagas events are now dispatched as normal Redux actions, so you can handle them by a normal middleware and/or reducer.
Second there are only 3 actions: EFFECT_TRIGGERED, EFFECT_RESOLVED, EFFECT_REJECTED. Every effect is identified by 3 props: its Id, its parent ID (forked/called sagas, child effects in a yield race or a yield [...effects]) and optionally a label (to support yield race({label1: effect1, ... }))
There is an example of a saga monitor that watches all monitoring actions and update an internal tree. You can play with all examples (except real-world), by running the example and dispatching a {type: 'LOG_EFFECT'} action to the store whenever you want to print the flow log into the console (the store is exposed as a global variable to allow playing with it).
for example
npm run counter
// execute some actions
// in the console type this
store.dispatch({type: 'LOG_EFFECT'})
Below a sample snabpshot

Another snapshot from the shopping-cart example

waouuuh I like the way you display the race :)
Happy you liked it! It took some tweaks but chrome console api is really awesome
This is amazing stuff! I've recently started to look at Redux, didn't like redux-thunk, found redux-saga, started thinking about how to make sagas more explicit, and here we are!
What I would love to see is a way to fully recreate the state of an app, not just the store/view, but also the implicit state of sagas.
Is the idea here that eventually you could do this by replaying the event stream, and whenever a saga calls a function, automagically turn that into a take on the event representing the effect being resolved?
I currently have an application that has all asynchronous logic in sagas. The problem I ran into is that the saga middleware spits out a huge amount of EFFECT_RESOLVED and EFFECT_TRIGGERED actions. This makes it hard to analyse the application state over time in the regular devtools. Any solution for this (maybe muting a way of muting these actions?).
@jfrolich I'm interested in this too.
Maybe we could mark some actions as being verbose, and the devtools could have a checkbox to only display those when we really want to? @gaearon ?
To be fair we already let you mute certain actions with https://github.com/zalmoxisus/redux-devtools-filter-actions. But I’m open to offering a built-in approach.
Awesome Dan, I should have checked for a solution before posting. Cheers! One thing it does not solve is for instance if we also have a logger. For my quite simple app with sagas it is spitting out 1k actions. Maybe there is a way to reduce it?
@jfrolich redux-logger has a predicate in the config object to drop or print the given action: https://github.com/fcomb/redux-logger#options
Great work so far! I wonder how difficult it would be to transform the sagaMonitor to an actual DevTools monitor.
Some sagas appear as "unknown" in SagaMonitor. Anything I can do about that?
@yelouafi Would you be interested in releasing https://github.com/yelouafi/redux-saga/blob/master/examples/sagaMonitor/index.js as a separate module on npm?
@davej Yes but unit tests are needed before making a separate release
@pke (sorry for the late answer) Those are either sagas called directly without the declarative form yield saga() instead of yield call(saga) or either using yield takeEvery(...) instead of yield* takeEvery(...) (the later could be fixed thou)
Have you all seen https://github.com/skellock/reactotron ?
@GantMan reactotron doesn't support sagas yet though, does it?
I know the guy working on it, and he's planning on making it extendable to support a variety of items. We use Sagas all the time (BECAUSE IT'S AWESOME) so it's on the roadmap.
As of now, just redux, but you can Reactotron.log in your sagas if you're desperate. I just found this UI way more useful than chrome, at this time. I figure the more demand the faster we'll deliver.
is there an example of using sagaMonitor with React Native?
Enhanced the sagaMonitor code to work in Node environment, for universal sagas.
Please take a look: https://github.com/yelouafi/redux-saga/pull/317