Reactotron: Redux Saga Monitor

Created on 8 Nov 2016  路  13Comments  路  Source: infinitered/reactotron

I am working on starting a React Native app that will rely heavily on Redux Saga and would love to see Reactotron be a bit more involved with testing when it comes to Sagas. I am playing around with the Redux SagaMonitor that is included in the examples folder of the repo and am considering how this might be able to be incorporated into Reactotron. Currently I have it printing to the console which means I have to have Remote Debugging turned on. I feel like this could be a great opportunity for an additional screen in Reactotron.

Ref: https://github.com/yelouafi/redux-saga/blob/master/examples/sagaMonitor/index.js

Thoughts?

Most helpful comment

@skellock nice work!.

As a small improv. you can replace the CALL,PUT, ... with a short descriptive message like call(myfn), put(MY_ACTION). Below is a function I used in my currently unfinished saga visualizer

import { is, asEffect } from 'redux-saga/utils'

/* eslint-disable no-cond-assign */
export function getEffectName(state, effectId) {

  const effect = state.effectsById[effectId].effect

  if(effect.root) {
    return `${effect.saga.name}`
  }

  let data
  if((data = asEffect.take(effect))) {
    return `take(${data.pattern || 'channel'})`
  }
  else if((data = asEffect.put(effect))) {
    return `put(${(data.channel ? data.action : data.action.type)})`
  }
  else if((data = asEffect.call(effect))) {
    return `call(${data.fn.name})`
  }
  else if((data = asEffect.cps(effect))) {
    return `cps(${data.fn.name})`
  }
  else if((data = asEffect.fork(effect))) {
    const type = data.detached ? 'spawn' : 'fork'
    return `${type}(${data.fn.name})`
  }
  else if((data = asEffect.join(effect))) {
    return `join(${data.name})`
  }
  else if((data = asEffect.cancel(effect))) {
    return `cancel(${data.name})`
  }
  else if(is.array(effect)) {
    return 'parallel'
  }
  else if((data = asEffect.race(effect))) {
    return 'race'
  }
  else if((data = asEffect.select(effect))) {
    return `select(${data.selector.name})`
  }
  else if((data = asEffect.actionChannel(effect))) {
    return `actionChannel(${data.pattern})`
  }
  else if((data = asEffect.cancelled(effect))) {
    return 'cancelled'
  }
  else if((data = asEffect.flush(effect))) {
    return 'flush(buffer)'
  }
  else if(is.iterator(effect)) {
    return effect.effect.name
  }
  else {
    return String(effect)
  }
}

All 13 comments

Absolutely.

I've already started on this too. There's a few issues with surfacing the information out of redux-saga though.

I plan on asking @yelouafi once I'm able to formulate the right question to ask.

My problem specifically is knowing that a generator is finished.

I know when an effect starts, and there's a id that i can use to track back a nested effect.

But if a generator gets kicked off via a takeLatest(), that becomes a fork signal on take that ends immediately. Even before the list of sub effects finish.

I get that within a while(take()), the function never "finishes", but I feel like in the case of takeLatest(), it'd be nice for the logging system to know the triggering generator is done.

With that mechanism in place, I could move on to the visualization part (which I do have a few ideas for).

He's also working on a visualizer (https://github.com/yelouafi/redux-saga/pull/609) too, so maybe he's encountering this too?

Or maybe I'm just holding it wrong?

It's probably the latter.

@skellock
When a saga is forked (or spawned) the method monitor#effectResolved(id, result) is invoked with a Task as the result. In the monitor example you can see how the end of the task is tracked (via chaining a handler to the done promise and reeinvoking resolveEffect again). Similarly there is an effectCancelled method that is invoked when a saga (or any other effect) is cancelled.

Perhaps I dont quite understand the issue with takeLatest but as it's just a helper it'll fire the sequence of the individual effects inside it which will trigger a sequence of monitor callbacks (effectTriggered, effectResolved/effectRejected/effectCancelled).

But feel free to ask any question. I'll be glad to help.

OOOoooooooooh.

^ The sound of a 1,000 dim lightbulbs flickering in my head right now.

So, the effortResolved for the fork is when I start track this. Ok. I totally missed this.

I was under the impression that the parentId was the key to reassembling the call. And it is... but it was just the timing of that fork'd resolving that threw me for a loop.

Will try tonight.

You're awesome.

@skellock if you need any help on any front bringing this to life for Reactotron let me know!

If it could be any help, here is how the monitor prints the visual effect tree.

The monitor example store all incoming effects in an effectsById map. Effects are added on effectTriggered calls, and updated on effectResolved/Rejected/Cancelled calls

The effect tree is constructed on demand in the logEffectTree function (which is called to print the visual effect tree on the console)

@skellock do you have your work thus far up somewhere on this? I would be interested in kicking it around if so.

Alrighty. Getting closer now. I understand how to track sagas now. Or at least the ones that are cleanly separated by forks (like takeLatest and takeEvery). Baby steps no doubt.

I just pushed this into the redux-saga branch https://github.com/reactotron/reactotron/commit/b2ef0e4ed42e8e1e6bf6037a298eaef341566178

This line of code was the key to putting this together... https://github.com/reactotron/reactotron/commit/b2ef0e4ed42e8e1e6bf6037a298eaef341566178#diff-4d88dcdc6dc5e7615822c35c4fd8784bR115

Thanks for helping me understand this.

So,

At this point, it's just a big dump of data. Looks ugly of course.

image

I think we're ready to start talking better visualizations now.

Placing an entry into the timeline (like in that picture) would be the first stop. The most important pieces of info (as I see it is): Triggered by, status (resolved, rejected, cancelled), and a nested effect tree (with duration).

I'd love to see a brand new tab for saga monitoring though (down the road). There's a bunch of awesome ways to view this stuff.

  • A tree graph
  • gantt chart
  • a simple list PENDING effects

Man.

Looking good @skellock. I think for the super short term dumping that JSON object to the timeline is helpful as it does give you access to the information. May I ask what triggers it to dump to the timeline? I am going to take a look at the code this morning.

As far as the longer term - I would love to see it as its own tab so the display of it could have more freedom. I could see some sort of visualization that shows the current state of all the sagas but you would probably want to still be able to see resolved sagas somehow. My concern with showing all the resolved sagas is that could become a ton of data on the tab in a large application.

A cleaned up version. Going to wrap things up & ship that today or tomorrow once I do a bit more testing.

image

@rmevans9 The SagaMonitor harvests and ships the data, and the SagaTaskCompleteCommand draws it.

That looks really good! I can't wait to play with it.

@skellock nice work!.

As a small improv. you can replace the CALL,PUT, ... with a short descriptive message like call(myfn), put(MY_ACTION). Below is a function I used in my currently unfinished saga visualizer

import { is, asEffect } from 'redux-saga/utils'

/* eslint-disable no-cond-assign */
export function getEffectName(state, effectId) {

  const effect = state.effectsById[effectId].effect

  if(effect.root) {
    return `${effect.saga.name}`
  }

  let data
  if((data = asEffect.take(effect))) {
    return `take(${data.pattern || 'channel'})`
  }
  else if((data = asEffect.put(effect))) {
    return `put(${(data.channel ? data.action : data.action.type)})`
  }
  else if((data = asEffect.call(effect))) {
    return `call(${data.fn.name})`
  }
  else if((data = asEffect.cps(effect))) {
    return `cps(${data.fn.name})`
  }
  else if((data = asEffect.fork(effect))) {
    const type = data.detached ? 'spawn' : 'fork'
    return `${type}(${data.fn.name})`
  }
  else if((data = asEffect.join(effect))) {
    return `join(${data.name})`
  }
  else if((data = asEffect.cancel(effect))) {
    return `cancel(${data.name})`
  }
  else if(is.array(effect)) {
    return 'parallel'
  }
  else if((data = asEffect.race(effect))) {
    return 'race'
  }
  else if((data = asEffect.select(effect))) {
    return `select(${data.selector.name})`
  }
  else if((data = asEffect.actionChannel(effect))) {
    return `actionChannel(${data.pattern})`
  }
  else if((data = asEffect.cancelled(effect))) {
    return 'cancelled'
  }
  else if((data = asEffect.flush(effect))) {
    return 'flush(buffer)'
  }
  else if(is.iterator(effect)) {
    return effect.effect.name
  }
  else {
    return String(effect)
  }
}

Damn, that's fantastic! Function names are now showing up. Didn't even know that was possible.

Thank you!

this is epic

Was this page helpful?
0 / 5 - 0 ratings

Related issues

nonameolsson picture nonameolsson  路  5Comments

Eyesonly88 picture Eyesonly88  路  4Comments

tolu360 picture tolu360  路  5Comments

lndgalante picture lndgalante  路  4Comments

scally picture scally  路  5Comments