---
title: "Assign each user to one variant for good · Agent Wranglers"
url: https://agentwranglers.se/playbook/data/product-analytics/experiment-assignment
updated: 2026-10-20
licence: "All rights reserved"
---

# Assign each user to one variant for good

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Required
- Chapter: [Product analytics](https://agentwranglers.se/playbook/data/product-analytics)
- Rule: Assign variants by hashing a stable user ID with the experiment name, so a person sees the same variant on every visit and device. Log an exposure when the variant is shown.
- How: Hash `experiment:user_id` with a stable hash, such as SHA-256, and map it to a bucket from 0 to 9999. Never use `Math.random()` per page view. Signed-out visitors get a random ID that moves to the account at sign-in. Log the exposure once, when the person sees the change, and analyse only exposed users. Before reading results, test for sample ratio mismatch. A chi-square test with p below 0.001 means assignment or logging is broken, so the results are void.
- Check: A unit test assigns the same user 1000 times and gets one variant. A test over 100 000 fake IDs gets a 50/50 split within 1 percent. The analysis script stops on a sample ratio mismatch.

Example:

```js
import { createHash } from 'node:crypto';

const BUCKETS = 10_000;
export function variantFor(experiment, userId, variants = ['control', 'treatment']) {
  const bucket = createHash('sha256').update(`${experiment}:${userId}`).digest().readUInt32BE(0) % BUCKETS;
  return variants[Math.floor((bucket * variants.length) / BUCKETS)];
}
```

```python
import hashlib
from scipy.stats import chisquare

BUCKETS = 10_000

def variant_for(experiment: str, user_id: str, variants=("control", "treatment")) -> str:
    digest = hashlib.sha256(f"{experiment}:{user_id}".encode()).digest()
    bucket = int.from_bytes(digest[:4], "big") % BUCKETS
    return variants[bucket * len(variants) // BUCKETS]

def has_sample_ratio_mismatch(counts: list[int], expected_shares: list[float]) -> bool:
    total = sum(counts)
    return chisquare(counts, [share * total for share in expected_shares]).pvalue < 0.001
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers data playbook: https://agentwranglers.se/playbook/data/product-analytics/experiment-assignment`.

- Previous: [Write the experiment plan before it starts](https://agentwranglers.se/playbook/data/product-analytics/experiment-plan)
- Next: [Delete raw events on a schedule](https://agentwranglers.se/playbook/data/product-analytics/analytics-retention)
- [Product analytics](https://agentwranglers.se/playbook/data/product-analytics)
