Recently I was tasked with adding Google Analytics to a React/Redux app for tracking an open-source project that my team at work built. While looking into it, I realized that integrating Redux and Google Analytics could be a confusing topic.
Why it’s complex
- There are a ton of packages on npm that claim to add analytics to a Redux app. These packages are at best unnecessary — at worst they will make this task more complicated than it needs to be.
- Google Analytics may not be loaded the same way as your other scripts (included via npm). This is a package that you want to show up even if your bundle fails, so in many instances it *might* be worthwhile to load it by including the standard HTML script tag that Google Analytics provides.
The requirements
- control over the actions that are tracked
- control over the payloads that are being sent with those actions
- separation of concerns — where the files containing your existing actions and middleware do not have random tracking code in them
A solution
There are two good ways to add analytics to a Redux app. The first involves making analytics calls from your existing async middleware (redux-saga, redux-observable, etc.) and the second involves writing a custom middleware. The differences between these solutions are somewhat cosmetic - meaning the choice depends on the question “how much do you enjoy leaning on your current async middleware?”
Although I prefer using redux-saga or redux-observable, for the purpose of this article I will touch on writing custom middleware. Sounds hard? Well, it isn’t. A middleware is just one single function that looks like this:
const analyticsMiddleware = store => next => action => {
if (window.gtag) {
window.gtag('event', 'action', {
event_category: action.type,
event_label: JSON.stringify(action.payload || {})
});
}
return next(action);
};export default analyticsMiddleware;
Here is a slightly more complicated form of the same function that gives you complete control over the payloads that you’re sending to Google Analytics:
// import your own action names
const SampleActions = {
UPDATE_USER: 'UPDATE_USER'
POST_A_MESSAGE: 'POST_A_MESSAGE'
SCROLL_DOWN: 'SCROLL_DOWN',
HOVER_MENU: 'HOVER_MENU'
};const trackingInformation = {
// gets tracking information just by filtering the action
[SampleActions.POST_A_MESSAGE]: ({id, content}) => ({id, length: content.length}),
// gets tracking information from the action and the store
[SampleActions.UPDATE_USER]: ({id, enabled}, store) => ({
id, name, friends: store.getState().friends
})
};// do not track these
const EXCLUDED_ACTIONS = [SampleActions.SCROLL_DOWN, SampleActions.HOVER_MENU];const analyticsMiddleware = store => next => action => {
if (window.gtag && !EXCLUDED_ACTIONS.includes(action.type)) {
// eslint-disable-next-line no-undef
window.gtag('event', 'action', {
event_category: action.type,
// defaults to tracking the entire stringified payload
event_label: JSON.stringify(trackingInformation[action.type]
? trackingInformation[action.type](action.payload, store)
: action.payload || {})
});
}
return next(action);
};export default analyticsMiddleware;
and then all you need to do is to add this middleware to your existing stack by doing something like this:
import analyticsMiddleware from './path-to-analytics-middleware';...export const middlewares = [
thunk,
routerMiddleware(hashHistory),
analyticsMiddleware];export default createStore(
combinedReducers,
initialState,
compose(applyMiddleware(...middlewares)));
If you want to see this bit of code implemented in a live repo, here is a link to the PR: https://github.com/uber/kepler.gl/pull/119/files#diff-d8ef93fc2b4d4f2fbf497c4a67934192
Good luck adding analytics to your own project! Please share your thoughts or comments.
[EDIT 2019]: with recent developments in terms of context, hooks, StateX, GraphQL etc., many apps are eschewing Redux altogether. Middleware can be integrated with other state and event management methodologies — meaning the core idea behind this article remains relevant. I will update this article or create a new article when I eventually add tracking to a non-Redux app