Lightweight A/B experiment bucketing with localStorage persistence. Assign users to experiment groups with a single function call — assignments are sticky, so returning users always see the same variant.
npm install @adeptmind/ab-testingimport { getBucketedValue, parseEnvPct } from "@adeptmind/ab-testing";
const pct = parseEnvPct(process.env.REACT_APP_HPDP_PCT);
const shouldOverlayHPDP = getBucketedValue("ab-tests", "am_hpdp", pct);
if (shouldOverlayHPDP) {
overlayHpdp(); // overlay the host PDP with the HPDP experience
}On the first visit, getBucketedValue randomly assigns the user to the experiment group and saves the result to localStorage. On every subsequent visit, the stored assignment is returned — no re-randomization.
The main function. Assigns a user to an experiment bucket and persists the result.
| Parameter | Type | Description |
|---|---|---|
storageKey |
string |
localStorage key for storing all experiment assignments (e.g., "ab-tests") |
experimentName |
string |
Unique identifier for the experiment (e.g., "am_hpdp") |
pctTrue |
number |
Probability (0–100) of assigning true on first draw |
Returns true if the user is in the experiment group, false for control.
Parses an environment variable string as a percentage. Useful for configuring rollout percentages without hardcoding.
import { parseEnvPct } from "@adeptmind/ab-testing";
const pct = parseEnvPct(process.env.REACT_APP_EXPERIMENT_PCT);
const showFeature = getBucketedValue("ab-tests", "new-feature", pct);Throws if the value is undefined, non-numeric, or outside 0–100.
A single non-sticky random draw. Returns true with probability pctTrue / 100.
import { drawFromUniform } from "@adeptmind/ab-testing";
if (drawFromUniform(30)) {
// ~30% chance per call — not persisted
}Reads and parses a JSON value from localStorage. Returns null if the key is missing or the data is malformed.
Serializes a value as JSON and writes it to localStorage.
This is how experimentation works when integrating with the AdeptMind HPDP experience.
This library is designed for a two-tier experimentation pattern:
Tier 1 — AMT script buckets am_hpdp at page load. If true, HPDP overlay is activated. The assignment is written to window.adeptmind_ab_testing so Tier 2 can read it.
Tier 2 — Host PDP's Alloy script (Adobe Target) runs post-hydration and reads window.adeptmind_ab_testing["am_hpdp"]:
true— use default splits for other Adobe Target experiments (HPDP overlays the page, so these splits don't affect the HPDP experience)false— proceed with regular experiment splits on the native PDP (can proceed as if HPDP experiment does not exist)
// Tier 1 — AMT script at page load
import { getBucketedValue, parseEnvPct } from "@adeptmind/ab-testing";
const pct = parseEnvPct(process.env.REACT_APP_HPDP_PCT);
const shouldOverlayHPDP = getBucketedValue("ab-tests", "am_hpdp", pct);
// window.adeptmind_ab_testing === { "am_hpdp": true }
if (shouldOverlayHPDP) {
overlayHpdp();
}
// Tier 2 — Adobe Target audience rule (post-hydration):
if (window.adeptmind_ab_testing["am_hpdp"] === true){
// → lock target splits to a consistent default set. This ensures
// that HPDP does not affect other ongoing experiment data
} else {
// → regular experiment splitting logic as if HPDP does not exist
}First visit:
1. Read localStorage["ab-tests"] → null (empty)
2. Draw random number, compare to pctTrue → true
3. Write localStorage["ab-tests"] → { "am_hpdp": true }
4. Set window.adeptmind_ab_testing["am_hpdp"] = true
5. Return true → Tier 1 activates HPDP overlay
6. Tier 2 (Adobe Target) reads window.adeptmind_ab_testing["am_hpdp"]
→ true → lock target splits to default set
Second visit:
1. Read localStorage["ab-tests"] → { "am_hpdp": true }
2. Key "am_hpdp" exists → skip random draw
3. Set window.adeptmind_ab_testing["am_hpdp"] = true
4. Return true → same experience as first visit
All experiments under the same storageKey are stored in a single JSON object. Adding a new experiment never overwrites existing assignments.
Pass 50 as the pctTrue parameter:
const inExperiment = getBucketedValue("ab-tests", "my-experiment", 50);The first call writes the assignment to localStorage. Every subsequent call reads from localStorage and returns the stored value — the random draw only happens once.
Yes. Use the same storageKey with different experimentName values. Each experiment is tracked independently:
const showNewNav = getBucketedValue("ab-tests", "new-nav", 50);
const showNewFooter = getBucketedValue("ab-tests", "new-footer", 30);Remove the assignment from localStorage:
localStorage.removeItem("ab-tests");This clears all experiments under that key. To clear a single experiment, read the object, delete the key, and write it back:
const stored = JSON.parse(localStorage.getItem("ab-tests") ?? "{}");
delete stored["my-experiment"];
localStorage.setItem("ab-tests", JSON.stringify(stored));Adobe Target (Tier 2) runs after page hydration via the host PDP's Alloy script. By that point, getBucketedValue has already written bucket assignments to window.adeptmind_ab_testing. Adobe Target reads window.adeptmind_ab_testing["am_hpdp"] in its audience rules: if true, it applies default splits for its own experiments (since HPDP overlays the page anyway); if false, it proceeds with regular experiment splits on the native PDP.
No — this library depends on localStorage, which is only available in the browser. For server-side rendering, bucket on the server using a different mechanism (e.g., cookie-based or user-ID-based hashing) and pass the assignment to the client.
Use parseEnvPct to safely parse the env var:
const pct = parseEnvPct(process.env.REACT_APP_FEATURE_PCT);
const showFeature = getBucketedValue("ab-tests", "my-feature", pct);This throws at startup if the env var is missing or invalid, so you catch configuration errors early.