Skip to Content
Delmare Digital

Assignment and targeting

How people are identified and assigned a version, what targeting rules can match, and how consent works.

Assignment decides which version a person sees. It happens in the Next.js proxy on each request, is deterministic, and is the same for the same person on every request and every server instance.

Units: visitors and signed-in users

A test counts one of two units:

  • Visitors (visitor, the default). Each browser gets a signed, opaque id in the xp_id cookie.
  • Signed-in users (user). The unit is your user id, so a person sees the same version on every device. You supply it through getUnit and getUser (see Signed-in users).

Page tests always use visitors.

How assignment works

Each test round has a random seed. A person's version is chosen by hashing the seed with their unit id (GrowthBook's SDK hash, version 2) and mapping the result onto the group percentages. Because the hash is deterministic, no assignment table is stored; any instance computes the same answer from the signed cookie and the manifest.

  • Group percentages are whole numbers that total 100. A holdout is an explicit group, never a dropped remainder.
  • Allocation is applied separately. Raising allocation widens each group's range, so nobody already taking part moves to another group.
  • A new round gets a new seed, so people are reassigned.

The proxy writes the assignment into a signed x-xp-assignments request header (at most 1 KB). Server code reads it with getAssignments, which verifies the signature and recomputes any stale or missing entries. A changed or unsigned header is ignored.

PropertyValue
Namexp_id (change with cookie.name)
Lifetime365 days (change with cookie.maxAgeDays)
FlagsHttpOnly, SameSite=Lax, Secure on HTTPS, path /
ContentA signed opaque id. No personal data.

A newly created cookie is forwarded into the same request, so server components see the assignment on the first visit.

Targeting

Targeting rules limit a test to people with matching attributes. All rules must match. People who do not match see the Original and are not counted.

Attributes come from the server, never from the browser:

  • path, owned by the plugin: the request path with no search string and no trailing slash (except /). Block tests run where their block is placed, so they cannot use a path rule; it is offered for setting and code tests.
  • loggedIn and staff, from your getUnit and attributesOf.
  • Your own attributes, declared in the plugin's attributeKeys.

Declare each attribute with a type, and optionally a list of string values for a dropdown in the admin:

payload.config.ts
createExperimentsPlugin({
  // ...
  attributeKeys: {
    staff: { type: 'boolean' },
    plan: { type: 'string', values: ['free', 'pro'] },
    visits: { type: 'number' },
  },
})
TypeOperators
stringeq, neq, in, nin, contains (all case-insensitive)
numbereq, neq, gt, gte, lt, lte
booleaneq

A rule on an attribute the request does not have never matches. An in or nin rule needs at least one value. Pass only attributes that do not identify a person.

To enrol people only after consent, give the proxy runtime a cookie.consent(request) function. It must read cookies and headers only: server helpers call it with a placeholder URL. If it throws, consent is treated as declined.

When it returns false:

  • Nobody is enrolled, including signed-in users. No visitor id is created or honoured, and an existing xp_id cookie is removed from the forwarded request.
  • Running tests and decided tests with an open holdout round show the Original. Other decided tests show their winner, subject to targeting, and archived page routing still applies. None of these record an exposure.
  • linkIdentity and trackConversion do nothing.

Leaving consent out permits the cookie and enrolment.

Preview

The setup checks open each version with a signed xp_preview token. A preview shows the chosen version without counting it, and preview responses are not cached.

Edit this page on GitHub

On this page