# Experiments

Source: https://docs.mirafive.io/guides/experiments

> Run A/B tests with flags or page snippets, and know exactly when a visitor is counted in an experiment.

An experiment splits visitors between variants and compares what they do next. MIRA FIVE runs two kinds: **flag experiments**, where your code reads a [feature flag](https://docs.mirafive.io/guides/feature-flags) and shows the variant, and **page experiments**, where a snippet in the page head swaps marked elements without code. Both count a visitor with an `$exposure` event, which needs [full mode](https://docs.mirafive.io/guides/consent#full-mode) and consent.

## Flag experiments

A flag experiment is a flag that MIRA FIVE counts. You read it like any other flag, where you show the variant:

**Script tag**

```html
<script>
  mirafive('flags', () => {
    const variant = mirafive('flag', 'pricing-test', 'a')

    document.querySelector('#price').textContent = variant === 'b' ? '€19 / month' : '€190 / year'
  })
</script>
```

**Browser**

```ts
mira.onFlags(() => {
  const variant = mira.flag('pricing-test', 'a')

  document.querySelector('#price')!.textContent = variant === 'b' ? '€19 / month' : '€190 / year'
})
```

**React**

```tsx
import { useFlag } from '@mirafive/sdk-react'

export function Price() {
  const variant = useFlag('pricing-test', 'a')

  return <p>{variant === 'b' ? '€19 / month' : '€190 / year'}</p>
}
```

The page needs full mode with the `identity()` and `flags()` plugins (`data-mode="full"` on the script tag), and the visitor's `experiments` consent to be counted. Without it, the visitor sees a variant but is not counted.

## Exposures

An exposure is the `$exposure` event that says "this unit saw this variant". MIRA FIVE compares variants only among exposed units. It carries:

| Property | Value |
| --- | --- |
| `$experiment` | The flag key |
| `$variant` | The variant shown |
| `$boot` | Browser only: `1` when the consent answer was known when the page first drew, else `0` |
| `$snippet` | Page experiments only: the hash of the snippet copy that drew the variant |

An exposure is sent only when all of these hold:

- The unit was drawn into the variant by the experiment's split. A visitor who gets a fixed variant from a rule, or falls outside the experiment's share, is not counted.
- The flag is read where the experiment is counted: in the browser or on the server (see [Where it is counted](#where-it-is-counted)).
- The read uses the value: `flag()`, `config()`, `useFlag()`, `useFlagConfig()`, or `enabled()`, `variant()` and `config()` on a server. `evaluate()`, `onFlags` listeners themselves, overrides and previews never count.
- In the browser: statistics and `experiments` consent are granted, and the id the unit is counted under still gives the same variant. Exposures read before the grant wait for it and are dropped on a decline.
- Nobody opted out: no Do Not Track, Global Privacy Control or `__mirafive_ignore`.

So read an experiment's flag only where you show its variant: a read in code that decides nothing still counts the visitor.

Deduplication: a browser sends at most one exposure per flag per page load. A server sends at most one per flag, variant and unit per hour; PHP shares that mark between processes through the cache you pass.

Exposures are needed for results but are not counted against your event allowance.

## Where it is counted

Each experiment is counted either in the browser or on the server; you choose when you create it in MIRA FIVE.

| Counted in | Read it with | Server reads |
| --- | --- | --- |
| The browser | A browser SDK | Answer the default variant with the error code `NOT_ALLOWED`, and count nothing |
| The server | A server SDK with a full-mode client | Count the exposure |

A server-rendered page can still show a browser-counted experiment without flicker: the [bootstrap block](https://docs.mirafive.io/guides/feature-flags#server-rendered-pages) carries the server's decision to the page, and the browser counts it when the page reads the flag. For an experiment assigned by person, the browser counts it only when the page has called `identify()` with the same user id the server used.

## Before consent

An experiment can run before the visitor answers the consent banner, in one of two ways, set per experiment in MIRA FIVE:

| Before consent | The visitor sees |
| --- | --- |
| Random | A random variant, drawn with a pending id kept in page memory (`window.__mirafive_aid_next`). On consent it becomes the anonymous id, so the visitor keeps the variant |
| Original | The default variant, until `experiments` consent is granted |

Either way, nothing is stored and no exposure is sent before consent. A page keeps the variant it first showed for the whole page load.

## Page experiments

A page experiment swaps parts of a page without code. It needs a full website source and a page in full mode.

1. In MIRA FIVE, create the page experiment and copy its head snippet: a `<style>` and a `<script>`.
2. Paste the snippet into `<head>`, above the MIRA FIVE script.
3. Mark the original and the new version of each element with the experiment key and the variant, `a` for the original and `b` for the new one.

```html title="index.html"
<head>
  <!-- The page-experiment snippet from MIRA FIVE goes here, above the script tag -->
  <script>window.mirafive=window.mirafive||function(){(mirafive.q=mirafive.q||[]).push(arguments)}</script>
  <script defer src="https://cdn.mirafive.io/mira.js" data-key="mf_…" data-mode="full"></script>
</head>
<body>
  <h1 data-mirafive-experiment="hero-copy" data-mirafive-variant="a">Analytics without cookies</h1>
  <h1 data-mirafive-experiment="hero-copy" data-mirafive-variant="b">Know what works on your site</h1>
</body>
```

The snippet runs before the page draws. It picks a variant, sets `data-mirafive-hero-copy="b"` on `<html>`, and its style hides every marked element of the other variant. It stores nothing and sends nothing. When it drew with an id, it pushes its decision onto `window.__mirafive_experiments`, and the browser SDK sends the exposure once `experiments` consent is granted and the anonymous id gives the same variant.

The script tag loads its experiments code by itself when a snippet has decided something. With the npm package, add the `experiments()` plugin next to `identity()` and `flags()`:

```ts
import { createMira } from '@mirafive/sdk-browser'
import { experiments } from '@mirafive/sdk-browser/experiments'
import { flags } from '@mirafive/sdk-browser/flags'
import { identity } from '@mirafive/sdk-browser/identity'
import { pageviews } from '@mirafive/sdk-browser/pageviews'

export const mira = createMira({
  key: import.meta.env.VITE_MIRAFIVE_KEY,
  mode: 'full',
  plugins: [pageviews(), identity(), flags(), experiments()],
})
```

Details:

- The snippet shows the original, and counts nothing, on hosts that are not the source's allowed origins (or their `www.` twins), after the expiry date built into the snippet (at least 180 days after it was generated), under Do Not Track, Global Privacy Control or `__mirafive_ignore`, and while the page is prerendering.
- It reads `window.__mirafive_consent`, so set that [before it runs](https://docs.mirafive.io/guides/consent#answer-before-the-sdk-loads) when your consent manager knows the answer.
- `mira.flag('hero-copy', 'a')` returns the variant the snippet chose.
- Preview a variant on any host with `?mirafive-preview=hero-copy:b`.
- When MIRA FIVE switches an experiment to show everyone the original (it was decided for the original, or ended early), the SDK answers `a`, sets `data-mirafive-hero-copy="a"` and sends no exposure, even while the snippet is still in the page. Remove the snippet and the markup of the variant you drop when you are done.

## Server-side experiments

An experiment counted on the server is read with a server SDK whose flag client can send events. Pass the visitor's `experiments` consent; without it the anonymous id is not used and nobody is counted:

**Node.js**

```ts
import { Mira } from '@mirafive/sdk-server'
import { MiraFlags } from '@mirafive/sdk-server/flags'

const mira = new Mira({ key: process.env.MIRAFIVE_SECRET_KEY })
const flags = new MiraFlags({ key: process.env.MIRAFIVE_SECRET_KEY, mira })

const user = await flags.for({ userId: session.userId, consent: { experiments: true } })
const variant = user.variant('onboarding-emails', 'a')
```

`mira` must be in full mode: a consentless client counts no exposures and reports `collection_mode_not_allowed` to `onError`. With a `waitUntil`, the exposure is flushed with the request.

**PHP**

```php
$flags = $mira->flags()->for(userId: (string) $user->id, consent: ['experiments' => true]);
$variant = $flags->variant('onboarding-emails', 'a');
```

`$mira->flags()` counts exposures through `$mira`.

**Laravel**

```php
use MiraFive\Laravel\Facades\Mira;

$variant = Mira::forUser($request->user())
    ->flags(consent: ['experiments' => true])
    ->variant('onboarding-emails', 'a');
```

Rendering a bootstrap block for a unit counts every server-counted experiment it decided for that unit, since the page is handed the value.
