@recruit-tech/react-redux-analytics

Analytics middleware for React+Redux

Usage no npm install needed!

<script type="module">
  import recruitTechReactReduxAnalytics from 'https://cdn.skypack.dev/@recruit-tech/react-redux-analytics';
</script>

README

Build Status

react-redux-analytics

Analytics middleware for React+Redux To send metrics to tracking server (Google Analytics, Adobe Analytics), use corresponding plugins. (I will release them soon)

Requirements

  • React (15.0+)
  • Redux
  • React-Redux

Features

  • Page view tracking with custom variables
  • Custom event tracking with custom variables
  • Map redux state to custom variables

Installation

npm install --save react-redux-analytics

Getting Started

1. Register Middleware

import { applyMiddleware } from 'redux'
import { analyticsMiddleware } from 'react-redux-analytics'
import { analyticsPluginMiddleware } from 'react-redux-analytics-plugin'

const enhancer = applyMiddleware(...,
    analyticsMiddleware({
      reducerName: 'analytics',
      ...
    }),
    analyticsPluginMiddleware({...})
)

2. Register Reducer

import { createStore } from 'redux'
import { analyticsMiddleware } from 'react-redux-analytics'

const store = createStore(combineReducers({
  'analytics': analyticsReducer,
  ...
}), initialState, enhancer);

3. Listen location change from history (optional)

import createHistory from 'history/createBrowserHistory'
import { createLocationSubscriber } from 'react-redux-analytics'

const history = createHistory()
const locationSubscriber = createLocationSubscriber(store)
history.listen((location) => {
  locationSubscriber.notify(location, "replace")
})

4. Track Page View on componentDidMount

import { connect } from 'react-redux'
import { compose } from 'recompose'
import { sendAnalytics } from 'react-redux-analytics'

class NewsPage extends React.Component {
  // ...
}

export default compose(
  connect({ ... }, { ... }),
  sendAnalytics({
    pageName: 'news:top',
    prop10: '/news',
  }),
)(IndexPage)

5. Track Custom Event on Click

import { sendEvent } from 'react-redux-analytics'

class NewsPage extends React.Component {
  // ...
  render(){
    const { dispatch } = this.props
    return(<div>
      ...
      <div className="button" onClick={()=>{
        dispatch(sendEvent({
          events: ['event1'],
          prop30: 'Click me',
          eVar5: 'clicked',
      }}>Click me</div>
      ...
    </div>)
  }
}

API

Higher-order component

sendAnalytics()

SendAnalytics HoC decorates a React component and provides it with the ability to dispatch SEND_PAGE_VIEW action when the component is mounted or updated.

sendAnalytics(options: {
  sendPageViewOnDidMount?: boolean | ((props: Object, state: Object) => boolean);
  sendPageViewOnDidUpdate?: boolean | (( prevProps: Object, props: Object, state: Object) => boolean);
  mapPropsToVariables?: (props: Object, state: Object) => Object;
  onDataReady?:  boolean | ((props: Object, state: Object) => boolean);
  snapshotPropsOnPageView? : boolean | ((props: Object, state: Object) => boolean);
  mixins?: string[];
} & { [key:string]: any }) : (wrapped: React.Component) => React.Component
arguments
options (Object)
  • [sendPageViewOnDidMount] (boolean | ((props: Object, state: Object) => boolean)) If true, the decorated component dispatches SEND_PAGE_VIEW at componentDidMount. If the argument is a function, the function is evaluated at componentDidMount lifecycle and the action will be dispatched only if the result value is true. Default value: true

  • [sendPageViewOnDidUpdate] (boolean | (( prevProps: Object, props: Object, state: Object) => boolean)) Similar to the above sendPageViewOnDidMount. The component will dispatches SEND_PAGE_VIEW at componentDidUpdate if the value is true or the result value of function is true. Default value: false

  • [mapPropsToVariables] ((props: Object, state: Object) => Object) Accepts a function that maps props and redux state to tracking variables when the component dispatches SEND_PAGE_VIEW. The mapped tracking variables are injected in variables payload of SEND_PAGE_VIEW. Default value: undefined

  • [onDataReady] (boolean | ((props: Object, state: Object) => boolean)) If a function is specified, the component will delay the dispach of SEND_PAGE_VIEW until the result value of the function becomes true. The function is first evaluated just after sendPageViewOnDidMount and sendPageViewOnDidUpdate is evaluated to be true. If the the result of this function is true, the component dispatches action immediately. Otherwise, the function is evaluated at every componentWillReceiveProps lifecycle until the result finally becomes true. (Note: Evaluation of any succeeding sendPageViewOnDidUpdate is skipped if the previous SEND_PAGE_VIEW dispatch is delayed and the result of corresponding onDataReady evaluation has not yet been true ) Default value: true

  • [snapshotPropsOnPageView] (boolean | ((props: Object, state: Object) => boolean)) Evaluated everytime the component dispatches SEND_PAGF_VIEW. If the result is true, it dispatches PAGE_SNAPSHOT_PROPS to save the props (at the moment) in the page-scope of redux store. (Under [store root] -> [analytics scope] -> [page] -> [snapshot]) Default value: false

  • [mixins] (string[]) Accpets an array of string to white-list keys of page-scope variables, global variables in the redux store, and global mapped variables. This array is merged with pageviewMixins in analyticsMiddleware, forming the concluding white-list. The analyticsMiddlware injects whilte-listed variables into variables of SEND_PAGE_VIEW payload. Default value: []

  • [staticVariables] ({ [key: string]: any }) The rest properties are regarded as static variables that are injected into everySEND_PAGE_VIEW payload. Default value: {}

Example
//`/src/components/NewsPageArticle/index.js'
import { connect } from 'react-redux'
import { compose } from 'recompose'
import { sendAnalytics } from 'react-redux-analytics'
import NewsPageArticleComponent from './presentation' // presentational component

export default compose(
  connect((state, { index })=>({
    loaded: !state.article.loaded,
    articleIndex: index,
    pageIdx: state.pager.index,
    title: state.article.body.title,
    author: state.article.body.author,
    conent: state.article.body.content,
  }),
  sendAnalytics({
    sendPageViewOnDidMount: true, // always send PageView when component is mounted
    sendPageViewOnDidUpdate: (prevProps, props) => prevProps.articleIndex !== props.articleIndex, // send pageView only when readying article is changed. (not when pager is changed)
    onDataReady: (props) => props.loaded, // send PageView when the article is loaded from server
    mapPropsToVariables: (props) => ({
      'prop5': props.title,
      'prop10': props.articleIndex,
      'eVar9': props.author,
    }), // dyanamic variables to be sent with PageView
    mixins: ['evar30', 'eVar31'], // variables saved in Redux store and to be sent with PV
    pageName: 'ArticlePage', // static variable to send with PV
    channel: 'news',         // static variable to send with PV
}),
)(NewsPageArticleComponent)

Redux Middleware

analyticsMiddleware()

Middleware that transform SEND_PAGE_VIEW and SEND_EVENT action before they reaches to the reducer or plugin middlewares. It extends variables property of the payload of each action by injecting values that come directly from analytics state of redux store, and the values that are mapped from entire redux store. The middleware can also override location property of SEND_PAGE_VIEW payload, and eventName property of SEND_EVENT payload.

analyticsMiddleware(options : {
  reducerName?: string;
  pageViewMixins?: string[];
  eventMixins?: string[];
  mapStateToVariables?: (state: Object) => Object;
  getLocationInStore?: (state: Object) => Object;
  composeEventName?: (composedVariables: Object, state: Object) => string | null;
  suppressPageView?: (state: Object, transformedAciton: Object) => boolean,
}) : (store: Redux.Store) => (next: function) => (action: Object) => void
arguments
options (Object)
  • [reducerName] (string) Specifies the name of this middleware's reducer ( that is the same as the name of analytics state in the redux store). This value has to be equal to the alias that you have guven toanalyticsReducer incombineReducers option. Default value: 'analytics'

  • [pageViewMixins] (string[]) Accpets an array of string. This array is merged with mixins in SEND_PAGE_VIEW payload, forming the final white-list of variables. The middleware selects white-listed variables from analytics state (that are consisted of page-scope variables and global-scope variables) and mapped variable (generated by mapStateToVariables), and then injects them into variables of SEND_PAGE_VIEW payload. Default value: []

  • [eventMixins] (string[]) Similar to pageViewMixins. This property is merged with mixins in SEND_EVENT payload, and is used to extend variables property of SEND_EVENT payload. Default value: []

  • [mapStateToVariables] ((state: Object) => Object) Accepts a function that maps the state of entire redux store to variables being used to extend variables property of SEND_PAGE_VIEW and SEND_EVENT payload. Only values whose key are included in the mixin white-list are injected to the payload. Default value: () => ({})

  • [getLocationInStore] ((state: Object) => Object) Accepts a function that maps the state of entire redux store to a location Object that is passed to reducer and plugin middlewares by SEND_PAGE_VIEW action. The middleware overrides the location property of the payload with the result of the function only if location property of the original payload is null and the return value is NOT null. Default value: () => null

  • [composeEventName] ((composedVariables: Object, state: Object) => string | null) If a function is specified, the middleware use the return value of the function to override eventName property of SEND_EVENT payload. The function takes two arguments: copmosedVariables (1st argument) that are resulting variables composed of original payload variables and injected values, and state (2nd argument) that are the state of entire redux store. Like getLocationInStore, the middleware overrides property only if original property is null and the output value is NOT null. Default value: () => null

  • [suppressPageView] ((state: Object, transformedAction: Object) => boolean) If a function is specified, analyticsMiddleware evaluates it just before passing a transfromed SEND_PAGE_VIEW action to the next middleware. If the return value is false, the action is discarded in this middleare and thus not be received by the plugin middlware (if it is properly placed after this one). This option is only useful if you cannot control dispatches of SEND_PAGE_VIEW by the lifecycle hooks of sendAnalytics. Default value: () => false

Example
//'/src/redux/createStore.js'
import { createStore, combineReducers, applyMiddleware } from 'redux'
import {analyticsReducer, analyticsMiddleware} from 'react-redux-analytics'
import { analyticsPluginMiddleware } from 'react-redux-analytics-plugin'
import { routerReducer, routerMiddleware } from 'react-router-redux'
import { articleReducer }  from './reducers'

const enhancer = applyMiddleware(
  routerMiddleware({
    /* ...routerMidlewareOptions, */
  }),
  analyticsMiddleware({
    reducerName: 'analytics', // (1) has to be equalt to the key to specify apnalyticsReducer
    pageViewMixins: ['prop40', 'prop41'], // custom variables that are always sent with page view
    eventMixins: ['pageName', 'channel'], // custom variables that are always sent with custom events
    mapStateToVariables: (state) => ({  //set router state to page view variables
        prop40: state.routing.locationBeforeTransitions.pathname,
        prop41: state.routing.locationBeforeTransitions.search,
    }),
    getLocationInStore: (state) => state.routing.locationBeforeTransitions, //use router state instead of window.location
    composeEventName: (composedVariables, state) => `${composedVariables}/${composedVariables.events.join(',')}`, //generate eventName automatically from pathname and custom event list
  }),
  analyticsPluginMiddleware({
    /* ...pluginOptions, */
  }),
)

export default (initialState = {}) => createStore(combineReducers({
  analytics: analyticsReducer, // the key to analyticsReducer has to be the same as 'reducerName' in analyticsMiddleware option
  article: articleReducer,
  routing: routerReducer
}), initialState, enhancer)

Utils

createLocationSubscriber()

Factory of a helper object (LocationSubscriber) that you can use with react-router. locationSubscriber subscribes changes of location (, which must implement at least [pathname, search, hash] properties of window.location interface) from the routing framework, and changes page state of analytics middlewares in the redux store. This helper is only useful if you want to utilize this middleware's complex page-scope variables management and historical page stack mechanism.

createLocationSubscriber(options: {
  dispatch: (action: Object) => void;
}): { notify: (location: Object, action: 'pop'|'push'|'replace') => void }

arguments
options (Object)
  • dispatch (function) Accepts the dispatch function of Redux store.
return
locationSubscriber (Object)

An object that has only one method notify that subscribes changes of location from a routing framework.

  • notify() If called, one of LOCATION_PUSH, LOCATION_POP, LOCATION_REPLACE actions is dispatched depending on action argument of this callback.

  • location (Object) *required Specifies a location object that specifies the location (pathname, search, hash) of next page.Supposed to be informed by the routing framework.

  • [action] ('pop'|'push'|'replace') Accepts one of 'pop', 'push', or 'replace'. If 'pop' is specified, the locationSubscriber dispatches LOCATION_POP. And 'pop' for LOCATION_PUSH, 'replace' for LOCATION_REPLACE.

Example
// '/src/app.js'
import createHistory from 'history/createBrowserHistory'
import { createLocationSubscriber } from 'react-redux-analytics'
import createStore from './redux/createStore' //described in above example

const store = createStore({})
const history = createHistory()
const locationSubscriber = createLocationSubscriber(store);

//set initial location
locationSubscriber.notify(history.location, 'pop')

// Listen for changes to the current location.
const unlisten = history.listen((location, action) => {
  locationSubscriber.notify(location, action);
  // if you do not need to use variable history, just keep 'replace' the page stack as follows
  // locationSubscriber.notify(location, 'replace')
});

Action Creators

The following action creators can be used to manually send tracks or to control the middleware's state.

sendPageView()

Creates SEND_PAGE_VIEW action. The payload of SEND_PAGE_VIEW is transformed in analyticsMiddlware, and the transformed action is then relayed to plugin middlewares that finally creates server request to the tracking server. (Adobe analytics, GA, ...)

sendPageView(
  location: Object = null,
  mixins: string[] = [],
  variables: { [key: string]: any } = {},
): Redux.Action
arguments
  • [location] (Object) Accepts a window.location like object (specs is described above). If null is passed, the analyticsMiddleware tries to override the property by page.location property of the state, or the result of getLocationInStore option of the middleware. (by the respective order) Default value: null

  • [mixins] (string[]) Same as the mixin property of sendAnalytics option. If an array of string is specified, it is merged with pageViewMixins in analyticsMiddleware option to form a white-list of variables to be injected byanalyticsMiddlware. Default value: []

  • [variables] ({ [key: string]: any }) Specifies an object in which key is the name of a variable, and the value is the value assigned to the variable. These variables are merged with the injected variables by analyticsMiddleware and is sent with a PageView track. Default value: {}

sendEvent()

Creates SEND_EVENT action. Like SEND_PAGE_VIEW, analyticsMiddleware transforms the payload of this action and relays it to plugin middlewares.

sendEvent(
  variables: { [key: string]: any} = {},
  mixins: string[] = [],
  eventName: string = null,
): Redux.Action
arguments
  • [variables] ({ [key:string]: any}) Specifies variables to be sent with SEND_EVENT in just the same manner as sendPageView. The variables are merged with injected ones by analyticsMiddleware and sent with an custom event track. (Custom link track in Adobe analytics) Default value: {}

  • [mixins] (string[]) Together with eventMixins in analyticsMiddleware, this array of string forms a white-list of variables that are injected to variables payload of resulting SEND_EVENT action. Default value: []

  • [eventName] (string) An unique name that is used to distinguish or aggregate custom events by the tracking server. If not specified, analyticsMiddleware tries to override the value by the result of composeEventName function specified at the middleware's option.

pushLocation()

Creates a LOCATION_PUSH action that pushes the current page-scope state to the top of internal page stack, and creates a new page-scope state.

pushLocation(
  location: Object,
  inherits: (boolean | string[]) = false,
  variables: {[key:string]: any} = {}
)
arguments
  • location (Object) *required Specifies a window.location like object. This value is stored in page-scope state of the middleware, and overrides the result of getLocationInStore property of anallyticsMiddleware option.

  • [inherits] (boolean | string[]) Accepts a boolean value or a string array. If true, variables property of the page-scope state is copied to the new page-scope state. If a string array is specified, only the variables whose name is included in the array are copied. Otherwise, the new page-scope state is created with a blank ({}) variable set. Defaul value: false

  • [variables] ({[key:string]: any}) If specified, the object is used as the initial value of variables property of new page-scope state. If inherits is not false, the inherited variables will be merged with this. Default value: {}

popLocation()

Creates a LOCATION_POP action that discards the current page-scope state and restore the one from the top of stack.

popLocation()

replaceLocation()

Creates a LOCATION_REPLACE action. This action discards the current page-scope state, and creates a new page-scope state.

replaceLocation(
  location: Object,
  inherits: (boolean | string[]) = false,
  variables: {[key:string]: any} = {}
)
arguments
  • location (Object) *required Same as location in pushLocation

  • [inherits] (boolean | string[]) Same as inherits in pushLocation

  • [variables] ({[key:string]: any}) Same as variables in pushLocation

updateGlobalVariables()

Creates a GLOBAL_VARIABLES_UPDATE action that sets or updates variables property of global-scope state, which is created with {} value when reducer is initialized, and is persistent over LOCATION_CHANGE, LOCATION_POP, LOCATION_REPLACE actions.

updateGlobalVariables(
  variables: {[key: string]: any},
)
arguments
  • variables ({[key:string]: any}) *required Specifies an object to update the variables property of global-scope state. If a variables with the same key already exists in the state, the value is overriden by the one specified in the argument.

clearGlobalVariables()

Creates a GLOBAL_VARIABLES_CLEAR action, that reset variables property of global-scope state to {}.

clearGlobalVariables()

updatePageVariables()

Creates a PAGE_VARIABLES_UPDATE action. Like GLOBAL_VARIABLES_UPDATE, this action sets or updates variables in Redux store. But they are stored in the variables propety of page-scope state, so they will be cleared when any of LOCATION_PUSH, LOCATION_POP, LOCATION_REPLACE actions is dispatched.

updatePageVariables(
  variables: {[key: string]: any},
)
arguments
  • variables ({[key:string]: any}) *required Specifies an object to be stored in page-scope state, just like variables argument in globalVariablesUpdate() to page-scope state.

clearPageVariables()

Creates a PAGE_VARIABLES_CLEAR action that resets the variables property of page-scope state. This is the counterpart to clearGlobalVariables(), as updatePageVariables() is to updateGlobalVariables().

clearPageVariables()