Svelte Split Testing — By PlayPilot

Svelte Split Testing

Split tests (or A/B tests) allow you to display different features or variants to test their effectiveness. Unfortunately popular options are very pricey, bloated, and don't work SSR. This package attempts to remedy all of that.

This package works with Svelte and SvelteKit.

Uses Google Tag Manager by default to keep track of your analytics, but this can be replaced with other methods.

  • Works SSR
  • Works with or without Kit
  • Works with GTM and GA4, or any other analytics solution
  • Lightweight
  • It's free!

Demo

What you are doing with your split tests is completely up to you of course. These examples show some basics of how it all works, but the sky is the limit.

The important part here is that the results are randomized, but consistent for the same user. SSR and CSR show the same result and revisiting the page will show the same result as the visit before. You can even show the same result for the same user across different devices.

You are currently being shown split test Variant B

Split tests are randomized based on a cookie or a given user identifier. You can override this by adding ?force-split-test=[Variant name] to the url. This is only meant for debugging.

Note: Forcing the variant doesn't work SSR, if you're viewing this page without Javascript you might not see a difference. Regular split tests do work during SSR.

You can include as many variants as you like.

Set up

Install using Yarn or NPM.

yarn add svelte-split-testing --dev npm install svelte-split-testing --save-dev

The component isn't quite plug-and-play, we will need to set up some details.

SvelteKit

When using SvelteKit we need to make sure the same identifier is set on the server as is set on the client.

In your (main) +layout.server.js load function import and set the identifier and pass it along.

import { serverGetSplitTestIdentifier } from 'svelte-split-testing/splitTesting'

/** @type {import('./$types').LayoutServerLoad} */
export async function load({ cookies }) {
  const splitTestIdentifier = serverGetSplitTestIdentifier(cookies)

  return {
    splitTestIdentifier,
  }
}

Important here is that you pass along the SSR cookies object.

Next, in your (main) +layout.js load function pass the identifier along again. We don't actually need to do anything with it just yet.

export async function load({ data }) {
  const { splitTestIdentifier } = data || {}

  return {
    splitTestIdentifier,
  }
}

This is a bit verbose, but this is assuming you have other stuff going on in the file as well. If you don't, you could simply pass it as return { ...data }.

Next, in your (main) +layout.svelte set the identifier using the Context API. Make sure to use the correct key.

<script>
  import { clientGetSplitTestIdentifier } from 'svelte-split-testing/splitTesting'
  import { setContext } from 'svelte'

  export let data

  setContext('splitTestIdentifier', data?.splitTestIdentifier)
</script>

<slot />

And that's the basic set up for SvelteKit. Next up will we go in to usage.

Svelte (without Kit)

For Svelte (without Kit) we do not yet need any set up. There might be some set up depending on your needs, but we will get to that in the Usage section.

Usage

import { SplitTest } from 'svelte-split-testing'

<SplitTest>...</SplitTest>

At it's most basic SplitTest is a wrapper component that takes care of some business, but for you as the user it's really just a slot. This components has a slot prop called variant, this will be used by you to determine what is shown in what variant.

When using the component there's 2 important props: key and variants.

key Is the name of the split test. It isn't very important for functionally what this key is, is doesn't even need to be unique. It's only used to identify what split test is for your analytics tracking.

variants Is an array of strings with each variant you want to include. What the names are is not important, but this name is what will be used in your analytics tracking. You could go for something simple like ['A', 'B'], or give them more explicit names like ['Large Sign Up', 'Small Sign Up'], it's up to you.

Variants

In the following example we have two variants, each having 50% chance to show. We use the slot prop variant to determine what to show for each variant.

<SplitTest
  key="Some test"
  variants={['Variant A', 'Variant B']}
  let:variant>

  {#if variant === 'Variant A'}
    <button>Do the thing</button>
  {/if}

  {#if variant === 'Variant B'}
    <button>Do the thing, but different</button>
  {/if}
</SplitTest>

You don't have to use if statements, you could use it like any other variable.

<SplitTest
  key="Some test"
  variants={['Variant A', 'Variant B']}
  let:variant>

  Current showing variant: {variant}
</SplitTest>

Using this you could quickly set up the use of different styles, for example.

<script>
  const variants = {
    Plain: 'button-plain',
    Colorful: 'button-colorful',
  }
</script>

<SplitTest
  key="Some test"
  variants={Object.keys(variants)}
  let:variant>

  <button class={variants[variant]}>...</button>
</SplitTest>

Tracking

By default the Split Test will use Google Tag Manager, using the data layer, to keep track of your events. This can be changed to any other method, which will we go in to later.

When a split test is shown an event is send to GTM with the given key as the label, and the current variant as the value.

To fire an event when a button is clicked, or any other action is performed, you can use the performAction slot prop. This will fire an event to GTM. You can optionally pass a value to change the event type from 'click' to whatever else.

The data sent looks a little like: { event: 'Split Test', action: [given action], label: [given key], value: [current variant] }.

<SplitTest
  key="Some test"
  let:variant
  let:performAction>

  <button on:click={performAction}>...</button>
</SplitTest>

Of course you might want to track more than just clicks, for this you can pass any string to the performAction function. Once again, this is just for tracking purposes, what the string is doesn't matter.

<SplitTest
  let:variant
  let:performAction>

  <form on:submit={() => performAction('form submit')}>
    ...
  </form>
</SplitTest>

If your action takes place outside of the component you can bind the property instead.

<script>
  let performAction

  function doTheThing() {
    performAction()
  }
</script>

<SplitTest
  let:variant
  bind:performAction>

  <form on:submit={doTheThing}>
    ...
  </form>
</SplitTest>

Using different analytics methods

If you do not wish to use GTM you can bring your own solution. Simply pass a function to the onView property, and this function will be used instead. This function will be fired every time the component is mounted in CSR. It will not fire during SSR.

<SplitTest onView={(data) => console.log(data)}>...</SplitTest>
<script>
  function customTracking(data) {
    ...
  }
</script>

<SplitTest onView={customTracking}>...</SplitTest>

The data returns an object { key, variant, action: 'view' }. What you do with this data is up to you.

For example, you could use GA4 using gtag.

<script>
  function customTracking(data) {
    gtag('event', 'Split Test', data)
  }
</script>

<SplitTest onView={customTracking}>...</SplitTest>

To track custom events other than views, you can use whatever method you like and use the variant slot prop to determine the current variant.

<script>
  const key = 'Some test'

  function trackClick(variant) {
    ...
  }
</script>

<SplitTest
  {key}
  variants={["A", "B"]}
  let:variant>

  <button on:click={() => trackClick(variant)}>...</button>
</SplitTest>

Other config options

serverGetSplitTestIdentifier

This function is used in the set up to set the identifier for SSR. The second parameter in this function is an object of options.

serverGetSplitTestIdentifier(servercookies, { userIdentifier, cookieName })

userIdentifier is used to pass an identifier to the function that will be used instead of a random identifier. This way you can make sure a user sees the same page across different devices, as long as they use the same identifier. Be aware that will identifier will be saved as a plain string in a cookie.

cookieName can be used to change the name of the cookie. Defaults to splitTestIdentifier

If you are setting this as done in the set up, you will need to make sure to pass it along to the client side as well, using clientGetSplitTestIdentifier with the same options, as a fallback value. This needs to be done when setting the context (refer back to the Set Up section). This is a safety net in case the value was not set as expected.

setContext('splitTestIdentifier', data?.splitTestIdentifier || clientGetSplitTestIdentifier({ userIdentifier, cookieName }))

clientGetSplitTestIdentifier

This used to set the identifier client side, this is optional if you are also using SSR. If you are using Svelte without Kit, this will be the only function you use. The first and only parameter is an object of options.

clientGetSplitTestIdentifier({ userIdentifier, cookieName })

userIdentifier is used to pass an identifier to the function that will be used instead of a random identifier. This way you can make sure a user sees the same page across different devices, as long as they use the same identifier. Be aware that will identifier will be saved as a plain string in a cookie.

cookieName can be used to change the name of the cookie. Defaults to splitTestIdentifier

Outside of components

In some cases you might want to perform split tests outside of a component, perhaps right inside a javascript file. In that case you can use the performSplitTestAction function.

This function will return the current variant. It will perform an action when called, sending it to GTM by default.

const variant = performSplitTestAction({
  key: 'Some test key',
  action: 'click',
  variants: ['A', 'B'],
  force,
  trackingFunction: ({ variant }) =>
    alert(`Performed action for variant "${variant}"`)
})

if (variant === "A") doThingA()
else if (variant === "B") doThingB()

Additionally the slot prop code can be bound to a variant, allowing it to be re-used for things outside of the component.

<script>
  let variant
</script>

<SplitTest bind:variant />

Properties

This is a list of all configurable properties for each component and function.

SplitTest

Property Default Description key 'Some Key'
Key used to identify the current Split Test. This is primary used during analytics tracking.
variants ['Variant A', 'Variant B']
An array of variant names. Can be as many variants as you like. What the names are is not important, but they show up during analytics tracking.
onView null
Optional function to be passed to track views of the current variant. Replaces the default GTM method.

serverGetSplitTestIdentifier

Property Default Description serverCookies null
Cookies object as served from +layout.server.js during SSR.
options { userIdentifier, cookieName }
Object of configurable options
options.userIdentifier null
An optional user identifier to use as the identifier. This is used to show a user the same split test across different devices, as long as they have the same identifier. If an identifier was already set before the user identifier was given the original cookie will be used instead. Be aware that this value will be saved in the cookie as a plain string. Do not use any data that you might not want to be public.
options.cookieName 'splitTestIdentifier'
The name of the cookie used to store the split testing identifier.

clientGetSplitTestIdentifier

Property Default Description options { userIdentifier, cookieName }
Object of configurable options
options.userIdentifier null
An optional user identifier to use as the identifier. This is used to show a user the same split test across different devices, as long as they have the same identifier. If an identifier was already set before the user identifier was given the original cookie will be used instead. Be aware that this value will be saved in the cookie as a plain string. Do not use any data that you might not want to be public.
options.cookieName 'splitTestIdentifier'
The name of the cookie used to store the split testing identifier.

performSplitTestAction

Property Default Description options null
Optional parameters
options.key ''
Key used to identify the current test
options.action 'view'
Action send to analytics tracking
options.variants []
Array of strings with all possible variants
options.userIdentifier null
Optional user identifier to override the cookie identifier
options.force null
Force a particular split test by string.
options.trackingFunction null
Function to override the default GTM data layer tracking. { action, key, variant } is passed as the first and only parameter.
Developed by PlayPilot