Experiments in the SDK

The browser API is synchronous and never throws: whenever the visitor is not enrolled, the key is unknown or the assignment is not known yet, it answers "control". Projects with experiments load the small plugin x3.experiments.js automatically (bundled in x3.full.js); sites without experiments never download it.

Browser API

const arm = x3.variant("hero-cta-text");                   // "control" | "b" | …
const info = x3.experiment("hero-cta-text");               // { arm, reason, inExperiment, params }
x3.variant("quiz-contact-split", { expose: false });       // decide now, expose later
x3.expose("quiz-contact-split");                           // where the arms really differ
await x3.experiments.ready(300);                           // assignment known (or timeout)
const off = x3.experiments.onChange((key) => rerender(key));
x3.experiments.all();                                      // debug: key → arm
  • variant(key, { expose, fallback }) logs the exposure (experiment_view) on the first call per pageview unless expose: false. Recommended: expose: false and expose(key) at the point of difference (the contact step rendered, the modal opened) – more sensitive, no dilution.
  • fallback is returned for keys the config does not know (e.g. before the experiment exists).
  • reason tells why: assigned, override (QA), rolled_out, not_running, unknown_key, targeting, coverage, no_consent, unit_unavailable, pending, stopped_arm.
  • Day and visitor units are resolved by GET /v1/x/{pk} (through your first-party proxy); the browser never sees the unit. Before that answer variant() returns control without exposure and onChange fires when it arrives.
  • Arm params come from the experiment definition (public): read texts or video ids from x3.experiment(key).params to change copy without a deploy.

Script tag (window.x3)

x3("variant", "hero-cta-text", function (arm, info) {
  document.querySelector("#cta").textContent = arm === "b" ? "Jetzt starten" : "Kostenlos testen";
  x3("expose", "hero-cta-text");
}, { expose: false });

The callback runs once the assignment is known – at most 300 ms after boot, otherwise with control. After boot window.x3.variant() is also available synchronously.

React

import { useVariant, useExpose, Variant } from "@x3growth/sdk/react";

function Cta() {
  const { arm, params, ready } = useVariant("hero-cta-text");
  return <button style={{ visibility: ready ? "visible" : "hidden" }}>{arm === "b" ? String(params.label) : "Jetzt anfragen"}</button>;
}

function ContactStep({ visible }: { visible: boolean }) {
  useExpose("quiz-contact-split", visible);
  return (
    <>
      <Variant experiment="quiz-contact-split" arm="control" expose={false}><OneStep /></Variant>
      <Variant experiment="quiz-contact-split" arm="split" expose={false}><TwoSteps /></Variant>
    </>
  );
}

useVariant re-renders when the assignment arrives and logs the exposure after commit (unless expose: false). With server assignments from <X3Experiments> the first client render equals the server render.

Anti-flicker rules

  1. Anything visible on first paint (hero, headline, video): assign on the server (Next.js).
  2. Client-only: hide only the experiment slot (keep its size) until experiments.ready(300); after the timeout render control without exposure.
  3. Never hide the whole page and never rewrite the DOM by selector.
  4. Steps reached after interaction (quiz steps, modals) need nothing special – the assignment is requested at boot.

Code conventions

  • One variant() / useVariant() per experiment and component tree; branch locally.
  • Mark every arm: // x3-experiment:<key> arm=<arm> – the cleanup searches for it.
  • Arm-only funnel steps send x3.funnelStep(funnel, step).
  • Server code must accept both arms (validation, mails, CRM). Prices and offers are assigned on the server (server helper), never trusted from the browser.

QA

?x3_force=<key>:<arm> (comma-separated for several) forces arms on your own browser and marks the visit as test traffic. Drafts and server-side assignment accept only the signed links from the experiment's QA links (x3_force_t, 7 days). Never commit QA links or put them into PR texts.