Shared Visitor Identity
A vs B measures results by matching visitor ids. When one metric is measured across two surfaces (the web-experiment snippet in the browser and the feature-flag SDK in your code), the numbers only line up if the same visitor carries the same id on both surfaces.
This page shows how to pass one id to both. If you have not read the concept yet, start with Cross-project metrics.
The two visitor ids
| Surface | Where the id comes from |
|---|---|
| Web experiment (snippet) | The snippet assigns each browser a visitor id and stores it in the _avsb_visitor cookie. Read it with avsb.getVisitorId(), which returns null when there is no id yet (before avsb.init() in Consent Mode, or for a visitor who denied analytics). Replace it with avsb.setVisitorId(). |
| Feature flag (SDK) | The id you supply: the context key, the same way on the server SDK and the browser SDK. |
The join is raw equality: the two ids must be byte-for-byte the same string. To make that happen, take the snippet's id and feed it into the SDK.
The _avsb_visitor cookie stores a small JSON object (the id plus the visitor's assignments), not a bare id, so do not use the raw cookie string as the key. Always call avsb.getVisitorId(), which returns just the id.
Recipe A: server SDK (@avsbhq/node)
Read the id in the browser, send it to your server, and pass it as the context key. The server SDK buckets and tracks on context.key, so using the snippet's id here makes both surfaces share one identity.
In the browser: read the id and forward it with your request:
avsb.ready?.(function () { const visitorId: string | null = avsb.getVisitorId() // the _avsb_visitor cookie UUID if (!visitorId) return // no id yet: consent pending, or analytics denied fetch('/api/checkout', { method: 'POST', headers: { 'x-avsb-visitor': visitorId }, body: JSON.stringify({ /* your payload */ }), })})On the server: use that id as the context key:
Name the server client something other than avsb: on a page that also runs the
snippet, avsb is already the browser global, and shadowing it is how the two
identities get mixed up in the first place.
import { AvsbServer } from '@avsbhq/node'import type { EvalContext } from '@avsbhq/node'const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })await server.onReady()// Your framework's incoming request.declare const req: { headers: Record<string, string | undefined> }// The id the browser forwarded: the SAME value avsb.getVisitorId() returned.const visitorId = req.headers['x-avsb-visitor']if (visitorId) { // Bind a per-request client to that id (ergonomic: every call reuses it). const user = server.forUser({ kind: 'user', key: visitorId }) // getFlag() returns a Flag object, not a plain value; .value is the boolean. const showNewCheckout = user.getFlag('new_checkout', false).value // Track the conversion against the same id: user.track('purchase', { value: 49.99 })}You can also pass the context per call instead of using forUser. getFlag
takes it as its third argument; track carries it on the payload:
if (visitorId) { const context: EvalContext = { kind: 'user', key: visitorId } server.getFlag('new_checkout', false, context) server.track('purchase', { value: 49.99, context })}Recipe B: browser SDK (@avsbhq/browser)
If your feature flags run in the browser, give the SDK a context whose key is the snippet's visitor id. That key is what buckets and attributes every evaluation, so matching it is what makes both surfaces agree. Wrap it in avsb.ready() so the id is available even if the flag SDK starts first:
import { AvsbClient } from '@avsbhq/browser'avsb.ready?.(function () { const visitorId = avsb.getVisitorId() if (!visitorId) return // no id yet: consent pending, or analytics denied const client = new AvsbClient({ sdkKey: 'sdk_production_xxxxxxxxxxxxxxxx', context: { kind: 'user', key: visitorId }, })})Anything else you want to target on rides the same object alongside kind and key:
const snippetVisitorId = avsb.getVisitorId()if (snippetVisitorId) { const client = new AvsbClient({ sdkKey: 'sdk_production_xxxxxxxxxxxxxxxx', context: { kind: 'user', key: snippetVisitorId, plan: 'premium', country: 'US' }, })}The SDK adopts this cookie on its own when both surfaces are on the same page (adoptSnippetVisitorId, on by default), so pass the id explicitly only when you are constructing the context yourself.
Web-to-web on the same domain
If both surfaces are the snippet on the same domain, there is nothing to do; they already share the _avsb_visitor cookie, so the id is identical everywhere. Sharing is only a concern when a different SDK, or a different domain, supplies its own id.
Across subdomains: one cookie for the whole site
By default the visitor cookie is written for the exact host that set it. A visitor who moves from www.example.com to shop.example.com therefore arrives as a brand-new visitor: new id, fresh bucketing (which variation they land in, decided again from scratch), and one journey split across two rows in your results.
Add data-avsb-cookie-domain to the loader tag with the parent domain, and the cookie covers every subdomain:
<script src="https://cdn.avsb.cloud/snippet.js?id=YOUR_SNIPPET_KEY" data-avsb="YOUR_SNIPPET_KEY" data-avsb-cookie-domain="example.com" async></script>Rules worth knowing:
-
Use the bare domain,
example.com. A leading dot is accepted and ignored. -
It must be the current host or a parent of it. A browser silently drops a cookie for any other domain, which would re-randomise the visitor on every page load, so A vs B refuses a value it cannot use and says so in the console:
Plain text[avsb] Cookie domain "other.com" does not cover "www.example.com". Visitor cookie stays host-only.Plain text1 line -
Put the same attribute on every subdomain's tag. The visitor id is created by whichever page loads first.
-
Widening the domain does not migrate existing visitors. Ids already stored per host stay as they are, so make the change before you start an experiment rather than during one.
Logged-in stitching: avsb.setVisitorId()
For an account that spans devices, browsers, or an anonymous-then-signed-in journey, hand A vs B your own user id:
avsb.ready?.(function () { if (currentUser) avsb.setVisitorId(currentUser.id)})From that call on, every event carries your id, and the same id in the feature-flag SDK joins to it with no forwarding at all.
Two consequences to plan for:
- The visitor is re-bucketed. A different id is a different visitor, so the assignments held by the old id are dropped and the experiments are evaluated again. Someone can see a different variation (one specific version being tested: the control or a challenger) than they did a moment ago.
- Both ids appear in your results. The anonymous id counts as one visitor and the account id as another. That is why the call belongs as early as possible in the page, before your interface has rendered anything a visitor could act on.
Allowed characters are letters, digits, . _ : @ | -, up to 128 of them. Anything else returns false and warns in the console.
If your only problem is www and shop seeing different visitors, the cookie-domain attribute is the whole fix and costs you no re-bucketing. Reach for setVisitorId when you genuinely need your own account id to be the identity.
Pitfalls to avoid
- Different ids for the same person. If the SDK keys on your internal account id (for example
u_123) while the snippet keys on the_avsb_visitorUUID, the two never match and the cross-surface join is empty. Pick one canonical id and use it on both sides. - A fresh id per request. Generating a new id on the server for each call defeats the join. Forward the browser's id; do not mint your own.
- Cross-domain handoff. The
_avsb_visitorcookie only travels with requests to the domain that set it. Subdomains of one domain are covered by the cookie-domain attribute above; genuinely different domains are not, so pass the id explicitly (header, query parameter, or request body) at the handoff. - Logged-in vs anonymous. Switching identity mid-journey always leaves two visitors in the data, whichever surface does it. Calling
alias()records that the switch happened; it does not retroactively join the two visitors' past events into one. To avoid the split, evaluate with the same id on both sides of the login: keep using the anonymous visitor id after sign-in, or set the account id before the very first evaluation. - Reading the id too early.
avsb.getVisitorId()returnsnullbeforeavsb.init()in Consent Mode and for a visitor who denied analytics. Guard the result instead of forwardingnullas a key.
Once both surfaces send the same visitor id, a single org-wide metric with a source project (see Cross-project metrics) reads one pool of events and reports one honest result across your web experiments and your feature-flag tests.