├── .editorconfig
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierrc
├── README.md
├── jsconfig.json
├── package-lock.json
├── package.json
├── src
├── app.d.ts
├── app.html
├── lib
│ ├── SplitTest.svelte
│ ├── index.js
│ └── splitTesting.js
├── routes
│ ├── +layout.js
│ ├── +layout.server.js
│ ├── +layout.svelte
│ └── +page.svelte
├── static
│ └── images
│ │ └── movies-and-shows.png
└── test
│ ├── SplitTest.test.js
│ ├── SplitTestTest.svelte
│ └── splitTesting.test.js
├── static
└── favicon.png
├── svelte.config.js
├── vite.config.js
└── vitest.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | indent_style = tab
7 | indent_size = 2
8 | charset = utf-8
9 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /dist
5 | /.svelte-kit
6 | /package
7 | .env
8 | .env.*
9 | !.env.example
10 | vite.config.js.timestamp-*
11 | vite.config.ts.timestamp-*
12 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 | resolution-mode=highest
3 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSameLine": true,
4 | "bracketSpacing": true,
5 | "embeddedLanguageFormatting": "auto",
6 | "htmlWhitespaceSensitivity": "css",
7 | "insertPragma": false,
8 | "plugins": ["prettier-plugin-svelte"],
9 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
10 | "printWidth": 100,
11 | "proseWrap": "preserve",
12 | "quoteProps": "as-needed",
13 | "requirePragma": false,
14 | "semi": false,
15 | "singleQuote": true,
16 | "svelteAllowShorthand": true,
17 | "svelteIndentScriptAndStyle": true,
18 | "svelteSortOrder": "options-scripts-markup-styles",
19 | "svelteStrictMode": false,
20 | "tabWidth": 2,
21 | "trailingComma": "all",
22 | "useTabs": false
23 | }
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Svelte Split Testing
2 |
3 | [](https://www.npmjs.com/package/svelte-split-testing)
4 | [](https://www.npmjs.com/package/svelte-split-testing)
5 | [](https://bundlephobia.com/package/svelte-split-testing)
6 |
7 | Run Split Tests (or A/B tests) with the power of Svelte(Kit). 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.
8 |
9 | This package works with Svelte and SvelteKit.
10 |
11 | - Works SSR
12 | - Works with or without Kit
13 | - Works with GTM and GA4, or any other analytics solution
14 | - Lightweight
15 | - Free, of course
16 |
17 | **Demo and Docs**: https://svelte-split-testing.playpilot.com/
18 |
19 | ### Installation
20 |
21 | Install using Yarn or NPM.
22 | ```js
23 | yarn add svelte-split-testing --dev
24 | ```
25 | ```js
26 | npm install svelte-split-testing --save-dev
27 | ```
28 |
29 | Include the component in your app.
30 | ```js
31 | import { SplitTest } from "svelte-split-testing"
32 | ```
33 | ```svelte
34 |
35 | Split tests (or A/B tests) allow you to display different features or variants to test their 36 | effectiveness. Unfortunately popular options are very pricey, bloated, and don't work SSR. 37 | This package attempts to remedy all of that. 38 |
39 |This package works with Svelte and SvelteKit.
40 |41 | Uses Google Tag Manager by default to keep track of your analytics, but this can be 43 | replaced with other methods. 44 |
45 | 46 |66 | What you are doing with your split tests is completely up to you of course. These examples 67 | show some basics of how it all works, but the sky is the limit. 68 |
69 | 70 |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.
71 | 72 | {#key $page.url.searchParams.get('force-split-test')} 73 |
81 | Split tests are randomized based on a cookie or a given user identifier. You can override this
82 | by adding ?force-split-test=[Variant name]
to the url. This is only meant for debugging.
83 |
104 | Note: Forcing the variant doesn't work SSR, if you're viewing this page without Javascript 106 | you might not see a difference. Regular split tests do work during SSR. 107 |
108 |You can include as many variants as you like.
109 | 110 |Install using Yarn or NPM.
136 |yarn add svelte-split-testing --dev
137 | npm install svelte-split-testing --save-dev
138 |
139 | The component isn't quite plug-and-play, we will need to set up some details.
140 |146 | When using SvelteKit we need to make sure the same identifier is set on the server as is set 147 | on the client. 148 |
149 |
150 | In your (main) +layout.server.js
load function import and set the identifier and pass
151 | it along.
152 |
156 | import { serverGetSplitTestIdentifier } from 'svelte-split-testing/splitTesting'
158 |
159 | /** @type {import('./$types').LayoutServerLoad} */
160 | export async function load({ cookies }) {
161 | const splitTestIdentifier = serverGetSplitTestIdentifier(cookies)
164 |
165 | return {
166 | splitTestIdentifier,
167 | }
168 | }
169 |
170 |
171 | Important here is that you pass along the SSR cookies object.
172 |
173 | Next, in your (main) +layout.js
load function pass the identifier along again. We
174 | don't actually need to do anything with it just yet.
175 |
179 | export async function load({ data }) {
180 | const { splitTestIdentifier } = data || {}
181 |
182 | return {
183 | splitTestIdentifier,
184 | }
185 | }
186 |
187 |
188 |
189 | This is a bit verbose, but this is assuming you have other stuff going on in the file as well.
190 | If you don't, you could simply pass it as return { ...data }
.
191 |
193 | Next, in your (main) +layout.svelte
set the identifier using the Context API. Make
194 | sure to use the correct key.
195 |
199 | <script>
200 | import { clientGetSplitTestIdentifier } from 'svelte-split-testing/splitTesting'
202 | import { setContext } from 'svelte'
203 |
204 | export let data
205 |
206 | setContext('splitTestIdentifier', data?.splitTestIdentifier)
209 | </script>
210 |
211 | <slot />
212 |
213 |
214 | And that's the basic set up for SvelteKit. Next up will we go in to usage.
215 |221 | For Svelte (without Kit) we do not yet need any set up. There might be some set up depending 222 | on your needs, but we will get to that in the Usage section. 223 |
224 |
230 | import { SplitTest } from
231 | 'svelte-split-testing'
232 |
233 | <SplitTest>...</SplitTest>
234 |
235 |
236 |
237 | At it's most basic SplitTest
is a wrapper component that takes care of some
238 | business, but for you as the user it's really just a slot. This components has a slot prop
239 | called variant
, this will be used by you to determine what is shown in what
240 | variant.
241 |
243 | When using the component there's 2 important props: key
and
244 | variants
.
245 |
247 | key
Is the name of the split test. It isn't very important for functionally what this
248 | key is, is doesn't even need to be unique. It's only used to identify what split test is for your
249 | analytics tracking.
250 |
252 | variants
Is an array of strings with each variant you want to include. What the
253 | names are is not important, but this name is what will be used in your analytics tracking. You
254 | could go for something simple like ['A', 'B']
, or give them more explicit names
255 | like ['Large Sign Up', 'Small Sign Up']
, it's up to you.
256 |
263 | In the following example we have two variants, each having 50% chance to show. We use the slot
264 | prop variant
to determine what to show for each variant.
265 |
269 | <SplitTest
270 | key="Some test"
271 | variants={['Variant A', 'Variant B']}
274 | let:variant>
275 |
276 | {#if variant === 'Variant A'}
277 | <button>Do the thing</button>
278 | {/if}
279 |
280 | {#if variant === 'Variant B'}
281 | <button>Do the thing, but different</button>
282 | {/if}
283 | </SplitTest>
284 |
285 |
286 |
287 | You don't have to use if
statements, you could use it like any other variable.
288 |
292 | <SplitTest
293 | key="Some test"
294 | variants={['Variant A', 'Variant B']}
297 | let:variant>
298 |
299 | Current showing variant: {variant}
300 | </SplitTest>
301 |
302 |
303 | Using this you could quickly set up the use of different styles, for example.
304 | 305 |
307 | <script>
308 | const variants = {
309 | Plain: 'button-plain',
310 | Colorful: 'button-colorful',
311 | }
312 | </script>
313 |
314 | <SplitTest
315 | key="Some test"
316 | variants={Object.keys(variants)}
317 | let:variant>
318 |
319 | <button class={variants[variant]}>...</button>
320 | </SplitTest>
321 |
322 | 328 | By default the Split Test will use Google Tag Manager, using the data layer, to keep track of 329 | your events. This can be changed to any other method, which will we go in to later. 330 |
331 | 332 |333 | When a split test is shown an event is send to GTM with the given key as the label, and the 334 | current variant as the value. 335 |
336 | 337 |
338 | 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
340 | to change the event type from 'click' to whatever else.
341 |
344 | The data sent looks a little like: { event: 'Split Test', action: [given action], label: [given key], value: [current
346 | variant] }
.
348 |
352 | <SplitTest
353 | key="Some test"
354 | let:variant
355 | let:performAction>
356 |
357 | <button on:click={performAction}>...</button>
358 | </SplitTest>
359 |
360 |
361 |
362 | Of course you might want to track more than just clicks, for this you can pass any string to
363 | the performAction
function. Once again, this is just for tracking purposes, what the
364 | string is doesn't matter.
365 |
369 | <SplitTest
370 | let:variant
371 | let:performAction>
372 |
373 | <form on:submit={() => performAction('form submit')}>
376 | ...
377 | </form>
378 | </SplitTest>
379 |
380 |
381 | If your action takes place outside of the component you can bind the property instead.
382 | 383 |
385 | <script>
386 | let performAction
387 |
388 | function doTheThing() {
389 | performAction()
390 | }
391 | </script>
392 |
393 | <SplitTest
394 | let:variant
395 | bind:performAction>
396 |
397 | <form on:submit={doTheThing}>
398 | ...
399 | </form>
400 | </SplitTest>
401 |
402 |
408 | 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
410 | every time the component is mounted in CSR. It will not fire during SSR.
411 |
415 | <SplitTest onView={(data) => console.log(data)}>...</SplitTest>
417 |
418 |
420 | <script>
421 | function customTracking(data) {
422 | ...
423 | }
424 | </script>
425 |
426 | <SplitTest onView={customTracking}>...</SplitTest>
429 |
430 |
431 |
432 | The data returns an object { key, variant, action: 'view' }
. What you
433 | do with this data is up to you.
434 |
For example, you could use GA4 using gtag
.
440 | <script>
441 | function customTracking(data) {
442 | gtag('event', 'Split Test', data)
443 | }
444 | </script>
445 |
446 | <SplitTest onView={customTracking}>...</SplitTest>
449 |
450 |
451 |
452 | 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.
454 |
458 | <script>
459 | const key = 'Some test'
460 |
461 | function trackClick(variant) {
462 | ...
463 | }
464 | </script>
465 |
466 | <SplitTest
467 | {key}
468 | variants={["A", "B"]}
469 | let:variant>
470 |
471 | <button on:click={() => trackClick(variant)}>...</button>
472 | </SplitTest>
473 |
474 | 481 | This function is used in the set up to set the identifier for SSR. The second parameter in 482 | this function is an object of options. 483 |
484 |
485 |
486 | serverGetSplitTestIdentifier(servercookies, { userIdentifier, cookieName
487 | })
488 |
489 |
491 | userIdentifier
is used to pass an identifier to the function that will be used instead
492 | of a random identifier. This way you can make sure a user sees the same page across different devices,
493 | as long as they use the same identifier. Be aware that will identifier will be saved as a plain
494 | string in a cookie.
495 |
497 | cookieName
can be used to change the name of the cookie. Defaults to
498 | splitTestIdentifier
499 |
501 | If you are setting this as done in the set up, you will need to make sure to pass it along to
502 | the client side as well, using clientGetSplitTestIdentifier
with the same options,
503 | as a fallback value. This needs to be done when setting the context (refer back to the Set Up section).
504 | This is a safety net in case the value was not set as expected.
505 |
setContext('splitTestIdentifier', data?.splitTestIdentifier || clientGetSplitTestIdentifier({ userIdentifier, cookieName }))
510 |
511 | 513 | This used to set the identifier client side, this is optional if you are also using SSR. If 514 | you are using Svelte without Kit, this will be the only function you use. The first and only 515 | parameter is an object of options. 516 |
517 |
518 |
519 | clientGetSplitTestIdentifier({ userIdentifier, cookieName })
520 |
521 |
523 | userIdentifier
is used to pass an identifier to the function that will be used instead
524 | of a random identifier. This way you can make sure a user sees the same page across different devices,
525 | as long as they use the same identifier. Be aware that will identifier will be saved as a plain
526 | string in a cookie.
527 |
529 | cookieName
can be used to change the name of the cookie. Defaults to
530 | splitTestIdentifier
531 |
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.
540 | 541 |
543 | const variant = performSplitTestAction({
544 | key: 'Some test key',
545 | action: 'click',
546 | variants: ['A', 'B'],
547 | force,
548 | trackingFunction: ({ variant }) =>
549 | alert(`Performed action for variant "${variant}"`)
550 | })
551 |
552 | if (variant === "A") doThingA()
553 | else if (variant === "B") doThingB()
554 |
555 |
556 | 557 | 558 |
559 | 560 |Additionally the slot prop variant
can be bound to a variable, allowing it to be re-used for things outside of the component.
564 | <script>
565 | let variant
566 | </script>
567 |
568 | <SplitTest bind:variant />
569 |
570 | This is a list of all configurable properties for each component and function.
576 | 577 |key
'Some Key'
583 | variants
['Variant A', 'Variant B']
587 | onView
null
592 | serverCookies
null
604 | options
{ userIdentifier, cookieName }
606 | options.userIdentifier
null
608 | options.cookieName
'splitTestIdentifier'
616 | options
{ userIdentifier, cookieName }
625 | options.userIdentifier
null
627 | options.cookieName
'splitTestIdentifier'
635 | 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.Test A
{/if} 14 | {#if variant === 'Variant B'}Test B
{/if} 15 | {/snippet} 16 |