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

# Browser

> Install the JavaScript SDK in any web app, with or without a bundler.

`@trevosdk/browser` assigns variants and records events in the browser. It never touches
the DOM — your own code branches on the variant it returns.

Under 10kb gzipped, no dependencies. This is the plain JavaScript SDK — on React use
[`@trevosdk/react`](/install/react), which wraps it in a provider and a hook.

## Install

```bash theme={null}
npm install @trevosdk/browser
```

### Without a bundler

The CDN build exposes a global called `Trevo`. **Pin the exact version in production** —
the floating channels change under you with no deploy on your side.

```html theme={null}
<script src="https://cdn.trevosdk.com/browser/v0.3.6/trevo.min.js"></script>
<script>
  Trevo.init({ apiKey: 'tsk_live_…' });
</script>
```

| URL | Updates | Cache |
| - | - | - |
| `…/browser/v0.3.6/trevo.min.js` | never | 1 year |
| `…/browser/v0/trevo.min.js` | patches and minors within v0 | 5 minutes |
| `…/browser/latest/trevo.min.js` | every release | 5 minutes |

Use `v0` or `latest` for prototyping only.

## Initialise

```ts theme={null}
import trevo from '@trevosdk/browser';

const apiKey = import.meta.env.VITE_TREVO_API_KEY;
if (!apiKey) throw new Error('Missing VITE_TREVO_API_KEY');
trevo.init({ apiKey });
```

Use your bundler's public prefix — `NEXT_PUBLIC_`, `VITE_`, `PUBLIC_`. Fail explicitly when
it is missing; passing an empty string makes setup fail later and is much harder to diagnose.

The key is publishable (`tsk_live_…`) and belongs in your client bundle. Server keys
(`tsk_secret_…`) are rejected from browsers.
[Where the key goes](/getting-started/quickstart#where-the-key-goes) lists the variable
for each stack — and why a key change needs a redeploy.

`init()` is re-entrant — calling it again reconfigures rather than duplicating.

### Options

| Option | Default | Notes |
| - | - | - |
| `apiKey` | — | Required |
| `bootstrap` | — | Pre-resolved `{ experimentKey: variantName }` for a correct first paint. See [Next.js](/install/nextjs) |
| `manualReveal` | `false` | With the anti-flicker snippet, wait for your `reveal()` instead of revealing automatically |
| `ingestionUrl` | — | Endpoint override for proxied installs |
| `configUrl` | — | Endpoint override for proxied installs |

## Identify the user

```ts theme={null}
trevo.identify(currentUser.id);
```

Before this, users are bucketed on an anonymous id kept in local storage and mirrored to a
`trevo_id` cookie (1 year, SameSite=Lax) — the cookie is what lets your backend and SSR
bucket the same visitor. After it, they
are bucketed on your user id — which means **`identify()` can change a user's variant**.
Call it as early as you can, and before rendering anything under test.

Anonymous ids are per browser, so the same person on a phone and a laptop is two
participants until they sign in.

On logout:

```ts theme={null}
trevo.reset();
```

The user id is cleared and a fresh anonymous id is minted, so the SDK keeps
assigning and tracking — the visitor is simply a new anonymous participant.

## Read a variant

```ts theme={null}
const apply = () => {
  if (trevo.isReady()) renderCheckout(trevo.getVariant('checkout-cta'));
};
const unsubscribe = trevo.onChange(apply);
void trevo.ready().then(apply);
```

Synchronous, no network call, and deterministic — the same identity and key always produce
the same variant. Unknown keys return `'control'`, so this is safe to ship before the
experiment exists.

The first call records an **exposure**, deduplicated per identity and variant within a
10-minute window. Calls made before config has loaded return `control` and record nothing,
so a page that reads only once, before `ready()`, never counts that visitor. The SDK warns in
the console and reports a `read_before_ready` SDK error for it, once per experiment, and the
experiment's page in the dashboard shows how many visitors were missed. A read after a failed
first config fetch is reported as `config_fetch_failed` instead. Read inside
`onChange()` and after `ready()`, as above, and it never fires. To read without recording an
exposure:

```ts theme={null}
trevo.getVariant('checkout-cta', { trackExposure: false });
```

### When an experiment stops

A visitor already in an experiment gets `'control'` once it leaves the config: paused, ended,
rejected, stopped by a guardrail, or shipped more than 30 days ago. An open page switches on the
next config poll (`onChange()` fires), and those reads record no exposure. The visitor's variant
stays cached, so a resumed experiment brings them back to the same one.

Until a config has loaded, the cached variant is still served. A variant passed in `bootstrap`
holds until a config fetch succeeds, so server-rendered pages don't flash control.

The shipped case means that on day 30 every bucketed visitor reverts to control, including those
on the winning variant. Merge the cleanup PR, or remove the `getVariant()` call, before then. See
[Unknown keys return control](/concepts/experiments-and-variants#unknown-keys-return-control).

### Typed variants

`defineExperiment()` returns a typed union and turns an unhandled variant into a compile
error — so a variant added in Trevo that your code does not handle fails the build instead
of silently falling through:

```ts theme={null}
import { defineExperiment, assertNever } from '@trevosdk/browser';

const emailTiming = defineExperiment('email-capture-timing', [
  'control',
  'before-checkout',
  'after-payment',
]);

const variant = trevo.getVariant(emailTiming);
switch (variant) {
  case 'control':          return;
  case 'before-checkout':  return showEmailBeforeCheckout();
  case 'after-payment':    return showEmailAfterPayment();
  default:                 return assertNever(variant);
}
```

## Track conversions

```ts theme={null}
trevo.track('purchase_completed', { value: 49.99, plan: 'pro' });
```

Batched automatically — a flush is triggered once 50 events are queued or every couple of
seconds, and on page unload; a single request carries up to 500 events (the server's cap).
`trevo.flush()` forces a send if you need one.

Every event carries the page's `path` and `host` (the hostname, lowercased, no port), so two
apps on one workspace, such as `example.com` and `app.example.com`, can be told apart. Query
strings and hashes are never captured.

Limits: event names up to 500 characters using `A–Z a–z 0–9 _ . - $` only (anything else
is dropped with a console warning); properties up to 8KB serialised.

## Waiting for config

On a first visit the SDK has no cached config, so `getVariant()` returns `'control'` until
the first fetch lands. Returning visitors read from a warm cache and are correct
immediately.

```ts theme={null}
if (trevo.isReady()) {
  // safe to read synchronously during render
}

await trevo.ready();  // resolves when config loads, or the first fetch fails
```

Config is polled every 60 seconds and cached in local storage for 24 hours, so a slow
network delays the first visit only.

`ready()` also resolves after the first fetch fails. Keep the `onChange()` subscription shown
above so a later successful poll updates the view instead of pinning that visitor to control.

To avoid the flash of control on a first visit entirely, either resolve variants on the
server ([Next.js](/install/nextjs)) or use the anti-flicker snippet below.

## Anti-flicker

Hides the page until variants are resolved, with a safety timeout so a blocked or failed
SDK can never leave your page blank:

```ts theme={null}
import { ANTI_FLICKER_SNIPPET } from '@trevosdk/browser';
```

Inline it in a `<script>` in `<head>`, before anything renders. It reveals automatically
after 1 second by default:

```html theme={null}
<script>window.__trevoConfig = { timeout: 2000 };</script>
```

Set it in a script that runs **before** the snippet — it's read once, at snippet
execution — and values above 10 s are clamped to 10 s.

With `manualReveal: true`, call `trevo.reveal()` once you have committed the variant.

Server-side bootstrapping is better where you can do it — nothing is hidden and there is
no timeout to tune.

## Consent and opt-out

```ts theme={null}
trevo.optOut();   // stops everything, clears identity, drops queued events unsent
trevo.reset();    // clears the user and mints a fresh anonymous id
```

`reset()` leaves the SDK working: the visitor becomes a new anonymous participant,
so post-logout page views, variant reads and conversions are still recorded.
`optOut()` is the opposite — queued events are discarded rather than delivered, and
no new anonymous id is minted. Both are idempotent and safe to call before `init()`.

## API summary

| Method | Purpose |
| - | - |
| `init(options)` | Start the SDK |
| `identify(userId)` | Set the authenticated user |
| `getVariant(key, options?)` | Resolve a variant, recording an exposure by default. A read before `ready()` records none and is reported as a `read_before_ready` SDK error |
| `trackExposure(key, variant)` | Record an exposure explicitly. Ignored (and reported as a `variant_mismatch` SDK error) if the experiment or variant isn't in the loaded config; deduplicated like any exposure |
| `track(name, properties?)` | Queue a conversion event |
| `flush()` | Send queued events now |
| `isReady()` / `ready()` | Whether config has loaded |
| `reset()` | Clear the user and mint a fresh anonymous id (logout) |
| `optOut()` | Stop all activity and discard queued events (consent withdrawal) |
| `onChange(listener)` | Subscribe to config and identity changes; returns an unsubscribe |
| `reveal()` | Reveal the anti-flicker overlay |
| `destroy()` | Tear down entirely |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.