Scalable (and generic) Mixpanel Tracking for React-Redux applications
31 Aug 2016If you’ve been following me online, even just for a couple of weeks you’d know that I’m rewriting a major and critical application using React-Redux.
At Gogobot, we A/B test quite a bit and we track behavior in order to make sure what we deliver to our users is top notch quality and not confusing.
One of the most used services around the Gogobot office is Mixpanel which allows you to track custom events with any payload and graph it. You can check with version of your application is behaving better and which does the user like better.
Before React-Redux, we had buttons around the site with .mixpanel
class. If one of those button was clicked we would fire the event and listen to it.
Since with React we got rid of jQuery, I wanted to make sure we have a scalable way to do it.
Higher Order Component to the rescue
I’ve written about Higher Order component in the past, feel free to go there and read on if you’re not sure what it means.
Connected Higher Order components with React and Redux
For this, a Higher Order Component really fit. Ideally I’d want something like this.
MixpanelButon
component with a trackEvent
function. The “child” component (a
) in our case will hold data-mixpanel-eventname
and data-mixpanel-eventproperties
(JSON). Once clicked it will fire the trackEvent
function that will fire the actionCreator.
Getting started
Middleware
In order to connect to mixpanel I found this handy mixpanel middleware that does precisely what I want redux-mixpanel-middleware.
All you need to do is dispatch an action with the mixpanel
object and the rest is taken care of in the middleware.
Action Creator
import * as actionTypes from '../constants/actionTypes';
export function sendMixpanelEvent(eventName, mixpanelParams = {}) {
return function(dispatch, getState) {
let params = mixpanelParams;
if (typeof(params) === 'string') {
params = JSON.parse(params);
}
dispatch({
type: actionTypes.MIXPANEL_EVENT,
meta: {
mixpanel: {
event: eventName,
props: Object.assign({}, params)
}
}
});
}
}
As you can see it’s pretty simple, I am just dispatching a MIXPANEL_EVENT
with the meta
object (that’s being taken care of in the middleware). eventName
is passed into the creator and the params are either an object OR a JSON string.
I could have also parsed the JSON before I dispatched the action but I figured the API will be cleaner with accepting an object or JSON. Matter of taste here I guess…
The Higher Order Component
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import { sendMixpanelEvent } from '../../actions/mixpanel';
import { bindActionCreators } from 'redux';
function MixpanelButtonWrapper(ComposedButton) {
class MixpanelButton extends Component {
render() {
return(
<ComposedButton
{ ...this.props }
{ ...this.state }
trackEvent={ this.props.track } />
);
}
}
function mapStateToProps(state) {
return {
};
}
function mapDispatchToProps(dispatch) {
return {
track: (e) => {
const target = e.target;
const eventName = target.getAttribute("data-mixpanel-eventname");
const eventAttributes = target.getAttribute("data-mixpanel-properties");
if (eventName && eventAttributes) {
dispatch(sendMixpanelEvent(eventName, eventAttributes));
}
}
};
}
return connect(mapStateToProps, mapDispatchToProps)(MixpanelButton);
}
export default MixpanelButtonWrapper;
This is the higher order component called MixpanelButton
and it’s being wrapped by the MixpanelButtonWrapper
function that returns the component.
It’s passing a track
prop through the mapDispatchToProps
function and that is being passed down into the ComposedButton
as trackEvent
prop.
The button
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import * as mixpanelEvents from '../../constants/MixpanelEvents';
import MixpanelButtonWrapper from '../MixpanelButton';
function BookButton(props) {
const { buttonClassName, price, trackEvent } = props;
const index = props.index || 1;
const mixpanelProps = {
"Booking Partner": price.provider_name,
"Metasearch Rank": index,
"Duration of stay": price.total_nights,
"Dates set": true,
"Currency Setting": "USD"
}
return(
<a className={ buttonClassName }
onClick={ (e) => trackEvent(e) }
target="_blank"
data-mixpanel-eventname={ mixpanelEvents.HOTEL_TRANSACTION }
data-mixpanel-properties={ JSON.stringify(mixpanelProps) }
href={ price.proxy_booking_url }>View deal</a>
);
}
export default MixpanelButtonWrapper(BookButton);
The button itself is also simple, it’s just an a
tag that has an onClick
event that fires the trackEvent
with the event
as a param.
As you can see at the very bottom of the component, I am wrapping the component with MixpanelButtonWrapper
.
Now, every button I want to have Mixpanel on, I can do the same thing. Very easy to do and figure out. It’s not as simple as just adding a class like we had previously but it’s simple enough for the use-case.
One more thing…
We don’t really have to pass trackEvent
as a prop to the ComposedButton
component. Since we spread all the props in {... this.props}
the ComposedButton
component has access to track
as part of it. It’s really a more verbose self documenting way of knowing what the event is.
In action
You can see the button accepting the params as expected. When I click the button the params from the data-
attributes are bring pulled in and passed to the action creator and later sent to Mixpanel.
Summary
To sum up. You can have common behaviors in your React applications if you use the code right and connecting those common behavior to Redux is also not a big issue.
I am really having a lot of fun working with the React-Redux combination. Feel free to add your comments and ask questions.