“The Lean Startup” by Eric Reis prescribed some medication for our venture’s success: Build, , Learn, repeat. This post focuses on the “measure” portion. Measure Split Testing and Analytics are not features, they are requirements. How can you know if anything is working if you don’t know what users are even doing? How can you know if an approach is more or less successful if you are not measuring? “When launching a new funnel, you don’t necessarily expect it to work right away — you are buying data.” — Ruda Krishna Not only are Split Testing and Analytics requirements; they are also equalizers. They can help your team eliminate “HiPPOs” or the Highest-Paid Person’s Opinion. This is when despite evidence for a solution being strong, the highest paid person’s opinion is still what ultimately gets implemented… cause they said so and they make more than you. Clearly a higher “band” means higher intelligence… 😜 I prefer a more scientific approach: Everything is a hypothesis until the data says otherwise. To this end, I’ve written a lot of split tests, and, I think that many people really over complicate the matter. I want to show how simple it is to add custom SSR split testing using Redux and Next.js. In this tutorial we will start with an empty Node.js project and walk through the process of building simple split-testing functionality with Next.js, Redux, and seamless analytics by creating custom Redux analytics middleware. This serves dual-purposes. As an introduction to Redux and Next.js, as well as how to build an actually useful feature using them. For those of you who are unfamiliar with the technologies: Redux is a predictable state container for JavaScript apps. It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. : A JavaScript library for building user interfaces Framework for server-rendered or statically-exported React apps Redux: React Next.js: This was written using next@5 — using next@6 break the tutorial. The concepts from the tutorial are the important part, and can be applied in any framework or language. I’ve added version numbers in the install commands so you can still follow along. NOTE: 1. We’ll start with a brand new Node.js project mkdir split-testcd split-testecho "node_modules\n.next" | tee .gitignorenpm init 2. Install and configure Next.js npm install --save next@5 react@16 react-dom@16 # and open up your code editor. I'm using VSCode.code . And to finish setting up next, we need to add the following to package.json "scripts": {"dev": "next","build": "next build","start": "next start"} Next, we need an index page. From the root directory, create a folder called and in it create a file pages index.js ./pages/index.js export default () => <div>Welcome to next.js!</div> If you’re not familiar with Next.js, it is an opinionated framework built around Webpack. Everything in the directory is a new which is . pages entrypoint code-split You can run your app now using . Hot Code reloading is enabled by default. Check out your progress by visiting localhost:3000! npm run dev But what if this page converted better if it said “Welcome to MY next.js”? Let’s proceed. 3. Configure Redux Redux is going to be managing the state of the application, including which experiments are currently active. Install Redux npm i --save redux@3 react-redux@5 next-redux-wrapper@1 Next, let’s create a module that will initialize Redux. Let’s call it and store it in a new folder . initRedux.js lib ./lib/initRedux.js import {createStore,combineReducers,applyMiddleware,compose} from 'redux' let reducers = {}let reduxStore = null // The following checks if the Redux DevTools extension// is available in your browser, and activates it if so.// Otherwise, it executes a no-op functionlet devtools = f => fif (process.browser && window.__REDUX_DEVTOOLS_EXTENSION__) {devtools = window.__REDUX_DEVTOOLS_EXTENSION__()} function create (initialState = {}) {return createStore(combineReducers({ // Setup reducers...reducers}),initialState, // Hydrate the store with server-side datadevtools)} export default function initStore (initialState) {// Make sure to create a new store for every server-side request so that data// isn't shared between connections (which would be bad)if (!process.browser) {return create(initialState)} // Reuse store on the client-sideif (!reduxStore) {reduxStore = create(initialState)} return reduxStore} In the above file we exported a default function, , that will create the store with a provided . Because Next.js is this means code will run on the client side as well as the server side. In we are also checking whether or not the process is running in the browser. If it is, we can reuse that was created on the server. initStore initialState isomorphic initStore reduxStore In either case, will be called next. This creates a new redux store with our reducers, the initialState, and Redux DevTools for a better Development experiment. create Next, let’s “wire up” Redux to our index page. ./pages/index.js import initStore from '../lib/initRedux'import withRedux from 'next-redux-wrapper' const Index = () => <div>Welcome to next.js!</div> const mapStateToProps = () => ({})const mapDispatchToProps = () => ({}) export default withRedux(initStore, mapStateToProps, mapDispatchToProps)(Index) We’ve modified by wrapping the component with . takes a function that return a redux store as it’s first argument, and two additional functions that return an object of that will be available to the component. We will fill these in soon. For now, we have successfully wired up Redux. Visiting localhost:3000, will show the enabled. Install it to check if you have not already. ./pages/index.js withRedux withRedux props Redux DevTools chrome extension 4. Experiments Reducer Reducers are where your applications logic is stored. Actions come in, and the reducer determines how this action changes the state. Let’s start by creating some new folders in lib, called and , and define a file in the new folder. redux redux/reducers experiments.js reducers ./lib/redux/reducers/experiments.js export default (state = {active: {}}, { type, payload }) => {switch (type) {case 'START_EXPERIMENT':let { name, variant } = payloadlet active = Object.assign({}, state.active)active[name] = variantreturn {active}default:return state}} export function startExperiment ({name, variant}) {return {type: 'START_EXPERIMENT',payload: {name,variant}}} Our entire reducer is pretty simple — it exports a reducer with an initialState of . All our application really needs to know about is which experiments are active, so this should be enough. { active: {} } Next we defined a for our reducer’s statement. This will add a new key to the state we defined with the experiment’s , and the value will be the active . case 'START_EXPERIMENT' switch active name variant Lastly, we export an . This takes a configuration object, and expects an experiment , and which is active. action startExperiment name variant ./lib/redux/reducers/index.js import experiments from './experiments' export default {experiments} Any time we have a new reducer, we can just add it to this export. And we just need to wire it up. ./lib/initRedux.js In replace the line: ./lib/initRedux.js let reducers = {} with: import reducers from './redux/reducers' Redux is now “wired up” with our reducers. Next we will need to start the experiment on the server side by making use of Next.js’s . getInitialProps 5. from dispatch getInitialProps Below the line, let’s add a function. When we used to wrap our component, this made the Redux accessible in the context object passed to . We’ll need to call the action we defined in our reducer, so import that at the top as well. const Index = ... getInitialProps withRedux store getInitialProps ./pages/index.js import { startExperiment } from '../lib/redux/reducers/experiments' // ... /* context: {req, res, query, isServer, store} */Index.getInitialProps = ({store}) => {const dispatchStartExperiment = ({name, variant}) => {store.dispatch(startExperiment({name,variant}))}} // ... Ok, we’ve defined a function that dispatches a action, and takes in the experiment and as parameters. startExperiment name variant Next we need experiments. We will naturally do so, with JavaScript. 6. Create a new Experiment() We will create an Experiment class, that extends as an easy way to hook in to each experiment’s state changing. EventEmitter First the Experiment class. In it a algorithm, which given a list of variants, and an id, will always select the same variant. Meaning all you need to do to ensure a user always get’s the same experiment, is pass in the same id. You can do this by storing their userId, or a hash of it, in a cookie. I borrowed this algorithm from . selectVariant react-ab-test I think that using React components to maintain state of experiments is over-complicated, and redux is a better fit for the task, but “props” for the algorithm . 👏 John Wehr ./lib/Experiment.js import { EventEmitter } from 'events'import crc32 from 'fbjs/lib/crc32' export default class Experiment extends EventEmitter {constructor ({name,variants,userId}) {super()this.name = namethis.variants = variantsthis.userId = userId} selectVariant (userId) { /\* Choosing a weighted variant: For C, A, B with weights 2, 4, 8 variants = A, B, C weights = 4, 8, 2 weightSum = 14 weightedIndex = 9 AAAABBBBBBBBCC ========^ Select B \*/ // Sorted array of the variant names, example: \["A", "B", "C"\] const variants = Object.keys(this.variants).sort() // Array of the variant weights, also sorted by variant name. For example, if // variant C had weight 2, variant A had weight 4, and variant B had weight 8 // return \[4, 8, 2\] to correspond with \["A", "B", "C"\] const weights = variants.reduce((weights, variant) => { weights.push(this.variants\[variant\].weight) return weights }, \[\]) // Sum the weights const weightSum = weights.reduce((a, b) => { return a + b }, 0) // A random number between 0 and weightSum let weightedIndex = typeof userId === 'string' ? Math.abs(crc32(userId) % weightSum) : Math.floor(Math.random() \* weightSum) // Iterate through the sorted weights, and deduct each from the weightedIndex. // If weightedIndex drops < 0, select the variant. If weightedIndex does not // drop < 0, default to the last variant in the array that is initially assigned. let selectedVariant = variants\[variants.length - 1\] for (let index = 0; index < weights.length; index++) { weightedIndex -= weights\[index\] if (weightedIndex < 0) { selectedVariant = variants\[index\] break } } return selectedVariant } start ({ userId }) {userId = userId || this.userIdlet variant = this.selectVariant(userId)this.emit('variant.selected', { name: this.name, variant })}} And let’s use it to create an Experiment! ./experiments/headerText.js import Experiment from '../lib/Experiment' const headerTextExperiment = new Experiment({name: 'Header Text',variants: {control: {weight: 50,displayName: 'control'},mine: {weight: 50,displayName: 'mine'}}}) export default headerTextExperiment In the experiment class we’ve defined, we simply need to pass in an object with some initialization options: 1) The name of the experiment, and 2) The variants and their respective weights. We have two variants in this example, split 50/50. 7. Activating the Experiment We want to activate the experiment on so let’s import it, and activate it in getInitialProps. As I mentioned earlier, we’ll want to pass in a userId if it exists, so let’s just assume that is available in the cookies for demonstration purposes. ./pages/index.js, npm install --save next-cookies@1 ./pages/index.js import initStore from '../lib/initRedux'import withRedux from 'next-redux-wrapper'import { startExperiment } from '../lib/redux/reducers/experiments' import headerTextExperiment from '../experiments/headerText'import cookies from 'next-cookies' const Index = ({experiments}) => (<div>{ experiments.active[headerTextExperiment.name] === 'control' ? "Welcome to Next.js!" : null }{ experiments.active[headerTextExperiment.name] === 'mine' ? "Welcome to MY Next.js!" : null }</div>) /* context: {req, res, query, isServer, store} */ const dispatchStartExperiment = ({name, variant}) => {console.log('starting experiment')store.dispatch(startExperiment({name,variant}))} Index.getInitialProps = (ctx) => {const { store } = ctx const activeExperiments = [headerTextExperiment] headerTextExperiment.once('variant.selected', dispatchStartExperiment) } let { userId } = cookies(ctx)activeExperiments.forEach((experiment) => {experiment.start({userId})}) const mapDispatchToProps = () => ({}) const mapStateToProps = ({ experiments }) => ({experiments}) export default withRedux(initStore, mapStateToProps, mapDispatchToProps)(Index) I’ve bolded the changes for clarity. First, we import and . requires next’s context object, so a quick refactor to getInitialProps to expose it at the top. Then, we create an array of experiments to activate, and use a forEach to start each one, passing in a if available. headerTextExperiment cookies cookies userId The experiment upon starting, will select a variant, and emit an event: . When that occurs, we will dispatch an action to redux which will update the global state. When the state changes, will fire, giving our component access via it’s to which experiments are active. 'variant.selected' mapStateToProps props If the experiment’s variant “control” is active, we show the original text, and if the variant “mine” is active, we show the alternative text. 8. Tracking events with Analytics middleware As we are using redux to change the state of our application, we can hook into this, and emit analytics events when redux actions occur. To do this we will create a custom analytics middleware. I’ll show how to integrate with google analytics as well as facebook analytics. You can use your imagination for other integrations. I especially like Mixpanel a lot for this type of analytics. ./lib/analytics.js export const track = ({event, value}) => {console.log('track', event, {value}) if (process.browser) {window.ga && window.ga('send', 'event', {eventCategory: event,eventLabel: value})window.fbq && window.fbq('track', event, {value})}} ./lib/redux/middleware/analytics.js import { track } from '../../analytics' export default ({ dispatch, getState }) => next => action => {const {analytics} = action.meta || {} next(action) if (analytics) {track(analytics)}} ./lib/initRedux.js import analyticsMiddleware from './redux/middleware/analytics' // ... function create (initialState = {}) {return createStore(combineReducers({ // Setup reducers...reducers}),initialState, // Hydrate the store with server-side data )} compose(applyMiddleware(analyticsMiddleware),devtools) We use and in order to use multiple middleware as well as the Redux DevTools. compose applyMiddleware Now we can simply add a key to our redux actions to track them when they occur. meta ./lib/redux/reducers/experiments.js // ... export function startExperiment ({name, variant}) {return {type: 'START_EXPERIMENT',payload: {name,variant} }} , meta: {analytics: {event: `${name} Experiment Played`,value: variant}} Now every time startExperiment is dispatched, an event “NAME Experiment Played” will be sent to your analytics endpoints with the selected variant. 9. Tracking other events As designed, however, this action DOES NOT occur on the client. Only the server. Let’s add a special exception to also track this event when the component mounts on the client as well, as we’ll want this data. There are a lot of good reasons for wanting analytics on the server and the client. Such as seeing how many people requested a page, but dropped off before it loaded for whatever reason. We need some lifecycle events, so this results in some refactoring to make Index extend React’s Component, and moving getInitialProps inside as well. ./pages/index.js // ... import { Component } from 'react'import { track } from '../lib/analytics' /* ctx: {req, res, query, isServer, store} */const { store } = ctxconst dispatchStartExperiment = ({name, variant}) => {store.dispatch(startExperiment({name,variant}))} class Index extends Component {static getInitialProps (ctx) { const activeExperiments = \[ headerTextExperiment \] headerTextExperiment.once('variant.selected', dispatchStartExperiment) let { userId } = cookies(ctx) activeExperiments.forEach((experiment) => { experiment.start({userId}) }) } componentDidMount () {const { experiments } = this.props **if (experiments.active\[headerTextExperiment.name\]) { // startExperiment just returns the redux action object // we can make use of this to look up the analytics // event name and value let analytics = startExperiment({ name: headerTextExperiment.name, variant: experiments.active\[headerTextExperiment.name\] }).meta.analytics track({ event: analytics.event, value: analytics.value }) } }** (<div>{ experiments.active[headerTextExperiment.name] === 'control' ? "Welcome to Next.js!" : null }{ experiments.active[headerTextExperiment.name] === 'mine' ? "Welcome to MY Next.js!" : null }</div>) render () {const { experiments } = this.propsreturn } } // ... The new function only runs on the client side. We are simply calling with the same values from the redux action object if an experiment is active. componentDidMount track 10. Include 3rd Party Analytics libraries We can simply grab the javascript out of any provided snippet’s tags, and store them as pure js. Next we will make use of to insert them using React’s API. <script> next/head dangerouslySetInnerHTML ./pages/index.js import ga from '../lib/analytics/ga'import fb from '../lib/analytics/fb'import Head from 'next/head' class Index extends Component {// ... render () {const { experiments } = this.propsreturn (<div> { experiments.active[headerTextExperiment.name] === 'control' ? "Welcome to Next.js!" : null }{ experiments.active[headerTextExperiment.name] === 'mine' ? "Welcome to MY Next.js!" : null }</div>)}} <Head><title>SSR Split Tests</title><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /><script dangerouslySetInnerHTML={{__html: ga}} /><script dangerouslySetInnerHTML={{__html: fb}} /></Head> // ... ./lib/analytics/fb.js export default `if(typeof fbq === 'undefined') {!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script',' );fbq('init', ' ');fbq('track', 'PageView');} else {fbq('track', 'PageView');}` https://connect.facebook.net/en_US/fbevents.js' YOUR FB ID GOES HERE ./lib/analytics/ga.js export default `(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script',' );ga('create', ' ', 'auto');ga('send', 'pageview');` https://www.google-analytics.com/analytics.js','ga' YOUR GA ID GOES HERE 11. It helps if there is a goal Additionally, we need a goal. Right now, we just know when an experiment is viewed. I originally made all of this for a Landing Page, so let’s stick with that example. So our goal for this page is for a user to enter their email address and submit it! When that happens, we want to store their email in our redux store, and say thank you. This also demonstrates the untouched and using multiple reducers. mapDispatchToProps Seems how we are tracking , let’s make a reducer. leads lead ./lib/redux/reducers/lead.js export default (state = {email: null}, { type, payload }) => {switch (type) {case 'SIGNUP_LEAD':let { email } = payloadreturn {...state,email}default:return state}} export function signupLead ({email}) {return {type: 'SIGNUP_LEAD',payload: {email},meta: {analytics: {event: `Signed Up`,value: email}}}} ./lib/redux/reducers/index.js import experiments from './experiments'import lead from './lead' export default {experiments,lead} Next we need to a new component to container the SignUp Form, and show that on the index page. ./components/SignUpForm.js export default function SignUpForm () {function submit(e) {e.preventDefault()let email = e.target.elements.email.valueif (email) {alert(email)} else {alert("Email is Required")}}return (<form onSubmit={submit}><input name="email" type="email" placeholder="Enter your email..." /><button>Submit</button></form>)} ./pages/index.js import SignUpForm from '../components/SignUpForm' // ...render () {const { experiments } = this.propsreturn (<div><Head><title>SSR Split Tests</title><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /><script dangerouslySetInnerHTML={{__html: ga}} /><script dangerouslySetInnerHTML={{__html: fb}} /></Head>{ experiments.active[headerTextExperiment.name] === 'control' ? <h1>Welcome to Next.js!</h1> : null }{ experiments.active[headerTextExperiment.name] === 'mine' ? <h1>Welcome to MY Next.js!</h1> : null } </div>)}// ... <SignUpForm/> 12. Connect redux to form Our form needs access to a redux action we’ve defined, as well as the state from our store. We can use and to provide access. lead mapDispatchToProps mapStateToProps ./pages/index.js // ... import { signupLead} from '../lib/redux/reducers/lead' class Index extends Component {// ... render () {const { experiments**, lead, signupLead** } = this.propsreturn (<div><Head><title>SSR Split Tests</title><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /><script dangerouslySetInnerHTML={{__html: ga}} /><script dangerouslySetInnerHTML={{__html: fb}} /></Head>{ experiments.active[headerTextExperiment.name] === 'control' ? <h1>Welcome to Next.js!</h1> : null }{ experiments.active[headerTextExperiment.name] === 'mine' ? <h1>Welcome to MY Next.js!</h1> : null } </div>)}} <SignUpForm lead={lead} signup={signupLead}/> const mapStateToProps = ({ experiments**, lead** }) => ({experiments, }) lead const mapDispatchToProps = (dispatch, ownProps) => ({signupLead: ({email}) => {dispatch(signupLead({email}))}}) // ... In the above we are simply importing the new action, and then mapping it to props with in . We are also modifying to have access to the state. dispatch mapDispatchToProps mapStateToProps lead Finally, let’s finish the form using our new props! ./components/SignUpForm.js export default function SignUpForm ( ) {function submit(e) {e.preventDefault()let email = e.target.elements.email.valueif (email) { } else {alert("Email is Required")}}return (<div> </div>)} {lead, signup} signup({email}) {typeof lead.email === 'string' && lead.email.length > 0 ?<p>Hello {lead.email}</p>:<form onSubmit={submit}><input name="email" type="email" placeholder="Enter your email..." /><button>Submit</button></form>} Now, when pressing submit, the input box will switch to say and track the event “Signed Up” with the email as the value. Hello {lead.email} Now in your funnel analytics you can make two queries: Starting with “Header Text Experiment Played” with value “control” Starting with “Header Text Experiment Played” with value “mine” You’ll be able to easily see the percentage of users who made it through each step for each experiment. Conclusion 🎉 There you have it, SSR split tests with (mostly) automatic analytics (so long as you continue to use redux)! Thanks for reading! Until next time! If you found this useful, please clap and share because it will help me reach more people! :) You can find all of the code in . Feel free to use it as a boilerplate! this GitHub repository Best,Patrick Scott — Interested in hearing MY DevOps Journey, WITHOUT useless AWS Certifications? Read it now on HackerNoon . I am available for consulting — send me a message on Twitter or LinkedIn . Please mention you saw my article! Don’t be shy! Want to learn how to build a custom analytics backend with microservices? In my upcoming course “ Microservice Driven ” I am doing just that. Starting with this example you will learn how to build a microservices backend for tracking analytics and leads, and run it all in production using Docker Swarm. Sign up now!