Node.js
We officially support Node 18 and above.
Installation
Install with a package manager
- npm
- Yarn
- pnpm
npm install --save @growthbook/growthbook
yarn add @growthbook/growthbook
pnpm add @growthbook/growthbook
Quick Usage
GrowthBook instances are scoped to a single incoming request. The easiest way to do this is with Middleware:
// 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"
});
// TODO: Add user targeting attributes from cookies, headers, etc.
req.growthbook.setAttributes({
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())
});
Then, you can access the GrowthBook instance from any route:
app.get("/", (req, res) => {
const gb = req.growthbook;
// Boolean on/off flag
if (gb.isOn("my-feature")) {
// Do something
}
// String/Number/JSON flag
const value = gb.getFeatureValue("my-string-feature", "fallback");
console.log(value);
})
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.
const gb = new GrowthBook({
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
});
// Wait for features to be downloaded with a timeout (in ms)
gb.init({ timeout: 2000 }).then(() => next())
The network request to download features is cached in memory and uses a stale-while-revalidate (SWR) pattern. So the first call to gb.init()
may be slow, but all subsequent calls should resolve immediately.
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:
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 or Redis.
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.
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 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.
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
Then, do the following during app startup:
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 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.
When a new GrowthBook instance is created in your middleware, it will use the latest available payload. The payload for this GrowthBook instance will be locked and frozen, so you don't have to worry about the payload changing mid-request and causing weird edge cases in your app.
Caching
The JavaScript SDK has 2 caching layers:
- In-memory cache (enabled by default)
- Persistent localStorage cache (disabled by default, requires configuration)
Configuring Local Storage
Here is an example of using Redis as your persistent localStorage cache:
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.
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,
// Set to `true` to completely disable both in-memory and persistent caching
disableCache: false,
})
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,
});
},
});
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.
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.
If that is the case for your app, do not specify a trackingCallback
in the constructor. This will queue up tracking calls in the GrowthBook instance.
You can export the queued tracking calls with the getDeferredTrackingCalls()
method. The result is a serializable JSON object:
const tracks = gb.getDeferredTrackingCalls();
Send those down to your front-end and you can fire them in one of two ways:
If Using GrowthBook on the Front-End
If you are already using the JavaScript or React SDK on the front-end, you can import with setDeferredTrackingCalls
. This does not fire them automatically. You must call fireDeferredTrackingCalls
after.
gb.setDeferredTrackingCalls(tracks);
gb.fireDeferredTrackingCalls();
This will use the trackingCallback
configured on your front-end GrowthBook instance.
Standalone Tracker
If you do NOT have a client-side GrowthBook instance, you can still fire these tracking calls with a small custom client-side script:
tracks.forEach(({experiment, result}) => {
// Example using Segment.io
analytics.track("Experiment Viewed", {
experimentId: experiment.key,
variationId: result.key,
});
})
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:
-
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. -
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 ExpressCookieStickyBucketService
:
const { ExpressCookieStickyBucketService } = require("@growthbook/growthbook");
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",
stickyBucketService: new ExpressCookieStickyBucketService({
req,
res
}),
})
})
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.
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");
}
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.
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"],
});