Javascript
Supports both browser and NodeJS environments.
Installation
Install with a package manager
- npm
- Yarn
- pnpm
npm install --save @growthbook/growthbook
yarn add @growthbook/growthbook
pnpm add @growthbook/growthbook
Quick Usage
Step 1: Configure your app
import { GrowthBook } from "@growthbook/growthbook";
// Create a GrowthBook instance
const gb = new GrowthBook({
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
// Targeting attributes
attributes: {
id: "123",
country: "US"
},
// Only required for A/B testing
// Called every time a user is put into an experiment
trackingCallback: (experiment, result) => {
console.log("Experiment Viewed", {
experimentId: experiment.key,
variationId: result.key,
});
},
});
// Download features and experiments from the CDN
// Also, start running any Visual Editor or URL Redirect experiments
await gb.init();
Step 2: Start Feature Flagging!
There are 2 main methods for evaluating features: isOn
and getFeatureValue
:
// Simple boolean (on/off) feature flag
if (gb.isOn("my-feature")) {
console.log("Feature enabled!");
}
// Get the value of a string/JSON/number feature with a fallback
const color = gb.getFeatureValue("button-color", "blue");
OpenFeature Provider
If you are using OpenFeature, we have created a GrowthBook Provider that you can use client-side.
Simply import our package and set GrowthbookClientProvider
as the provider for your OpenFeature client.
Similarly to using our SDK directly, you'll want to pass in GrowthBook Context
into the new provider instance as well as any InitOptions
.
import { GrowthBook, Context, InitOptions } from '@growthbook/growthbook';
import { GrowthbookClientProvider } from '@openfeature/growthbook-client-provider';
/*
* Configure your GrowthBook instance with GrowthBook context
* @see https://docs.growthbook.io/lib/js#step-1-configure-your-app
*/
const gbContext: Context = {
apiHost: 'https://cdn.growthbook.io',
clientKey: 'sdk-abc123',
// Only required if you have feature encryption enabled in GrowthBook
decryptionKey: 'key_abc123',
};
/*
* optional init options
* @see https://docs.growthbook.io/lib/js#switching-to-init
*/
const initOptions: InitOptions = {
timeout: 2000,
streaming: true,
};
OpenFeature.setProvider(new GrowthbookClientProvider(gbContext, initOptions));
Node.js
The GrowthBook SDK officially supports Node v18 and above.
In browser environments, you typically want a single global GrowthBook instance.
In server environments, you instead want a separate GrowthBook instance for every incoming request. Here's an example middleware you can use:
// 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"
});
// 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())
});
Then, you can access the GrowthBook instance from any route:
app.get("/", (req, res) => {
const gb = req.growthbook;
// ...
})
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 GrowthBook constructor, it will handle the network requests, caching, retry logic, etc. for you automatically. If your feature payload is encrypted, you can also pass in a decryptionKey
.
const gb = new GrowthBook({
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
// Only required if you have feature encryption enabled in GrowthBook
decryptionKey: "key_abc123",
});
// Wait for features to be downloaded with a timeout (in ms)
await gb.init({
timeout: 2000,
});
Until features are loaded, all features will evaluate to null
. If you're ok with a potential flicker in your application (features going from null
to their real value), you can call init
without awaiting the result.
If you want to refresh the features at any time (e.g. when a navigation event occurs), you can call gb.refreshFeatures()
.
Error Handling
In the case of network issues, 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:
const res = await gb.init({
timeout: 1000
});
console.log(res);
The return value has 3 properties:
- status -
true
if the GrowthBook instance was populated with features/experiments. Otherwisefalse
- source - Where this result came from. One of the following values:
network
,cache
,init
,error
, ortimeout
- error - If status is
false
, this will contain anError
object with more details about the error
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 and send it down to your front-end as part of your app's initial bootstrap API call.
await gb.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.
You can update the payload at any time by calling setPayload(newPayloadJSON)
and there are also getPayload()
and getDecryptedPayload()
methods, which are useful in hybrid apps where you want to hydrate the client with data from the server.
Note: you don't need to specify clientKey
or apiHost
on your GrowthBook instance unless you want to enable streaming (see below) or call refreshFeatures()
later.
Synchronous Init
There is a 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 - If you use sticky bucketing, you MUST pass
stickyBucketAssignmentDocs
into your GrowthBook constructor - The return value is the GrowthBook instance to enable easy method chaining
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.
Streaming in Browser Environments
SSE is supported on all major browsers, so enabling streaming is as easy as passing streaming: true
into your init
call:
gb.init({
streaming: true,
// Other settings...
})
You may also differentiate your streaming host URL from your API host by setting the streamingHost
property in the GrowthBook constructor (ex: Remote Evaluation is done on a CDN edge worker while Streaming is done through a GrowthBook Proxy server).
Streaming in Node.js
Node.js does not natively support SSE, but there is a small library you can install:
- npm
- Yarn
- pnpm
npm install --save eventsource
yarn add eventsource
pnpm add eventsource
Instead of enabling streaming separately for every GrowthBook instance, we recommend opening a single shared stream at app startup instead:
const { setPolyfills, prefetchPayload } = require("@growthbook/growthbook");
// Configure GrowthBook to use the eventsource library
setPolyfills({
EventSource: require("eventsource"),
});
// Start a streaming connection
prefetchPayload({
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
streaming: true
}).then(() => console.log("Streaming connection open!"))
This will work as long as you use the exact same apiHost
and clientKey
when creating GrowthBook instances in your middleware.
Remote Evaluation
When used in a front-end context, the JS SDK may be run in Remote Evaluation mode. This mode brings the security benefits of a backend SDK to the front end by evaluating feature flags exclusively on a private server. Using Remote Evaluation ensures that any sensitive information within targeting rules or unused feature variations are never seen by the client. Note that Remote Evaluation should not be used in a backend context.
You must enable Remote Evaluation in your SDK Connection settings. Cloud customers are also required to self-host a GrowthBook Proxy Server or custom remote evaluation backend.
To use Remote Evaluation, add the remoteEval: true
property to your SDK instance. A new evaluation API call will be made any time a user attribute or other dependency changes. You may optionally limit these API calls to specific attribute changes by setting the cacheKeyAttributes
property (an array of attribute names that, when changed, trigger a new evaluation call).
const gb = new GrowthBook({
apiHost: "https://gb-proxy.mydomain.io/",
clientKey: "sdk-abc123",
// Enable remote evaluation
remoteEval: true,
// Optional: only trigger a new evaluation call when the `id` and `email` attribute changes
cacheKeyAttributes: ["id", "email"],
});
If you would like to implement Sticky Bucketing while using Remote Evaluation, you must configure your remote evaluation backend to support Sticky Bucketing. In the case of the GrowthBook Proxy Server, this means implementing a Redis database for sticky bucketing use. You will not need to provide a StickyBucketService instance to the client side SDK.
Caching
The JavaScript SDK has 2 caching layers:
- In-memory cache (available on all platforms)
- Persistent localStorage cache (only available in browsers by default)
There are a number of cache settings you can configure within GrowthBook. This must be done BEFORE creating a GrowthBook instance.
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.
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 (24 hours default)
maxAge: 1000 * 60 * 60 * 24,
// For Remote Eval only - limit the number of cache entries (~1 entry per user)
maxEntries: 10,
// When `false`, we add a `visibilitychange` listener to disable SSE when the page is idle
disableIdleStreams: false,
// Consider a page "idle" when it is hidden for this long (default 20 seconds)
idleStreamInterval: 20000,
// Set to `true` to completely disable both in-memory and persistent caching
disableCache: false,
})
Polyfilling localStorage
Outside of a browser environment, you can still use persistent caching. You just need to provide an implementation of the localStorage interface.
Here's an example of using Redis in Node.js:
const { setPolyfills } = require("@growthbook/growthbook");
setPolyfills({
localStorage: {
// Example using Redis
getItem: (key) => redisClient.get(key),
setItem: (key, value) => redisClient.set(key, value),
}
});
This must be done BEFORE you call either prefetchPayload
or create the first GrowthBook instance.
Re-rendering When Features Change
When features change (e.g. by calling gb.refreshFeatures()
), you need to re-render your app so that all of your feature flag checks can be re-evaluated. You can specify your own custom rendering function for this purpose:
// Callback to re-render your app when feature flag values change
gb.setRenderer(() => {
// TODO: re-render your app
});
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.).
const gb = new GrowthBook({
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
trackingCallback: (experiment, result) => {
// Example using Segment
analytics.track("Experiment Viewed", {
experimentId: experiment.key,
variationId: result.key,
});
},
});
This same tracking callback is used for both feature flag experiments and Visual Editor experiments.
Feature Flag Experiments
There is nothing special you have to do for feature flag experiments. Just evaluate the feature flag like you would normally do. If the user is put into an experiment as part of the feature flag, it will call the trackingCallback
automatically in the background.
// If this has an active experiment and the user is included,
// it will call trackingCallback automatically
const newLogin = gb.isOn("new-signup-form");
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.
Visual Editor Experiments
Experiments created through the GrowthBook Visual Editor will run automatically as soon as their targeting conditions are met.
Note: Visual Editor experiments are only supported in a web browser environment. They will not run in Node.js, Mobile apps, or Desktop apps.
If you are using this SDK in a Single Page App (SPA), you will need to let the GrowthBook instance know when the URL changes so the active experiments can update accordingly.
// Call this every time a navigation event happens in your SPA
function onRouteChange() {
gb.setURL(window.location.href);
}
Visual Editor experiments are enabled by default, but can be disabled with various GrowthBook constructor settings:
- disableVisualExperiments - If true, all visual editor experiments will be skipped
- disableJsInjection - If true, any visual editor experiment that injects custom javascript will be skipped.
Content Security Policy
If you plan to use the Custom Javascript feature of the Visual Editor and you have a Content Security Policy on your site, there are two options:
- Enable
unsafe-inline
script-src - OR generate a unique nonce value, add it to your script-src directive, and pass it into the GrowthBook constructor as
jsInjectionNonce
URL Redirect Experiments
Similarly to Visual Editor experiments, URL redirect tests will run automatically if targeting conditions are met.
If you are using this SDK in a Single Page App (SPA), you'll want to pass in a custom navigation function into the SDK (as default navigation for URL Redirects uses window.location.replace(url)
) and set the navigateDelay
to 0.
// Example in Next.js
import router from "next/router";
const gb = new GrowthBook({
navigate: (url) => router.replace(url),
navigateDelay: 0,
// ... other settings
});
For SPA's you will also need to let the GrowthBook instance know when the URL changes so the active experiments can update accordingly.
// Call this every time a navigation event happens in your SPA
function onRouteChange() {
gb.setURL(window.location.href);
}
URL Redirect experiments are enabled by default, but can be disabled with various GrowthBook constructor settings:
- disableUrlRedirectExperiments - If true, all URL Redirect experiments will be skipped
- disableCrossOriginUrlRedirectExperiments - If true, any URL Redirect with a destination pointing to a different origin will be skipped.
Deferred Tracking
Sometimes, your analytics tracker is loaded after GrowthBook. In that case, you should not specify a trackingCallback
in the constructor and instead use setTrackingCallback
later when ready. When you do this, the GrowthBook instance will queue up tracking calls and then fire them all at once when you set the callback.
There are some scenarios where you need to queue up tracking calls in one GrowthBook instance and fire them in another. For example, if your analytics tracker is only available on the front-end, but you are running experiments in Node.js.
Export the queue tracking calls with the getDeferredTrackingCalls()
method. The result is a serializable JSON object:
const tracks = gb.getDeferredTrackingCalls();
Then, import with setDeferredTrackingCalls
. This does not fire them automatically. You must call fireDeferredTrackingCalls
after.
gb2.setDeferredTrackingCalls(tracks);
gb2.fireDeferredTrackingCalls();
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 for more information. If your organization and experiment supports 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:
-
LocalStorageStickyBucketService
— For simple bucket persistence using the browser's LocalStorage (can be polyfilled for other environments). -
BrowserCookieStickyBucketService
— For simple bucket persistence using browser cookies, which are transportable to the back end. Assumesjs-cookie
is implemented (can be polyfilled). Cookie attributes can also be configured. The default cookie expiry is 180 days; override by passingexpires: {days}
into the constructor'scookieAttributes
. -
ExpressCookieStickyBucketService
— For NodeJS/Express controller-level bucket persistence using browser cookies; intended to be interoperable withBrowserCookieStickyBucketService
. Assumescookie-parser
is implemented (can be polyfilled). Cookie attributes can also be configured. The default cookie expiry is 180 days; override by passingmaxAge: {ms}
into the constructor'scookieAttributes
. -
RedisStickyBucketService
— For NodeJS Redis-based bucket persistence. Requires anioredis
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 BrowserCookieStickyBucketService
:
import { BrowserCookieStickyBucketService } from "@growthbook/growthbook";
import Cookies from 'js-cookie';
const gb = new GrowthBook({
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
stickyBucketService: new BrowserCookieStickyBucketService({
jsCookie: Cookies,
}),
// ...
});
TypeScript
When used in a TypeScript project, GrowthBook includes basic type inference out of the box:
// Type will be `string` based on the fallback provided ("blue")
const color = gb.getFeatureValue("button-color", "blue");
// You can manually specify types as well
// feature.value will be type `number`
const feature = gb.evalFeature<number>("font-size");
console.log(feature.value);
// Experiments will use the variations to infer the return value
// result.value will be type "string"
const result = gb.run({
key: "my-test",
variations: ["blue", "green"],
});
Strict Typing
If you want to enforce stricter types in your application, you can do that when creating the GrowthBook instance:
// Define all your feature flags and types here
interface AppFeatures {
"button-color": string;
"font-size": number;
"newForm": boolean;
}
// Pass into the GrowthBook instance
const gb = new GrowthBook<AppFeatures>({
...
});
Now, all feature flag methods will be strictly typed.
// feature.value will by type `number`
const feature = gb.evalFeature("font-size");
console.log(feature.value);
// Typos will cause compile-time errors
gb.isOn("buton-color"); // "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.
Updating
As a general philosophy, we aim to keep the SDK 100% backwards compatible at all times. View the Changelog for a complete list of all SDK changes.
Updating to 1.0.0
Updating from a 0.X.X release to 1.0.0 is still backwards compatible for the vast majority of use cases, although there are a few minor changes:
- The
enableDevMode: true
setting previously also disabled cache as a side-effect. This is no longer the case in 1.0.0, and you must explicitly also setdisableCache: true
- Previously, a network request to fetch features was started immediately upon creating a GrowthBook instance. Starting in 1.0.0, it waits until you call
loadFeatures
(or the newinit
method) before starting the network request. As a replacement, there is now a standaloneprefetchPayload
function that you can use to kick off a network request outside of the context of a GrowthBook instance.
Switching to init
GrowthBook 1.0.0 introduced a new init
(and initSync
) method.
We recommend everyone starts using this in their implementation. It solves many pain points including easier error handling and more control over caching and streaming.
For those currently using loadFeatures
, init
is a direct replacement. The only difference is that streaming is now opt-in instead of on-by-default.
// Previous
await gb.loadFeatures({ timeout: 1000 });
// New
await gb.init({ timeout: 1000, streaming: true });
For those currently NOT using loadFeatures
and passing features/experiments directly into the GrowthBook constructor, init
and initSync
can be used instead. The code below assumes you have a payload
variable with the contents from the SDK Connection Endpoint.
// Previous
const gb = new GrowthBook({
features: payload.features,
experiments: payload.experiments
});
// New (async)
const gb = new GrowthBook();
await gb.init({ payload: payload });
// New (non-async)
const gb = (new GrowthBook()).initSync({ payload: payload });
GrowthBook Instance (reference)
Attributes
You can specify attributes about the current user and request. These are used for two things:
- Feature targeting (e.g. paid users get one value, free users get another)
- 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.
new GrowthBook({
attributes: {
id: "123",
loggedIn: true,
deviceId: "abc123def456",
company: "acme",
paid: false,
url: "/pricing",
browser: "chrome",
mobile: false,
country: "US",
},
});
Updating Attributes
If attributes change, you can call setAttributes()
to update. This will completely overwrite any existing attributes. To do a partial update, use the following pattern:
gb.setAttributes({
// Only update the `url` attribute, keep the rest the same
...gb.getAttributes(),
url: "/new-page"
})
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.
const salt = "f09jq3fij"; // Your organization's secure attribute salt (see Organization Settings)
// hashing a secureString attribute
const userEmail = sha256(salt + user.email);
// hashing an secureString[] attribute
const userTags = user.tags.map(tag => sha256(salt + tag));
gb.setAttributes({
id: user.id,
loggedIn: true,
email: userEmail,
tags: userTags,
});
await gb.init();
// In this example, we are using Node.js's built-in crypto library
function sha256(str) {
return crypto.createHash("sha256").update(str).digest("hex");
}
Note that in a browser context, we will not be able to natively access the Node.js crypto library. In modern browsers window.crypto.subtle
is available, although calls are asynchronous. You would need to await all attribute hashing to complete before calling gb.setAttributes()
.
async function sha256(str) {
const buffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(str));
const hashArray = Array.from(new Uint8Array(buffer));
return hashArray.map(byte => byte.toString(16).padStart(2, "0")).join("");
}
Alternatively, CryptoJS (https://www.npmjs.com/package/crypto-js) provides a synchronous API:
import sha256 from 'crypto-js/sha256';
const userEmail = sha256(salt + user.email);
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 NewRelic or DataDog.
new GrowthBook({
onFeatureUsage: (featureKey, result) => {
console.log("feature", featureKey, "has value", result.value);
},
});
The result
argument is the same thing returned from gb.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.
Dev Mode
There is a GrowthBook Chrome DevTools Extension that can help you debug and test your feature flags in development.
In order for this to work, you must explicitly enable dev mode when creating your GrowthBook instance:
const gb = new GrowthBook({
enableDevMode: true,
});
To avoid exposing all of your internal feature flags and experiments to users, we recommend setting this to false
in production in most cases.
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.
// Get detailed information about the feature evaluation
const result = gb.evalFeature("my-feature");
// 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 context and referencing them by ids in your code, you can also just run an experiment directly. This is done with the gb.run
method:
// These are the only required options
const { value } = gb.run({
key: "my-experiment",
variations: ["red", "blue", "green"],
});
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:
// Option 1: Using weights and coverage
gb.run({
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],
});
// Option 2: Specifying ranges
gb.run({
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],
],
});
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:
gb.run({
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",
});
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.
gb.run({
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",
},
]
})
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.
// Will include 60% of users - ones with a hash between 0 and 0.6
gb.run({
key: "experiment-1",
variation: [0, 1],
filters: [
{
seed: "pricing",
attribute: "id",
ranges: [[0, 0.6]]
}
]
});
// Will include the other 40% of users - ones with a hash between 0.6 and 1
gb.run({
key: "experiment-2",
variation: [0, 1],
filters: [
{
seed: "pricing",
attribute: "id",
ranges: [[0.6, 1.0]]
}
]
});
Note - If a user is excluded from an experiment due to a filter, the rule will be skipped and the next matching rule will be used instead.
Holdout Groups
To use global holdout groups, use a nested experiment design:
// The value will be `true` if in the holdout group, otherwise `false`
const holdout = gb.run({
key: "holdout",
variations: [true, false],
// 10% of users in the holdout group
weights: [0.1, 0.9]
});
// Only run your main experiment if the user is NOT in the holdout
if (!holdout.value) {
const res = gb.run({
key: "my-experiment",
variations: ["A", "B"]
})
}
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 GrowthBook 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:
{
"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:
gb.run({
key: "my-experiment",
variation: [0, 1],
condition: {
"browser.vendor": "firefox",
"country": {
"$in": ["US", "CA", "IN"]
}
}
})
Inline Experiment Return Value
A call to gb.run(experiment)
returns an object with a few useful properties:
const {
value,
key,
name,
variationId,
inExperiment,
hashUsed,
hashAttribute,
hashValue,
} = gb.run({
key: "my-experiment",
variations: ["A", "B"],
});
// 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);
// 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.
Feature Definitions (reference)
The feature definition JSON file contains information about all of the features in your application.
Each feature consists of a unique key, a list of possible values, and rules for how to assign those values to users.
{
"feature-1": {...},
"feature-2": {...},
"another-feature": {...},
}
Basic Feature
An empty feature always has the value null
:
{
"my-feature": {}
}
Default Values
You can change the default assigned value with the defaultValue
property:
{
"my-feature": {
defaultValue: "green"
}
}
Override Rules
You can override the default value with rules.
Rules give you fine-grained control over how feature values are assigned to users. There are 2 types of feature rules: force
and experiment
. Force rules give the same value to everyone. Experiment rules assign values to users randomly.
Rule Ids
Rules can specify a unique identifier with the id
property. This can help with debugging and QA by letting you see exactly why a specific value was assigned to a user.
Rule Conditions
Rules can optionally define targeting conditions that limit which users the rule applies to. These conditions are evaluated against the attributes
passed into the GrowthBook 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:
{
"id": "123",
"browser": {
"vendor": "firefox",
"version": 94
},
"country": "CA"
}
The following condition would evaluate to true
:
{
"browser.vendor": "firefox",
"country": {
"$in": ["US", "CA", "IN"]
}
}
If a condition evaluates to false
, the rule will be skipped. This means you can chain rules together with different conditions to support even the most complex use cases.
Force Rules
Force rules do what you'd expect - force a specific value for the feature
// Firefox users in the US or Canada get "green"
// Everyone else gets the default "blue"
{
"button-color": {
defaultValue: "blue",
rules: [
{
id: "rule-123",
condition: {
browser: "firefox",
country: {
$in: ["US", "CA"]
}
},
force: "green"
}
],
}
}
Gradual Rollouts
You can specify a range
for your rule, which determines what percent of users will get the rule applied to them. Users who do not get the rule applied will fall through to the next matching rule (or default value). You can also specify a seed
that will be used for hashing.
In order to figure out if a user is included or not, we use deterministic hashing. By default, we use the user attribute id
for this, but you can override this by specifying hashAttribute
for the rule:
This is useful for gradually rolling out features to users (start with a small range and slowly increase).
{
"new-feature": {
defaultValue: false,
rules: [
{
force: true,
hashAttribute: "device-id",
seed: 'new-feature-rollout-abcdef123',
// 20% of users
range: [0, 0.2]
// Increase to 40%:
// range: [0, 0.4]
}
]
}
}
Experiment Rules
Experiment rules let you adjust the percent of users who get randomly assigned to each variation. This can either be used for hypothesis-driven A/B tests or to simply mitigate risk by gradually rolling out new features to your users.
// Each variation gets assigned to a random 1/3rd of users
{
"image-size": {
rules: [
{
variations: ["small", "medium", "large"]
}
]
}
}
Customizing the Traffic Split
By default, an experiment rule will include all traffic and do an even split between all variations. There are 2 ways to customize this behavior:
// Option 1: Using weights and coverage
{
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]
}
// Option 2: Specifying ranges
{
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]
]
}
A user is assigned a number from 0 to 1 and whichever variation's range includes their number will be assigned to them.
Variation Meta Info
You can use the meta
setting to provide additional info about the variations such as name.
{
"image-size": {
rules: [
{
variations: ["sm", "md", "lg"],
ranges: [
[0, 0.5],
[0.5, 0.75],
[0.75, 1.0]
],
meta: [
{
key: "control",
name: "Small",
},
{
key: "v1",
name: "Medium",
},
{
key: "v2",
name: "Large",
}
]
}
]
}
}
Tracking Key and Name
When a user is assigned a variation, we call the trackingCallback
function so you can record the exposure with your analytics event tracking system. By default, we use the feature id to identify the experiment, but this can be overridden if needed with the key
setting. You can also optionally provide a human-readable name.
{
"feature-1": {
rules: [
{
// Use "my-experiment" as the key instead of "feature-1"
key: "my-experiment",
name: "My Experiment",
variations: ["A", "B"]
}
]
},
}
Hash Attribute
We use deterministic hashing to make sure the same user always gets assigned the same value. By default, we use the attribute id
, but this can be overridden with the hashAttribute
setting:
const gb = new GrowthBook({
attributes: {
id: "123",
company: "acme",
},
features: {
"my-feature": {
rules: [
// All users with the same "company" value
// will be assigned the same variation
{
variations: ["A", "B"],
hashAttribute: "company",
},
// If "company" is empty for the user (e.g. if they are logged out)
// The experiment will be skipped and fall through to this next rule
{
force: "A",
},
],
},
},
});
Filters
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.
{
"feature1": {
rules: [
// Will include 60% of users - ones with a hash between 0 and 0.6
{
variations: [false, true],
filters: [
{
seed: "pricing",
attribute: "id",
ranges: [[0, 0.6]]
}
]
}
]
},
"feature2": {
rules: [
// Will include the other 40% of users - ones with a hash between 0.6 and 1
{
variations: [false, true],
filters: [
{
seed: "pricing",
attribute: "id",
ranges: [[0.6, 1.0]]
}
]
},
]
}
}
Note - If a user is excluded from an experiment due to a filter, the rule will be skipped and the next matching rule will be used instead.