Shipeasy

User attributes

The shape of the eval context — pass everything you know about the user, and Shipeasy uses it for targeting, holdouts, and analysis breakdowns.

Production readyOn this page · 7 min readUpdated · May 3, 2026Works with · Server SDK · Browser SDK · Targeting · Experiments

A user attribute is any key/value pair you attach to a user when calling the SDK. Shipeasy uses attributes for three things:

  1. Targeting rules on feature flags — "only enable this for plan = pro".
  2. Variant assignment in experiments — "assign by account_id so all teammates see the same thing".
  3. Segmentation in analysis — "how did the lift differ for country = US vs EU?".

The richer your attribute payload, the more precise your targeting and analysis. Most teams under-invest here on day one and regret it.

The eval context

Every SDK call that resolves a flag, config, or experiment takes an eval context — the user object plus any attributes you want available to the rule engine.

const ctx = {
  user_id: "u_4f2a", // required for deterministic bucketing
  anonymous_id: "anon_xyz", // optional, auto-managed in the browser

  // Custom attributes — flat top-level keys
  plan: "pro",
  country: "US",
  beta_tester: true,
  team_size: 18,
  signup_at: "2026-01-12T00:00:00Z",
};

const flags = new Client(ctx);
flags.getFlag("new-checkout");

You bind a Client to the eval context once, then read with no per-call user argument. The attributes transform you pass to configure() is what maps your own user object onto this context shape — see Install.

Reserved attributes

Two keys are special:

Prop

Type

Shipeasy bucketing uses user_id ?? anonymous_id — so a user gets a stable assignment before and after login. When the user authenticates, call shipeasy.alias(anonymous_id, user_id) to stitch their pre-login exposures to their post-login identity. Without this stitch, exposure events from before login show up as a separate user in analysis.

`user_id` is required for deterministic bucketing

Without a stable id, every evaluation gets a fresh random bucket — rollouts oscillate, experiment variants flip between page loads, and analysis falls apart. Pass user_id as soon as you have it; pass anonymous_id for the pre-login window.

Custom attributes

Everything else lives at the top level of the context object. There is no custom: { … } wrapper.

shipeasy.identify({
  user_id: "u_4f2a",
  plan: "pro",
  country: "US",
  signup_at: "2026-01-12T00:00:00Z",
  beta_tester: true,
  team_size: 18,
  roles: ["admin", "billing"],
});

Supported value types:

Prop

Type

Nested objects are not supported — flatten them with dot-notation if you need to (org.tier becomes a literal key "org.tier").

Standard attributes worth passing

Most projects benefit from a few standard fields. Set them up once and your future targeting + analysis is much easier.

Registering attributes in the dashboard

Go to Project → User attributes → Register and declare the attributes you plan to send. Registration is optional but unlocks two things:

  1. The dashboard rule builder can autocomplete attribute names and validate types.
  2. Analysis surfaces attributes as breakdown dimensions in experiment results.

Unregistered attributes still work — they just won't appear in pickers.

Targeting operators

Targeting rules use these operators against attribute values:

Prop

Type

Multiple rules on a feature flag are ANDed. Use in with a list to express OR. Rule order doesn't matter — there is no fall-through.

Server vs browser

On the server, you bind a Client to the context, then read with no user argument:

const flags = new Client({ user_id, plan, country });
flags.getFlag("new-ui");

In the browser, you bind the Client once after the user logs in and reuse it for every subsequent flag / config / experiment read:

const flags = new Client({ user_id, plan, country });

if (flags.getFlag("new-ui")) {
  // …
}

Anonymous IDs

Before login, the browser SDK auto-generates an anonymous_id and persists it in a cookie. The cookie is Lax, Secure (in production), and scoped to your domain.

On the server, generate an anonymous ID yourself — a UUID stored in AsyncStorage (React Native) or a session cookie (server-rendered apps) is the right pattern. The SDK doesn't care where the value comes from, only that it's stable per browser/device.

When the user logs in:

shipeasy.alias(anonymous_id, user_id);

This call writes a single row to D1 mapping the two IDs. The next analysis run merges their exposures into one user.

Bucketing by an attribute

By default Shipeasy buckets users by user_id. To bucket by something else (typical for B2B: bucket by account so teammates align):

const flags = new Client({
  user_id: "u_4f2a",
  account_id: "acct_910",
  plan: "pro",
});
flags.getFlag("new-ui");

Set the gate's (or experiment's) bucket_by to account_id in the dashboard so every read keys on the account attribute you pass on the bound client. The same identity is then used for both assignment and exposure stitching.

Privacy & PII

Shipeasy stores attributes you send as part of exposure events for analysis. Don't send PII you wouldn't want in your analytics warehouse.

Hard rules
  • Pass user IDs, not emails. Emails are PII and they change. - Pass country, not full IP. Shipeasy already derives a coarse geo from the request. - Hash anything sensitive on your side first — names, phone numbers, account secrets. - Never put an API token, password, or session cookie into an attribute.

A handful of fields are auto-treated:

Prop

Type

You can set per-attribute retention in Project → Privacy (default 90 days for Pro, 30 days for Free). After retention, exposure rows are aggregated and the per-user attribute payload is dropped.

Troubleshooting

Targeting rules don't match

Two common causes: type mismatch ("5" is not 5) and missing attribute (an undefined attribute fails every comparison except not_in against a list that contains it). Check the dashboard's Test rules panel — paste a sample context, see exactly which rules pass.

Experiment assignments flip between page loads

You're evaluating without a stable user_id. Either pass user_id (preferred) or pass a stable anonymous_id for the pre-login window.

NEXT

Ship your first flag.

The shortest path from install to a flag in production. Five minutes, no card.

Create a feature flag
$shipeasy gate create new-checkout
Roll out to 5%
$shipeasy gate rollout new-checkout --percent 5
Was this page helpful?
✎ Edit this page

On this page