6

A/B Testing Using React, GraphQL and Apollo

 3 years ago
source link: http://engineering.teacherspayteachers.com/2017/05/31/a-b-testing-using-react-graphql-and-apollo.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

A/B Testing Using React, GraphQL and Apollo

By Ryan Sydnor, May 31, 2017

Here at Teachers Pay Teachers, we want to ensure that any change we make to our site benefits our community of educators. In order to do this effectively we take an extraordinarily data driven approach to releasing code. In addition to what you’d normally expect of a production ready feature (well tested, instrumented for performance, monitoring/alerting set up) we also rely heavily on Google Analytics to record not only actions taken on each page – but what “experiments” were active when those actions were taken. This lets us build dashboards of graphs that show us how our experiments are performing. Throughout this post I’ll share the details of our A/B testing implementation.

dashboard.png

Here’s an example from an experiment’s dashboard that was maintaining feature parity while migrating to new infrastructure - looks like a success to me!

Now that you have some context about why you’d want to run an A/B test, let’s dive into our implementation!

From our Elixir Phoenix backend we expose a GraphQL API. There’s a featureVariants query that has the following signature:

query FeatureVariants ($names: [String]) {
  featureVariants(names: $names) {
    name
    variant
  }
}

This endpoint is quite flexible as it allows us to request multiple flags simultaneously. It also allows for “variants” rather than “flags.” This means we can run A/B/C/D tests if we’d like.

We created a higher order component that allows any component to ask for feature flags from GraphQL via react-apollo. Here’s the code for it (slight modifications for simplicity):

export default function featureVariants(...names) { return (WrappedComponent) => { const FeatureVariantComponent = ({ featureVariantData, ...other }) => { // convert graphQL response from array to dict const featureVariants = reduce(featureVariantData.featureVariants, (acc, featureItem) => ({ ...acc, [featureItem.name]: featureItem.variant }), {});

// pass feature variant dictionary to original component return ( <WrappedComponent featureVariants={ featureVariants } { ...other } /> ); };

// make graphql request for feature flags return graphql(featureVariantsQuery, { name: 'featureVariantData', options: { variables: { names: Array.isArray(names[0]) ? names[0] : names } } })(FeatureVariantComponent); }; }

This decorator will transform the response from an array of objects into a map and pass it along to the decorated component. For example, it’ll turn this:

[
  { "name": "flag1", "variant": "on" },
  { "name": "flag2", "variant": "hover" },
  { "name": "flag3", "variant": "off" }
]

into this:

{ "flag1": "on", "flag2": "hover", "flag3": "off" }

Here’s a trivial example of the decorator in action:

@featureVariants(‘flag1’) class MyComponent extends Component { render() { return( <div> <span>My feature flag is </span> { this.props.featureVariants.flag1 === ‘on’ ? <span> ACTIVE! </span> : <span> off </span> } </div> ); } } @featureVariants('flag2') class MyOtherComponent extends Component { // TODO: component internals here }

Using feature flags in this fashion worked extremely well until we started increasing the number of experiments running simultaneously. Each experiment caused us to make another HTTP request to our API. We decided to more fully leverage Apollo’s integration with Redux to cache responses from the API. This was as easy as batching feature variant requests per domain. We created a shared array that could be imported in multiple locations:

export const myFeatureFlags = [
  'flag1',
  'flag2',
  'flag3'
];

And here’s an example of it in action:

import { myFeatureFlags } from ‘path/to/featureFlags/file’;

@featureVariants(myFeatureFlags) class MyComponent extends Component { // component code }

@featureVariants(myFeatureFlags) class MyOtherComponent extends Component { // TODO: component internals here }

With that simple fix we saw a dramatic decrease in traffic to our graph API!

requestdrop.png

The dip in traffic when we began batching requests - and this was when we only had approximately 40% of our traffic querying for feature variants!

Finally, once we had all of that infrastructure set up and humming, it became extremely easy for us to include all experiments and their values in Google Tag Manager’s Custom Dimension Data Layer. Here’s the code snippet that does it (layers of abstraction removed for readability):

window.dataLayer.push({ featureVariantsCD: Object.entries(this.props.featureVariants).map(([name, variant]) => `${name},${variant}`).join(','); });

And here’s an example line from a google analytics HTTP request from the network tab of a browser:

cd8: flag1,on,flag2,hover,flag3,off

Do you want to know more about our feature flagging system? Do you have questions about why we did something a certain way? Do you think there’s a better way? Questions about how we use this data? Leave a comment below!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK