# Obelism Improve

> Improve your platform today. Obelism Improve lets you create experiments and feature flags with audience targeting, deliver them to your app through framework SDKs (JavaScript, React, Next.js) or a public config endpoint, and track conversions with real-time analytics across the develop, staging and production environments.

This file is a condensed reference to the SDK and public API for LLMs, served at https://improve.obelism.studio/sdk.md. Keep it in sync with `content/docs/**` and the SDK in `../obelism-improve-sdk`. Docs are published at https://improve.obelism.studio/docs; the index of every page, with guidance on when to use Improve, is https://improve.obelism.studio/llms.txt.

## Core concepts

- **Test**: an A/B experiment: a slug, an audience, an allocation (% of matching visitors entered), weighted options (control + variations), and events (`start`, `metrics[]`, `conversion`, each an `{event, scope}` pair — both required; a step only counts an analytic that matches both).
- **Flag**: a feature flag: a slug, an audience, and weighted options. Like a test but without allocation/events.
- **Option**: one variant of a test/flag: `name`, `slug`, `value`, `split` (weight).
- **Audience**: targeting rules matched against the visitor: `pointer` (coarse/fine), `device`, `browser`, `os` (all from the user agent, resolve anywhere), plus `country` (ISO 3166-1 alpha-2, e.g. `NL`). `country` only resolves during server-side/middleware bucketing (the browser can't read it from the user agent), via the Next.js middleware (auto) or the server SDK's `geo` arg; a `country` audience never matches client-side. `AudienceParamKey = 'country' | 'pointer' | 'device' | 'browser' | 'os'`.
- **Visitor ID**: anonymous, stable per visitor (prefix `visi_`), stored in a cookie so a visitor keeps the same variant across requests.
- **Environment**: `develop` | `staging` | `production`; each has its own API tokens, origins and config.
- **Datafile / config**: the `ImproveConfiguration` JSON (tests, flags, audiences) the SDK evaluates locally.

## Recommended default: track page views

Before anything else, fire a GA4-style `page_view` event once per page load, with the current path as its `scope` (e.g. `postAnalytic('page_view', pathname)` from a small mounted-once component reading `usePathname()`). This is the default **start event** for new tests, and it's what powers the Events dashboard's **Unique visitors per day** chart and **Popular pages** overview (which reads the page path off `page_view`'s `scope`). See [Events guide](https://improve.obelism.studio/docs/guides/events#default-track-page-views-like-google-analytics).

## SDK packages

Three packages. The core package has **no root export**: import from `/server`, `/client`, or `/types`. Core `1.0.0` requires `ua-parser-js@^2` as a peer dependency.

- `@obelism/improve-sdk`: core JS SDK (server + client classes, types)
- `@obelism/improve-sdk-react`: React provider + hooks (wraps the client SDK)
- `@obelism/improve-sdk-next`: Next.js middleware for server-side tests (wraps the server SDK)

### Setup args (`ImproveSetupArgs`, from `@obelism/improve-sdk/types`)

```ts
{
  organizationId: string
  environment: 'develop' | 'staging' | 'production'
  state?: 'draft' | 'active' | 'finished' | 'archived'
  config?: ImproveConfiguration
  baseUrl?: string        // default https://improve.obelism.studio
  fetchTimeout?: number   // default 3000 ms
  dataLayer?: boolean     // client only; mirror events to window.dataLayer (GTM), default true
  disableWarnings?: boolean // silence dev warnings (e.g. the snake_case event-name nudge)
}
// Server also accepts: token?: string (to fetch config) OR config, and maxVisitors?: number (default 10000)
```

### Core: `@obelism/improve-sdk/server` → `ImproveServerSDK`

Initialise with **either** a `token` (to `fetchConfig`) **or** a ready `config`. Evaluates per request; bounded in-memory visitor cache (`maxVisitors`).

```ts
new ImproveServerSDK(args)
fetchConfig(config?: RequestInit): Promise<ImproveConfiguration>
loadConfig(config: ImproveConfiguration): void
generateVisitorId(): string
getVisitorCookieName(): string
validateVisitorId(id: string): boolean
validateTestValue(testSlug: string, value: string): boolean
getFlagConfig(flagSlug: string): ImproveFlag | undefined
getTestConfig(testSlug: string): ImproveTest | undefined
getFlagValue(flagSlug: string, visitorId: string, userAgent: string, geo?: { country?: string }): string | null
getTestValue(testSlug: string, visitorId: string, userAgent: string, geo?: { country?: string }): string | null
```

The optional `geo` arg supplies coarse location (server-derived from request geo headers) so audiences can target `country`; the browser can't determine it, so pass it in server-side. The Next.js middleware wires this up automatically.

### Core: `@obelism/improve-sdk/client` → `ImproveClientSDK`

Runs in the browser; persists the visitor ID and chosen values in cookies. `getFlagValue`/`getTestValue` take only a slug (visitor + UA are resolved internally via `setupVisitor`).

```ts
new ImproveClientSDK(args)
fetchConfig(config?: RequestInit): Promise<ImproveConfiguration>
setupVisitor(userAgent?: string): string | null   // defaults to navigator.userAgent
getFlagValue(flagSlug: string): string | null     // reading a value emits an exposure the first time a non-holdout variant resolves
getTestValue(testSlug: string): string | null     // reading a value emits an exposure the first time a non-holdout variant resolves
setAnalyticsUrls(url: string): void                // proxy analytics through your own domain
postAnalytic(event: string, scope: string, payload?: ImproveAnalyticPayload): Promise<Response> | null
// scope is required: a short, stable identifier for where/what the event refers to (not display text) — the second half of the event's identity, e.g. postAnalytic('page_view', 'homepage').
// ImproveAnalyticPayload = { value?: number; currency?: string; params?: Record<string, unknown>; dedupeKey?: string }
// value(+currency) → revenue/AOV per variant; params spread onto the GTM dataLayer.
// events are test-independent (deduped once per visitor/page, per event name); attribution is server-side by joining the visitor's exposures. reading a variant (getTestValue/getFlagValue) records the exposure. audience-excluded/holdout visitors aren't exposed.
// dedupeKey scopes the dedup to event+dedupeKey instead of event alone, so the same event name can fire once per distinct subject (e.g. postAnalytic('view_promotion', { params: { type: 'Hero' }, dedupeKey: 'Hero' }) fired once per promotion block).
// getFlagValue/getTestValue trust a valid existing assignment cookie before re-evaluating audience/allocation (and record its exposure), which honors a country-targeted variant assigned server-side by the middleware.
// prefer snake_case event names (GA4/GTM); gtm.* names are ignored; non-snake_case warns unless disableWarnings is set.
// also inherits loadConfig / generateVisitorId / getVisitorCookieName / validate* from the base
```

### React: `@obelism/improve-sdk-react`

```ts
const {
  ImproveProvider,      // <ImproveProvider>{children}</ImproveProvider>: fetches SDK + config on mount
  useImproveStatus,     // () => 'loading' | 'setup' | 'error'
  usePostAnalytic,      // () => (event, scope, payload?) => Promise<Response>  (scope required, payload: ImproveAnalyticPayload)
  useTestValue,         // (testSlug, fallback?) => string
  useFlagValue,         // (flagSlug, fallback?) => string
} = generateImproveProvider(args: ImproveSetupArgs)
```

Note: `usePostAnalytic` takes no arguments and returns a global post function; call it with the event name, a required `scope`, and an optional payload. Events are test-independent; attribution is server-side, so reading a variant with `useTestValue`/`useFlagValue` records the exposure that a visitor's events are attributed to.

### Next.js: `@obelism/improve-sdk-next`

Server-side tests via middleware: it reads/writes the visitor + test cookies and rewrites the request to an internal route based on the visitor's variant (no client layout shift). Provide an `ImproveServerSDK` already loaded with a config. It auto-detects the visitor's country from edge geo headers (`x-vercel-ip-country`, falling back to `cf-ipcountry`) and passes it as `geo` into bucketing, so `country` audiences resolve with no extra code.

```ts
generateImproveNextMiddleware({
  improveSdk: ImproveServerSDK,
  serverABtests: ServerABTestConfig[],
  options?: { visitorId?: ResponseCookie; testValue?: ResponseCookie },
}) => (request: NextRequest) => NextResponse

type ServerABTestConfig = {
  slug: string                 // matches a test in Improve
  routeHandler: string         // route to run on
  formatSlug?: (url: NextURL, matchingOption: OptionConfig) => NextURL
  options: OptionConfig[]
}
type OptionConfig = { value: string /* matches the option value */; slug: string /* route to rewrite to */ }
```

## Public API endpoints

- `GET /config/:organizationId/:environment/:status`: datafile for the SDK. Auth required: a browser request must send an `Origin` in the environment's Allowed origins (CORS), or a server request must send a valid `token` header; otherwise `403`. Served with a short CDN `Cache-Control` (`s-maxage=30`, `Vary: Origin`). The browser (Origin) path is rate-limited per IP (`600/min` → `429`) and sends `RateLimit-Policy: "browser";q=600;w=60` (IETF RateLimit headers draft); a `429` adds `RateLimit: "browser";r=0;t=60` and `Retry-After`. The token/server path is exempt and gets no rate limit headers. `:status` is optional (`draft | active | finished | archived`) and defaults to `active`; omitting it (`/config/:organizationId/:environment`) returns the active datafile.
- `POST /api/log`: analytics ingestion; the client SDK's `postAnalytic` (events) and automatic exposure beacons post here. Body is discriminated by `type: 'event' | 'exposure'`: event bodies carry `event`/`scope` (both required, non-empty)/`value`/`currency`/`params` and NO `testId`/`testValue` (attribution is server-side via exposures); exposure bodies carry `subjectKind: 'test'|'flag'`, `subjectId`, `variant`. Both events and exposures are stored with a coarse, server-derived location: optional `country` (ISO 3166-1 alpha-2) and `region` (subdivision) computed from request edge geo headers (`x-vercel-ip-country`/`-country-region`, `cf-ipcountry` fallback); the SDK never sends them and the raw IP is never stored. `country` is audience-targetable (server-side only); `region` is not. Same origin/token auth as the datafile (`403` on failure). Body must include `organizationId` and `environment`; requests over 8KB → `413`, malformed/unknown-enum payloads or an event still carrying testId/testValue → `400`. Ingestion is never blocked by plan usage: events past the monthly quota are still stored, the limit applies where results are shown.
- Machine-readable spec: `GET /openapi.json` (OpenAPI 3.1) describes both endpoints.
- Versioning: both endpoints are API version 1 and send `Api-Version: 1`. Paths are stable; a breaking change ships under a new `/v2/` prefix next to the current paths. A retiring endpoint sends `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers at least 6 months before it stops working. Additive changes (new fields, headers, error codes) ship any time, so ignore unknown fields. See https://improve.obelism.studio/docs/api/versioning.
- Errors: every non-2xx response from `/api/*` and `/config/*` is RFC 9457 problem details (`Content-Type: application/problem+json`): `{ type, title, status, detail, code, hint }`. Branch on `code` (`invalid_json`, `payload_too_large`, `invalid_request`, `reserved_event_name`, `unknown_organization`, `forbidden`, `rate_limited`, `ingest_failed`, `not_found`, `method_not_allowed`, `internal_error`); `hint` says what to change. `/api/log` errors also keep `success: false`. A `429` carries `Retry-After` and `RateLimit`. See https://improve.obelism.studio/docs/api/errors.
## Documentation index

### Getting started
- [Getting started](https://improve.obelism.studio/docs/getting-started): account, organization and first test setup
- [JavaScript](https://improve.obelism.studio/docs/getting-started/javascript): server + client integration walkthrough
- [React](https://improve.obelism.studio/docs/getting-started/react): provider + hooks walkthrough
- [Next.js](https://improve.obelism.studio/docs/getting-started/nextjs): server-side testing via middleware

### Guides
- [Guides](https://improve.obelism.studio/docs/guides): conceptual, non-developer guides
- [Events](https://improve.obelism.studio/docs/guides/events): start / metric / conversion events
- [What to test](https://improve.obelism.studio/docs/guides/what-to-test): choosing experiments
- [Multi-variant tests](https://improve.obelism.studio/docs/guides/multi-variant-tests): factorial combination testing vs. plain A/B tests, and when it's worth the traffic cost

### SDK reference
- [SDK overview](https://improve.obelism.studio/docs/sdk): server vs client, which package to use
- [JavaScript SDK](https://improve.obelism.studio/docs/sdk/javascript): classes, methods, and all exported types
- [React SDK](https://improve.obelism.studio/docs/sdk/react): `generateImproveProvider` and hooks
- [Next.js SDK](https://improve.obelism.studio/docs/sdk/nextjs): `generateImproveNextMiddleware`

### API reference
- [API overview](https://improve.obelism.studio/docs/api)
- [Datafile](https://improve.obelism.studio/docs/api/datafile): config endpoint and `ImproveConfiguration` shape
- [Analytics](https://improve.obelism.studio/docs/api/analytics): the `/api/log` event endpoint
- [Errors](https://improve.obelism.studio/docs/api/errors): every error code
- [Versioning](https://improve.obelism.studio/docs/api/versioning): version header, deprecation policy, rate limit headers

### Integrations
- [Vercel](https://improve.obelism.studio/docs/integrations/vercel): sync the datafile to Edge Config for the middleware
- [Google Tag Manager](https://improve.obelism.studio/docs/integrations/gtm): the `dataLayer` integration for GTM / Google Ads
- [Shopify](https://improve.obelism.studio/docs/integrations/shopify): connect a store to an org/environment, initiated from the Shopify side
- [Webhooks](https://improve.obelism.studio/docs/integrations/webhooks)

### Examples
- [Examples overview](https://improve.obelism.studio/docs/examples)
- [Static HTML](https://improve.obelism.studio/docs/examples/html): vanilla JS, client SDK
- [React](https://improve.obelism.studio/docs/examples/react): React SPA
- [Next.js](https://improve.obelism.studio/docs/examples/nextjs): middleware server-side test
- [Next.js + Vercel](https://improve.obelism.studio/docs/examples/nextjs-vercel): datafile from Edge Config
- [Hono](https://improve.obelism.studio/docs/examples/hono): server SDK with manual cookies

### Reference
- [Terminology](https://improve.obelism.studio/docs/terminology)
- [Privacy & GDPR](https://improve.obelism.studio/docs/gdpr): what personal data Improve processes about your visitors (cookies, retention, sub-processors)
- [Change log](https://improve.obelism.studio/docs/change-log)
