> ## Documentation Index
> Fetch the complete documentation index at: https://docs.platform.embeddables.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Experiment assignment, hooks & events

> JavaScript & server init, methods and hooks, and the experiment:assigned event Experiments emits

Install, init, and walkthrough examples live in [Overview & setup](/sdks/experiments/overview). **This page** is the reference for `initExperiments` / `initExperimentsServer`, the assignment handle they return, [methods and hooks](#methods), and the `experiment:assigned` event Experiments emits.

<h2 id="initExperiments">
  Set up & use experiments
</h2>

In JavaScript, call `initExperiments`; on the server, call `initExperimentsServer`. Each reads your experiment list from Core once, then lets you open an assignment handle with `initExperiment({ experimentId })`.

<Tabs>
  <Tab title="JavaScript">
    ```typescript theme={null}
    import { initExperiments } from '@embeddables/experiments'

    const experiments = initExperiments(options)
    const hero = experiments.initExperiment({ experimentId: 'exp-hero' })
    ```
  </Tab>

  <Tab title="Server">
    <span id="server" />

    ```typescript theme={null}
    import { initEmbeddablesServer } from '@embeddables/core/server'
    import { initExperimentsServer } from '@embeddables/experiments/server'

    const server = initEmbeddablesServer({
      ...config,
      publishableKey: 'pk_sandbox_<your-key>',
      cookies: { get: (key) => request.cookies.get(key) ?? null },
    })

    const experiments = initExperimentsServer({ server })
    const hero = experiments.initExperiment({ experimentId: 'exp-hero' })
    const variantKey = await hero.getAssignedVariantKey()
    ```

    Use this when Experiments runs during a server page load (SSR). You open experiments the same way with
    `initExperiment`, but assignments are read from request cookies — Experiments never writes cookies back.
    Pass resolved variants to the client as `serverAssignments`. Returns the same client shape as
    [`initExperiments`](#initExperiments); assignment methods are in [JavaScript & Server](#javascript).

    <ParamField body="server" type="EmbeddablesInstance" required>
      An initialized `@embeddables/core/server` instance exposing the cookie / `getProjectId` /
      `getExperiments` contract. Throws `ConfigError` without a `projectId`.
    </ParamField>

    <ParamField body="analytics" type="ExperimentsAnalytics">
      Optional analytics client — same behavior as the [client option](#options).
    </ParamField>

    <ParamField body="cookieStorage" type="CookieStorageAdapter">
      Optional override for reading/writing assignments. The default adapter only reads request cookies via
      `server.getCookie()`; `write` is a no-op so SSR must set response cookies itself when you need
      cross-request stickiness.
    </ParamField>
  </Tab>
</Tabs>

<ParamField path="options" type="object" required>
  Init options — a Core instance plus optional SSR seed and analytics wiring. See **Options** below.
</ParamField>

<ParamField path="experimentId" type="string" required>
  Passed to `initExperiment`. Must be the `id` of an experiment in your Core config. Typed against
  the registered list — an unknown id is caught while you write code and throws `ConfigError` at
  runtime.
</ParamField>

<h3 id="options">
  Options
</h3>

<AccordionGroup>
  <Accordion title="core" description="Required initialized Core instance">
    ```typescript theme={null}
    import { initEmbeddables } from '@embeddables/core'
    import { initExperiments } from '@embeddables/experiments'

    const embeddables = initEmbeddables({ ...config, publishableKey: 'pk_sandbox_<your-key>' })
    const experiments = initExperiments({ core: embeddables })
    ```

    <ParamField body="core" type="EmbeddablesInstance" required>
      An initialized `@embeddables/core` instance. Experiments reads the configured experiment list
      from its `getExperiments()` and the visitor/project ids from it — it never passes a separate
      `experiments` option. Throws `ConfigError` if the instance is missing the required Core methods
      or has no `projectId`.
    </ParamField>
  </Accordion>

  <Accordion title="serverAssignments" description="SSR pre-seed of already-resolved variants">
    ```typescript theme={null}
    const experiments = initExperiments({
      core: embeddables,
      serverAssignments: { 'exp-hero': 'control' },
    })
    ```

    <ParamField body="serverAssignments" type="Record<string, string>">
      Map of `experimentId → variantKey` the server already resolved, so the first client render is
      synchronous and does not flash variants. Entries for unknown experiment ids or unconfigured
      variant keys are dropped. Client-only — it is **not** a server option.
    </ParamField>
  </Accordion>

  <Accordion title="analytics" description="Optional analytics client for experiment:assigned">
    ```typescript theme={null}
    import { initAnalytics } from '@embeddables/analytics'

    const analytics = initAnalytics({ core: embeddables })
    const experiments = initExperiments({ core: embeddables, analytics })
    ```

    <ParamField body="analytics" type="ExperimentsAnalytics">
      Optional analytics client. When present, each **new** assignment emits one
      [`experiment:assigned`](#analytics-events) event. Experiments never imports Analytics itself and
      never sends identity on the assignment request — the injected client owns identity. A failed
      `trackEvent` never fails assignment; the variant is persisted first.
    </ParamField>
  </Accordion>
</AccordionGroup>

<h3 id="methods">
  Methods and hooks
</h3>

**React** covers the [`experiments()`](#experiments-module) module factory and the hooks — register the factory on `EmbeddablesProvider` before calling hooks. **JavaScript & Server** lists assignment methods on the `ExperimentsClient` from [`initExperiments`](#initExperiments) or [`initExperimentsServer`](#server) — the same API on both entry points. [`useExperiment`](#hooks) throws if Experiments was not registered on `EmbeddablesProvider` once Core is ready; [`useAssignExperiment`](#hooks) does not throw — `assignExperiment()` returns `undefined` when the module is missing.

<Tabs>
  <Tab title="React">
    <span id="hooks" />

    <AccordionGroup>
      <Accordion title="experiments()" description="Module factory for EmbeddablesProvider — not a hook">
        <span id="experiments-module" />

        ```typescript theme={null}
        import { EmbeddablesProvider } from '@embeddables/core/react'
        import { experiments } from '@embeddables/experiments/react'

        <EmbeddablesProvider config={config} modules={[experiments({ serverAssignments, analytics })]}>
          {children}
        </EmbeddablesProvider>
        ```

        Returns an `EmbeddablesReactModule` for the provider's `modules` prop. The generated CLI setup
        wires this for you; pass options only to override. `core` is supplied by the provider — do not
        pass it here.

        <ParamField body="serverAssignments" type="Record<string, string>">
          Same SSR pre-seed as the [client option](#options). Pass the same map the server used so
          hydration does not flash variants.
        </ParamField>

        <ParamField body="analytics" type="ExperimentsAnalytics">
          Optional analytics client — enables the [`experiment:assigned`](#analytics-events) event. The
          generated React setup wires Analytics automatically when both SDKs are installed.
        </ParamField>
      </Accordion>

      <Accordion title="useExperiment()" description="Assign and read a variant on render">
        ```typescript theme={null}
        import { useExperiment } from '@embeddables/experiments/react'

        const { assignedVariantKey, assignedVariantTitle, status, error } = useExperiment({
          experimentId: 'exp-hero',
        })
        ```

        Assigns as soon as it renders. If a matching `serverAssignments` seed was passed to
        [`experiments()`](#experiments-module), the first render is `ready` synchronously. Throws if
        Core is ready and Experiments was not registered on `EmbeddablesProvider`'s `modules`.

        <ParamField path="experimentId" type="string" required>
          The `id` of a configured experiment. Inferred against the registered experiment list — no type
          arguments needed. Throws `ConfigError` for an unknown id.
        </ParamField>

        <ResponseField name="assignedVariantKey" type="string | null" required>
          Assigned variant key, or `null` while `status` is `pending` / `error`.
        </ResponseField>

        <ResponseField name="assignedVariantTitle" type="string | null" required>
          Assigned variant title, or `null` while `status` is `pending` / `error`.
        </ResponseField>

        <ResponseField name="status" type="'pending' | 'ready' | 'error'" required>
          `pending` until the assignment resolves, `ready` when the variant is usable, `error` when the
          assignment request failed.
        </ResponseField>

        <ResponseField name="error" type="Error | null" required>
          The failure when `status` is `error`, otherwise `null`.
        </ResponseField>
      </Accordion>

      <Accordion title="useAssignExperiment()" description="Assign a variant on demand instead of on render">
        ```typescript theme={null}
        import { useAssignExperiment } from '@embeddables/experiments/react'

        const { assignExperiment, isPending, isError, error } = useAssignExperiment()

        const assigned = await assignExperiment({ experimentId: 'exp-hero' })
        if (assigned) console.log(assigned.variantKey, assigned.variantTitle)
        ```

        Leaves the assignment call to you — fire it when the visitor actually reaches the experiment,
        rather than booking an exposure for everyone who loads the page. A later `useExperiment` for the
        same id reads the assignment back without another request. The hook itself does not throw when
        Experiments is missing — only `assignExperiment()` returns `undefined`.

        <ParamField path="experimentId" type="string" required>
          Passed to `assignExperiment`. Must be the `id` of an experiment in your Core config. Throws
          `ConfigError` for an unknown id when the module is registered.
        </ParamField>

        <ResponseField name="assignExperiment" type="(params: { experimentId: string }) => Promise<{ variantKey: string; variantTitle: string } | undefined>" required>
          Assigns and resolves the variant. Returns `undefined` when the Experiments module is not
          registered; rejects (after setting `error`) when the request fails.
        </ResponseField>

        <ResponseField name="isPending" type="boolean" required>
          `true` while at least one assignment is in flight.
        </ResponseField>

        <ResponseField name="isError" type="boolean" required>
          `true` when the latest assignment failed.
        </ResponseField>

        <ResponseField name="error" type="unknown" required>
          Details from the failed assignment, when `isError` is true.
        </ResponseField>
      </Accordion>
    </AccordionGroup>
  </Tab>

  <Tab title="JavaScript & Server">
    <span id="javascript" />

    Call these on the client returned from `initExperiments` or `initExperimentsServer`.

    <AccordionGroup>
      <Accordion title="experiments.initExperiment()" description="Get a handle for one configured experiment">
        ```typescript theme={null}
        const hero = experiments.initExperiment({ experimentId: 'exp-hero' })
        ```

        <ParamField body="experimentId" type="string" required>
          The `id` of an experiment in your Core config. Typed against the registered experiment list, so
          an unknown id is caught while you write code. Throws `ConfigError` at runtime if the id is not
          configured.
        </ParamField>

        Returns an [`ExperimentHandle`](#returns) with `getAssignedVariantKey`, `getAssignedVariantTitle`,
        and `getExperimentId`.
      </Accordion>

      <Accordion title="handle.getAssignedVariantKey()" description="Resolve the sticky variant key">
        ```typescript theme={null}
        const variantKey = await hero.getAssignedVariantKey()
        ```

        **Browser** (`initExperiments`): the first call asks the Embeddables API, persists the result to
        `localStorage` and assignment cookies, and returns the variant key. Later calls reuse the stored
        value with no request.

        **Server** (`initExperimentsServer`): reads request cookies or fetches once; by default new
        assignments live only in request memory (cookie `write` is a no-op). Cross-request stickiness
        needs assignment cookies, a persisting `cookieStorage`, or
        [`serverAssignments`](#options) on browser `initExperiments`.

        <ResponseField name="variantKey" type="string" required>
          The assigned variant key — typed to the configured variants of this experiment.
        </ResponseField>
      </Accordion>

      <Accordion title="handle.getAssignedVariantTitle()" description="Resolve the human-readable variant title">
        ```typescript theme={null}
        const variantTitle = await hero.getAssignedVariantTitle()
        ```

        Resolves the same sticky assignment as `getAssignedVariantKey`, then maps it to the variant's
        `title` (falling back to its key when no title is configured).

        <ResponseField name="variantTitle" type="string" required>
          Display title for the assigned variant.
        </ResponseField>
      </Accordion>

      <Accordion title="handle.getExperimentId()" description="Read back the experiment id for this handle">
        ```typescript theme={null}
        const id = hero.getExperimentId() // 'exp-hero'
        ```

        <ResponseField name="experimentId" type="string" required>
          The experiment id this handle was created for. Synchronous — no assignment is triggered.
        </ResponseField>
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>

<h3 id="analytics-events">
  Analytics events
</h3>

When an [`analytics`](#options) client is wired, each **new** assignment auto-emits one
`experiment:assigned` event — a variant already in storage never re-emits. See the
[Analytics reference](/sdks/analytics/reference#events) for the full event catalog.

```typescript theme={null}
await analytics.trackEvent({
  event_name: 'experiment:assigned',
  experiment_key: 'hero_test',
  variant: 'control',
})
```

<ResponseField name="experiment_key" type="string" required>
  The experiment key (not the id) resolved by the assignment.
</ResponseField>

<ResponseField name="variant" type="string" required>
  The assigned variant key.
</ResponseField>

<h3 id="returns">
  Returns
</h3>

`initExperiments` and `initExperimentsServer` return an `ExperimentsClient`; `initExperiment` returns an `ExperimentHandle`.

<ResponseField name="ExperimentsClient" type="object" required>
  Exposes `initExperiment` — documented under [Methods and hooks](#methods).
</ResponseField>

<ResponseField name="ExperimentHandle" type="object" required>
  Exposes `getAssignedVariantKey`, `getAssignedVariantTitle`, and `getExperimentId` — documented
  under [Methods and hooks](#methods).
</ResponseField>

<ResponseField name="initExperiment" type="(options: { experimentId: string }) => ExperimentHandle" required>
  Builds a handle for one configured experiment.
</ResponseField>

<ResponseField name="getAssignedVariantKey" type="() => Promise<string>" required>
  Resolves the sticky variant key for the handle's experiment.
</ResponseField>

<ResponseField name="getAssignedVariantTitle" type="() => Promise<string>" required>
  Resolves the display title for the assigned variant.
</ResponseField>

<ResponseField name="getExperimentId" type="() => string" required>
  Returns the experiment id the handle was created for.
</ResponseField>

```json theme={null}
{
  "experimentId": "exp-hero",
  "variantKey": "control",
  "variantTitle": "Control"
}
```
