# Javascript SDK (/docs/sdk/javascript)

## Install [#install]

```bash
npm i @obelism/improve-sdk
```

The SDK relies on `ua-parser-js` to read the visitor's user agent and declares it as a **peer dependency**. Make sure `ua-parser-js` **v2** is installed (v1 is not compatible since v1.0.0 of the SDK):

```bash
npm i ua-parser-js@^2
```

There is no root export; import from one of the three subpaths: `@obelism/improve-sdk/server`, `@obelism/improve-sdk/client`, or `@obelism/improve-sdk/types`.

## Class [#class]

The Improve JS SDK exposes two classes:

- [ImproveServerSDK](/docs/sdk/javascript#server)
- [ImproveClientSDK](/docs/sdk/javascript#client)
- [Types](/docs/sdk/javascript#types)

While similar, they both work slightly differently but are based on the same base. This base defines the constructor and three methods;

### constructor [#constructor]

```ts
constructor({
	organizationId: string
	environment: 'develop' | 'staging' | 'production'
	state?: 'draft' | 'active' | 'finished' | 'archived'
	config?: ImproveConfiguration
	baseUrl?: string
	fetchTimeout?: number
	dataLayer?: boolean
}) => void
```

- **organizationId** - Unique identifier of your Organization, available from your dashboard
- **environment** - Application environment, can be one of three values
- **state** - (optional) Which test state to load from; defaults to the active config
- **config** - (optional) Configuration file, this can be either fetched or provided on initialization
- **baseUrl** - (optional) Base URL of the Improve instance to talk to; defaults to `https://improve.obelism.studio`.
- **fetchTimeout** - (optional) When fetching the config after what amount of ms should it abort, default; 3000ms
- **dataLayer** - (optional, client only) Mirror analytics onto `window.dataLayer` for Google Tag Manager / Google Ads. Enabled by default; see the [GTM integration](/docs/integrations/gtm)
- **token** - (server only) API token required for server usage
- **maxVisitors** - (server only) Size of the in-memory visitor cache for long-running servers; defaults to `10000`

The server constructor additionally requires **either** a `config` **or** a `token` (to fetch the config). See the [Server](#server) section.

### fetchConfig [#fetchconfig]

```ts
async fetchConfig(config?: RequestInit) => Promise<ImproveConfiguration>
```

Asynchronous function that fetches the config from Improve and returns it. Either provide the config on initialization or use this function to get started. An optional `RequestInit` is forwarded to the underlying `fetch` call.

### loadConfig [#loadconfig]

```ts
loadConfig = (config: ImproveConfiguration) => void
```

When loading an Improve config file from a different source you can either pass it directly into the class constructor or load it with the `loadConfig` method.

### generateVisitorId [#generatevisitorid]

```ts
generateVisitorId() => string
```

Method to get a unique identifier for a visitor. Recommended to save the generated visitor ID in a cookie with a max-age so that between visits you'll be able to keep serving the same version to this visitor.

### getVisitorCookieName [#getvisitorcookiename]

```ts
getVisitorCookieName() => string
```

Helper method that gives you the name for the visitorID cookie name. If this name is used on the server, the client will be able to pick up and send events to Improve.

### validateTestValue [#validatetestvalue]

```ts
validateTestValue = (testName: string, testValue: string) => boolean
```

Validate if a test name and test value are a valid combination. Requires a config to be loaded by either passing it in the constructor, using [loadConfig](/docs/sdk/javascript#loadconfig) or [fetchConfig](/docs/sdk/javascript#fetchconfig).

### validateVisitorId [#validatevisitorid]

```ts
validateVisitorId = (possibleVisitorId: string) => boolean
```

Validate whether a string is a well-formed visitor ID. Useful on the server when reading a visitor ID from a cookie before trusting it. Fall back to [generateVisitorId](/docs/sdk/javascript#generatevisitorid) when it doesn't validate.

## Server [#server]

The server class is set up so it can run on either serverless or serverful environments. When running in a serverful environment it will use memory to keep track of recent visitors' decisions. However, it is recommended to always store the made decision in a cookie in case the memory has been purged between visits of a visitor.

Initialise it in one of two ways: pass a `token` so it can [fetchConfig](/docs/sdk/javascript#fetchconfig) from Improve, **or** pass a ready `config` directly (e.g. loaded from [Vercel Edge Config](/docs/integrations/vercel)). On long-running servers the in-memory visitor cache is bounded by `maxVisitors` (default `10000`).

### getFlagConfig [#getflagconfig]

```ts
getFlagConfig(flagSlug: string) => {
	id: string
	name: string
	audience: string
	options: {
        name: string
        slug: string
        value: string | undefined
        split: number
    }[]
} | undefined
```

Exposed for when needed but in most cases it's recommended to use the [getFlagValue](/docs/sdk/javascript#getflagvalue) directly.

### getTestConfig [#gettestconfig]

```ts
getTestConfig(testSlug: string) => {
	id: string
	name: string
	defaultValue: string
	audience: string
	allocation: number
	options: {
        name: string
        slug: string
        value: string | undefined
        split: number
    }[]
	events: {
        start: { event: string; scope: string }
        metrics: { event: string; scope: string }[]
        conversion: { event: string; scope: string }
    }
} | undefined
```

Exposed for when needed but in most cases it's recommended to use the [getTestValue](/docs/sdk/javascript#gettestvalue) directly.

### getFlagValue [#getflagvalue]

```ts
getFlagValue = (
	flagSlug: string,
	visitorId: string,
	userAgent: string,
	geo?: { country?: string },
) => string | null
```

Based on the slug, visitorId and userAgent this method gives you back what version of the flag this visitor should get. If no config is loaded yet or no flag with the given slug is found it returns `null`. When the userAgent of the visitor doesn't match the audience of the Flag it will always return the control value. If the visitor does match the audience it will receive the control or variation based on the randomization using the odds set up in the split.

The optional `geo` argument supplies coarse location so audiences can target by `country` (ISO 3166-1 alpha-2, e.g. `NL`). The browser can't determine country from the user agent, so it has to be resolved server-side and passed in here: from the request's edge geo headers (`oxygen-buyer-country` on Shopify Oxygen, `x-vercel-ip-country` on Vercel, `cf-ipcountry` on Cloudflare). If you use the [Next.js middleware](/docs/sdk/nextjs) this is wired up for you.

### getTestValue [#gettestvalue]

```ts
getTestValue = (
	testSlug: string,
	visitorId: string,
	userAgent: string,
	geo?: { country?: string },
) => string | null
```

Based on the slug, visitorId and userAgent this method gives you back what version of the AB test this visitor should get. If no config is loaded yet or no AB test with the given slug is found it returns `null`. When the userAgent of the visitor doesn't match the audience of the Test it will always return the control value. If the visitor does match the audience it will then check the allocation. If that's lower than 100%, it will, based on randomization, check if this visitor should get the AB test. After that it will receive a random version of the test based on the randomization using the odds set up in the split.

The optional `geo` argument supplies coarse location so audiences can target by `country` (ISO 3166-1 alpha-2, e.g. `NL`). The browser can't determine country from the user agent, so it has to be resolved server-side and passed in here: from the request's edge geo headers (`oxygen-buyer-country` on Shopify Oxygen, `x-vercel-ip-country` on Vercel, `cf-ipcountry` on Cloudflare). If you use the [Next.js middleware](/docs/sdk/nextjs) this is wired up for you.

## Client [#client]

### setupVisitor [#setupvisitor]

```ts
setupVisitor(userAgent: string = window.navigator.userAgent) => string | null
```

Optional method to set up the visitor, if this is not called before using any of the other Client methods it will be called automatically. You only have to call this yourself if you want to make sure it gets the `userAgent` from somewhere else.

This method will automatically save the visitorID to the class and cookie + parse the userAgent to be used in the postAnalytic.

### getFlagValue [#getflagvalue-1]

```ts
getFlagValue = (flagSlug: string) => string | null
```

Same as [getFlagValue](/docs/sdk/javascript#getflagvalue) on the server class this generates and give the unique value. However on the client it also saves the value to a cookie that's stored for one month.

### getTestValue [#gettestvalue-1]

```ts
getTestValue = (testSlug: string) => string | null
```

Same as [getTestValue](/docs/sdk/javascript#gettestvalue) on the server class this generates and give the unique value. However on the client it also saves the value to a cookie that's stored for one month.

<Callout type="info">
	Both client `getFlagValue` and `getTestValue` trust a valid existing
	assignment cookie before re-evaluating audience/allocation, and record the
	exposure for that value. This is what lets a country-targeted variant chosen
	by the [Next.js middleware](/docs/sdk/nextjs) server-side (where geo is known)
	be honored in the browser; the middleware sets the cookie, and the client
	reads it rather than re-bucketing without a country.
</Callout>

### setAnalyticsUrls [#setanalyticsurls]

```ts
setAnalyticsUrls = (url: string) => void
```

Configure the analytics url to post message towards. Convenient in case add blockers block the direct post requests you can proxy them through your domain and pathname. By default (from 3.1) events go to [Obelism numbers](https://numbers.obelism.studio), `https://numbers.obelism.studio/api/log`; with a custom `baseUrl` to that url's `/api/log`.

### postAnalytic [#postanalytic]

```ts
postAnalytic = (
	event: ImproveEventName,
	scope: string,
	payload?: ImproveAnalyticPayload,
) => Promise<Response | undefined> | null

type ImproveAnalyticPayload = {
	value?: number // GA4 `value`, aggregated into revenue / AOV per variant
	currency?: string // ISO 4217 code for `value`, e.g. 'USD'
	params?: Record<string, unknown> // extra params, e.g. { ecommerce: {...} }
	dedupeKey?: string // distinguishes repeated firings of the same event name — see below
}
```

Posts an analytic event to Obelism numbers or the url configured with [setAnalyticsUrls](/docs/sdk/javascript#setanalyticsurls), through the numbers SDK ([`@obelism/numbers-sdk`](https://www.npmjs.com/package/@obelism/numbers-sdk)), which adds the page's path and UTM tags. Use the numbers SDK on its own for page views and click tracking without tests. Events are **test-independent**: you post an event name + scope (deduped once per visitor per page/session) and the server attributes it to whichever tests/flags the visitor was exposed to.

Attribution depends on **exposure**: reading a variant with [getTestValue](/docs/sdk/javascript#gettestvalue) / [getFlagValue](/docs/sdk/javascript#getflagvalue) emits an exposure beacon the first time a visitor resolves a non-holdout value, and the server joins events against those exposures. A visitor who never reads a test's variant (or who is audience-excluded or in the allocation holdout) is not exposed, so their events aren't attributed to that test.

`scope` is **required** — a short, stable identifier for where/what the event refers to (e.g. `"homepage_hero_login"`), not display text. It's the one consistent key/value identity used everywhere: here, the [test setup UI](/docs/guides/events), and Monitor's per-step matching. Pass a `value` (with `currency`) on your conversion event to unlock **revenue and average order value per variant** in the test Monitor. Any `params` are stored and mirrored onto the [GTM dataLayer](/docs/integrations/gtm); provide `{ ecommerce: {...} }` for GA4 ecommerce tags.

<Callout type="info">
	The once-per-session dedup keys on the **event name alone** by default, so
	firing the same name more than once per page (e.g. a `view_promotion` fired
	once per promotion block) only ever sends the first call. Pass `dedupeKey`
	to scope the dedup to `event` + `dedupeKey` instead, so the same event name
	can fire once per distinct subject:

    ```ts
    postAnalytic('view_promotion', 'homepage_hero', {
    	params: { title: 'Hero', type: 'Hero' },
    	dedupeKey: 'Hero',
    })
    ```

</Callout>

<Callout type="info">
	Prefer **`snake_case`** event names (`add_to_cart`, `purchase`) to match GA4 /
	GTM. The SDK warns in development when a name isn't `snake_case`; silence it
	with `disableWarnings: true` at setup. Names in the reserved `gtm.*` namespace
	are ignored.
</Callout>

## Types [#types]

All types are available to be imported from: `@obelism/improve-sdk/types`

```ts
// `country` is ISO 3166-1 alpha-2 (e.g. 'NL') and only resolves server-side;
// the others come from the user agent. See the server getTestValue/getFlagValue
// `geo` argument and the Next.js middleware.
export type AudienceParamKey =
	'country' | 'pointer' | 'device' | 'browser' | 'os'

export type ImproveEnvironmentOption = 'develop' | 'staging' | 'production'

export type ImproveTestState = 'draft' | 'active' | 'finished' | 'archived'

export type ImproveSetupArgs = {
	organizationId: string
	environment: ImproveEnvironmentOption
	state?: ImproveTestState
	config?: ImproveConfiguration
	baseUrl?: string
	fetchTimeout?: number
	dataLayer?: boolean // client only, see /docs/integrations/gtm
	disableWarnings?: boolean // silence dev warnings (e.g. the snake_case nudge)
}

export type ImproveAnalyticPayload = {
	value?: number
	currency?: string
	params?: Record<string, unknown>
	dedupeKey?: string
}

export type ImproveFlagOption = {
	name: string
	slug: string
	value: string | undefined
	split: number
}

export type ImproveTestOption = {
	name: string
	slug: string
	value: string | undefined
	split: number
}

export type ImproveFlag = {
	id: string
	name: string
	audience: string
	options: ImproveFlagOption[]
}

export type ImproveFlags = {
	[flagSlug in string]: ImproveFlag
}

export type ImproveEventRef = {
	event: ImproveEventName
	scope: string
}

export type ImproveEvents = {
	start: ImproveEventRef
	metrics: ImproveEventRef[]
	conversion: ImproveEventRef
}

export type ImproveTest = {
	id: string
	name: string
	defaultValue: string
	audience: string
	allocation: number
	options: ImproveTestOption[]
	events: ImproveEvents
}

export type ImproveTests = {
	[testSlug in string]: ImproveTest
}

export type ImproveAudienceValue = { [Key in AudienceParamKey]: string }

export type ImproveAudience = {
	[audienceSlug in string]: ImproveAudienceValue
}

export type ImproveConfiguration = {
	name: string
	version: number
	flags: ImproveFlags
	tests: ImproveTests
	audience: ImproveAudience
}

export type ImproveResult = {
	result: {
		[variant: string]: {
			[metric: string]: number
		}
	}
	resultsByDay: {
		label: string
		values: { [x in string]: string | number }[]
	}[]
}
```
