Want to take your software engineering career to the next level? Join the mailing list for career tips & advice Click here


A JavaScript port of Facebook's PlanOut Experimentation Framework

Subscribe to updates I use PlanOut.js

Statistics on PlanOut.js

Number of watchers on Github 205
Number of open issues 8
Average time to close an issue about 1 month
Main language JavaScript
Average time to merge a PR about 12 hours
Open pull requests 1+
Closed pull requests 3+
Last commit about 3 years ago
Repo Created about 5 years ago
Repo Last Updated about 2 years ago
Size 2 MB
Organization / Authorhubspot
Latest Releasev4.0.0
Page Updated
Do you use PlanOut.js? Leave a review!
View open issues (8)
View PlanOut.js activity
View on github
Fresh, new opensource launches 🚀🚀🚀
Software engineers: It's time to get promoted. Starting NOW! Subscribe to my mailing list and I will equip you with tools, tips and actionable advice to grow in your career.
Evaluating PlanOut.js for your project? Score Explanation
Commits Score (?)
Issues & PR Score (?)


Build Status npm downloads

PlanOut.js is a JavaScript-based implementation of PlanOut. It provides a complete implementation of the PlanOut native API framework and a PlanOut language interpreter.

PlanOut.js is implemented in ES6 and can also be used with ES5. It can be integrated client-side as well as with server-side with node.js.


PlanOut.js is available on npm and can be installed by running:

npm install planout

It is also available on bower and can be installed by running:

bower install planout

Comparison with Reference Implementation

PlanOut.js provides an implementation of all PlanOut features (including the experiment class, interpreter, and namespaces). The underlying randomization ops in the JavaScript implementation return different results for efficiency reasons. If you are using PlanOut cross-platform and want to enable compatibility mode then you can enable it by utilizing the planout_core_compatible.js distribution bundle instead of the default planout.js bundle. You can also utilize v2.0.2 which contains both compat and non-compat modes in the main distribution.

The planout_core_compatible.js bundle should be used only if you want your random operation results to match that of the results from other planout implementations (java, python, etc). The filesize of the planout_core_compatible.js bundle is fairly larger (by ~100kb) and random operations are processed slower.


This is how you would use PlanOut.js in ES6 to create an experiment:

import PlanOut from 'planout';

class MyExperiment extends PlanOut.Experiment {

  configureLogger() {
    //configure logger

  log(event) {
    //log the event somewhere

  previouslyLogged() {
    //check if weve already logged an event for this user
    //return this._exposureLogged; is a sane default for client-side experiments

  setup() {
    //set experiment name, etc.

  This function should return a list of the possible parameter names that the assignment procedure may assign.
  You can optionally override this function to always return this.getDefaultParamNames() which will analyze your program at runtime to determine what the range of possible experimental parameters are. Otherwise, simply return a fixed list of the experimental parameters that your assignment procedure may assign.

  getParamNames() {
    return this.getDefaultParamNames();

  assign(params, args) {
    params.set('foo', new PlanOut.Ops.Random.UniformChoice({choices: ['a', 'b'], 'unit': args.userId}));


Then, to use this experiment you would simply need to do:

var exp = new MyExperiment({userId: user.id });
console.log("User has foo param set to " + exp.get('foo'));

If you wanted to run the experiment in a namespace you would do:

class MyNameSpace extends PlanOut.Namespace.SimpleNamespace {

  setupDefaults() {
    this.numSegments = 100;

  setup() {

  setupExperiments() {
    this.addExperiment('MyExperiment', MyExperiment, 50);

Then, to use the namespace you would do:

var namespace = new MyNamespace({userId: user.id });
console.log("User has foo param set to " + namespace.get('foo'));

An example of using PlanOut.js with ES5 can be found here

An example with the PlanOut interpreter can be found here

Experimental Overrides

There are two ways to override experimental parameters. There are global overrides and local overrides. Global overrides let you define who should receive these overrides and what those values should be set. It is not recommended to be used for anything apart from feature rollouts.

To use global overrides simply do something similar the following in your namespace class:

allowedOverride() {
  //(you may need to pass additional information to the namespace this to work)
  //some criteria for determining who should receive overrides
  return this.inputs.email.indexOf('hubspot.com') >= 0;

getOverrides() {
  return {
    '[param name]': {
      'experimentName': [experiment Name],
      'value': [value of override]
    'show_text': {
      'experimentName': 'Experiment1',
      'value': 'test'

Local overrides are basically client-side overrides you can set via query parameters or via localStorage.

For example, suppose you want to override the show_text variable to be 'test' locally. You would simply do


or you could set experimentOverride=Experiment1 and show_text=test in localStorage

Note that in both cases exposure will be logged as though users had been randomly assigned these values.

The primary use of global overrides should be for feature rollouts and the primary use of local overrides should be for local testing

Registering experiment inputs

PlanOut.js comes packaged with an ExperimentSetup utility to make it easier to register experiment inputs outside from experiment initialization.

By calling ExperimentSetup.registerExperimentInput('key', 'value', [optional namespace name]), you can register a particular value as an input to either all namespaces (by not passing the third argument, it assumes that this should be registered as an input across all experiments) or to a particular namespace (by passing the namespace name as the third argument).

This allows you to keep your namespace class definition and initialization separate from your core application bootstrapping and simply makes it necessary to call ExperimentSetup when you have fetched the necessary inputs.

For instance, one could have a namespace defined in a file called 'mynamespace.js'

var namespace = new MyNamespace({ 'foo': 'bar'});

and register a user identifier input to it when the application bootstraps and fetches user information.

getUserInfo().then((response) => {
  ExperimentSetup.registerExperimentInput('userid', response.userId);

This is also useful when an experiment is intended to interface with external services and allows certain experiment-specific inputs to be restricted to the namespaces that they are intended for.

With this it is important to watch out for race conditions since you should ensure that before your application ever fetches any experiment parameters it registers the necessary inputs.

Use with React.js

If you are using React.js for your views, react-experiments should make it very easy to run UI experiments


The event structure sent to the logging function is as follows:

  'event': 'EXPOSURE',
  'name': [Experiment Name],
  'time': [time]
  'inputs': { ...inputs }
  'params': { ...params},
  'extra_data': {...extra data passed in}

Here are several implementations of the log function using popular analytics libraries:

Mixpanel / Amplitude.

Both Mixpanel and Amplitude effectively have the same API for logging events so just swap out the last line depending on which library you're using.

This log function brings the inputs and params fields onto the top level event object so that they're queryable in Mixpanel / Amplitude and uses the following as the event name [Experiment Name] - [Log Type] so for exposure logs it would look like [Experiment Name] - EXPOSURE.

log(eventObj) {

  //move inputs out of nested field into top level event object
  var inputs = eventObj.inputs;
  Object.keys(inputs).forEach(function (input) {
    eventObj[input] = inputs[input];

  //move params out of nested field into top level event object
  var params = eventObj.params;
  Object.keys(params).forEach(function (parameter) {
    eventObj[parameter] = params[parameter];

  var eventName = eventObj.name + ' - ' + eventObj.event;

  //if using mixpanel
  return mixpanel.track(eventName, eventObj);

  //if using amplitude*
  return amplitude.logEvent(eventName, eventObj);

Google Analytics

Google Analytics unfortunately has a relatively weak events API compared to Mixpanel and Amplitude, which means that we have to forego some event fields when using it.

Here is the anatomy of the resulting log function:

The event category field to equal EXPERIMENT so that all experiment events are grouped under the same category. The event name is [Experiment Name] - [Log Type] so for exposure logs it would look like [Experiment Name] - EXPOSURE. The event label takes all experiment parameter values and joins them together into a comma-delimited single string due to the constraints of the API.

log(eventObj) {
  var eventCategory = 'EXPERIMENT';
  var eventName = eventObj.name + ' - ' + eventObj.event;

  var params = eventObj.params;
  var paramVals = Object.keys(params).map(function (key) {
    return params[key];
  var eventLabel = paramVals.join(',');

  return ga('send', 'event', eventCategory, eventName, eventLabel);


This project uses Jest for testing. The tests can be found in the tests folder and building + running the tests simply requires running the command: npm run-script build-and-test

If you are making changes to the ES6 implementation, simply run npm run-script build and it will transpile to the corresponding ES5 code.

PlanOut.js open issues Ask a question     (View All Issues)
  • almost 4 years Intermittent Build Failures on Node v4
  • almost 4 years Investigate lodash modularized utilities
  • almost 4 years Tests don't pass on windows
  • almost 5 years Add flow for type-checking
PlanOut.js open pull requests (View All Pulls)
  • Separate out a core compatible dist bundle packaged w/ bignumber
PlanOut.js list of languages used
PlanOut.js latest release notes
v4.0.0 V4.0 major version release (may result in enrollment shift)

Depending on your usage of planout.js, upgrading to v4.0 may result in a shift in experiment parameter value assignment, and namespace enrollment. Because of this, we recommend that you only upgrade to v4.0 when you don't have any experiments actively running.

  • Core compatible bundle fixes:
    • Core compatible namespace allocations now match core reference namespace allocations from python version of planout.
    • Core compatible interpreted experiment enrollment now matches core reference interpreted experiment enrollment from python version of planout.
  • Separated the concerns of planout core random operations, and the planout API. The planout.js API (experiment, assignment, namespace, etc) are now composed with the random operations they are passed. This change has no effect on the usage of planout.js and only affects the development experience for contributors.
  • Added planoutAPIFactory.js to keep planout bundles consistent, and to make it easier to compose new planout bundles with the random operations of choice.
  • Made experiment & namespace names required to fix https://github.com/HubSpot/PlanOut.js/issues/57
  • Fixes WeightedChoice with false-y choices.
  • Fixes tests on windows + running the travis tests on windows as well if this is possible.
v3.0.3 Default Value for Assignment

Allows for a default value in the Assignment class

v3.0.2 Bug Fixes

Fixes a bug when defining multiple interpreted experiments from a single instruction object.

Other projects in JavaScript