Browser & React
The client build of @shipeasy/sdk — client-key init, identify, in-browser evaluation, the DevTools overlay, and SSR bootstrap.
The client build (@shipeasy/sdk/client) runs in the browser. It manages an anonymous-id cookie, fetches an evaluation bundle on init, evaluates locally, and batches event uploads with navigator.sendBeacon on page hide.
Install
Same package as the server — bundlers pick the browser build automatically, or import @shipeasy/sdk/client explicitly.
Configure
Initialise once at startup, with the public client key. The browser SDK requires the explicit clientKey call — there is no bootstrap-key auto-init. Pass an optional attributes transform that maps your user object onto the Shipeasy attribute map.
import { configure, Client } from "@shipeasy/sdk/client";
configure({
clientKey: import.meta.env.VITE_SHIPEASY_CLIENT_KEY ?? "",
attributes: (u) => ({ user_id: u.id, plan: u.plan }),
});Never put a server key in a browser bundle. The two key kinds aren't interchangeable. See Keys & environments.
Bind a client to the user
Construct a Client from your user object and await ready() once — it fetches the evaluation bundle, merges browser context (locale, timezone, path, referrer, screen_*, user_agent) and the persisted anonymous_id, so gate rules can target by locale and holdouts can hash anonymous visitors out of the box.
const flags = new Client(user);
await flags.ready();Read flags, configs, experiments
The bound getters take no user argument — the bound context is implicit:
if (flags.getFlag("new_checkout")) {
// ship it
}
flags.getFlag("new_checkout", true); // default only when not-ready / not-found
const cfg = flags.getConfig<{ max_uploads: number }>("upload_limits");
const { inExperiment, group, params } = flags.getExperiment("hero_cta", {
primary_label: "Sign up",
});Track an event
flags.track("checkout_viewed", { source: "nav" });Fire-and-forget — events are batched and flushed with sendBeacon on page hide.
React is a thin wrapper
React conveniences are a thin layer on top of the framework-agnostic API. There are no React-only
methods — drop the SDK into an Astro island, an HTMX page, or a plain
<script> and every primitive works without React.
The browser SDK dispatches se:override:change whenever a re-poll updates the cached blob or the devtools overlay flips an override — wrap it in your framework's reactivity:
import { useEffect, useState } from "react";
import { configure, Client } from "@shipeasy/sdk/client";
configure({
clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY ?? "",
attributes: (u) => ({ user_id: u.id, plan: u.plan }),
});
const flags = new Client(currentUser);
function CheckoutButton() {
const [on, setOn] = useState(() => flags.getFlag("new_checkout"));
useEffect(() => {
const h = () => setOn(flags.getFlag("new_checkout"));
window.addEventListener("se:override:change", h);
return () => window.removeEventListener("se:override:change", h);
}, []);
return <button>{on ? "New flow" : "Old flow"}</button>;
}SSR bootstrap & the anon cookie
For Next.js / Remix / SvelteKit, the server configure({ apiKey }) call evaluates the bundle and lands the same data on window.__SE_BOOTSTRAP. Configure the client with configure({ clientKey }) and the bootstrap auto-hydrates before your first read — no flicker.
Logged-out visitors bucket on a shared first-party __se_anon_id cookie, minted by middleware on HTML routes. Because the server and browser read the same cookie, a fractional rollout buckets a visitor identically on both sides. The cookie is non-HttpOnly by design so the browser SDK can read it.
DevTools overlay
Press Shift+Alt+S on any page running the SDK (or append ?se=1). The panel mounts in a Shadow DOM overlay and flips every gate / config / experiment / translation for the current session only — for QA, demos, and bug repro.
import { loadDevtools } from "@shipeasy/sdk/client";
if (process.env.NODE_ENV !== "production") loadDevtools();Full guide: DevTools overlay.
Testing
Test against the Engine directly — forTesting() does zero network:
import { Engine } from "@shipeasy/sdk/client";
const engine = Engine.forTesting();
engine.overrideFlag("new_checkout", true);
engine.getFlag("new_checkout"); // true — no network
engine.clearOverrides();Browser override precedence: programmatic override > URL/devtools override (?se_gate_…) > the server's evaluation. See Testing.
Errors & feedback
Two layers, both part of this SDK — no separate error package, no second key.
Auto-collected client errors. The autoCollect errors group wraps fetch (5xx + network failures, each named to a specific endpoint) and ships type:"error" events to the errors primitive. It's on by default the moment you configure({ clientKey }) — nothing to enable. It does not blanket-capture window.onerror / unhandledrejection. Opt out without losing vitals/engagement:
configure({ clientKey, autoCollect: { errors: false } });Handled exceptions with see(). Report a caught exception with a one-sentence consequence so it folds into the same errors primitive:
import { see } from "@shipeasy/sdk/client";
try {
await submitOrder(order);
} catch (e) {
see(e).causes_the("checkout").to("use cached prices").extras({ order_id: order.id });
}Use see.Violation("name") for a non-exception problem and see.ControlFlowException(e).because("…") for expected control flow. Full grammar: Error reporting with see(). The in-app bug/feature report overlay is the devtools overlay shown above.