redux-sagaのタスクをモニタリングする

## はじめに
[redux-saga](https://redux-saga.js.org/)のタスクをモニタリングする仕組みが用意されていたので、その紹介と実際に使用した時の話をまとめました。

[公式APIリファレンス](https://redux-saga.js.org/docs/api/#sagamonitor)

[前回の記事](https://qiita.com/r-nouchi/items/8a0bbb078114959c3a02)でeffectの挙動を調べるためにforkするすべてのタスクにログ出力を仕込んでいましたが、この仕組みを利用することでわざわざ全部のタスクにログ出力を仕込む必要がなくなりました。
今回の記事では、モニターの挙動を確認するために前回のサンプルを使用しています。

## TL;DR
- redux-sagaでは、タスクのモニタリングのためのインターフェースが用意されている。
- モニターはタスクのTriggeredやResolvedなどにイベントにhookするMiddlewareとして実装する。
- 公式のサンプルが利用できる他、自作のモニターも利用できる。

## 今回使用したバージョン
- redux-saga: 1.0.5
- @redux-saga/simple-saga-monitor: 1.0.2

## 公式のサンプルを使用する
公式のサンプルはこちら[@redux-saga/simple-saga-monitor](https://www.npmjs.com/package/@redux-saga/simple-saga-monitor)

インストールする

```shell
$ npm i @redux-saga/simple-saga-monitor
```

`createSagaMiddleware()`の引数に渡す

```store.js
import { createStore, applyMiddleware } from "redux"
import rootReducer from "./reducer"
import rootSaga from "./saga"
import createSagaMiddleware from "redux-saga"
import simpleSagaMonitor from "@redux-saga/simple-saga-monitor"

const sagaMiddleware = createSagaMiddleware({sagaMonitor: mySagaMonitor})
const store = createStore(
  rootReducer,
  applyMiddleware(sagaMiddleware),
)

sagaMiddleware.run(rootSaga)

export default store
```

以上で準備は完了です。簡単ですね。

このモニターは通常は特に何も出力しません。以下の画像は`takeEvery`ボタンを1回押した状態です。
![スクリーンショット 2019-08-31 12.04.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/f7e1563f-bbdf-06ad-76c0-cbaba1db8eb7.png)

モニタリングの情報を出力したい場合は、デバッグコンソール上で`$$Logsagas()`を実行します。
![スクリーンショット 2019-08-31 12.06.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/dd456eb9-6d63-ef3a-415c-93f5655108ef.png)


ブラウザでアプリのページにアクセスしてから起動したsagaのタスクが階層構造で出力されています。
どのタスクがどんなタスクを起動して、それらがどう終了したかなどがわかる様になっています。


## モニターを自作する
公式のサンプルがredux-sagaのバージョンの違いで使用できなかったり、そんなに機能が必要なかったり、逆に機能を追加したい！などの場合は自分でモニターを作ることができます。
以下のモニターは、公式のサンプルを参考にしてタスクのイベントがdispatchされた時に単純にログ出力するだけのモニターとして作成したものです。
使用方法は公式のサンプルと同様、`createSagaMiddleware()`の引数に渡すだけです。


```store.js
import { createStore, applyMiddleware } from "redux"
import rootReducer from "./reducer"
import rootSaga from "./saga"
import createSagaMiddleware from "redux-saga"
import simpleSagaMonitor from "@redux-saga/simple-saga-monitor"
import mySagaMonitor from "./saga/monitor/sagaMonitor"

const sagaMiddleware = createSagaMiddleware({sagaMonitor: mySagaMonitor})
const store = createStore(
  rootReducer,
  applyMiddleware(sagaMiddleware),
)
sagaMiddleware.run(rootSaga)

export default store
```


```saga/monitor/sagaMonitor.js
const effects = []
const taskStatus = {
  PENDING: "PENDING",
  RESOLVED: "RESOLVED",
  REJECTED: "REJECTED",
  CANCELLED: "CANCELLED",
}

const rootSagaStarted = (desc) => {
  effects[desc.effectId] = {
    ...desc,
    status: taskStatus.PENDING,
    start: Date.now(),
  }
  console.log("Root saga started:", desc.saga.name || "anonymous", desc.args)
}

const effectTriggered = (desc) => {
  effects[desc.effectId] = {
    ...desc,
    status: taskStatus.PENDING,
    start: Date.now(),
  }
  console.log("Saga monitor: effectTriggered:", desc)
}

const effectResolved = (effectId, result) => {
  resolveEffect(effectId, result)
  console.log("Saga monitor: effectResolved:", effects[effectId])
}

const effectRejected = (effectId, error) => {
  rejectEffect(effectId, error)
  console.log("Saga monitor: effectRejected:", effects[effectId])
}

const effectCancelled = (effectId) => {
  cancelEffect(effectId)
  console.log("Saga monitor: effectCancelled:", effects[effectId])
}

const calcEffectDuration = (effect) => {
  const now = Date.now()
  Object.assign(effect, {
    end: now,
    duration: now - effect.start,
  })
}

const resolveEffect = (effectId, result) => {
  const effect = effects[effectId]
  calcEffectDuration(effect)
  effect.status = taskStatus.RESOLVED
  effect.result = result
}

const rejectEffect = (effectId, error) => {
  const effect = effects[effectId]
  calcEffectDuration(effect)
  effect.status = taskStatus.REJECTED
  effect.error = error
}

const cancelEffect = (effectId) => {
  const effect = effects[effectId]
  calcEffectDuration(effect)
  effect.status = taskStatus.CANCELLED
}

// Export the `sagaMonitor` to pass to the middleware.
export default {
  rootSagaStarted,
  effectTriggered,
  effectResolved,
  effectRejected,
  effectCancelled,
  actionDispatched: () => { },
}
```

ページを読み込んだ時点で結構ログ出力されています。ルートSagaが起動したタスクですね。
![スクリーンショット 2019-08-31 20.15.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/4d77e1ba-add0-ae68-8d85-853e1e7f1a2b.png)

試しにtaleLatestのボタンを押してみましょう。
![スクリーンショット 2019-08-31 20.15.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/2a816340-9d62-2adb-9ce6-c6cdd2eda29e.png)

うーん、ゴチャゴチャしてちょっとわかりにくような‥？出力する内容は必要に応じて絞った方が良さそうですね。
ログ出力からtakeLatestの動きは確認できるかと思います。

## まとめ
以上、redux-sagaのタスクをモニタリングする方法として公式のサンプルを使用する場合と自分で作成したモニターを作成する場合のふた通りの方法を紹介しました。
こういった仕組みが用意されていると、デバッグがしやすかったりAPIの使用が理解しやすかったりと使う側として非常に助かります。

また、モニター側に開発時に必要なログ出力を集約させることでプロダクションコードを綺麗に保つことができる副次効果もありました。
環境変数を読み取って、開発環境でのみモニターを使用するように実装すればなおいいかと思います。