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.
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.
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.
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.
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.
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.
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.
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>
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>
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>
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 }))
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
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 />
This is a list of all configurable properties for each component and function.
key
'Some Key'
variants
['Variant A', 'Variant B']
onView
null
serverCookies
null
options
{ userIdentifier, cookieName }
options.userIdentifier
null
options.cookieName
'splitTestIdentifier'
options
{ userIdentifier, cookieName }
options.userIdentifier
null
options.cookieName
'splitTestIdentifier'
options
null
options.key
''
options.action
'view'
options.variants
[]
options.userIdentifier
null
options.force
null
options.trackingFunction
null
{ action, key, variant }
is passed as the first and only parameter.