Go
The Shipeasy Go server SDK — context-aware client, local evaluation, configs, experiments, and metric tracking.
Server-side flags, configs, experiments, and tracking for Go. Evaluates locally against a background-polled blob — see the shared evaluation model.
Install
go get github.com/shipeasy-ai/sdk-goConfigure
Initialise once at startup with Configure, passing the server key plus an optional Attributes transform that maps your user object onto a shipeasy.User. This does the first fetch and starts the background poll.
import (
"os"
shipeasy "github.com/shipeasy-ai/sdk-go"
)
shipeasy.Configure(shipeasy.Options{
APIKey: os.Getenv("SHIPEASY_SERVER_KEY"),
Attributes: func(u any) shipeasy.User {
user := u.(*User)
return shipeasy.User{"user_id": user.ID, "plan": user.Plan}
},
})Bind a client to the user
NewClient now takes the user object — the Attributes transform turns it into the evaluation context, so the getters take no user argument. It panics if Configure was not called first.
flags := shipeasy.NewClient(currentUser)Evaluate a feature flag
if flags.GetFlag("new_checkout") {
// ship it
}Go has no default arguments, so use the …Or variant for a fallback. It's returned only when the flag can't be evaluated (client not ready or gate absent) — a gate that evaluates false returns false:
on := flags.GetFlagOr("new_checkout", true)For the value plus the reason, use GetFlagDetail — see Evaluation reasons:
d := flags.GetFlagDetail("new_checkout")
// d.Value bool, d.Reason e.g. shipeasy.ReasonRuleMatchRead a dynamic config
cfg, ok := flags.GetConfig("billing_copy")
copy := flags.GetConfigOr("billing_copy", map[string]any{"cta": "Buy"})Resolve an experiment
r := flags.GetExperiment("checkout_button", map[string]any{"color": "blue"})
if r.InExperiment {
_ = r.Group // e.g. "treatment"
_ = r.Params // map[string]any
}Track an event
flags.Track("purchase", map[string]any{"amount": 49})The low-level Engine
The per-call style still works for back-compat — construct an Engine directly with NewEngine and pass the user on every call:
engine := shipeasy.NewEngine(shipeasy.Options{APIKey: os.Getenv("SHIPEASY_SERVER_KEY")})
if err := engine.Init(context.Background()); err != nil {
panic(err)
}
defer engine.Destroy()
engine.GetFlag("new_checkout", shipeasy.User{"user_id": "u_123"})Anonymous visitors
shipeasy.Middleware mints the shared __se_anon_id cookie for any request without one; read it with shipeasy.AnonID(r) so the server buckets identically to the browser:
http.ListenAndServe(":8080", shipeasy.Middleware(mux))
func handler(w http.ResponseWriter, r *http.Request) {
flags := shipeasy.NewClient(anonUser(shipeasy.AnonID(r)))
if flags.GetFlag("new_checkout") { /* ... */ }
}Testing
NewTestEngine does zero network — no key, Init/Track are no-ops. Seed with Override* and reset with ClearOverrides. See Testing.
engine := shipeasy.NewTestEngine()
engine.OverrideFlag("new_checkout", true)
engine.GetFlag("new_checkout", shipeasy.User{"user_id": "u_123"}) // true
engine.ClearOverrides()Errors & feedback
Report a handled exception with See() so it folds into the errors primitive with a one-sentence consequence — what feature broke and how it degraded. See() rides the same Configure() boot (server key); there is no separate error SDK or second key.
if err := chargeCard(order, prices); err != nil {
shipeasy.See(err).CausesThe("checkout").To("use cached prices").
Extras(map[string]any{"order_id": order.ID})
}Use shipeasy.SeeViolation("large query") for a non-exception problem. Full grammar — consequence phrasing, control-flow exceptions, anti-patterns — is in Error reporting with see().
The in-app bug & feature report overlay is a standalone <script> tag you drop into your frontend — platform-agnostic, no server SDK required. See The devtools overlay.