A subscriber gets the 'ebook bonus' pitch in your email, clicks through, and lands on a page testing the 'free trial' pitch. Two tools, two experiments, one confused user, and two datasets that can't be joined. This playbook covers keeping a single variant assignment authoritative across every surface.
01How cross-channel contamination happens
Contamination is rarely one bug. It's a chain of small, independently reasonable decisions that add up to a broken experiment. Your ESP randomizes on a contact ID at send time. Your landing-page tool randomizes on a device cookie at page load. Your ad platform randomizes on a click ID somewhere in between. Each tool is internally deterministic (ask it twice with the same input and you get the same answer), but the three systems have no idea they're describing the same person, so "deterministic" in three incompatible ways is really just three separate coin flips wearing a lab coat.
The failure shows up as small, specific breakages that compound. Google Ads' parallel tracking can send a user straight to your final URL while measurement fires in the background, so anything expecting an intermediate tracker to set assignment state loads too late. Mailchimp, SendGrid, Braze, and Iterable all rewrite outbound links for click tracking, and any of them can mangle or drop a query parameter your assignment logic depended on. Safari and other WebKit browsers block or partition third-party cookies and storage by default; Firefox strips parameters it recognizes as tracking. None of this is a vendor bug (it's privacy engineering working as intended), but an assignment scheme built to survive one channel's constraints usually can't survive all three at once.
The reason this matters more than "the numbers looked a little off" is that selective contamination doesn't just add noise, it adds bias. Random data loss shrinks your sample and widens your confidence interval: annoying but honest. Contamination that correlates with browser, device, or channel (which it almost always does, because the mechanisms above are systematic, not random) means the users who leak between variants aren't a random subset of your audience. Your lift number stops measuring the effect of the variant and starts partly measuring who uses Safari. Two experiments running on the same user through different tools don't fail independently; they poison each other's validity simultaneously, because the "control" group in one experiment may be silently exposed to the "treatment" of the other.
02The single-brain pattern
Every team that has actually solved this converges on the same shape: one authoritative assignment function, called by every surface, instead of N surfaces each running their own randomization. Concretely, that function takes a stable subject key plus an experiment namespace, hashes the concatenation, and maps the result to a variant bucket. The bucket-count granularity differs by vendor (Optimizely uses 10,000 buckets, LaunchDarkly's percentage rollouts use 100,000, Harness FME's documented example uses 100), but the mechanism is identical everywhere: hash(namespace + subject_key) → bucket → variant. The namespace (sometimes called a seed or salt) is what keeps unrelated experiments statistically independent and is the thing that must be versioned deliberately: Amplitude documents this explicitly as a bucketing salt, and VWO's campaign bucketing seed exists for exactly the same reason. Treat the namespace as part of the experiment's design, not a config field a PM can rename in passing; renaming it is a silent rerandomization.
The critical implementation choice is where that function runs on first paint. Client-side evaluation (a browser SDK calling out and rendering after the fact) depends on cookies existing, storage not being partitioned, and the request completing before the user notices a flash of the wrong content. That's a fragile set of assumptions for a user who just clicked through from an email promising a specific offer. Server-side or edge evaluation is the safer default precisely because it does not depend on any of that: the landing server resolves the assignment before it renders a single byte, using whatever identity it has (a signed token from the link, a first-party cookie, or a lookup by anonymous ID), and the page is right the first time.
In TraqLyte's terms, this pattern is `/assign` and nothing else. An ESP merge tag calls `/assign` at send time and bakes the returned cohort into the email's CTA link (as a token, never as raw identity; more on that below). The landing page, on load, calls `/assign` again with whatever identity it has on hand. Because assignment is deterministic and keyed off the same campaign version, both calls resolve to the same cohort for the same person without either surface needing to trust the other's cache: the "single brain" isn't a service that email and web synchronize with, it's a pure function both of them happen to call.
03Identity across channels
TraqLyte's identity model is built around one rule that's easy to state and easy to get wrong in implementation: linking never re-keys an assignment. When a visitor first hits your site anonymously, `/assign` accepts an optional `anonymous_id` and creates an `Assignment` row filed under whatever key was presented at the time: the anonymous key, if that's all you had. Later, when the visitor gives you an email address and you call `POST /identity/link`, TraqLyte does not go back and rewrite that assignment's `user_key`. It can't, by construction: `assignment_id` is derived from `sha256(campaign_id || user_key || campaign_version || salt)` and doubles as the primary key, so mutating `user_key` after the fact would desync the row's identity from its own derivation; a later unlink or anonymize call would recompute the same PK and collide.
Instead, resolution happens at read time. Linking just records a relationship in a `UserIdentity` cluster: a canonical `known` key with a set of linked anonymous aliases pointing at it. When `/assign` (or any lookup) is asked about a subject, it resolves whatever key it was handed to its canonical identity and then searches across the whole cluster (canonical key plus every alias linked to it) to find the assignment that was already made. The email-click journey and the anonymous web-browse journey end up pointing at the same cohort not because anything was moved, but because both keys now resolve into the same cluster and the lookup walks the whole cluster before answering. This is the same idea the research calls an "anonymous bridge ID," except TraqLyte keeps the bridge permanently rather than discarding it once a stronger ID shows up; the anonymous key stays valid and keeps resolving correctly even after the person is fully identified.
This also defines what happens when a link is missing or arrives late, which is the normal case, not the exception, for a newsletter click. If the landing page has never seen this person before, `/assign` just creates a fresh assignment under whatever identity it has: an anonymous ID from a cookie, or nothing at all if storage is blocked, in which case it degrades to a new assignment scoped to that one request. Nothing errors. The one edge case worth naming: if both the anonymous key and the known key already hold separate assignments in the same campaign and version before they're linked, TraqLyte can't merge two rows that violate a uniqueness constraint on `(campaign_id, user_key, campaign_version)`, so it picks a policy instead: the identified assignment wins for every key in the cluster going forward, the superseded assignment is preserved (not deleted) and logged as a conflict, and the person sees one consistent cohort regardless of which key their client happens to send next. Analytics has to know this: until the person's two assignment rows are deduplicated through the cluster, cohort counts mean "assignments," not "unique humans."
04A worked example
Say you're running a campaign that tests two subject-line-and-landing-page pairings: "Get the ebook" versus "Start your free trial." The newsletter goes out Tuesday morning. At send time, your ESP's merge-tag pipeline calls `/assign` once per contact, passing each subscriber's contact ID as the subject key and the campaign's namespace. TraqLyte hashes the two together, lands each subscriber deterministically in cohort A or cohort B, and returns an `assignment_id` plus the cohort's variant. Rather than dropping the raw `assignment_id` into the email link (which would sit in browser history, server logs, and any tool that ever touches that URL), the send pipeline mints a short signed token carrying the namespace and variant, and the merge tag drops that token into the CTA: `https://example.com/offer?exp=eyJhbGciOi...`. The subscriber who's a Cohort B match gets an email pitching the free trial, with a link that says so.
A subscriber clicks the trial-pitch email. The landing server receives the request, verifies the token's signature, and (this is the part that matters) renders the free-trial variant server-side, before the first byte goes out, rather than painting a default page and swapping content in with client-side JavaScript after an SDK call resolves. It also sets a first-party cookie on its own domain carrying that same assignment, both to survive a page refresh and because the visitor may not click through to a conversion event on this exact pageview. If the visitor is new to TraqLyte entirely (no prior anonymous ID), the landing page's own `/assign` call, using the token's identity, resolves to the identical cohort the ESP already assigned, because both calls hash the same subject key against the same namespace. One brain, asked twice, gives the same answer.
Three days later, that same person returns directly (no link, no token) and signs up, this time from a different device. They enter their email during signup, which fires `POST /identity/link`, joining their anonymous web cookie to their newly known identity. Their original `Assignment` row (filed under the anonymous key from the first landing-page visit) is untouched; it's still sitting there under its original `assignment_id`. The signup event calls `POST /outcome` against that assignment, and because it resolves through the identity cluster rather than through a literal key match, it attributes correctly to Cohort B even though the key presented at signup (their known identity) differs from the key the assignment was originally filed under. The email said "free trial," the landing page rendered "free trial," and the signup outcome landed against the same cohort that was promised in Tuesday's subject line, which is the entire point of doing any of this.
Sources
- Optimizely Feature Experimentation: deterministic MurmurHash bucketing, bucketing IDs, user profile service for sticky assignment
- LaunchDarkly: percentage rollouts over 100,000 partitions, anonymous-to-known context guidance, non-PII context key recommendations
- VWO: campaign bucketing seed, 1–10,000 bucket range, cross-platform user ID consistency
- Harness FME: feature-flag seeds, sticky treatment while rules are unchanged, environment-specific hashing
- Amplitude Experiment: murmur3_x86_32 bucketing, bucketing salt for cross-experiment independence, sticky bucketing, variant-jumping documentation
- Adobe Target: thirdPartyId for cross-channel identity, email/non-JavaScript implementation via Redirector and AdBox
- Mailchimp, SendGrid, Braze, and Iterable documentation on merge tags, dynamic template data, Liquid personalization, and link-rewriting click tracking
- Google Ads parallel tracking and Final URL suffix behavior; GA4's experience_impression / exp_variant_string integration pattern for third-party experiment tools
- OWASP guidance on sensitive data exposure via query strings, logs, and referrers
- Microsoft and Google published research on sample ratio mismatch as a symptom of data-quality failure in online experiments
