Frames, Audiences and Lanes
Episode 4: before any coin flip, a filter stack decides which experiment a visitor can even enter: frames, conditions, audiences, lanes, and bots
Copy this guide and your current settings as a prompt for your AI agent.
Preview agent promptSee the guide, code and settings included when you copy.
Three visitors, same page. One lands in the mobile experiment, one in the returning-visitor experiment, one matches nothing and is not measured at all. None of that was a coin flip. Before the hashes of episode 1 ever run, a filter stack decides which experiment a visitor can even enter. This episode is that stack.
On this page
01The hierarchy
| Entity | What it is |
|---|---|
| Surface | A page: URL patterns (url, url_regex) |
| Element | A region on a surface: a CSS or XPath selector |
| Frame | The container you create: surfaces, elements, a goal metric, and its own holdout split |
| Experiment | Attached to a frame; several can share one, typically one per audience |
The payload ships this whole tree.
02The frame gate
Targeting starts with one filter, activeFrames: every frame whose
activation condition passes for this URL and visitor. If that list is empty,
getVariantsAndElements returns immediately. No frame, no global holdout, no
measurement. The frame gate is the door to everything.
03The condition language
Conditions are one recursive type, ActivationCondition
(sdk/shared/types/coframe.ts), evaluated by checkActivationCondition in
sdk/shared/utils.ts. Two combinators, and and or, nest freely. The
leaves come in three families:
| Family | Leaves |
|---|---|
| URL | Exact match (normalized: protocol, www and trailing slashes ignored), base-URL match, regex, query parameters |
| Visitor | Device, platform, language, country, region, city, first-time versus returning (via the visit count), your own user context |
| Mechanism | Time windows, a seeded traffic slice (the same hash math as episode 1), an arbitrary JS function (CSP-safe executeJs; an error counts as false and logs js_eval_error) |
04Three-valued logic
A condition does not just return true or false. It can return
VALUE_UNKNOWN_YET: the URL is not resolved, the token is not minted, the JS
runner is not available. The combinators respect it: in an and, false
beats unknown but unknown beats true; in an or, true wins immediately.
At the audience gate only a hard false blocks (activated !== false):
unknown lets the visitor through.
05Audiences: first match wins
Within an active frame, experiments are tried in order. Each carries audience
ids (saved conditions) and every one of them must pass: an AND-gate
(checkAllAudiences). No audiences means catch-all. The first experiment
whose audiences pass takes the frame and the loop breaks: one experiment per
frame, per visitor. Different frames on the same page can all apply.
06Lanes: the claim
A lane is a named group of experiments that must never share a visitor.
Membership lives in the payload. The visitor’s side is a claim, experiment id
plus iteration id, one per lane (sdk/shared/lanes.ts), stored in
localStorage always and in the coframe.lanes cookie once consent allows.
Stale claims are dropped when the lane is gone, the experiment left, or its
iteration rolled: release and reclaim, iteration-aware.
07Lanes: picking the winner
Per lane: if a valid claim exists, that experiment wins, so commitment
survives. Otherwise a seeded tiebreak (pickWinner): sort the reachable
members, hash the token with the lane’s seed, take the modulo. Stable per
visitor, evenly split across members. Every reachable non-winner is excluded
and its frame falls through to baseline. The winner’s claim is committed on
entry, before the holdout flip, so even a control-bucketed visitor stays
committed.
08Bots: two different levers
| Lever | Effect |
|---|---|
Bot cloaking (x-is-bot with bot_cloaking_enabled) |
The visitor skips the coin flip and is forced onto the original page, so crawlers always index the canonical version. An SEO lever |
| Blocklisted IP | Still sees variants (your own QA needs to) and is removed from analytics later, at query time |
What bots see; what analytics counts. Two levers.
09In short
Frames gate the page. Conditions speak a three-valued language. Audiences AND-gate each experiment, first match wins. Lanes keep experiments apart with sticky, seeded claims. Bots are cloaked, not counted.
10Where in the code
| File | What to look for |
|---|---|
sdk/shared/types/coframe.ts |
ActivationCondition |
sdk/shared/utils.ts |
checkActivationCondition, normalizeUrl, VALUE_UNKNOWN_YET |
sdk/shared/assignment.ts |
activeFrames, checkAllAudiences, lane reachability, the per-frame loop, bot cloaking |
sdk/shared/lanes.ts |
Claims, validateClaims, pickWinner, resolveLaneWinners |
sdk/sdk-js/src/init/modifyPage.ts |
persistLaneClaims |
# Frames, Audiences and Lanes Episode 4: before any coin flip, a filter stack decides which experiment a visitor can even enter: frames, conditions, audiences, lanes, and bots Three visitors, same page. One lands in the mobile experiment, one in the returning-visitor experiment, one matches nothing and is not measured at all. None of that was a coin flip. Before the hashes of [episode 1](../docs/bucketing.html) ever run, a filter stack decides which experiment a visitor can even enter. This episode is that stack. [Video: Video: Runtime Walkthrough, episode 4: the entity hierarchy, the frame gate, the condition language, three-valued logic, audiences, lanes, and bots (3m 55s, narrated)](assets/videos/sdk-internals-04-frames-audiences-lanes.mp4) Watch: 3m 55s — Runtime Walkthrough, episode 4: the entity hierarchy, the frame gate, the condition language, three-valued logic, audiences, lanes, and bots ## The hierarchy Entity | What it is | Surface | A page: URL patterns (`url`, `url_regex`) | Element | A region on a surface: a CSS or XPath selector | Frame | The container you create: surfaces, elements, a goal metric, and its own holdout split | Experiment | Attached to a frame; several can share one, typically one per audience | The payload ships this whole tree. ## The frame gate Targeting starts with one filter, `activeFrames`: every frame whose activation condition passes for this URL and visitor. If that list is empty, `getVariantsAndElements` returns immediately. No frame, no global holdout, no measurement. The frame gate is the door to everything. ## The condition language Conditions are one recursive type, `ActivationCondition` (`sdk/shared/types/coframe.ts`), evaluated by `checkActivationCondition` in `sdk/shared/utils.ts`. Two combinators, `and` and `or`, nest freely. The leaves come in three families: Family | Leaves | URL | Exact match (normalized: protocol, `www` and trailing slashes ignored), base-URL match, regex, query parameters | Visitor | Device, platform, language, country, region, city, first-time versus returning (via the visit count), your own user context | Mechanism | Time windows, a seeded traffic slice (the same hash math as episode 1), an arbitrary JS function (CSP-safe `executeJs`; an error counts as `false` and logs `js_eval_error`) | ## Three-valued logic A condition does not just return `true` or `false`. It can return `VALUE_UNKNOWN_YET`: the URL is not resolved, the token is not minted, the JS runner is not available. The combinators respect it: in an `and`, `false` beats unknown but unknown beats `true`; in an `or`, `true` wins immediately. At the audience gate only a hard `false` blocks (`activated !== false`): unknown lets the visitor through. ## Audiences: first match wins Within an active frame, experiments are tried in order. Each carries audience ids (saved conditions) and every one of them must pass: an AND-gate (`checkAllAudiences`). No audiences means catch-all. The first experiment whose audiences pass takes the frame and the loop breaks: one experiment per frame, per visitor. Different frames on the same page can all apply. ## Lanes: the claim A lane is a named group of experiments that must never share a visitor. Membership lives in the payload. The visitor’s side is a claim, experiment id plus iteration id, one per lane (`sdk/shared/lanes.ts`), stored in `localStorage` always and in the `coframe.lanes` cookie once consent allows. Stale claims are dropped when the lane is gone, the experiment left, or its iteration rolled: release and reclaim, iteration-aware. ## Lanes: picking the winner Per lane: if a valid claim exists, that experiment wins, so commitment survives. Otherwise a seeded tiebreak (`pickWinner`): sort the reachable members, hash the token with the lane’s seed, take the modulo. Stable per visitor, evenly split across members. Every reachable non-winner is excluded and its frame falls through to baseline. The winner’s claim is committed on entry, before the holdout flip, so even a control-bucketed visitor stays committed. ## Bots: two different levers Lever | Effect | Bot cloaking (`x-is-bot` with `bot_cloaking_enabled`) | The visitor skips the coin flip and is forced onto the original page, so crawlers always index the canonical version. An SEO lever | Blocklisted IP | Still sees variants (your own QA needs to) and is removed from analytics later, at query time | What bots see; what analytics counts. Two levers. ## In short Frames gate the page. Conditions speak a three-valued language. Audiences AND-gate each experiment, first match wins. Lanes keep experiments apart with sticky, seeded claims. Bots are cloaked, not counted. ## Where in the code File | What to look for | `sdk/shared/types/coframe.ts` | `ActivationCondition` | `sdk/shared/utils.ts` | `checkActivationCondition`, `normalizeUrl`, `VALUE_UNKNOWN_YET` | `sdk/shared/assignment.ts` | `activeFrames`, `checkAllAudiences`, lane reachability, the per-frame loop, bot cloaking | `sdk/shared/lanes.ts` | Claims, `validateClaims`, `pickWinner`, `resolveLaneWinners` | `sdk/sdk-js/src/init/modifyPage.ts` | `persistLaneClaims` |