Advanced
Exposure is automatic but fires on read, not at assign() time: assign() is side-effect free, and the first get(...) on an enrolled assignment POSTs one…
/docs/ — also served raw at https://shipeasy-ai.github.io/sdk-java/pages/advanced.md.Exposure logging
Exposure is automatic but fires on read, not at assign() time:
assign() is side-effect free, and the first get(...) on an enrolled
assignment POSTs one {type:"exposure", experiment, group, user_id, ts} event to
/collect. An assignment that is computed but never read logs nothing. There is
no manual logExposure. Just assign and read at the point you present the
experiment:
// Assumes Shipeasy.configure(...) ran at startup — see Installation.
Client c = new Client(Map.of("user_id", "u_123", "plan", "pro")); // construct once per callsite
Assignment a = c.universe("hero_cta").assign(); // side-effect free
String label = (String) a.get("primary_label", "Sign up"); // first get() logs the exposure
String peeked = (String) a.peek("primary_label", "Sign up"); // read WITHOUT logging an exposureThe exposure is deduped per process and, durably, per
(unit, experiment, group) server-side: repeated reads for the same
(unit, experiment, group) POST only one exposure. Use peek(...) to read a
param without logging at all. No-op in test/snapshot mode or when the unit isn't
enrolled.
Private attributes
Mark attribute names that may be used for targeting but must never be
persisted in analytics (LD/Statsig privateAttributes). The server evaluates
locally, so private attrs never leave for evaluation; the only egress is
/collect, and these keys are stripped from every outbound track() payload
(and see() extras):
Shipeasy.configure(Shipeasy.options(System.getenv("SHIPEASY_SERVER_KEY"))
.privateAttributes(List.of("email", "ip")));Bucketing identifier (bucketBy)
bucketBy is a server-side experiment property — it is set on the
experiment in the Shipeasy dashboard (e.g. company_id), and the SDK reads it
from the experiment blob automatically. When set, the experiment buckets on that
attribute instead of user_id/anonymous_id. Make sure the bucketing attribute
is present in the user map you pass. There is no per-call bucketBy knob in the
SDK.
Sticky bucketing
Supply a StickyBucketStore so an enrolled unit stays locked to its
first-assigned variant even if you change allocation % or group weights
(changing the experiment salt is the reshuffle lever). Absent ⇒ deterministic
(fully backward compatible):
import ai.shipeasy.InMemoryStickyStore;
Shipeasy.configure(Shipeasy.options(System.getenv("SHIPEASY_SERVER_KEY"))
.stickyStore(new InMemoryStickyStore()));InMemoryStickyStore is a process-local, thread-safe store (good for tests and
single-process servers). Implement StickyBucketStore (get(unit) /
set(unit, exp, entry)) over a shared cache (Redis, DB) for multi-process
deployments.
Anonymous visitors — AnonIdFilter (zero-config bucketing)
For logged-out traffic you need a stable unit so a fractional rollout buckets
the same on the server and in the browser. AnonIdFilter is a servlet Filter
that mints the shared __se_anon_id first-party cookie for any request without
one; evaluations then default to it as anonymous_id, so a logged-out
request needs no per-call wiring.
// Spring Boot
@Bean
FilterRegistrationBean<AnonIdFilter> shipeasyAnonId() {
var reg = new FilterRegistrationBean<>(new AnonIdFilter());
reg.addUrlPatterns("/*");
return reg;
}// logged-out request → buckets on the __se_anon_id cookie automatically
new Client(Map.of()).getFlag("new_checkout");An explicit user_id/anonymous_id always wins. The cookie is non-HttpOnly
by design so the browser SDK buckets identically. Non-servlet stacks (Ktor,
http4k, Javalin) can use the AnonId primitives directly.
Server-side rendering (SSR) bootstrap
Emit the request's evaluated flags as a declarative <script> tag so the
browser SDK has them on first paint. bootstrapScriptTag carries the payload in
data-* attributes (no key — the server key must never reach the browser);
the /sdk/runtime.js browser runtime reads them, installs window.shipeasy,
republishes window.__SE_BOOTSTRAP for the npm client SDK and writes the
__se_anon_id cookie so the browser buckets identically to the server:
// Assumes Shipeasy.configure(...) ran at startup — see Installation.
Map<String, Object> user = Map.of("user_id", "u_123");
String head = Shipeasy.bootstrapScriptTag(user, anonId)
+ Shipeasy.i18nScriptTag();Every argument is optional
The tag helpers overload down to a no-argument call: the client key, profile,
project id and CDN base all come from the configure options, and a null
argument (or no user) falls back the same way.
| Helper | No-argument form | Defaults from the options |
|---|---|---|
Shipeasy.i18nScriptTag() | i18nScriptTag(clientKey, profile, baseUrl) | .clientKey, .profile, .cdnBaseUrl |
Shipeasy.bootstrapScriptTag() | bootstrapScriptTag(user, anonId, i18nProfile, baseUrl) | anonymous request, .profile, .cdnBaseUrl |
Shipeasy.devtoolsScriptTag() | devtoolsScriptTag(projectId, clientKey, baseUrl, defer) | .projectId, .clientKey, .cdnBaseUrl |
Shipeasy.configure(Shipeasy.options(System.getenv("SHIPEASY_SERVER_KEY"))
.clientKey(System.getenv("SHIPEASY_CLIENT_KEY")) // PUBLIC key, for the tags
.projectId(System.getenv("SHIPEASY_PROJECT_ID")) // for the devtools tag
.profile("en:prod"));A tag still renders when a value is missing (the browser bundle reports what it needs), but the SDK logs a warning naming the option to fill in — once per option, not once per render.
Devtools overlay tag
Shipeasy.devtoolsScriptTag() emits the hosted devtools overlay bundle —
nothing to install, no overlay code in your artifact. It reads the project id and
public client key off the tag and opens with Shift+Alt+S or on any page
loaded with ?se=1. It is deferred unless you pass defer = false: a developer
tool never belongs on the critical rendering path.
String devtools = Shipeasy.devtoolsScriptTag();Adding it unconditionally is fine: the overlay only opens for someone with a signed-in Shipeasy session, so on a page where nobody has authenticated it renders nothing and says nothing. Gating it on your own staff or environment check is optional — worth it only if you'd rather the bundle not load for end users at all:
String devtools = user.isStaff() ? Shipeasy.devtoolsScriptTag() : "";Identity coherence (no anon to identified flip)
When the user you evaluate carries an identified attribute (a user_id,
email, or any trait other than anonymous_id), the tag also emits a
data-user attribute — the HTML-escaped JSON of those traits, with
anonymous_id and null values dropped. The browser SDK reads it and adopts that
server-known identity on first paint, so a Java backend with a JS frontend never
shows the anonymous-then-identified flip (the same identity buckets flags on both
sides). An anonymous request — only an anonymous_id, or an empty user — emits no
data-user. See Identity & bucketing.
Change listeners
Register a listener that fires after a background poll applies new data (an
HTTP 200, not a 304). onChange returns a cancel Runnable:
// Start the background poll so listeners can fire (configure owns the lifecycle):
Shipeasy.configure(Shipeasy.options(System.getenv("SHIPEASY_SERVER_KEY")).poll(true));
Runnable cancel = Shipeasy.onChange(() -> log.info("flags updated"));
// ... later
cancel.run(); // unsubscribeListeners never fire in local/test/snapshot mode (those do no polling) and a throwing listener is isolated — it's logged, others still run.