Home

Awesome

Redux Remote DevTools for Production

Receive logs/reports from production and get them replicated with Redux DevTools extension (or other monitoring apps). Unlike other solutions (like Remote Redux DevTools), it aims to be optimized for production and suitable for different use cases (see the options). Even though it's designed to be used together with remotedev-server, it can be easily integrated with any other server or serverless architectures.

Installation

npm install --save redux-remotedev

Optionally, install isomorphic fetch polyfill for Node and unsupported browsers:

npm install --save isomorphic-fetch es6-promise

Usage

Just add the store enhancer to your Redux store:

import remotedev from 'redux-remotedev';
import 'isomorphic-fetch'; // include in the main script once
createStore(reducer, remotedev({ sendTo: 'http://localhost:8000' }));

More detailed example:

import { createStore, applyMiddleware, compose } from 'redux';
// import thunk from 'redux-thunk';
import remotedev from 'redux-remotedev';
import 'isomorphic-fetch';
import reducer from '../reducers';

export default function configureStore(initialState) {
  const enhancer = compose(
    // applyMiddleware(thunk),
    remotedev({
      sendTo: 'http://localhost:8000',
      sendOn: 'SOME_ACTION',
      maxAge: 50
    })
  );
  const store = createStore(reducer, initialState, enhancer);
  return store;
}

See also remotedev-server for integrating the server part.

API

remotedev(options)

At least sendTo or sender option should be present.

Enhancer's options

sendTo

string - url of the remote server where logs will be posted.

Example:

createStore(reducer, remotedev({ sendTo: 'http://localhost:8000' }))
sendOn

string or array of strings - action type(s) dispatching of which will trigger sending the history log. Useful for analytics or for triggering sending explicitly (for example, from a report form or when an exception is caught).

Example:

createStore(reducer, remotedev({
  sendTo: 'http://localhost:8000',
  sendOn: 'SOME_ACTION_ERROR' // or array: ['FETCH_ERROR', 'TARGETED_ACTION']
}))
sendOnCondition

function (state, action) - when returns true (the condition is satisfied), the report will be sent. Unlike sendOn, here you can check not only the action type, but the whole action object and also the state object. Another difference is that by default the report will be sent only first time the condition is satisfied. If you want to send it multiple times, set options.sentOnCondition to false in beforeSending function.

Example:

createStore(reducer, remotedev({
  sendTo: 'http://localhost:8000',
  sendOnCondition: (action, state) => state.counter.count === 5,
  // sendOnCondition: (action, state) => state.user.id !== undefined,
  // sendOnCondition: (action, state) => action.user.id !== undefined,
  // sendOnCondition: (action, state) => action.type === 'SOME_ACTION'
}))
sendOnError

boolean - when set to true, will listen for all exceptions from console.error, window.onerror and ErrorUtils (for React Native). When an exception occurs in the app, will send a report including all the details about the exception (also as a Redux action to see the exact failed point when debugging).

sendingStatus

object of functions

Example:

createStore(reducer, remotedev({
  sendTo: 'http://localhost:8000',
  sendingStatus: {
    started(report) {
      console.log('Sending a report', report);
    },
    done(reportId) {
      console.info(
        'The report sent. ' +
        'Open this url to replicate: ' +
        'http://localhost:3000/?remotedev_report=' + reportId
       );
    },
    failed(error, store) {
      console.warn('Report cannot be sent.', error);
      // store.dispatch({ type: 'REPORT_FAILED` });
    }
  }
}))
beforeSending

function(report, sender, options) - called before attempting to send a report, so you can show a dialog and append some data to the report object.

createStore(reducer, remotedev({
  sendTo: 'http://localhost:8000',
  beforeSending(report, send) {
    send({ ...report, title: prompt('Please describe what happened') });
  }
}))
headers

object - custom headers to inject into the sending request additionally to { 'content-type': 'application/json' } (which can be overwritten).

Example:

createStore(reducer, remotedev({
  sendTo: 'http://localhost:8000',
  headers: {
    'content-type': 'application/javascript', // to use JSON-P instead
    'grant_type': 'client_credentials', // some systems may require credentials
    'client123': 'secret123' // custom header `'<Client-Id>': '<Secret>'`
  }
}))
sender

function - custom function used to post the data. Usually, you don't need to specify it. By default fetch function will be used, so make sure to include the polyfill in case you're not targeting for React Native only and want to support all browsers (add import 'isomorphic-fetch' in the consuming code).

Example:

createStore(reducer, remotedev({
  sendTo: 'http://localhost:8000',
  sender: (data, sendTo) => {
    fetch(sendTo, {
      method: 'POST',
      headers: {
        'content-type': 'application/json'
      },
      body: JSON.stringify(data)
    })
  }
}))

You can also just log the data instead of posting (for example when included a monitoring service):

createStore(reducer, remotedev({
  sender: (data) => {
    console.warn(data);
  }
}))
maxAge

number - maximum allowed actions to be stored in the history tree, the oldest actions are removed once maxAge is reached. Default is 30.

To restore the history tree, preloadedState will be added to the log, which represents the state for the committed (removed) action.

In case you want to send all actions from the beginning, set maxAge to Infinity, but it's not recommended as having lots of actions with large payloads can consume a lot of RAM and can cause CPU spikes when serializing the data.

withState

boolean - when set to true, will include also the current state for every action. It's not recommended as the state object can grow too large, and it's better to reconstruct states by recomputing actions.

every

boolean - when set to true, every dispatched action will be posted. It's not recommended as there will be lots of requests and serialization can affect the application's performance significantly.

onlyState

boolean - when set to true, will send only the current state (as preloadedState) without action list. Useful when only reproducing the state without time traveling would be enough. It's recommended for gaining the performance as nothing will be stored for feature posting.

actionsBlacklist

string or array of strings as regex - actions types to be omitted from sending.

Example:

createStore(reducer, remotedev({
  sendTo: 'http://localhost:8000',
  actionsBlacklist: 'SOME_ACTION'
  // or actionsBlacklist: ['SOME_ACTION', 'SOME_OTHER_ACTION']
  // or just actionsBlacklist: 'SOME_' to omit both
}))
actionsWhitelist

string or array of strings as regex - only actions with indicated types will be sent. Use the same as in the example above. If specified, actionsBlacklist is ignored.

actionSanitizer

function which takes the action object and returns it back. Used to sanitize sensitive data (for example credit cards numbers containing in the payload). Also it's used to strip large payloads (like image blobs) in order to make serializing faster.

Example:

createStore(reducer, remotedev({
  sendTo: 'http://localhost:8000',
  actionSanitizer: (action) => (
   action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data ?
   { ...action, data: '<<LONG_BLOB>>' } : action
  )
}))
stateSanitizer

function which takes the state and returns it back. As well as actionSanitizer, it's used to sanitize sensitive data and to strip large payloads.

Example:

createStore(reducer, remotedev({
  sendTo: 'http://localhost:8000',
  stateSanitizer: (state) => state.data ? { ...state, data: '<<LONG_BLOB>>' } : state
}))

Also you can specify alternative values right in the reducer (in the state object) by adding toJSON function:

In the example bellow it will always send { conter: 'sensitive' }, regardless of the state value:

function counter(state = { count: 0, toJSON: () => ({ conter: 'sensitive' }) }, action) {
  switch (action.type) {
    case 'INCREMENT': return { count: state.count + 1 };
    default: return state;
  }
}

You could also alter the value. In the example below when state is { count: 1 }, we'll send { counter: 10 } (notice we don't have an arrow function this time to use the object's this):

function counter(
  state = { count: 0, toJSON: function (){ return { conter: this.count * 10 }; } },
  action
) {
  // ...
}

In case you want to sanitize only specific values, use:

function reducer(
  state = {
    v1: 1, v2: 2, v3: 3, v4: 4,
    toJSON: function (){
      return { ...this, v2: 'sanitized', v4: 'sanitized' };
      // Or return Object.assign({}, this, { v2: 'sanitized', v4: 'sanitized' })
    }
  },
  action
) {
  // ...
}
stringifyReplacer

function or array - a function that alters the behavior of the stringification process, or an array of String and Number objects that serve as a whitelist for selecting the properties of the value object to be included in the JSON string. As a function, it takes two parameters, the key and the value being stringified. Also useful if state is not plain object.

Example (for converting mori data structures):

createStore(reducer, remotedev({
  sendTo: 'http://localhost:8000',
  stringifyReplacer: (key, value) => (
    value && mori.isMap(value) ? mori.toJs(value) : value
  )
}))
Info

Add these optional options for better analytics: title, description, screenshot, version, userAgent, user, meta.

Example:

createStore(reducer, remotedev({
  sendTo: 'http://localhost:8000',
  sendOn: 'SOME_ACTION',
  title: 'Nothing works',
  description: 'It was supposed to be an useless report, but it\'s not ;)',
  screenshot: 'add here an image blob or an url of the stored screenshot',
  version: 'app version to git checkout that release tag',
  appId: 'id to identify the application',
  instanceId: 'id to identify the store in case there are multiple stores',
  userAgent: Platform.Version, // On React Native with: import { Platform } from 'react-native';
  // for browsers userAgent is detected automatically, so no need to specify it explicitely.
  user: {
    id: 'user_id',
    name: 'User Name',
    email: 'user@email',
    photo: 'url or image blob' 
  },
  // or just a string:
  // user: 'user id or user name to identify him',
  meta: 'everything else you want to send'
}))

Exclude the enhancer from development builds

Usually you want to send logs only for production, so you should either use it under a flag excluding from development or you use our helper to have no-op when process.env.NODE_ENV !== 'production':

import remotedev from 'redux-remotedev/productionOnly'

Exclude the enhancer from production builds

If you want to receive logs only from devs, not to affect the performance in production, you can use our helper to have the module stripped when process.env.NODE_ENV === 'production':

import remotedev from 'redux-remotedev/developmentOnly'

LICENSE

MIT

Created By

If you like this, follow @mdiordiev on twitter.