@dif.sh/svelte
v0.8.0
Published
Svelte 5 adapter for @dif.sh/sdk feature flags and A/B tests: SSR-safe assignment, a server load helper, and exposure stores.
Maintainers
Readme
@dif.sh/svelte
Svelte 5 + SvelteKit adapter for @dif.sh/sdk.
It does the SSR-safe parts for you:
- Server assignment:
difLoad()mints a stable anonymousdif_uidcookie, derives audience attributes from request headers, and assigns every registered experiment with the SDK's pure assigner (no exposure firing, no shared state). - No flicker: the client reuses the server's decision and the same
dif_uid, so the first client render matches SSR. - Client-only exposures: the
experiment()store fires exactly one exposure on the client, never on the server.
npm i @dif.sh/sdk @dif.sh/svelteUsage
src/routes/+layout.server.ts (assign on the server):
import "../../dif/generated/client"; // side effect: registers active experiments
import { difLoad } from "@dif.sh/svelte/server";
export const load = (event) => ({ dif: difLoad(event) });src/routes/+layout.svelte (init the SDK once and publish the data to context):
<script lang="ts">
import "../../dif/generated/client"; // the browser bundle has its own registry
import { setContext } from "svelte";
import { initDif, DIF_CONTEXT_KEY } from "@dif.sh/svelte";
import { events } from "../../dif/generated/events"; // cloud config + publishable key
let { data, children } = $props();
setContext(DIF_CONTEXT_KEY, data.dif);
initDif({
data: data.dif,
events, // publishable key + apiUrl, baked in by `dif build`
});
</script>
{@render children()}Keep the dif/generated/client import in +layout.svelte. The import in
+layout.server.ts registers experiments on the server only. Without the
browser import the page still renders the right variant, but the browser's
registry is empty and experiment() drops the exposure with no warning.
dif build writes both files to dif/generated/ at the project root (the
build.out setting in dif/config.yaml), so the paths above are relative to
src/routes/.
Connect the project once with dif connect --key dif_pk_live_… (or dif init
--key …; add --url for a self-hosted deployment). The key and cloud URL live
in dif/config.yaml and dif build bakes them into
dif/generated/events.ts, so there are no PUBLIC_DIF_PUBLISHABLE_KEY /
PUBLIC_DIF_CLOUD_URL env vars to wire up.
Any component (read an experiment as a store):
<script lang="ts">
import { experiment, track } from "@dif.sh/svelte";
const cta = experiment("insights-cta-copy", {
control: () => "Read more",
variant_a: () => "Get the full breakdown",
});
</script>
<a onclick={() => track("insights_cta_click")}>{$cta.value}</a>ISR note
On an ISR-cached route the server load doesn't re-run per visitor, so the
cached HTML is shared. There, experiment() falls back to assigning on the
client from the dif_uid cookie: the server renders the control branch and the
client swaps once after hydration. Don't server-assign on an ISR route unless you
also vary the cache key on the headers difLoad reads.
Preview & QA forcing
Anyone can force a variant by opening a ?_dif= link, with no code and no
devtools. initDif reads it automatically (both difLoad server-side and the
client):
# force one experiment (persists for the tab session, then auto-clears)
https://staging.example.com/insights/foo?_dif=insights-cta-copy=variant_a
# multiple at once
…?_dif=insights-cta-copy=variant_a,home-hero=control
# clear
…?_dif=off
# generate the exact link from the CLI
dif qa --force insights-cta-copy=variant_a --preview-url https://staging.example.com/insights/fooA small preview badge appears whenever a force is active (showing the
experiment → variant and a one-click clear). A forced assignment never fires
an exposure, so QA can't pollute results. The force is stored in a session
_dif cookie (survives navigation, clears on tab close) and the param is
stripped from the address bar. Works in production too. Pass
allowOverrides: false to initDif/difLoad to disable per-env, or
preview: false to hide the badge.
API
difLoad(event, opts?)→DifData: server load helper (@dif.sh/svelte/server).attributesFromHeaders(headers)→AttributeBag: default header mapping; override viaopts.deriveAttributes.initDif(opts): client init; call once in the root layout.opts.allowOverrides/opts.preview(default true).experiment(id, branches)→Readable<{ value, variant }>.track(metric, opts?): fire a metric event.DIF_CONTEXT_KEY: the context key forDifData.
