> ## 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.

# Experiments SDK

> @embeddables/experiments — sticky A/B test variant assignment

## Overview

Experiments assigns each visitor to a variant of a configured A/B test and keeps that assignment sticky — the same visitor receives the same variant on every subsequent visit, resolved locally after the first assignment rather than requested from the API again.

## Implementation

### Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @embeddables/experiments @embeddables/core
  ```

  ```bash pnpm theme={null}
  pnpm add @embeddables/experiments @embeddables/core
  ```

  ```bash yarn theme={null}
  yarn add @embeddables/experiments @embeddables/core
  ```
</CodeGroup>

### Quick start

Experiment config belongs on Core — the `experiments` array you pass to `initEmbeddables` or `EmbeddablesProvider`. `initExperiments` reads it through `core.getExperiments()`; you never pass a separate `experiments` option.

```typescript theme={null}
import { initEmbeddables } from '@embeddables/core'
import { initExperiments } from '@embeddables/experiments'
import { config } from './embeddables/_dist'

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

const experiments = initExperiments({ core: embeddables })

// `experimentId` must match an entry's `id` field from config.experiments
const hero = experiments.initExperiment({ experimentId: 'exp-hero' })
const variantKey = await hero.getAssignedVariantKey()
const variantTitle = await hero.getAssignedVariantTitle()
```

The first call for a visitor with no stored assignment calls the Embeddables API, persists the result locally, and returns the variant. Every call after that reads the sticky value — no extra network round-trip, and `getAssignedVariantKey()` is typed so an invalid variant key for that experiment fails at compile time, not at runtime.

Assignment requests only ever send the project and experiment identifiers — never the visitor's identity.

#### `initExperiments` options

| Option              | Purpose                                                                            |
| ------------------- | ---------------------------------------------------------------------------------- |
| `core`              | Required. An initialized Core instance (config comes from `core.getExperiments()`) |
| `analytics`         | Optional — see [Analytics](#analytics) below                                       |
| `serverAssignments` | Pre-seed `{ [experimentId]: variantKey }` for SSR hydration                        |

### Server (SSR)

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

const server = initEmbeddablesServer({
  ...config,
  cookies: { get: (key) => request.cookies.get(key) ?? null },
})

const experiments = initExperimentsServer({ server })

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

The server entry reads experiment config from `server.getExperiments()` and sticky assignments from request cookies. It never writes cookies back — setting the response cookie stays in your SSR layer. `serverAssignments` is **not** a server option; pass it to `initExperiments` on the **client** to pre-seed variants the server already resolved before hydration.

#### `initExperimentsServer` options

| Option          | Purpose                                                          |
| --------------- | ---------------------------------------------------------------- |
| `server`        | Required. An initialized `@embeddables/core/server` instance     |
| `analytics`     | Optional — see [Analytics](#analytics) below                     |
| `cookieStorage` | Optional override to read/write sticky assignments on the server |

### React

Register Experiments through `EmbeddablesProvider`'s `modules` prop — the CLI generates the list in `embeddables/_dist/modules` — then read variants with `useExperiment`.

```tsx theme={null}
import { EmbeddablesProvider } from '@embeddables/core/react'
import { useExperiment } from '@embeddables/experiments/react'
import { config } from './embeddables/_dist'
import { modules } from './embeddables/_dist/modules'

function Hero() {
  // No type arguments: `em build` registers the experiment list for you.
  const { variantKey, variantTitle, status } = useExperiment({ experimentId: 'exp-hero' })

  if (status === 'pending') return <p>Loading…</p>

  return <h1>{variantTitle ?? variantKey}</h1>
}

export function Root() {
  return (
    <EmbeddablesProvider config={config} modules={modules}>
      <Hero />
    </EmbeddablesProvider>
  )
}
```

The generated list is ordered `analytics → experiments → forms`, so Analytics is on Core before Experiments initializes. The experiments config is read from the ambient Core instance's `getExperiments()` — same as the non-React entry points. `useExperiment({ experimentId })` returns `{ variantKey, variantTitle, status, error }`, where `status` is one of:

| Status    | Meaning                                  |
| --------- | ---------------------------------------- |
| `pending` | Assignment not resolved yet              |
| `ready`   | `variantKey` / `variantTitle` are usable |
| `error`   | The assignment request failed            |

<Warning>
  `useExperiment` throws if the Experiments module is not enabled in your generated `modules`.
</Warning>

#### Assign on demand

`useExperiment` assigns as soon as it renders. To book the exposure at a specific step instead, use `useAssignExperiment`:

```tsx theme={null}
import { useAssignExperiment } from '@embeddables/experiments/react'

function StartTrial() {
  const { assignExperiment, isPending } = useAssignExperiment()

  return (
    <button
      disabled={isPending}
      onClick={() => {
        void assignExperiment({ experimentId: 'exp-hero' }).then((assigned) => {
          if (assigned) console.log(assigned.variantKey, assigned.variantTitle)
        })
      }}
    >
      Start
    </button>
  )
}
```

A later `useExperiment({ experimentId: 'exp-hero' })` reads the sticky assignment back without another request. `assignExperiment` resolves to `undefined` while the module isn't registered, and rejects when the request fails.

### Analytics

Experiments never imports Analytics. In non-React code, create an Analytics client yourself and pass it in:

```typescript theme={null}
import { initAnalytics } from '@embeddables/analytics'

const analytics = initAnalytics({ core: embeddables })

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

In React, the CLI orders modules `analytics → experiments → forms`, so Experiments auto-wires `core.getAnalyticsInstance()` for exposure tracking — you don't wire anything by hand. Enable both modules in your `config.yaml` and the generated `modules` handles the rest.

When Analytics is enabled, each **newly fetched** assignment fires one `experiment:assigned` event — a sticky assignment already in storage never fires it again. If that event fails to send, the visitor's variant assignment is unaffected; it was already persisted before tracking ran.
