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

# JavaScript SDK

> Browser client SDK for framework-free apps and as the foundation for the React and Svelte SDKs.

`@traffical/js-client` is the framework-agnostic browser SDK. It fetches the config bundle, resolves parameters in the browser, tracks exposure events, and manages anonymous-user identity automatically.

If you're using React or Svelte, prefer the [React SDK](/sdks/react) or [Svelte SDK](/sdks/svelte) — they wrap this client with framework-native APIs.

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @traffical/js-client
  ```

  ```bash pnpm theme={null}
  pnpm add @traffical/js-client
  ```

  ```bash yarn theme={null}
  yarn add @traffical/js-client
  ```
</CodeGroup>

## Setup

```typescript theme={null}
import { createTrafficalClient } from "@traffical/js-client";

const traffical = await createTrafficalClient({
  orgId: "org_acme",
  projectId: "proj_marketplace",
  env: "production",
  apiKey: "traffical_sk_...",
});
```

<Note>
  Use an **SDK key** (`traffical_sk_...`, scopes `sdk:read`+`sdk:write`) in browser code. SDK keys can fetch the bundle and send events but cannot modify configuration — safe to ship in client bundles.
</Note>

## Resolving parameters

```typescript theme={null}
const params = traffical.getParams(
  { userId: "user_789" },
  {
    "homepage.hero_headline": "Welcome back",
    "homepage.show_banner": true,
  },
);

document.querySelector("h1")!.textContent = params["homepage.hero_headline"];
```

Both `getParams(context, defaults)` and `decide(context, defaults)` take **context first, defaults second**, and both are **synchronous**. In the browser you can usually omit the unit key — the SDK fills it from the auto-generated stable ID (see below).

<Note>
  The legacy object-bag form — `getParams({ context, defaults })` / `decide({ context, defaults })` — still works but is deprecated. Prefer the positional form.
</Note>

An exposure event is emitted automatically the first time a user/assignment combination is resolved in a session.

## Anonymous users and `identify`

Browser-side experiments often need to resolve parameters before a user is logged in. To make that work without you having to think about it, the browser SDK auto-generates a stable UUID on first visit, stores it in `localStorage` (with a cookie fallback), and fills the project's unit-key slot with that value whenever the caller doesn't pass one.

So if the project's unit key is `userId`, the SDK effectively passes `context.userId = "<auto-generated-uuid>"` on every pre-login resolution. Bucketing works, exposures fire, the user gets a stable assignment for as long as their `localStorage` persists.

When the user logs in, call `identify(realUserId)`:

```typescript theme={null}
traffical.identify("user_789");
```

This overwrites the stored stable ID with the real `userId`. Every subsequent resolution uses the new value.

<Warning>
  **Bucketing changes at login.** The pre-login bucket and the post-login bucket are computed from different inputs (`hash(uuid + layerId)` vs `hash("user_789" + layerId)`) — they almost always land in different allocations. A user who saw the treatment variant while anonymous may see the control variant after logging in, and vice versa.

  This is unavoidable with a single unit of randomization per project. There's no "anonymous-to-identified" continuity layer: the SDK doesn't remember which bucket the stable ID was in and remap the userId to match. If you need user-stable assignments to persist across login, do the experiment on logged-in users only — gate the policy with a condition that the unit key matches a real user-ID format, or have your backend force `identify()` before the SDK ever resolves the parameter.
</Warning>

You can read the current stable ID with `traffical.getStableId()`. After `identify()`, this returns the value you passed in.

## Tracking events

`track(event, properties?, options?)` — the event name first, an optional `properties` payload second, and an options bag (`unitKey`, `decisionId`, `value`, `values`, `eventTimestamp`) third. In the browser `unitKey` defaults to the current stable ID, so you rarely pass it:

```typescript theme={null}
traffical.track("signup");

traffical.track("purchase", { order_total: 29.99, currency: "USD" }, { value: 29.99 });
```

Events are batched in memory and flushed in the background.

## Lifecycle

`createTrafficalClient` (async) awaits the first bundle fetch. When you can't await at construction, use `createTrafficalClientSync` and await readiness before the first resolution:

```typescript theme={null}
import { createTrafficalClientSync } from "@traffical/js-client";

const traffical = createTrafficalClientSync({ orgId, projectId, env, apiKey });
await traffical.initialize();
await traffical.waitForReady();   // resolves once config loads — or fails open; never hangs
```

Force a flush of queued events (e.g. after a critical conversion, before navigating):

```typescript theme={null}
await traffical.flushEvents();
```

Tear the client down with the single teardown verb, `close()`. It stops the background refresh and awaits a final event flush before returning (on page unload it falls back to `sendBeacon` so the last batch still ships):

```typescript theme={null}
await traffical.close();
```

<Note>
  `destroy()` still exists but is deprecated — prefer `close()`, which awaits the final flush.
</Note>

## Options

| Option                       | Type                         | Default                    | Description                                                                |
| ---------------------------- | ---------------------------- | -------------------------- | -------------------------------------------------------------------------- |
| `orgId`                      | `string`                     | required                   | Organization ID                                                            |
| `projectId`                  | `string`                     | required                   | Project ID                                                                 |
| `env`                        | `string`                     | required                   | Environment name                                                           |
| `apiKey`                     | `string`                     | required                   | SDK key (`traffical_sk_...`, scopes `sdk:read`+`sdk:write`) — browser-safe |
| `baseUrl`                    | `string`                     | `https://sdk.traffical.io` | SDK API base URL                                                           |
| `refreshIntervalMs`          | `number`                     | `60000`                    | Bundle refresh interval                                                    |
| `localConfig`                | `ConfigBundle`               | —                          | Embedded bundle for cold-start                                             |
| `evaluationMode`             | `"bundle" \| "server"`       | `"bundle"`                 | See [how it works](/how-it-works#evaluation-modes)                         |
| `attributionMode`            | `"cumulative" \| "decision"` | `"cumulative"`             | Track-event attribution strategy                                           |
| `batchSize`                  | `number`                     | `10`                       | Events per delivery batch before flushing                                  |
| `flushIntervalMs`            | `number`                     | `30000`                    | Max time before flushing pending events                                    |
| `eventMaxQueueSize`          | `number`                     | `1000`                     | Max events buffered before the oldest is dropped                           |
| `configTimeoutMs`            | `number`                     | `10000`                    | Timeout for the config-bundle fetch                                        |
| `eventsTimeoutMs`            | `number`                     | `10000`                    | Timeout for event-delivery POSTs                                           |
| `resolveTimeoutMs`           | `number`                     | `5000`                     | Timeout for server-mode resolve calls (`POST /v1/resolve`)                 |
| `exposureSessionTtlMs`       | `number`                     | `1800000`                  | Exposure-dedup session TTL (30 min)                                        |
| `trackDecisions`             | `boolean`                    | `true`                     | Emit decision events alongside exposures                                   |
| `decisionDeduplicationTtlMs` | `number`                     | `3600000`                  | Window in which the same unit + assignment won't re-emit a decision event  |
| `disableCloudEvents`         | `boolean`                    | `false`                    | Stop sending events to Traffical (config is still fetched)                 |
| `disableAutoStableId`        | `boolean`                    | `false`                    | Disable auto-generation of `stableId`                                      |
| `storage`                    | `StorageProvider`            | `LocalStorageProvider`     | Persistence for the stable ID                                              |
| `assignmentLogger`           | `AssignmentLogger`           | —                          | Route assignment rows to your own warehouse pipeline                       |
| `plugins`                    | `TrafficalPlugin[]`          | `[]`                       | Plugins (DOM bindings, redirect tests, devtools)                           |

<Note>
  **Deprecated option aliases.** Earlier releases used `eventBatchSize`, `eventFlushIntervalMs`, and a single `requestTimeoutMs`. These still work and forward to the canonical names above (`batchSize`, `flushIntervalMs`, and the three-way timeout split). `requestTimeoutMs` is honored as the legacy fallback for any of `configTimeoutMs` / `eventsTimeoutMs` / `resolveTimeoutMs` that you don't set explicitly. Prefer the canonical names — the aliases will be removed in a future major.
</Note>

## Bundle caching

The browser SDK caches the bundle in memory. The first page load fetches it; subsequent loads benefit from HTTP caching at the CDN edge (`Cache-Control: public, max-age=60, must-revalidate` with ETag). On warm loads, resolution is available immediately.

## Plugins

The browser SDK has a small plugin system that powers some of Traffical's other tools:

| Plugin                      | Purpose                                                                                         |
| --------------------------- | ----------------------------------------------------------------------------------------------- |
| **DOM bindings**            | Auto-apply parameter values to DOM elements (used by the [visual editor](/tools/visual-editor)) |
| **Redirect tests**          | URL split-testing — redirect users to different URLs based on assignment                        |
| **Warehouse-native logger** | Forward assignment events to your warehouse                                                     |
| **DevTools**                | Live SDK inspection in the browser (used by the [DevTools bookmarklet](/tools/devtools))        |

```typescript theme={null}
import { createTrafficalClient, createDOMBindingPlugin } from "@traffical/js-client";

const traffical = await createTrafficalClient({
  orgId, projectId, env, apiKey,
  plugins: [createDOMBindingPlugin()],
});
```

## Framework SDKs

If you're using React or Svelte, use the framework-specific SDKs instead:

* [`@traffical/react`](/sdks/react) — provider and `useTraffical` hook
* [`@traffical/svelte`](/sdks/svelte) — stores and context API

Both wrap `@traffical/js-client` and expose the same plugin system.
