Scalable (and generic) Mixpanel Tracking for React-Redux applications

If 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.