Repository navigation
feat(sveltekit-example): add SvelteKit example application (#70) #221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| PUBLIC_ABBY_PROJECT_ID=clfn3hs1t0002kx08x3kidi80 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| .svelte-kit | ||
| node_modules | ||
| dist | ||
| .env | ||
| .env.* | ||
| !.env.example |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # SvelteKit Example App for A/BBY | ||
|
|
||
| This example application showcases the features of **[A/BBY](https://tryabby.com)** with **SvelteKit**. | ||
|
|
||
| ## Features Demonstrated | ||
|
|
||
| - **A/B Testing (`useAbby`)**: Dynamic variants with class lookups, bucket persistence via cookies, and conversion tracking via `onAct()`. | ||
| - **Feature Flags (`useFeatureFlag`)**: Reactive boolean feature flag toggles (e.g., promotional banner). | ||
| - **Remote Config (`useRemoteConfig`)**: Dynamically loaded headlines and copy text from remote configuration. | ||
| - **Variant Reset (`getABResetFunction`)**: Reset user buckets on demand. | ||
| - **Server-Side Integration (`withAbby` & `AbbyProvider`)**: Zero-flicker SSR hydration and cookie management. | ||
|
|
||
| ## Getting Started | ||
|
|
||
| 1. Copy `.env.example` to `.env`: | ||
| ```bash | ||
| cp .env.example .env | ||
| ``` | ||
|
|
||
| 2. Set your `PUBLIC_ABBY_PROJECT_ID` in `.env` (or use the provided default demo project). | ||
|
|
||
| 3. Start the development server: | ||
| ```bash | ||
| pnpm dev | ||
| ``` | ||
|
|
||
| 4. Build for production: | ||
| ```bash | ||
| pnpm build | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "name": "sveltekit-example", | ||
| "version": "0.0.1", | ||
| "private": true, | ||
| "scripts": { | ||
| "dev": "vite dev", | ||
| "build": "vite build", | ||
| "preview": "vite preview", | ||
| "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", | ||
| "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" | ||
| }, | ||
| "devDependencies": { | ||
| "@sveltejs/adapter-auto": "^2.0.0", | ||
| "@sveltejs/kit": "^1.5.0", | ||
| "autoprefixer": "^10.4.14", | ||
| "postcss": "^8.4.21", | ||
| "svelte": "^3.54.0", | ||
| "svelte-check": "^3.0.1", | ||
| "tailwindcss": "^3.3.1", | ||
| "tslib": "^2.4.1", | ||
| "typescript": "^5.0.0", | ||
| "vite": "^4.3.0" | ||
| }, | ||
| "type": "module", | ||
| "dependencies": { | ||
| "@tryabby/svelte": "workspace:*" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export default { | ||
| plugins: { | ||
| tailwindcss: {}, | ||
| autoprefixer: {}, | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| @tailwind base; | ||
| @tailwind components; | ||
| @tailwind utilities; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| declare global { | ||
| namespace App { | ||
| // interface Error {} | ||
| // interface Locals {} | ||
| // interface PageData {} | ||
| // interface Platform {} | ||
| } | ||
| } | ||
|
|
||
| export {}; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <link rel="icon" href="%sveltekit.assets%/favicon.png" /> | ||
| <meta name="viewport" content="width=device-width" /> | ||
| %sveltekit.head% | ||
| </head> | ||
| <body data-sveltekit-preload-data="hover"> | ||
| <div style="display: contents">%sveltekit.body%</div> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { createAbby } from "@tryabby/svelte"; | ||
|
|
||
| export const { | ||
| useAbby, | ||
| useFeatureFlag, | ||
| useRemoteConfig, | ||
| getFeatureFlagValue, | ||
| getRemoteConfig, | ||
| getABTestValue, | ||
| getABResetFunction, | ||
| getVariants, | ||
| withAbby, | ||
| AbbyProvider, | ||
| __abby__, | ||
| } = createAbby({ | ||
| environments: ["production", "development"], | ||
| projectId: "clfn3hs1t0002kx08x3kidi80", | ||
| currentEnvironment: "production", | ||
| tests: { | ||
| ButtonColor: { | ||
| variants: ["blue", "green", "red"], | ||
| }, | ||
| Home: { | ||
| variants: ["A", "B", "C"], | ||
| }, | ||
| }, | ||
| flags: ["showBanner", "clientFlag", "serverFlag"], | ||
| remoteConfig: { | ||
| headline: "String", | ||
| copyText: "String", | ||
| }, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| <script lang="ts"> | ||
| import { useAbby, getABResetFunction } from '$lib/abby'; | ||
|
|
||
| const { variant, onAct } = useAbby('ButtonColor', { | ||
| blue: 'bg-blue-600 hover:bg-blue-700 text-white', | ||
| green: 'bg-emerald-600 hover:bg-emerald-700 text-white', | ||
| red: 'bg-rose-600 hover:bg-rose-700 text-white', | ||
| }); | ||
|
|
||
| let clicked = false; | ||
| let clickCount = 0; | ||
|
|
||
| function handleClick() { | ||
| onAct(); | ||
| clicked = true; | ||
| clickCount += 1; | ||
| } | ||
|
|
||
| function handleReset() { | ||
| const reset = getABResetFunction('ButtonColor'); | ||
| reset(); | ||
| if (typeof window !== 'undefined') { | ||
| window.location.reload(); | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| <div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm"> | ||
| <div class="mb-2 flex items-center space-x-2"> | ||
| <span class="inline-flex rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-semibold uppercase tracking-wider text-blue-800"> | ||
| A/B Test | ||
| </span> | ||
| </div> | ||
| <h2 class="text-xl font-bold text-gray-900"> | ||
| Button Color Experiment | ||
| </h2> | ||
| <p class="mt-1 text-sm text-gray-500"> | ||
| Experiment: <code class="font-mono bg-gray-100 px-1 py-0.5 rounded">ButtonColor</code> | Current Variant: | ||
| <span class="font-bold text-gray-900 uppercase">{$variant}</span> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Show the bucket name instead of CSS classes. The lookup passed to Also applies to: 48-48
|
||
| </p> | ||
|
|
||
| <div class="mt-6 flex flex-wrap items-center gap-4"> | ||
| <button | ||
| type="button" | ||
| on:click={handleClick} | ||
| class="rounded-lg px-6 py-3 font-semibold shadow-sm transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 {$variant}" | ||
| > | ||
| Click to Convert ({$variant.toUpperCase()}) | ||
| </button> | ||
|
|
||
| <button | ||
| type="button" | ||
| on:click={handleReset} | ||
| class="rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" | ||
| > | ||
| Reset Bucket | ||
| </button> | ||
| </div> | ||
|
|
||
| {#if clicked} | ||
| <div class="mt-4 rounded-md bg-green-50 p-3 text-sm text-green-700"> | ||
| Conversion recorded via <code class="font-mono font-bold">onAct()</code>! (Clicks: {clickCount}) | ||
| </div> | ||
| {/if} | ||
| </div> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| <script lang="ts"> | ||
| import { useFeatureFlag } from '$lib/abby'; | ||
|
|
||
| const showBanner = useFeatureFlag('showBanner'); | ||
| const clientFlag = useFeatureFlag('clientFlag'); | ||
| </script> | ||
|
|
||
| {#if $showBanner} | ||
| <div class="bg-indigo-600 px-4 py-3 text-white shadow-md"> | ||
| <div class="mx-auto flex max-w-7xl items-center justify-between"> | ||
| <div class="flex items-center space-x-2"> | ||
| <span class="inline-flex rounded-full bg-indigo-800 px-2.5 py-0.5 text-xs font-semibold uppercase tracking-wider text-indigo-200"> | ||
| Feature Flag | ||
| </span> | ||
| <p class="text-sm font-medium"> | ||
| Special announcement banner is active! (Flag: <code class="font-mono bg-indigo-700 px-1 py-0.5 rounded">showBanner</code>) | ||
| </p> | ||
| </div> | ||
| {#if $clientFlag} | ||
| <span class="text-xs text-indigo-200"> | ||
| (clientFlag is also enabled) | ||
| </span> | ||
| {/if} | ||
| </div> | ||
| </div> | ||
| {/if} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <script lang="ts"> | ||
| import { useRemoteConfig } from '$lib/abby'; | ||
|
|
||
| const headline = useRemoteConfig('headline'); | ||
| const copyText = useRemoteConfig('copyText'); | ||
| </script> | ||
|
|
||
| <div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm"> | ||
| <div class="mb-2 flex items-center space-x-2"> | ||
| <span class="inline-flex rounded-full bg-emerald-100 px-2.5 py-0.5 text-xs font-semibold uppercase tracking-wider text-emerald-800"> | ||
| Remote Config | ||
| </span> | ||
| </div> | ||
| <h1 class="text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl"> | ||
| {$headline || 'A/B Testing with A/BBY & SvelteKit'} | ||
| </h1> | ||
| <p class="mt-3 text-lg text-gray-500"> | ||
| {$copyText || 'In this demo we use cookies and SvelteKit to assign variant buckets, feature flags, and remote config at the edge.'} | ||
| </p> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { withAbby } from "$lib/abby"; | ||
|
|
||
| export const load = withAbby(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <script lang="ts"> | ||
| import type { LayoutData } from './$types'; | ||
| import { AbbyProvider, __abby__ } from '$lib/abby'; | ||
| import '../app.css'; | ||
|
|
||
| export let data: LayoutData; | ||
| </script> | ||
|
|
||
| <AbbyProvider {data} abby={__abby__}> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep the page renderable when project data is unavailable. If the Abby request fails or returns a non-OK response,
|
||
| <slot /> | ||
| </AbbyProvider> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| <script lang="ts"> | ||
| import FeatureFlagBanner from '$lib/components/FeatureFlagBanner.svelte'; | ||
| import RemoteConfigHeadline from '$lib/components/RemoteConfigHeadline.svelte'; | ||
| import ABTestButton from '$lib/components/ABTestButton.svelte'; | ||
| import { useFeatureFlag } from '$lib/abby'; | ||
|
|
||
| const clientFlag = useFeatureFlag('clientFlag'); | ||
| const serverFlag = useFeatureFlag('serverFlag'); | ||
| </script> | ||
|
|
||
| <svelte:head> | ||
| <title>A/B Testing with A/BBY (SvelteKit Example)</title> | ||
| </svelte:head> | ||
|
|
||
| <FeatureFlagBanner /> | ||
|
|
||
| <main class="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> | ||
| <div class="mx-auto max-w-4xl space-y-8"> | ||
| <RemoteConfigHeadline /> | ||
|
|
||
| <ABTestButton /> | ||
|
|
||
| <div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm"> | ||
| <h2 class="text-xl font-bold text-gray-900">Feature Flags Overview</h2> | ||
| <p class="mt-1 text-sm text-gray-500"> | ||
| Feature flags can be toggled in your A/BBY dashboard in real time. | ||
| </p> | ||
|
|
||
| <div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2"> | ||
| <div class="rounded-lg border border-gray-100 bg-gray-50 p-4"> | ||
| <span class="text-xs font-semibold uppercase text-gray-500">Client Flag</span> | ||
| <p class="mt-1 text-lg font-medium text-gray-900"> | ||
| Status: <span class="font-bold {$clientFlag ? 'text-green-600' : 'text-gray-400'}">{$clientFlag ? 'ENABLED' : 'DISABLED'}</span> | ||
| </p> | ||
| {#if $clientFlag} | ||
| <p class="mt-1 text-xs text-green-600">If you see this, clientFlag is enabled.</p> | ||
| {/if} | ||
| </div> | ||
|
|
||
| <div class="rounded-lg border border-gray-100 bg-gray-50 p-4"> | ||
| <span class="text-xs font-semibold uppercase text-gray-500">Server Flag</span> | ||
| <p class="mt-1 text-lg font-medium text-gray-900"> | ||
| Status: <span class="font-bold {$serverFlag ? 'text-green-600' : 'text-gray-400'}">{$serverFlag ? 'ENABLED' : 'DISABLED'}</span> | ||
| </p> | ||
| {#if $serverFlag} | ||
| <p class="mt-1 text-xs text-green-600">If you see this, serverFlag is enabled.</p> | ||
| {/if} | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm"> | ||
| <h2 class="text-xl font-bold text-gray-900">Explore Route Variations</h2> | ||
| <p class="mt-1 text-sm text-gray-500"> | ||
| In this demo, cookies assign a bucket with the variant to show on different routes. | ||
| </p> | ||
| <ul class="mt-4 space-y-2"> | ||
| <li> | ||
| <a href="/home" class="inline-flex items-center font-medium text-indigo-600 hover:text-indigo-800"> | ||
| → Visit /home (Multi-variant test) | ||
| </a> | ||
| </li> | ||
| <li> | ||
| <a href="/marketing" class="inline-flex items-center font-medium text-indigo-600 hover:text-indigo-800"> | ||
| → Visit /marketing (Marketing test) | ||
| </a> | ||
| </li> | ||
| </ul> | ||
| </div> | ||
| </div> | ||
| </main> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| <script lang="ts"> | ||
| import { useAbby, getABResetFunction } from '$lib/abby'; | ||
|
|
||
| const { variant, onAct } = useAbby('Home'); | ||
|
|
||
| function handleReset() { | ||
| const reset = getABResetFunction('Home'); | ||
| reset(); | ||
| if (typeof window !== 'undefined') { | ||
| window.location.reload(); | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| <svelte:head> | ||
| <title>Home Variant - SvelteKit Example</title> | ||
| </svelte:head> | ||
|
|
||
| <main class="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> | ||
| <div class="mx-auto max-w-2xl rounded-xl border border-gray-200 bg-white p-8 shadow-sm"> | ||
| <div class="mb-4 flex items-center space-x-2"> | ||
| <span class="inline-flex rounded-full bg-indigo-100 px-2.5 py-0.5 text-xs font-semibold uppercase tracking-wider text-indigo-800"> | ||
| A/B Test Route | ||
| </span> | ||
| </div> | ||
|
|
||
| <h1 class="text-3xl font-extrabold text-gray-900">Home Page Variant</h1> | ||
| <p class="mt-4 text-lg text-gray-600"> | ||
| You are currently assigned to bucket: | ||
| <span class="inline-block rounded bg-indigo-50 px-3 py-1 text-xl font-bold uppercase text-indigo-700"> | ||
| Variant {$variant} | ||
| </span> | ||
| </p> | ||
|
|
||
| <div class="mt-6 flex items-center gap-4"> | ||
| <button | ||
| type="button" | ||
| on:click={onAct} | ||
| class="rounded-lg bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" | ||
| > | ||
| Track Action (onAct) | ||
| </button> | ||
|
|
||
| <button | ||
| type="button" | ||
| on:click={handleReset} | ||
| class="rounded-lg border border-gray-300 bg-white px-5 py-2.5 text-sm font-semibold text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" | ||
| > | ||
| Reset Bucket | ||
| </button> | ||
| </div> | ||
|
|
||
| <div class="mt-8 border-t border-gray-100 pt-4"> | ||
| <a href="/" class="text-sm font-medium text-indigo-600 hover:text-indigo-800"> | ||
| ← Back to Overview | ||
| </a> | ||
| </div> | ||
| </div> | ||
| </main> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Read the documented project ID from the environment.
The README tells users to set
PUBLIC_ABBY_PROJECT_ID, butcreateAbbyalways receives this literal ID. A user who supplies another project still sees data from the demo project. Read the public environment value here, with the demo ID as a fallback if desired. (raw.githubusercontent.com)