Shipeasy
ReferenceKotlin

Advanced

Attribute names usable for targeting but never persisted in analytics (LD/Statsig privateAttributes). Since the server evaluates locally, private attrs…

Generated from the SDK's own /docs/ — also served raw at https://shipeasy-ai.github.io/sdk-kotlin/pages/advanced.md.

Private attributes

Attribute names usable for targeting but never persisted in analytics (LD/Statsig privateAttributes). Since the server evaluates locally, private attrs never leave for evaluation at all; the only egress is /collect, and the listed keys are stripped from every outbound track() payload and see() extras.

configure(
    apiKey = System.getenv("SHIPEASY_SERVER_KEY"),
    privateAttributes = listOf("email", "ip"),
)

Sticky bucketing

Pass a StickyBucketStore so getExperiment locks a unit to its first-assigned variant — changing allocation % or weights won't re-bucket enrolled units (changing the experiment salt is the reshuffle lever). Absent ⇒ deterministic (fully backward compatible). A built-in in-memory store is provided:

import ai.shipeasy.InMemoryStickyStore

configure(
    apiKey = System.getenv("SHIPEASY_SERVER_KEY"),
    stickyStore = InMemoryStickyStore(),
)

Implement the StickyBucketStore interface to back it with Redis/DB for multi-process servers.

Anonymous-id bucketing (AnonIdFilter)

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.

// Spring Boot — a default FilterRegistrationBean maps to all paths
@Bean
fun shipeasyAnonId() = FilterRegistrationBean(AnonIdFilter())

// logged-out request → buckets on the __se_anon_id cookie automatically
val flags = Client(loggedOutUser)
flags.getFlag("new_checkout")     // bound Client with no explicit unit

jakarta.servlet-api is compileOnly. Non-servlet stacks (Ktor, http4k, Javalin) can use the AnonId primitives directly. An explicit user_id/anonymous_id always wins. The cookie is non-HttpOnly by design so the browser SDK buckets identically; a request with no unit still resolves a fully-rolled (100%) gate as on. Cookie name + format are a cross-SDK contract — see 18-identity-bucketing.md.

Manual exposure

The server is stateless and never auto-logs. Call logExposure where you present the treatment. The bound Client derives the unit from the bound user:

val flags = Client(currentUser)
flags.logExposure("checkout_button")

See Experiments.

Change listeners — onChange

Subscribe to data-change notifications with the top-level onChange function. The listener fires after a background poll brings new data (HTTP 200, not 304) — not for the initial fetch, and never under configureForTesting / configureForOffline. Requires configure(..., poll = true). Returns an unsubscribe function.

import ai.shipeasy.onChange

val unsubscribe = onChange {
    // rebuild any cached evaluations, warm a downstream cache, etc.
}
// later
unsubscribe()

Each listener runs in a try/catch; a throwing listener is logged and does not affect the others.

Server-side rendering (SSR)

Emit the request's evaluated flags as a declarative <script> tag so the browser SDK has them on first paint. bootstrapScriptTag (a top-level function backed by the global configure() state) carries the payload in data-* attributes (no key); the static loader hydrates window.__SE_BOOTSTRAP and writes the __se_anon_id cookie so the browser buckets identically to the server.

import ai.shipeasy.bootstrapScriptTag
import ai.shipeasy.i18nScriptTag

val user = mapOf("user_id" to "u_123")

// Two tags for the document <head>. The PUBLIC client key (not the server key)
// goes on the i18n loader tag.
val head = bootstrapScriptTag(user, anonId = anonId) +
           i18nScriptTag(clientKey, "en:prod")

bootstrapScriptTag also accepts i18nProfile and baseUrl (default https://cdn.shipeasy.ai).

Was this page helpful?
✎ Edit this page

On this page