Skip to content

@ogygia/flags

Overview

Feature flags and A/B tests for ogygia. A kill switch, a rollout, a targeting rule and an A/B/n test are one primitive, flag(), decided on the server during SSR, so the chosen variant is what renders, with no client SDK and no JS for the decision.

On this page

@ogygia/flags gives a kill switch, a staged rollout, a targeting rule and an A/B/n test one primitive: flag(), a name with variants, decided per visitor. The decision happens on the server during SSR, so the chosen variant is what renders. There is no flicker and no client SDK, and a csr = false page can run an A/B test without shipping any JavaScript for it.

Install

pnpm add @ogygia/flags
import { sveltekit } from '@sveltejs/kit/vite';
import { ogygia } from 'ogygia/vite';
import { flags } from '@ogygia/flags/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [ogygia(), flags(), sveltekit()]
});

A flag decides from its own rule even without the plugin, but keep it listed in every app that reads flags: it is how the request learns that a flag was read (for stored pages and federation), and it runs a vendor source and the exposure sink once per request.

A first flag

The second argument is the flag’s shape:

import { flag } from '@ogygia/flags';

export const checkout = flag('new-checkout');                    // kill switch, off until decided on
export const promo = flag('summer-promo', 10);                   // sticky rollout to 10%
export const hero = flag('hero', { control: 80, bold: 20 });     // weighted variants

You read a flag by calling it with the request. On plain SvelteKit, the Kit event is that context:

import { hero } from '$lib/flags';

export const load = (event) => ({ variant: hero(event) });   // 'control' | 'bold', sticky per visitor

On plain SvelteKit, also import the flags module in hooks.server.ts, so every flag is registered before the first request, and give signed-in visitors an identity (Plain SvelteKit). To connect a vendor, call decide() there once:

import { decide } from '@ogygia/flags';
import { openfeature } from '@ogygia/flags/openfeature';
import '$lib/flags';

decide({ source: openfeature(OpenFeature.getClient()), exposure: (events) => analytics.track(events) });

With ogygia’s regions

A component’s schedule rides on its import attribute, so two imports of one file are two different things to test. This experiment asks whether an interactive version of a section is worth its JavaScript:

<script>
  import Pricing from '$lib/Pricing.svelte';                                  // server HTML, zero JS
  import PricingLive from '$lib/Pricing.svelte' with { wake: 'visible' };    // the same file, as an island
  let { data } = $props();
</script>

{#if data.variant === 'bold'}<PricingLive />{:else}<Pricing />{/if}

With other packages

  • @ogygia/router gives every flag the visitor declared once on the table (routes(table, { visitor })), picks a page per variant with hero.pick({ control: Hero, bold: HeroBold }), and gates a whole route with when(checkout, page(NewCheckout)).
  • @ogygia/postrender never stores a page whose render read a flag, so one visitor’s variant is never served to everyone. This is one reason to keep the plugin listed.
  • @ogygia/federation carries every decision across a mount, so a visitor sees the same variant in each team’s part of the page (One world across teams).

Where to go next