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

# Node.js SDK

> Install the GrowthBook Node.js SDK to evaluate feature flags and run A/B tests in server-side JavaScript. Requires Node 18 or above.

We officially support Node 18 and above.

node

Node SDK Resources

Get help on Slack

## Installation

Install with a package manager

```bash npm2yarn theme={null}
npm install --save @growthbook/growthbook
```

## Quick Usage

First, create and instantiate a singleton GrowthBook client. You can do this as part of your server startup script.

```js theme={null}
const gbClient = new GrowthBookClient({
  apiHost: "https://cdn.growthbook.io",
  clientKey: "sdk-abc123"
});
await gbClient.init({timeout: 3000});
```

Then, you can evaluate a feature anywhere in your app by providing a feature key and a `userContext`.

```js theme={null}
// User context with attributes that feature flags can use for targeting
const userContext = {
  attributes: {
    id: "123",
  }
}

// Boolean on/off flags
if (gbClient.isOn("my-feature", userContext)) {
  console.log("My feature is on!");
}

// String, Number, or JSON flags
const value = gbClient.getFeatureValue("my-string-feature", "fallback", userContext);
console.log(value);
```

### Express Middleware

In Express, it can be helpful to add a user-scoped GrowthBook instance. This will let you define a `userContext` object once instead of passing it into every feature evaluation call throughout your app.

This can be accomplished easily with a middleware.

```js theme={null}
app.use((req, res, next) => {
  // Define your user context once
  const userContext = {
    attributes: {
      url: req.url,
      id: req.user.id
    }
  }

  // Create a scoped GrowthBook instance and store in req
  req.growthbook = gbClient.createScopedInstance(userContext);

  next();
});
```

Now, you can use this scoped instance from any route without needing to pass in `userContext` again.

```js theme={null}
app.get("/", (req, res) => {
  // No need to pass in userContext to a scoped instance
  req.growthbook.isOn("my-feature");
  req.growthbook.getFeatureValue("my-string-feature", "fallback");
});
```

Scoped instances were first introduced in SDK version 1.3.1.

## Plugins

GrowthBook comes with a number of built-in plugins that add additional functionality. You can enable these by passing them into the GrowthBook client's `createScopedInstance()` factory method or into the GrowthBook constructor.

Plugins require GrowthBook version 1.4.0 or higher, released in Feb 2025.

### GrowthBook Tracking

Sends experiment view, feature usage, and custom events to GrowthBook's [Managed Warehouse](https://docs.growthbook.io/app/managed-warehouse.md) or [Event Forwarder](https://docs.growthbook.io/app/event-forwarder.md).

Requires `@growthbook/growthbook` v1.7.0 or later.

```js theme={null}
import { GrowthBookClient } from "@growthbook/growthbook";
import { growthbookTrackingPlugin } from "@growthbook/growthbook/plugins";

const gb = new GrowthBookClient({
  clientKey: "YOUR_CLIENT_KEY",
  plugins: [
    growthbookTrackingPlugin({
      ingestorHost: "https://us-east-1.gb-ingest.com",
    }),
  ],
});
```

Log custom events with `logEvent`, passing the user context:

```js theme={null}
gb.logEvent("Sign Up", { accountPlan: "pro" }, userContext);
```

User-scoped instances (for example `req.growthbook` from the [Express middleware](https://docs.growthbook.io/lib/node.md#express-middleware)) don't need the user context:

```js theme={null}
req.growthbook.logEvent("Sign Up", { accountPlan: "pro" });
```

Set `enableFeatureUsageEvents: false` (v1.8.0+) to stop sending feature usage events.

### Tracing

The tracing plugin records every experiment assignment as a tag of the form `gb.exp:<experimentKey>=<variationKey>` so you can attach it to an LLM trace in Langfuse, Arize Phoenix, or any OpenTelemetry-based tool. GrowthBook's Langfuse and Phoenix data source integrations read the tags back out of the trace to compute exposures. See [Experimenting on LLM Features](https://docs.growthbook.io/integrations/llm-tracing.md) for the full setup and the plugin options in the [JavaScript SDK docs](https://docs.growthbook.io/lib/js.md#tracing).

Pass the plugin in the `GrowthBookClient` options. The shared client has no assignments of its own, so the plugin runs for each `createScopedInstance()` call and every scoped instance keeps its own tag set. Read the tags off the scoped instance with `getTracingTags()`.

```js theme={null}
import { GrowthBookClient } from "@growthbook/growthbook";
import { tracingPlugin, getTracingTags } from "@growthbook/growthbook/plugins";

const gbClient = new GrowthBookClient({
  apiHost: "https://cdn.growthbook.io",
  clientKey: "sdk-abc123",
  plugins: [tracingPlugin()],
});
await gbClient.init();

app.post("/chat", async (req, res) => {
  const scoped = gbClient.createScopedInstance({
    attributes: { user_id: req.user.id },
  });

  // Evaluate flags first so the tags exist before the trace starts
  const promptLabel = scoped.getFeatureValue("support-prompt", "production");
  const tags = getTracingTags(scoped); // e.g. ["gb.exp:support-prompt=prod-b"]

  // Hand `tags` (and req.user.id as the user id) to your tracing SDK
});
```

### DevTools for back-end

The [GrowthBook DevTools Browser Extension](https://docs.growthbook.io/tools/chrome-extension.md) now supports debugging back-end environments. This includes sending overrides (attribute, feature, and experiment) from the browser extension to the back end, and also displaying back-end logged events (feature evaluations, experiment exposure events).

We provide first-class devtools support via plugins and helper functions for a variety of Node.js-based environments, including:

* Next.js
* Express
* Node.js (platform-agnostic)

See the [Implementing DevTools guide](#implementing-devtools) below for more information.

### GrowthBook class (legacy)

The `GrowthBookClient` class was introduced in SDK version 1.3.0 to improve performance in back-end environments by up to 3x. It does this by re-using the core instance across many different user requests.

For backwards compatibility, we still support the old way of doing things with the `GrowthBook` class, where you would create a new instance for every incoming request.

```js theme={null}
// Example using Express
app.use(function(req, res, next) {
  // Create a GrowthBook instance and store in the request
  req.growthbook = new GrowthBook({
    apiHost: "https://cdn.growthbook.io",
    clientKey: "sdk-abc123",
    // Also include user/route specific settings
    attributes: {
      id: req.user.id
    }
  });

  // Clean up at the end of the request
  res.on('close', () => req.growthbook.destroy());

  // Wait for features to load (will be cached in-memory for future requests)
  req.growthbook.init({timeout: 1000}).then(() => next())
});
```

The `GrowthBook` class is identical to the one used in client-side environments. Check out the [Client-side JavaScript SDK docs](https://docs.growthbook.io/lib/js.md) for more info on all of the settings and methods available.

## Loading Features and Experiments

In order for the GrowthBook SDK to work, it needs to have feature and experiment definitions from the GrowthBook API. There are a few ways to get this data into the SDK.

### Built-in Fetching and Caching

If you pass an `apiHost` and `clientKey` into the `GrowthBookClient` constructor, it will handle the network requests, caching, retry logic, etc. for you automatically.

```ts theme={null}
const gbClient = new GrowthBookClient({
  apiHost: "https://cdn.growthbook.io",
  clientKey: "sdk-abc123",
});

// Wait for features to be downloaded with a timeout (in ms)
await gbClient.init({ timeout: 2000 });
```

#### Error Handling

In the case of network issues that prevent the features from downloading in time, the `init` call will not throw an error. Instead, it will stay in the default state where every feature evaluates to `null`.

You can still get access to the error if needed:

```ts theme={null}
const res = await gbClient.init({
  timeout: 1000
});

if (res.error) {
  throw res.error;
}
```

The return value of init has 3 properties:

* **success** - `true` if the GrowthBook instance was populated with features/experiments. Otherwise `false`
* **source** - Where this result came from. One of the following values: `network`, `cache`, `init`, `error`, or `timeout`
* **error** - If success is `false`, this will contain an `Error` object with more details about the error

#### Fallback Payload

To use a bundled payload only when features can't be downloaded, pass it to `setPayload` when `init` doesn't succeed:

```ts theme={null}
const res = await gbClient.init({ timeout: 2000 });
if (!res.success) {
  await gbClient.setPayload(fallbackPayload);
}
```

Don't pass the fallback to `init({ payload })` because that skips the network request entirely. A cached payload counts as a success, so the fallback is only used when there's nothing usable in the cache.

The fallback stays in place until a later request succeeds. If `init` timed out and `streaming` or `pollingInterval` is enabled, the SDK applies the original request when it finishes. After a network error, only `pollingInterval` retries.

### Custom Integration

If you prefer to handle the network and caching logic yourself, you can pass in a full JSON "payload" directly into the SDK. For example, you might store features in Postgres or Redis.

```ts theme={null}
await gbClient.init({
  payload: {
    features: {
      "feature-1": {...},
      "feature-2": {...},
      "another-feature": {...},
    }
  }
})
```

The data structure for "payload" is exactly the same as what is returned by the GrowthBook SDK endpoints and webhooks.

Note: you don't need to specify `clientKey` or `apiHost` on your GrowthBook instance since no network requests are being made in this case.

#### Synchronous Init

There is an alternate synchronous version of init named `initSync`, which can be useful in some environments. There are some restrictions/differences:

* You MUST pass in `payload`
* The `payload` MUST NOT have encrypted features or experiments
* The return value is the GrowthBook instance to enable easy method chaining
* If using sticky bucketing, you should use an instance of `StickyBucketServiceSync`, such as `ExpressCookieStickyBucketService`.

```ts theme={null}
const gbClient = new GrowthBookClient().initSync({
  payload: {
    features: {...}
  }
});
```

## Refreshing Features

By default, the GrowthBookClient will only fetch features once during initialization. This works great for short-running processes (e.g. serverless functions).

For long-running Node.js processes, there are 2 main approaches to keeping feature definitions up-to-date.

1. Streaming
2. Polling

> [!NOTE]
>
> **Streaming vs cache refresh vs `backgroundSync`**
>
> These are easy to mix up:
>
> * **Streaming**: GrowthBook (Cloud or Proxy) pushes a notification when features change. On Node, set up an `EventSource` polyfill and use `init({ streaming: true })` so this client subscribes to those updates (below).
> * **Cache refresh**: the SDK refetches the features payload (for example by calling `refreshFeatures()`). Cache settings like `staleTTL` only apply when a fetch already runs; they do **not** start fetches on their own.
> * **`backgroundSync`**: a kill switch for background updates (default `true`). Setting it to `false` disables both streaming and `pollingInterval`. (Setting `cacheSettings: { backgroundSync: true }` alone does **not** enable auto-refresh on Node.)
>
> To keep features current, use `streaming: true`, `pollingInterval`, or your own `refreshFeatures()` timer. Without one of these, the process keeps the payload from `init` until it restarts.

### Streaming Updates

The GrowthBook SDK supports streaming with Server-Sent Events (SSE). When enabled, changes to features within GrowthBook will be streamed to the SDK in realtime as they are published. This is only supported on GrowthBook Cloud or if running a GrowthBook Proxy Server.

Node.js does not natively support SSE, but there is a small library you can install:

```bash npm2yarn theme={null}
npm install --save eventsource
```

Then, configure the polyfill and enable streaming during init:

```js theme={null}
const { setPolyfills } = require("@growthbook/growthbook");
// Note the destructuring - `eventsource` v3+ uses a named export
const { EventSource } = require("eventsource");

// Configure GrowthBook to use the eventsource library
setPolyfills({ EventSource });

const gbClient = new GrowthBookClient({
  clientKey: "sdk-abc123"
});

// Enable streaming in the init call
await gbClient.init({ streaming: true });
```

> [!WARNING]
>
> Which import form you need depends on the `eventsource` version you have installed. The snippet above is for v3 and up, which is what a fresh `npm install eventsource` gives you:
>
> | `eventsource` | Import |
> | - | - |
> | v3 and up (v3 needs Node 18+, v4 needs Node 20+) | `const { EventSource } = require("eventsource")` |
> | v2 and below | `const EventSource = require("eventsource")` |
>
> v3.0.0 moved the constructor from the default export to a named export. Using the wrong form for your version passes the module object rather than the constructor, which prevents streaming from starting — and the two forms fail in opposite directions, so match the form to the version you actually have rather than copying whichever one you saw first.
>
> If streaming is enabled but features never update, check this before anything else — the SDK logs a warning naming the reason whenever it detects that streaming could not be started.

This will make an initial network request to download the features payload from the GrowthBook API. Then, it will open a streaming connection to listen to updates.

Every call to evaluate feature flags will use the latest available payload.

To stop all background updates (streaming and polling), call `configureCache({ backgroundSync: false })`. This also stops any refresh that's already running. `GrowthBookClient` does not take a `backgroundSync` constructor option; the setting only lives on the cache config. To turn off streaming but keep polling, leave out `streaming: true` and pass only `pollingInterval`.

### Polling Updates

If your environment doesn't support streaming updates or you want a simpler option, pass a `pollingInterval` (in milliseconds) into `init`. This requires no polyfills.

```js theme={null}
const gbClient = new GrowthBookClient({
  clientKey: "sdk-abc123"
});

// Refresh once per minute
await gbClient.init({ pollingInterval: 60 * 1000 });
```

Polling is deduped per `clientKey`, so many instances sharing a key produce a single request per interval. Requests bypass the cache, so the interval you set is the interval used. Failed requests back off exponentially (capped at 5 minutes), and the timer never keeps a short-lived process alive.

`pollingInterval` is ignored while streaming is active, so it is safe to set both — polling only runs when the stream isn't connected.

We don't recommend polling more often than once per minute. If you need faster updates, use Streaming (above).

You can also drive refreshes yourself instead:

```js theme={null}
setInterval(() => gbClient.refreshFeatures(), 5*60*1000);
```

Note that `refreshFeatures()` goes through the cache, so it will not issue a request if the payload is newer than `staleTTL` (60 seconds by default). Pass `skipCache: true` to force one.

## Caching

The JavaScript SDK has 2 caching layers:

1. In-memory cache (enabled by default)
2. Persistent localStorage cache (disabled by default, requires configuration)

Caching controls how a features payload is stored and reused **when the SDK fetches**. It is not a substitute for [streaming or polling](#refreshing-features) in long-running Node processes.

### Configuring Local Storage

Here is an example of using Redis as your persistent localStorage cache:

```js theme={null}
const { setPolyfills } = require("@growthbook/growthbook");

setPolyfills({
  localStorage: {
    // Example using Redis
    getItem: (key) => redisClient.get(key),
    setItem: (key, value) => redisClient.set(key, value),
  }
});
```

### Cache Settings

There are a number of cache settings you can configure within GrowthBook.

Below are all of the default values. You can call `configureCache` with a subset of these fields and the rest will keep their default values.

```ts theme={null}
import { configureCache } from "@growthbook/growthbook";

configureCache({
  // The localStorage key the cache will be stored under
  cacheKey: "gbFeaturesCache",
  // Consider features stale after this much time (60 seconds default)
  staleTTL: 1000 * 60,
  // Cached features older than this will be ignored (4 hours default)
  maxAge: 1000 * 60 * 60 * 4,
  // Set to `true` to completely disable both in-memory and persistent caching
  disableCache: false,
  // Set to `false` to disable streaming and polling (default true).
  // `true` does not enable either one. See Refreshing Features.
  backgroundSync: true,
})
```

### Skip Cache

The cache layers apply to both the `init` and `refreshFeatures` methods. Both of these accept a `skipCache: true` option to bypass the cache layers if desired.

## Experimentation (A/B Testing)

In order to run A/B tests, you need to set up a tracking callback function. This is called every time a user is put into an experiment and can be used to track the exposure event in your analytics system (Segment, Mixpanel, GA, etc.).

You can specify this globally in your GrowthBookClient class and/or per-request in your user context.

We recommend using a global callback if you plan to track events directly from Node and using a user context callback if you plan to send events to the front-end to fire.

Starting in version 1.7.0, tracking callbacks receive a third `userContext` argument: a `TrackingUserContext` containing the `attributes` and optional `url` used when the experiment was evaluated. We recommend recording attributes from this argument so exposure events consistently reflect the attributes GrowthBook used for targeting and assignment.

```js theme={null}
// Callback configured globally
const gbClient = new GrowthBookClient({
  trackingCallback: (experiment, result, userContext) => {
    const userId = userContext.attributes.id;

    console.log("Viewed Experiment", userId, {
      experimentId: experiment.key,
      variationId: result.key,
      attributes: userContext.attributes,
    });
  }
});

// Callback configured in the user context
const userContext = {
  attributes: {
    id: req.user.id
  },
  trackingCallback: (experiment, result, userContext) => {
    console.log("Viewed Experiment", userContext.attributes.id, {
      experimentId: experiment.key,
      variationId: result.key,
      attributes: userContext.attributes,
    });
  }
}
```

### Feature Flag Experiments

Evaluating the feature is what enrolls a user and fires `trackingCallback`. See [Feature Flag Experiments](https://docs.growthbook.io/features/feature-flag-experiments.md).

```js theme={null}
// If this has an active experiment and the user is included,
// it will call trackingCallback automatically
const newLogin = gbClient.isOn("new-signup-form", userContext);
```

If the experiment came from a feature rule, `result.featureId` in the trackingCallback will contain the feature id, which may be useful for tracking/logging purposes.

### Deferred Tracking

Sometimes, you aren't able to track analytics events from Node.js and you need to do it from the front-end instead.

In this case, define a trackingCallback in the user context, queue up events, and serialize it in your response. Here's an example:

```js theme={null}
// Middleware
app.use((req, res, next) => {
  // Queue up tracking calls and store in the request
  req.trackingData = [];
  const userContext = {
    attributes: {id: "123"},
    trackingCallback: (experiment, result, userContext) => {
      req.trackingData.push({experiment, result, user: userContext});
    }
  }
  req.growthbook = gbClient.createScopedInstance(userContext);
  next();
});

// Serialize and include in your response
app.get("/", (req, res) => {
  res.send(`<html>
  <head>
    <script>
      (function() {
        const data = ${JSON.stringify(req.trackingData)};

        data.forEach(({experiment, result, user}) => {
          // Example using Segment.io
          analytics.track("Experiment Viewed", {
            experimentId: experiment.key,
            variationId: result.key,
            attributes: user.attributes,
          });
        })
      })();
    </script>
    ...
  `);
})
```

If you have also integrated the GrowthBook SDK on your front-end, there's a helper method you can use instead of manually firing tracking calls.

```ts theme={null}
gb.setDeferredTrackingCalls(data);
gb.fireDeferredTrackingCalls();
```

This will use the `trackingCallback` configured on your front-end GrowthBook instance.

### Sticky Bucketing

Sticky bucketing ensures that users see the same experiment variant, even when user session, user login status, or experiment parameters change. See the [Sticky Bucketing docs](https://docs.growthbook.io/app/sticky-bucketing.md) for more information. If your organization and experiment support sticky bucketing, you must implement an instance of the `StickyBucketService` to use Sticky Bucketing. The JS SDK exports several implementations of this service for common use cases, or you may build your own:

* `ExpressCookieStickyBucketService` — For NodeJS/Express controller-level bucket persistence using browser cookies; intended to be interoperable with `BrowserCookieStickyBucketService`. Assumes `cookie-parser` is implemented (can be polyfilled). Cookie attributes can also be configured. The default cookie expiry is 180 days; override by passing `maxAge: {ms}` into the constructor's `cookieAttributes`.

* `RedisStickyBucketService` — For NodeJS Redis-based bucket persistence. Requires an `ioredis` Redis client instance to be passed in.

* Build your own — Implement the abstract `StickyBucketService` class and connect to your own data store, or custom wrap multiple service implementations (ex: read/write to both cookies and Redis).

Implementing most StickyBucketService implementations is straightforward and works with minimal setup. For instance, to use the `ExpressCookieStickyBucketService`:

```js theme={null}
const { ExpressCookieStickyBucketService } = require("@growthbook/growthbook");

app.use(async (req, res, next) => {
  const stickyBucketService = new ExpressCookieStickyBucketService({
      req,
      res
  });
  const userContext = await gbClient.applyStickyBuckets({
    attributes: {
      id: req.user.id
    }
  }, stickyBucketService);

  req.growthbook = gbClient.createScopedInstance(userContext);

  next();
});
```

## TypeScript

When used in a TypeScript project, GrowthBook includes basic type inference out of the box:

```ts theme={null}
// Type will be `string` based on the fallback provided ("blue")
const color = gbClient.getFeatureValue("button-color", "blue", userContext);

// You can manually specify types as well
// feature.value will be type `number`
const feature = gbClient.evalFeature<number>("font-size", userContext);
console.log(feature.value);
```

### Strict Typing

If you want to enforce stricter types in your application, you can do that when creating the GrowthBook instance:

```ts theme={null}
// Define all your feature flags and types here
interface AppFeatures {
  "button-color": string;
  "font-size": number;
  "newForm": boolean;
}

// Pass into the GrowthBook instance
const gbClient = new GrowthBookClient<AppFeatures>({
  ...
});
```

Now, all feature flag methods will be strictly typed.

```ts theme={null}
// feature.value will be type `number`
const feature = gbClient.evalFeature("font-size", userContext);
console.log(feature.value);

// Typos will cause compile-time errors
gbClient.isOn("buton-color", userContext); // "buton" instead of "button"
```

Instead of defining the `AppFeatures` interface manually like above, you can auto-generate it from your GrowthBook account using the [GrowthBook CLI](https://docs.growthbook.io/tools/cli.md).

## Updating

As a general philosophy, we aim to keep the SDK 100% backwards compatible at all times. View the [Changelog](https://github.com/growthbook/growthbook/blob/main/packages/sdk-js/CHANGELOG.md) for a complete list of all SDK changes.

## GrowthBook Instance (reference)

### Attributes

You can specify attributes about the current user and request. These are used for two things:

1. Feature targeting (e.g. paid users get one value, free users get another)
2. Assigning persistent variations in A/B tests (e.g. user id "123" always gets variation B)

The following are some commonly used attributes, but use whatever makes sense for your application.

```ts theme={null}
const userContext = {
  attributes: {
    id: "123",
    loggedIn: true,
    deviceId: "abc123def456",
    company: "acme",
    paid: false,
    url: "/pricing",
    browser: "chrome",
    mobile: false,
    country: "US",
  },
};
```

#### Global Attributes

Sometimes there are global attributes that apply to all users. For example, the ip of your server. These can be specified on the GrowthBookClient instance and will be merged with any attributes in the user context.

```js theme={null}
const gbClient = new GrowthBookClient({
  globalAttributes: {
    serverIp: "10.1.1.1"
  }
})
```

#### Secure Attributes

When *secure attribute hashing* is enabled, all targeting conditions in the SDK payload referencing attributes with datatype `secureString` or `secureString[]` will be anonymized via SHA-256 hashing. This allows you to safely target users based on sensitive attributes. You must enable this feature in your SDK Connection for it to take effect.

If your SDK Connection has secure attribute hashing enabled, you will need to manually hash any `secureString` or `secureString[]` attributes that you pass into the GrowthBook SDK.

To hash an attribute, use a cryptographic library with SHA-256 support, and compute the SHA-256 hashed value of your attribute *plus* your organization's secure attribute salt.

First, define a sha256 function using Node's built-in crypto support.

```js theme={null}
function sha256(str) {
  return crypto.createHash("sha256").update(str).digest("hex");
}
```

Then, you can use this when setting attributes.

```js theme={null}
const salt = "f09jq3fij"; // Your organization's secure attribute salt (see Organization Settings)

// hashing a secureString attribute
const userEmail = sha256(salt + user.email);

// hashing a secureString[] attribute
const userTags = user.tags.map(tag => sha256(salt + tag));

const userContext = {
  attributes: {
    id: user.id,
    loggedIn: true,
    email: userEmail,
    tags: userTags,
  }
}
```

### Feature Usage Callback

GrowthBook can fire a callback whenever a feature is evaluated for a user. This can be useful to update 3rd party tools like New Relic or Datadog.

Like with the `trackingCallback`, this can be defined on either the GrowthBookClient instance or as part of the user context. When defined on the GrowthBookClient instance, a 3rd argument with the user context will be sent so you can identify which user evaluated a feature.

```js theme={null}
// Callback configured globally
const gbClient = new GrowthBookClient({
  onFeatureUsage: (featureKey, result, userContext) => {
    console.log(`${featureKey}=${result.value} for user ${userContext.attributes.id}`);
  }
});

// Callback configured on the userContext
const userContext ={
  onFeatureUsage: (featureKey, result) => {
    console.log(`${featureKey}=${result.value}`);
  },
};
```

The `result` argument is the same thing returned from `evalFeature`.

Note: If you evaluate the same feature multiple times (and the value doesn't change), the callback will only be fired the first time.

### evalFeature

In addition to the `isOn` and `getFeatureValue` helper methods, there is the `evalFeature` method that gives you more detailed information about why the value was assigned to the user.

```ts theme={null}
// Get detailed information about the feature evaluation
const result = gbClient.evalFeature("my-feature", userContext);

// The value of the feature (or `null` if not defined)
console.log(result.value);

// Why the value was assigned to the user
// One of: `override`, `unknownFeature`, `defaultValue`, `force`, or `experiment`
console.log(result.source);

// The string id of the rule (if any) which was used
console.log(result.ruleId);

// Information about the experiment (if any) which was used
console.log(result.experiment);

// The result of the experiment (or `undefined`)
console.log(result.experimentResult);
```

### Inline Experiments

Instead of declaring all features up-front in the payload and referencing them by ids in your code, you can also just run an experiment directly. This is done with the `runInlineExperiment` method:

```js theme={null}
// These are the only required options
const { value } = gbClient.runInlineExperiment({
  key: "my-experiment",
  variations: ["red", "blue", "green"],
}, userContext);
```

#### Customizing the Traffic Split

By default, this will include all traffic and do an even split between all variations. There are 2 ways to customize this behavior:

```js theme={null}
// Option 1: Using weights and coverage
gbClient.runInlineExperiment({
  key: "my-experiment",
  variations: ["red", "blue", "green"],
  // Only include 10% of traffic
  coverage: 0.1,
  // Split the included traffic 50/25/25 instead of the default 33/33/33
  weights: [0.5, 0.25, 0.25],
}, userContext);

// Option 2: Specifying ranges
gbClient.runInlineExperiment({
  key: "my-experiment",
  variations: ["red", "blue", "green"],
  // Identical to the above
  // 5% of traffic in A, 2.5% each in B and C
  ranges: [
    [0, 0.05],
    [0.5, 0.525],
    [0.75, 0.775],
  ],
}, userContext);
```

#### Hashing

We use deterministic hashing to assign a variation to a user. We hash together the user's id and experiment key, which produces a number between `0` and `1`. Each variation is assigned a range of numbers, and whichever one the user's hash value falls into will be assigned.

You can customize this hashing behavior:

```js theme={null}
gbClient.runInlineExperiment({
  key: "my-experiment",
  variations: ["A", "B"],

  // Which hashing algorithm to use
  // Version 2 is the latest and the one we recommend
  hashVersion: 2,

  // Use a different seed instead of the experiment key
  seed: "abcdef123456",

  // Use a different user attribute (default is `id`)
  hashAttribute: "device_id",
}, userContext);
```

**Note**: For backwards compatibility, if no `hashVersion` is specified, it will fall back to using version `1`, which is deprecated. In the future, version `2` will become the default. We recommend specifying version `2` now for all new experiments to avoid migration issues down the line.

#### Meta Info

You can also define meta info for the experiment and/or variations. These do not affect the behavior, but they are passed through to the `trackingCallback`, so they can be used to annotate events.

```js theme={null}
gbClient.runInlineExperiment({
  key: "results-per-page",
  variations: [10, 20],

  // Experiment meta info
  name: "Results per Page",
  phase: "full-traffic",

  // Variation meta info
  meta: [
    {
      key: "control",
      name: "10 Results per Page",
    },
    {
      key: "variation",
      name: "20 Results per Page",
    },
  ]
}, userContext)
```

#### Mutual Exclusion

Sometimes you want to run multiple conflicting experiments at the same time. You can use the `filters` setting to run mutually exclusive experiments.

We do this using deterministic hashing to assign users a value between 0 and 1 for each filter.

```js theme={null}
// Will include 60% of users - ones with a hash between 0 and 0.6
gbClient.runInlineExperiment({
  key: "experiment-1",
  variations: [0, 1],
  filters: [
    {
      seed: "pricing",
      attribute: "id",
      ranges: [[0, 0.6]]
    }
  ]
}, userContext);

// Will include the other 40% of users - ones with a hash between 0.6 and 1
gbClient.runInlineExperiment({
  key: "experiment-2",
  variations: [0, 1],
  filters: [
    {
      seed: "pricing",
      attribute: "id",
      ranges: [[0.6, 1.0]]
    }
  ]
}, userContext);
```

#### Holdout Groups

To use global holdout groups, use a nested experiment design:

```js theme={null}
// The value will be `true` if in the holdout group, otherwise `false`
const holdout = gbClient.runInlineExperiment({
  key: "holdout",
  variations: [true, false],
  // 10% of users in the holdout group
  weights: [0.1, 0.9]
}, userContext);

// Only run your main experiment if the user is NOT in the holdout
if (!holdout.value) {
  const res = gbClient.runInlineExperiment({
    key: "my-experiment",
    variations: ["A", "B"]
  }, userContext)
}
```

#### Targeting Conditions

You can also define targeting conditions that limit which users are included in the experiment. These conditions are evaluated against the `attributes` passed into the user context. The syntax for conditions is based on the MongoDB query syntax and is straightforward to read and write.

For example, if the attributes are:

```json theme={null}
{
  "id": "123",
  "browser": {
    "vendor": "firefox",
    "version": 94
  },
  "country": "CA"
}
```

The following condition would evaluate to `true` and the user would be included in the experiment:

```js theme={null}
gbClient.runInlineExperiment({
  key: "my-experiment",
  variations: [0, 1],
  condition: {
    "browser.vendor": "firefox",
    "country": {
      "$in": ["US", "CA", "IN"]
    }
  }
}, userContext)
```

#### Inline Experiment Return Value

A call to `runInlineExperiment` returns an object with a few useful properties:

```ts theme={null}
const {
  value,
  key,
  name,
  variationId,
  inExperiment,
  hashUsed,
  hashAttribute,
  hashValue,
} = gbClient.runInlineExperiment({
  key: "my-experiment",
  variations: ["A", "B"],
}, userContext);

// If user is included in the experiment
console.log(inExperiment); // true or false

// The index of the assigned variation
console.log(variationId); // 0 or 1

// The value of the assigned variation
console.log(value); // "A" or "B"

// The key and name of the assigned variation (if specified in `meta`)
console.log(key); // "0" or "1"
console.log(name); // ""

// If the variation was randomly assigned by hashing
console.log(hashUsed); // true or false

// The user attribute that was hashed
console.log(hashAttribute); // "id"

// The value of that attribute
console.log(hashValue); // e.g. "123"
```

The `inExperiment` flag will be false if the user was excluded from being part of the experiment for any reason (e.g. failed targeting conditions).

The `hashUsed` flag will only be true if the user was randomly assigned a variation. If the user was forced into a specific variation instead, this flag will be false.

## Implementing DevTools

The DevTools plugins are designed to work with both GrowthBookClient and GrowthBook (legacy) SDK instances. Your SDK must be in dev mode (`enableDevMode: true`) for the plugin to work.

### Helper Functions

The framework-specific sections below (Next.js, Express, and platform-agnostic Node.js) all rely on the same two helper functions, exported from `@growthbook/growthbook/plugins`, to send evaluation logs back to the DevTools extension.

#### `getDebugScriptContents(gb, source?)`

Returns a `<script>` body that pushes the SDK's current dev-mode logs (feature evaluations, experiment exposures, decrypted payload, and attributes) onto `window._gbdebugEvents`. The GrowthBook DevTools extension reads this array from the page to display back-end evaluation results. This is the only contract between your server and the extension for back-end logging.

```ts theme={null}
import { getDebugScriptContents } from "@growthbook/growthbook/plugins";

const scriptBody = getDebugScriptContents(growthbook, "my-service");
// scriptBody === '(window._gbdebugEvents = (window._gbdebugEvents || [])).push({...});'
```

Notes:

* Returns an **empty string** (not `null`/`undefined`) when `enableDevMode` is off, or when the instance has no evaluation logs yet. Always check for a non-empty result before injecting a `<script>` tag, to avoid emitting an empty `<script></script>`.
* It snapshots `gb.logs` **at call time**. Call it *after* you've finished evaluating every feature/experiment you want visible in DevTools for that request, not before.
* `source` is a free-text label (e.g. `"nextjs"`, `"express"`, your service name) shown in the DevTools UI to identify which back-end emitted the event. It has no effect on behavior.
* The returned string is raw JavaScript, not HTML. Wrap it in your own `<script>` tags:

```ts theme={null}
const debugScript = getDebugScriptContents(growthbook, "my-service");
if (debugScript) {
  responseHtml += `<script>${debugScript}</script>`;
}
```

* If your platform caches or shares rendered responses across requests/users (e.g. an edge/CDN worker), make sure this injection happens **after** any cache read/write step, not before. Otherwise one visitor's debug logs can end up baked into a response served to other visitors.

#### `getDebugEvent(gb, source?)`

The lower-level function `getDebugScriptContents` wraps. It returns the raw `LogEvent` object (`{ logs, sdkInfo }`) instead of a serialized script string. Use this directly if you want to transport the event yourself (e.g. via a response header, as shown in the Next.js middleware example below) rather than embedding it as an inline script.

### Next.js

#### RSCs and SSR

For RSCs (React server components) or SSR (server-side rendering), in order to apply SDK overrides from DevTools, pass the readonly request cookies and search params into the `devtoolsNextjsPlugin` plugin. In Next 15+, these may need to be awaited.

```js theme={null}
import { cookies } from "next/headers";
import { devtoolsNextjsPlugin } from "@growthbook/growthbook/plugins";

export default async function Home({ searchParams }) {
  const requestCookies = await cookies();
  const awaitedSearchParams = await searchParams;

  const growthbook = new GrowthBook({
    apiHost: "https://cdn.growthbook.io",
    clientKey: "sdk-abc123",
    enableDevMode: true,
    plugins: [
      devtoolsNextjsPlugin({ searchParams: awaitedSearchParams, requestCookies })
    ],
  });

  return (
    <main>
      <h1>Hello World</h1>
      <Widget growthbook={growthbook} />
      <DevtoolsLogger growthbook={growthbook} />
    </main>
  );
}
```

Note the component named `<DevtoolsLogger>` which is used to hydrate SDK information and log events back into the DevTools extension. When using this component, DevTools will automatically ingest any data generated by the `getDebugScriptContents()` method. You may implement it like this:

```js theme={null}
import { getDebugScriptContents } from "@growthbook/growthbook/plugins";

export function DevtoolsLogger({ growthbook }) {
  const source = "nextjs";
  return (
    <script dangerouslySetInnerHTML={{ __html: getDebugScriptContents(growthbook, source) }} />
  );
}
```

#### Middleware and API routes

For middleware implementations or API routes, pass the `request` object to `devtoolsNextjsPlugin` instead of searchParams and requestCookies. A middleware example is below; it can be modified to be an API route:

```js theme={null}
import { NextResponse } from 'next/server';
import { GrowthBook } from "@growthbook/growthbook";
import { devtoolsNextjsPlugin, getDebugEvent } from "@growthbook/growthbook/plugins";

export function middleware(request) {
  const response = NextResponse.next();

  const growthbook = new GrowthBook({
    apiHost: "https://cdn.growthbook.io",
    clientKey: "sdk-abc123",
    enableDevMode: true,
    plugins: [
      devtoolsNextjsPlugin({ request })
    ],
  });

  // etc...

  response.headers.set(
    "x-gbdebug-events",
    JSON.stringify(getDebugEvent(growthbook, "nextjs_middleware"))
  );
  return response;
}
```

For passing the debug events back to DevTools, this middleware example assumes the receiving end has access to headers. This may not always be the case (e.g. redirects); you may need to rely on cookies or an encoded URL parameter.

For API routes or middleware redirects, you are responsible for parsing the `x-gbdebug-events` header value on the front-end and pushing it to `window._gbdebugEvents`. You may wish to import the `getDebugScriptContents()` method on the front-end to ingest these debug events. An example integration may look like this:

```js theme={null}
const response = await fetch(apiUrl);
const event = response.headers.get("x-gbdebug-events");
if (event) {
  window._gbdebugEvents?.push(JSON.parse(event));
}
```

### Express

In an Express.js context, in order to apply SDK overrides from DevTools, pass the request object into the `devtoolsExpressPlugin` plugin. You must use the standard Express **cookie-parser** middleware (npm: `cookie-parser`) in order to use this plugin.

```js theme={null}
import {
  devtoolsExpressPlugin,
  getDebugEvent,
} from "@growthbook/growthbook/plugins";

app.use((req, res, next) => {
  const userContext = {
    attributes: {
      url: req.url,
      id: req.user.id
    },
    enableDevMode: true,
  };

  // Create a scoped GrowthBook instance and store in req
  req.growthbook = gbClient.createScopedInstance(
    userContext,
    [ devtoolsExpressPlugin({ request: req }) ]
  );

  res.set("x-gbdebug-events", JSON.stringify(getDebugEvent(req.growthbook, "express")));

  // etc...
  next();
});
```

Similar to the Next.js middleware example above, you are responsible for parsing the `x-gbdebug-events` header value on the front-end and pushing it to `window._gbdebugEvents`. You may use `getDebugScriptContents()` on the front-end to help.

### Node.js (platform agnostic)

We provide a simple `devtoolsPlugin` for other Node.js back-end implementations. This plugin is a stub for the other platform-specific plugins (Next.js, Express); however, it may be used directly.

When using this plugin, you are responsible for building the `devtoolsState` object by parsing the `_gbdebug` cookie and the `_gbdebug` URL query parameter. `devtoolsState` represents the overrides applied to attributes, features, and experiments; it takes the form:

```ts theme={null}
type DevtoolsState = {
  attributes?: Record<string, any>;
  features?: Record<string, any>;
  experiments?: Record<string, number>;
};
```

Note: unlike the query param (already decoded by most URL parsers), the `_gbdebug` **cookie** value is percent-encoded. Decode it before parsing, or overrides will silently fail to apply:

```ts theme={null}
import { devtoolsPlugin } from "@growthbook/growthbook/plugins";

function getDevtoolsState(request): DevtoolsState {
  try {
    const raw =
      new URL(request.url, "http://localhost").searchParams.get("_gbdebug") ??
      decodeURIComponent(getCookie(request, "_gbdebug") ?? "");
    return raw ? JSON.parse(raw) : {};
  } catch {
    return {};
  }
}

const growthbook = new GrowthBook({
  apiHost: "https://cdn.growthbook.io",
  clientKey: "sdk-abc123",
  enableDevMode: true,
  plugins: [devtoolsPlugin(getDevtoolsState(request))],
});
```

Similar to the Express and Next.js middleware examples above, you can generate your own `_gbdebugEvents` payload using `getDebugEvent()`.

## Examples

* [TypeScript example app with strict typing ](https://github.com/growthbook/examples/tree/main/vanilla-typescript).

## Supported Features
