Using Redux Middleware for Simpler Analytics and Event Tracking
..
Tracking events across your application can sometimes be a pain. Whether they are Google Analytics, Sentry errors, or BigQuery tracking, the practice I’ve seen many times is to place trackThisEvent(..args) calls all around the code. This isn’t a terrible solution, especially in smaller codebases. But it can have issues when something changes with the trackThisEvent definition, or if you need to pass in more data into each call like userId or some other arbitrary value, you’ll be stuck going through 100’s of files to make updates.
Use Redux Middleware
The great thing about using redux is that all of your actions are defined with your reducers, which are then available to the entire application. Whenever an action is dispatched, your combined store is updated. You probably have a setup similar to this:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import AppContainer from 'components/app-container';
import { createStore, combineReducers } from 'redux';
import * as reducers from 'store/ducks/index';
const rootReducer = {
...reducers,
// probably more reducers, like from the router
};
const store = createStore(combineReducers(rootReducer));
ReactDOM.render(
<Provider store={store}>
<AppContainer />
</Provider>,
document.getElementById('root')
);
That store object has your entire combined state. Before your changes reach the reducer and become available to the store, they can go through Middleware for any transformations you need.
From Redux (https://redux.js.org/advanced/middleware) - It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer. People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more.
Here’s what a simple middleware function looks like for tracking a page view event:
const trackingMiddleware = (store: Store<any>) => (next: Dispatch<any>) => (action: Action) => {
if (action.type === '@@router/LOCATION_CHANGE') {
const nextPage = `${action.payload.pathname}${action.payload.search}`;
trackPage(nextPage, store.getState());
}
return next(action);
};
function trackPage(page: string, state: any) {
ReactGA.set({
page,
...options,
});
ReactGA.pageview(page);
}
In the above example, whenever the route changes and a @@router/LOCATION_CHANGE event is dispatched, the trackPage method is called with the page we’re navigating to, and the current state store.getState(). For just tracking page views, there’s not too much more you would need to do. What if you want to track more than just page views?
Something we’ve been trying out is storing all events that we’d like to track in a single place. An event-tracking file that exports an object with action types and associated data to go along with the event.
const events = {
'AUTH/LOGIN_SUCCESS': {
category: 'auth',
action: 'login',
eventDataOne: 'user.name',
},
'AUTH/INVALID_LOGIN_ATTEMPT': {
category: 'auth',
action: 'invalid login attempt',
eventDataOne: 'user.email',
},
};
export default events;
In the example above, the category and the action static strings for these events. The eventDataOne however takes a path that I can use to retrieve data from the state.
import eventTracking from './events';
import { createStore, combineReducers, applyMiddleware } from 'redux';
const trackEvent = (properties, state) => {
ReactGA.event(properties); // Track event to google analytics
// Here we can get extra data from our state to attach to the event.
const bigQueryRecord = createRecord(buildRecordParam(properties, state));
postBigQueryEvent(bigQueryRecord); // Track event to BigQuery
};
const trackingMiddleware = store => next => action => {
if (action.type === '@@router/LOCATION_CHANGE') {
const nextPage = `${action.payload.pathname}${action.payload.search}`;
trackPage(nextPage, store.getState());
}
if (eventTracking[action.type]) {
trackEvent(eventTracking[action.type], store.getState());
}
return next(action);
};
// Apply the middleware like this
let myApp = combineReducers(reducers);
let store = createStore(myApp, applyMiddleware(trackingMiddleware));
Now we can track any event we dispatch in our application without having to place trackEvent calls all over our reducers. For tracking new events you just need to update your event tracking objects.
I hope this helps anyone who needs to setup event tracking in an app that uses Redux. Feel free to post any questions you may have in the comments. Thanks!