Advanced
For logged-out traffic you need a stable unit so a fractional rollout buckets identically on the server and in the browser. shipeasy.Middleware mints a…
Generated from the SDK's own /docs/ — also served raw at
https://shipeasy-ai.github.io/sdk-go/pages/advanced.md.
Anonymous-visitor bucketing & middleware
For logged-out traffic you need a stable unit so a fractional rollout buckets
identically on the server and in the browser. shipeasy.Middleware mints a
first-party __se_anon_id cookie (shared with every Shipeasy SDK) for any
request lacking one, and exposes it via shipeasy.AnonID(r):
mux := http.NewServeMux()
// ... register handlers ...
http.ListenAndServe(":8080", shipeasy.Middleware(mux))
func handler(w http.ResponseWriter, r *http.Request) {
// Bind the anon id as the user, then read as usual.
c := shipeasy.NewClient(shipeasy.User{"anonymous_id": shipeasy.AnonID(r)}) // or {"user_id": ...}
if c.GetFlag("new_checkout") { /* ... */ }
}The cookie is non-HttpOnly by design (the browser SDK reads it). A request with
no unit still resolves a fully-rolled (100%) gate as on; only fractional
gates need the id. Lower-level helpers: MintAnonID, ReadOrMintAnonID,
SetAnonIDCookie. The cookie name/format is a cross-SDK contract
(18-identity-bucketing.md).
SSR bootstrap
Emit the request's evaluated flags as a declarative <script> tag so the browser
SDK has them on first paint (no key embedded). Both helpers are package-level and
run off the global configuration:
user := shipeasy.User{"user_id": "u_123"}
head := shipeasy.BootstrapScriptTag(user, shipeasy.BootstrapTagOptions{AnonID: anonID}) +
shipeasy.I18nScriptTag(clientKey, "en:prod", shipeasy.BootstrapTagOptions{})BootstrapTagOptions accepts AnonID, I18nProfile, and BaseURL (defaults to
https://cdn.shipeasy.ai).
Manual exposure
The server is stateless and never auto-logs exposures. When you actually present
a treatment, call LogExposure on the bound Client. It re-evaluates the
experiment against the bound attributes (so bucketBy and anonymous_id-only
traffic resolve correctly); if the user is enrolled, one
{type:"exposure", experiment, group, user_id/anonymous_id, ts} event is POSTed
to /collect:
c := shipeasy.NewClient(shipeasy.User{"anonymous_id": anonID}) // bind once
c.LogExposure("checkout_button")No-op in local mode (test/offline) or when the user isn't enrolled.
Private attributes
Options.PrivateAttributes lists event-property keys stripped from every
outbound /collect payload (Track, LogExposure, See extras). Server
evaluation is local, so private attrs never egress for evaluation either —
only Track/exposure/error events ever leave the process.
shipeasy.Configure(shipeasy.Options{
APIKey: os.Getenv("SHIPEASY_SERVER_KEY"),
PrivateAttributes: []string{"email", "ssn"},
})bucketBy
An experiment can bucket on an attribute other than the individual (e.g.
company_id to keep a whole org on one variant). It's a property of the
experiment definition; supply the attribute on the user you bind and the SDK uses
it as the bucketing unit (falling back to user_id ?? anonymous_id):
c := shipeasy.NewClient(shipeasy.User{"user_id": "u_123", "company_id": "acme"})
r := c.GetExperiment("new_dashboard", nil)
_ = rSticky bucketing
A StickyBucketStore locks in experiment assignments per bucketing unit so a
later weight/allocation change can't reshuffle an enrolled user (a salt change
still reshuffles). Supply one via Options.StickyStore:
store := shipeasy.NewInMemoryStickyStore() // process-local; or implement StickyBucketStore
shipeasy.Configure(shipeasy.Options{
APIKey: os.Getenv("SHIPEASY_SERVER_KEY"),
StickyStore: store,
})NewInMemoryStickyStore is process-local (handy for tests and single-process
servers). Implement the StickyBucketStore interface (Get/Set, keyed by
unit) for a shared/persistent store. Implementations must be safe for concurrent
use. Absent ⇒ purely deterministic bucketing.
Change listeners
Register a callback fired after a background poll loads new data (a 200, not
a 304). It is package-level and returns a cancel func. It requires
Configure(Options{Poll: true}) — no poll runs otherwise:
cancel := shipeasy.OnChange(func() {
log.Println("flags/experiments changed; re-render or warm caches")
})
defer cancel()A panicking listener is recovered and logged so it can't take down the poll loop. Test/offline configurations never poll, so they never fire listeners.
OpenFeature provider
The Go SDK ships a first-class OpenFeature provider so apps standardized on the CNCF OpenFeature API can plug Shipeasy in as the backing flag provider. It…
Admin API client (optional) — `admin` module
The base SDK evaluates flags, configs, and experiments (Configure() + NewClient(user)). The Admin API client is a separate, optional surface for…