Shipeasy
ReferencePython

Advanced

For logged-out traffic you need a stable unit so a fractional rollout buckets the same on the server and in the browser. The middleware mints a first-party…

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

Anonymous-id bucketing + middleware

For logged-out traffic you need a stable unit so a fractional rollout buckets the same on the server and in the browser. The middleware mints a first-party __se_anon_id cookie (shared with every Shipeasy SDK) for any request without one; evaluations then default to it as anonymous_id, so get_flag on an anonymous request just works — no per-call wiring.

# WSGI (Flask, Django, ...)
from shipeasy.middleware import AnonIdMiddleware
app.wsgi_app = AnonIdMiddleware(app.wsgi_app)

# ASGI (FastAPI, Starlette)
from shipeasy.middleware import AnonIdASGIMiddleware
app.add_middleware(AnonIdASGIMiddleware)
# logged-out request → buckets on the __se_anon_id cookie automatically
client = shipeasy.Client({})
client.get_flag("new_checkout")

An explicit user_id/anonymous_id always wins. The id is also on the request (environ["shipeasy.anon_id"]). 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.

Private attributes

Pass private_attributes to configure() to strip the named keys from every outbound event properties bag before it POSTs to /collect (LD/Statsig privateAttributes). The server evaluates locally, so private attrs still drive targeting — they just never leave the process on the telemetry path:

shipeasy.configure(api_key="sdk_server_...", private_attributes=["email", "ssn"])

Sticky bucketing

Pass a sticky_store to configure() to pin a user's experiment assignment across allocation changes. InMemoryStickyStore is built in; implement the StickyBucketStore protocol (get(unit) / set(unit, exp, entry)) for a durable backend:

from shipeasy import InMemoryStickyStore

shipeasy.configure(api_key="sdk_server_...", sticky_store=InMemoryStickyStore())

Absent a store, bucketing is deterministic (MurmurHash3 over the unit).

Bucketing unit (bucketBy)

The bucketing unit per experiment is server-driven: an experiment can be configured to bucket on a non-default attribute (e.g. company_id) in the dashboard, and the SDK reads it from the experiment definition (exp.bucketBy) — falling back to user_id then anonymous_id. Make sure that attribute is present in the user map you pass.

Manual exposure — log_exposure

The server is stateless and never auto-logs exposures. Call log_exposure at the real decision point (when you actually present the treatment) for parity with the browser's auto-exposure. It re-evaluates the experiment and, if the user is enrolled, POSTs a single exposure event — on the same bound Client, no user argument:

# construct once per callsite (cheap; binds the user)
client = shipeasy.Client(current_user)

client.log_exposure("checkout_button")

No-op in test mode or when the user isn't enrolled.

Server-side rendering (SSR)

Emit the request's evaluated flags as a declarative <script> tag so the browser SDK has them on first paint. shipeasy.bootstrap_script_tag carries the payload in data-* attributes (no key); the static se-bootstrap.js loader hydrates window.__SE_BOOTSTRAP and writes the __se_anon_id cookie so the browser buckets identically to the server. Both helpers delegate to the engine configured via configure() — you never touch it directly.

import shipeasy

user = {"user_id": "u_123"}

# Two tags for the document <head>. The PUBLIC client key (not the server key)
# goes on the i18n loader tag.
head = shipeasy.bootstrap_script_tag(user, anon_id=anon_id) \
     + shipeasy.i18n_script_tag(client_key, "en:prod")

bootstrap_script_tag also accepts i18n_profile= and base_url=.

Was this page helpful?
✎ Edit this page

On this page