Optimizely A/B testing in Makeswift

Utilizing Optimizely to build A/B testing in Makeswift

September 17, 2024

A/B testing is a powerful strategy for refining and perfecting your digital presence. By comparing different versions of your components or pages, you can uncover what resonates best with your audience. It's a data-driven approach that reveals user preferences and boosts performance.



In this guide, we’ll walk you through how to conduct an A/B test in Makeswift using Optimizely in four steps:



  1. Creating an Optimizely Project 
  2. Connecting Makeswift with Optimizely 
  3. Creating an Experiment Component 
  4. Tracking with Optimizely



Once these steps are complete you will be able to transform your data-driven insights into actionable improvements to upgrade your end user experience.



See the full example in Github



Lets get started!

1. Creating an Optimizely Project



The first step will be to sign-up and access Optimizely. Once you’ve logged into Optimizely, the next step is to set up a new Feature Experimentation project, like so:

Within your project, you will then create a new flag. In Optimizely, a flag refers to a feature flag that allows you to control the visibility and functionality of specific features within your application. Flags enable you to implement A/B tests that automatically toggle features on or off for different users without needing to deploy new code.



Once we have created and named the flag, we can then create a new Rule for A/B testing. You can do this by adding a rule and selecting A/B Test.

A critical part of configuring the Rule is adding an event that we can track to determine how each variantion is performing. This can be found under Metrics.

You can learn more on what events you can capture in the learn more under Create Event. For now, we are going to track when a button is pressed. We will name this event accordingly: "Button is pressed"

Once added, you will need to append this to the Metric field on the Rule.

When doing this, we want to ensure the variations selected are Off & On, and hit Save.

Finally, enable both the Flag and the Rule.

2. Connecting Makeswift with Optimizely



Follow the steps in the CLI to create a new Next.js app integrated with Makeswift. For more information on getting started with this, check out our Quickstart documentation.

npx makeswift@latest init

Let's now add the required package to our app.

pnpm add @optimizely/react-sdk

In lib/optimizely/provider.tsx, let’s create our own provider to pass down necessary data from the layout which handles the fetching.

'use client'

import { useMemo } from 'react'

import { OptimizelyProvider as Provider, createInstance } from '@optimizely/react-sdk'

export default function OptimizelyProvider({
  sessionId,
  datafile,
  children,
}: {
  sessionId: string
  datafile: object
  children: React.ReactNode
}) {
  const user = useMemo(() => ({ id: sessionId }), [sessionId])
  const optimizelyClient = createInstance({ datafile })

  return (
    <Provider optimizely={optimizelyClient} isServerSide user={user}>
      {children}
    </Provider>
  )
}

Now in lib/optimizely/datafile.ts in let's create the function to create the datafile the Optimizely SDK uses to determine what's on or off.

export async function getOptimizelyDatafile() {
  const fetchedDatafile = await fetch(
    `https://cdn.optimizely.com/datafiles/${process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY}.json`,
    { next: { revalidate: 3600 } }
  )
  const datafile = await fetchedDatafile.json()

  return datafile
}

Be sure to include the correct Optimizely environment (Development vs Production) for NEXT_PUBLIC_OPTIMIZELY_SDK_KEY in .env.local



You can find your variable in Settings within your Optimizely project.

Finally, we need to update app/layout to fetch the datafile and render our new Provider.

import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { cookies } from 'next/headers'

import { DraftModeScript } from '@makeswift/runtime/next/server'

import '@/lib/makeswift/components'
import { MakeswiftProvider } from '@/lib/makeswift/provider'
import { getOptimizelyDatafile } from '@/lib/optimizely/datafile'
import OptimizelyProvider from '@/lib/optimizely/provider'

import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  const sessionId = cookies().get('sessionId')?.value ?? 'session-id'
  const datafile = await getOptimizelyDatafile()

  return (
    <html lang="en">
      <head>
        <DraftModeScript />
      </head>
      <body className={inter.className}>
        <OptimizelyProvider sessionId={sessionId} datafile={datafile}>
          <MakeswiftProvider>{children}</MakeswiftProvider>
        </OptimizelyProvider>
      </body>
    </html>
  )
}

3. Creating an Experiment Component



This component will digest the SDK with useDecision, which returns the variation value from Optimizely based on the userId passed in OptimizelyProvider.

'use client'

import React, { ReactNode, Ref, forwardRef } from 'react'

import { useDecision } from '@optimizely/react-sdk'

type Props = {
  className?: string
  flagKey?: string
  A?: ReactNode
  B?: ReactNode
  variation?: 'optimizely' | 'on' | 'off'
}

const Experiment = forwardRef(function Experiment(
  { className, flagKey = '', A, B, variation }: Props,
  ref: Ref<HTMLDivElement>
) {
  const [decision] = useDecision(flagKey)

  const variationKey = (variation === 'optimizely' ? decision?.variationKey : variation) ?? 'off'

  if (flagKey === '') {
    return (
      <div className="max-w-5xl text-balance p-4 text-center text-gray-400">
        Please select a flag key from the combobox
      </div>
    )
  }

  return (
    <div className={className} ref={ref}>
      {{ on: A, off: B }[variationKey] ?? (
        <div className="max-w-5xl text-balance p-4 text-center text-gray-400">
          Expected the variation key to be "on" or "off" but got "{variationKey}" from Optimizely
          from the flag "{flagKey}". Make sure the variation keys match.
        </div>
      )}
    </div>
  )
})

export default Experiment

This component is rendering either A or B depending on the variation prop passed by Makeswift. The view can be hard selected to configure component A, component B, and then give render control back to Optimizely with the Optimizely A/B variation.



We now need to register this component so it can show up in Makeswift's custom component list.

'use client'

import { lazy } from 'react'

import { Combobox, Select, Slot, Style } from '@makeswift/runtime/controls'
import { createInstance } from '@optimizely/react-sdk'

import { runtime } from '@/lib/makeswift/runtime'

const client = createInstance({ sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY })

runtime.registerComponent(
  lazy(() => import('./Experiment')),
  {
    type: 'Experiment',
    label: 'Custom / Experiment',
    props: {
      className: Style(),
      flagKey: Combobox({
        label: 'Flag Key',
        getOptions: async () => {
          // This grabs the possible keys from the Optimizely SDK
          await client.onReady()
          const featureKeys = Object.keys(client.getOptimizelyConfig()?.featuresMap ?? {})

          return [
            ...featureKeys.map(key => ({
              label: key,
              value: key,
              id: key,
            })),
          ]
        },
      }),
      A: Slot(),
      B: Slot(),
      variation: Select({
        label: 'Variation',
        options: [
          { label: 'Optimizely A/B', value: 'optimizely' },
          { label: 'A', value: 'on' },
          { label: 'B', value: 'off' },
        ],
        defaultValue: 'optimizely',
      }),
    },
  }
)

We will need to import this Makeswift file into lib/makeswift/components.ts to get it officially registered.

import '@/components/Accordions/Accordions.makeswift'
import '@/components/Marquee/Marquee.makeswift'
import '@/components/Tabs/Tabs.makeswift'
import '@/components/Experiment/Experiment.makeswift'

If you refresh the builder, you should see the Experiment component under the custom components dropdown. Drag Experiment into the empty slot.

Once in the canvas, we will be dragging components into their respective A/B slots. Select A to force the selection so we can drag our component A into the slot.

We then do the same with variant B.

You will then need to use the same flag key name that was created in Optimizely, and assign it to the combobox input defined as Flag Key. The Optimizely A/B variacan then be selected so Optimizely controls the render.

If you publish this page and visit it in incognito, you will now see either A or B at a 50% rate. Alternatively you can randomize the userId passed to OptimizelyProvider.





4. Tracking activity with Optimizely



Let's now implement a button that tracks the metric called "Button is pressed" which we defined earlier in the guide.

'use client'

import { Ref, forwardRef } from 'react'

import { useTrackEvent } from '@optimizely/react-sdk'
import clsx from 'clsx'

type Props = {
  className?: string
  event?: string
}

export const Button = forwardRef(function Button(
  { className, event = '' }: Props,
  ref: Ref<HTMLButtonElement>
) {
  const [track] = useTrackEvent()

  return (
    <button
      ref={ref}
      onClick={() => {
        track(event)
      }}
      className={clsx(
        className,
        'hover:bg-primary-400 cursor-pointer bg-blue-500 px-6 py-3 text-white transition-shadow duration-500 hover:shadow-[0_3px_50px_rgba(95,120,255,.5)]'
      )}
    >
      Track!
    </button>
  )
})

And register the component with the props to control.

import { MakeswiftComponentType } from '@makeswift/runtime/components'
import { Style, TextInput } from '@makeswift/runtime/controls'

import { runtime } from '@/lib/makeswift/runtime'

import { Button } from './Button'

export const props = {
  className: Style({ properties: [Style.Margin] }),
  event: TextInput({ label: 'Event', defaultValue: '' }),
}

runtime.registerComponent(Button, {
  type: MakeswiftComponentType.Button,
  label: 'Button',
  props,
})

Don't forget to import this Makeswift file!

import '@/components/Accordions/Accordions.makeswift'
import '@/components/Marquee/Marquee.makeswift'
import '@/components/Tabs/Tabs.makeswift'
import '@/components/Experiment/Experiment.makeswift'
import '@/components/Button/Button.makeswift'

This overrides the default button, so we can use the button icon to drag in the newly defined button. Set the value of the event control as the name of the event we created and used for the metrics field under the Rule.

You should now see this event being tracked under Reports / My Rule.

Variation A or B will have the event tracked under a summary table. You will be able see how one is performing compared to the other.

That’s a wrap! Read more about Optimizely's React SDK here. You’re now set to run A/B tests and monitor your experiment results. You can continue to check on Experiments Results to track your data and make informed decisions for the future of your pages. If you have any questions about A/B testing with Makeswift and Optimizely, don’t hesitate to reach out to us at support@makeswift.com. We’re here to help you every step of the way!