Giter Site home page Giter Site logo

planout.js's Introduction

This repository is no longer actively maintained, but the library is functional.

PlanOut.js

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.

Installation

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

npm 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.

Usage

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

import * as PlanOut from "planout";

class MyExperiment extends PlanOut.Experiment {

  configureLogger() {
    return;
    //configure logger
  }

  log(event) {
    //log the event somewhere
  }

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

  setup() {
    //set experiment name, etc.
    this.setName('MyExperiment');
  }

  /*
  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() {
    this.setName('MyNamespace');
    this.setPrimaryUnit('userId');
  }

  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'));

Note that the import for PlanOut has changed as of v5. The update modified the way that users should import PlanOut from import PlanOut from 'planout'; to import * as PlanOut from "planout";

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

http://[some_url]?experimentOverride=Experiment1&show_text=test

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

Logging

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:

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 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);
}

Development

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's People

Contributors

alexindigo avatar anuragagarwal561994 avatar carbonphyber avatar chrisvoll avatar dependabot[bot] avatar engvik avatar eytan avatar frandias avatar gita-vahdatinia avatar irvifa avatar jcwilson avatar manpreetssethi avatar mattrheault avatar niklasnordlund avatar rammie avatar rawls238 avatar timbeyer avatar yjqg6666 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

planout.js's Issues

Create InterpretedExperiment class

There is a demo / test for the interpreter itself, but AFAIK no code that demonstrates how the two would work together (in particular, this requires some integration with the inExperiment flag that specifies whether or not the experiment should do an exposure log). @rawls238, are you guys using the interpreter in production? If so, perhaps this example would be easy to hack up, otherwise I could give it a shot when I have some time... It would also be great to have the PlanOut editor use PlanOut.js instead of rely on a Python/Flask-based backend.

Is there a way to get all possible variants an experiment can produce?

I'd like to be able to get a list of all possible variants (or assignments) for a given experiment using planout. Is this currently possible? For example, if I have an experiment using weighted Choice with "control", and "test", is there a way I can get back that "control" and "test" are the possible assignments using planout library?

Calculation of random integer needs a fix ?

https://github.com/HubSpot/PlanOut.js/blob/2e917673fc47146c09a4b040188b0cc7479cc203/es6/ops/randomPlanoutCoreCompatible.js#L25-L29

Test case where it is failing

Min Value = 7
Max Value = 20
getHash() = 108
Result = 108.plus(7).modulo(20 - 7 + 1) = 115 % 14 = 3

Issue: 3 is not in range between 7 and 20

The formula will not work if the min > 0.

Should not the formula be
return this.getHash().modulo(maxVal - minVal + 1).plus(minVal).toNumber();
instead of
return this.getHash().plus(minVal).modulo(maxVal - minVal + 1).toNumber();

cc @anuragagarwal561994 @devenvyas

PlanOut.js from typescript?

Hi,

This isn't a defect report, hope that's ok.

Has anyone been able to make the above work ideally against raw ES5 planout.js? We're not currently using npm or babel, so we're relying on plain old require.js and amd against non-ES6 .js libraries using d.ts. I understand that I'm going to have to add dependencies for sha1 and bignumber in as well..

I'm working my way through this: http://www.jbrantly.com/es6-modules-with-typescript-and-webpack/ but hoping that someone has a quicker recipe or has done this before. I'll follow up if I figure this out in interim. (I'm under a bit of pressure to use existing Facebook python implementation from server API but I'd much rather use a .js client.)

thanks!

No longer loads with webpack 5

Bug report

I have an issue with planout that bundled fine with Webpack 4 but no longer works with Webpack 5.

What is the current behavior?
Module is not loaded:

Uncaught TypeError: Getter must be a function: A
    at Function.defineProperty (<anonymous>)
    at Function.__webpack_require__.d (bundle.js:963)
    at eval (index.js:2)
    at Module../build/index.js (planout.js:102)
    at __nested_webpack_require_539__ (planout.js:25)
    at eval (planout.js:89)
    at eval (planout.js:92)
    at webpackUniversalModuleDefinition (planout.js:3)
    at eval (planout.js:5)
    at Object../node_modules/planout/dist/planout.js (bundle.js:291)

If the current behavior is a bug, please provide the steps to reproduce.
Repo Here:

https://github.com/gita-v/webpack_issue

What is the expected behavior?

For module to load :)

Other relevant information:
webpack version: 5.3.2
Node.js version: 14.14.0

Random number generation

Hi guys,
First, enjoying your library so far, and want to thank you for investing your time into porting this great library to js. I do have one question, and it's about this line:

https://github.com/HubSpot/PlanOut.js/blob/master/es6/ops/randomBase.js#L8
hashCalculation(hash) { return parseInt(hash.substr(0, 13), 16); }

Are you sure that should be 13, because I'm doing some C# library and using yours as a reference. When I use 15 instead of 13, I get normal probabilities also normalizing the numbers works fine as well. But with 13 this part of the code is not working for me:
var zeroToOne = this.zeroToOneCalculation(appendedUnit); return zeroToOne * (maxVal - minVal) + minVal;
Check out the Java port is also using 15 char substring:
https://github.com/Glassdoor/planout4j/blob/master/core/src/main/java/com/glassdoor/planout4j/planout/ops/random/PlanOutOpRandom.java#L73

Also the original Facebook library is using 15:
https://github.com/facebook/planout/blob/master/python/planout/ops/random.py#L30

Experiment Inputs Don't Register Outside Namespaces

Registered experiment inputs only work for experiments defined within the context of a namespace.

This is a bit annoying! We should also allow experiment inputs to be defined on a per experiment basis, even when they are not defined inside a namespace

Add LICENSE

What license is this source published under?

Rethink namespace / experiment name as part of assignment algorithm in JS port only

Methods in question:

I don't think that we should be using the namespace / experiment class names as part of the core planout assignment algorithm in the javascript port (specifically for the non-core compatible bundle).

The reasoning for this is that most frontend apps will compile, transpile, & minify their es6, or coffeescript, or typescript, or whatever. A common part of this process is to munge variable names to save on filesize. This means that planout.js cannot rely on the experiment / namespace class name to be consistent between builds.

Pre compile, transpile, & variable munge:

class FooExperiment extends planout.Exeriment {}
class BarNamespace extends planout.Namespace {}

Post compile, transpile, & variable munge:

var a = function a(_pe) {...}
var b = function b(_pn) {...}

This process often renames variables based on declaration order. If we were to swap the ordering of FooExperiment and BarNamespace, then enrollment would shift since the munged variable names will change.

What I propose:

All experiments & namespaces should have a static default name if none is set. EX: "GenericExperiment".

But we should provide an optional flag to allow people to use the classname as the default name if they choose. This flag should be disabled by default.

With this change, I would also recommend a major version bump to avoid shifting enrollment for people relying on the parsed experiment / namespace classname.


@rawls238 @eytan
/cc @geoffdaigle

Improve Namespace performance

I've noticed that when having many namespaces, and many experiments to set up and tear down again, which is the recommended way of handling changing experiments that are added and removed without reshuffling all assignments, performance starts to become an issue.
We've recently had to rewrite all of our experiments code to use caching via cookies because calculating those on every request significantly slowed down our application. The whole stateless aspect of planout is one of the major selling points though, so I do not find it a particularly elegant solution.

I've put up a benchmark on http://requirebin.com/?gist=a584dd4d020dabd830fa
Try playing around with the numSegments and experiments that get added and removed and you'll see that currently namespaces don't scale really well.
Assuming you are using several namespaces in parallel with 100 segments each and several running experiments, you'll quickly end up with hundreds of milliseconds of processing time.

I wonder if there's room for optimization so Planout.js scales better, or if this is because of the hashing and cannot be improved on.

In any case I'd be happy to help out with this if you have any ideas for low hanging fruits.

Default Experiment causes unnecessary exposure logging

I wanted to check if this was the intended behavior first before submitting a PR.
Namespace.js line 281, it will cause the default experiment to evaluate even if there is an experiment assigned to that segment. If the default experiment defines auto exposure logging, then the event will be logged twice, and given that the default experiment has a different salt (does not include the namespace prefix), those two values will potentially be different.

planout_core_compatible namespaces are not core compatible

Problem

The planout_core_compatible.js distribution in 3.0.* does not allocate namespace segments in a compatible manner.

I have done a little digging into the webpack'd distribution, but I'm at a loss to explain the root cause. The best I can tell is that when SampleBuilder.sample() calls this.compatSampleIndexCalculation() it gets routed to SampleBuilder.compatSampleIndexCalculation which is the non-compatible version, rather than to SampleCoreCompatible.compatSampleIndexCalculation.

As far as I can tell, the compatible random ops are working as expected when called directly. Perhaps it's a bug in webpack?

Reproduction Case

var planout_202 = require('./node_modules/planout/dist/planout_202.js')
var planout = require('./node_modules/planout/dist/planout.js')
var planout_compat = require('./node_modules/planout/dist/planout_core_compatible.js')


Object.getOwnPropertyDescriptors = function getOwnPropertyDescriptors(obj) {
  var descriptors = {};
  for (var prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      descriptors[prop] = Object.getOwnPropertyDescriptor(obj, prop);
    }
  }
  return descriptors;
};

Function.prototype.extend = function extend(proto) {
    var superclass = this;
    var constructor;

    if (!proto.hasOwnProperty('constructor')) {
      Object.defineProperty(proto, 'constructor', {
        value: function () {
            // Default call to superclass as in maxmin classes
            superclass.apply(this, arguments);
        },
        writable: true,
        configurable: true,
        enumerable: false
      });
    }
    constructor = proto.constructor;

    constructor.prototype = Object.create(this.prototype, Object.getOwnPropertyDescriptors(proto));

    return constructor;
};

var range = function(max) {
  var l = [];
  for (var i = 0; i < max; i++) {
    l.push(i);
  }
  return l;
};

function getSample(planout) {
    var assignment = new planout.Assignment('user');
    assignment.set('sampled_segments', new planout.Ops.Random.Sample({
        'choices': range(10),
        'draws': 5,
        'unit': 'user'
    }));
    return assignment.get('sampled_segments');
}

function allocateSegments(planout) {
  var TestNamespace = planout.Namespace.SimpleNamespace.extend({
    setup: function() {
      this.name = 'TestNamespace';
      this.numSegments = 10;
      this.setPrimaryUnit('userId');
    },

    setupExperiments: function() {
      this.addExperiment('My Experiment', planout.Experiment, 5);
    }
  });
  return Object.keys(new TestNamespace().segmentAllocations);
}

function runExperiment(planout) {
  var TestExperiment = planout.Experiment.extend({
    configureLogger: function() {},
    log: function(event) {},
    previouslyLogged: function() { return false; },
    setup: function() {},
    getParamNames: function() { return this.getDefaultParamNames(); },
    assign: function(params, args) {
      params.set('foo', new planout.Ops.Random.UniformChoice({choices: ['a', 'b'], 'unit': args.userId}));
    }
  });

  var result = [];
  for (var i = 0; i < 10; i++) {
    result.push(new TestExperiment({userId: i}).get('foo'));
  }
  return result;
}

function runTests(desc, test) {
  console.log('Testing ' + desc);
  console.log('------------------------');
  planout_202.ExperimentSetup.toggleCompatibleHash(false);
  console.log('202|compat=false: ' + test(planout_202));
  console.log('303|            : ' + test(planout));
  planout_202.ExperimentSetup.toggleCompatibleHash(true);
  console.log('202|compat=true : ' + test(planout_202));
  console.log('303|compatible  : ' + test(planout_compat));
  console.log()
}

runTests('sample functions', getSample);
runTests('experiment assignment', runExperiment);
runTests('namespace segment allocation', allocateSegments);

Output

Testing sample functions
------------------------
202|compat=false: 4,7,5,6,3
303|            : 4,7,5,6,3
202|compat=true : 2,3,9,1,8
303|compatible  : 2,3,9,1,8

Testing experiment assignment
------------------------
202|compat=false: a,a,b,b,b,b,b,b,b,a
303|            : a,a,b,b,b,b,b,b,b,a
202|compat=true : a,a,a,a,a,a,a,a,b,a
303|compatible  : a,a,a,a,a,a,a,a,b,a

Testing namespace segment allocation
------------------------
202|compat=false: 0,1,2,7,9
303|            : 0,1,2,7,9
202|compat=true : 0,1,3,4,6
303|compatible  : 0,1,2,7,9

Notice the last two lines. They should match, but do not.

I considered that it might be due to loading all three versions into one script, but the results are consistent if I just run one version's tests at a time.

Safari hangs when using planout_core_compatible.js

I have installed planout_core_compatible.js and using UniformChoice. It works good across all browsers except Safari (Mac OS 10.12.6, Safari 10.1.2). In safari is enters loop in randomIndexCalculation. Is it known issue ?

Do not parse assign function source code to get parameter names

I just spent several hours trying to debug a mistake in my code, when I noticed that experiment.experimentParameters was returning weird data.

So I looked up the code and realized what's going on: https://github.com/HubSpot/PlanOut.js/blob/0d26ab31a3995cd490d01c271984629ec2051954/es6/experiment.js#L32-L40

You are parsing the actual code of the assign function to find this.set('foo', bar) so you'd know that 'foo' is being set. Sadly, this does not work at all if the parameter name that is being set is actually coming from a variable, which broke the library for me.

I'd suggest to not do this magic by default and instead require this function to be overridden. It's absolutely opaque that such kind of logic could happen and the library fails in a completely unpredictable way without any error messages. It just never finds your parameters if they are within a namespace.

Investigate lodash modularized utilities

@chrisvoll pointed out that we could replace a lot of the functions in the [utils file](https://github.com/HubSpot/PlanOut.js/blob/master/es6/lib/utils.js with) with lodash modularized utilities. The whole point of the utils file is to reduce the bundle size by not having to pull in underscore/lodash since we only require a small subset of the functions supplied.

Switching to lodash modularized utilities basically gives us that as well as hopefully making this library more future proof since those utility libraries will adapt as javascript/browser implementations change whereas the local utils file here likely will not. I also wonder if it will give us any further reductions in bundle size, though I'm not sure how big this reduction will be (or if there will even be one).

Interpreter fails if input value is "non-truthy"

An interpreted experiment will always return non-true values for non-truthy inputs, e.g. {userid: 0} or {userid: false}.

'use strict';

const planout = require('planout');

class Repro extends planout.Experiment {
  assign(params, args) {
    const script = {
      op: 'seq',
      seq: [
        {
          op: 'set',
          var: 'num_of_categories',
          value: {
            choices: {
              op: 'array',
              values: [1, 5, 10],
            },
            unit: {
              op: 'get',
              var: 'userid',
            },
            op: 'uniformChoice',
          },
        },
      ],
    };

    const interpreterInstance = new planout.Interpreter(
      script,
      this.getSalt(),
      args
    );
    const results = interpreterInstance.getParams();

    Object.keys(results).forEach(result => {
      params.set(result, results[result]);
    });
    return interpreterInstance.inExperiment;
  }

  // The equivalent works
  //assign(params, args) {
  //  params.set('num_of_categories', new planout.Ops.Random.UniformChoice({choices: [1,5,10], 
  //                                                                       'unit': args.userid})); 
  //}

  configureLogger() {
    return;
  }

  setup() {
    this.name = 'exp0';
  }

  log(event) {
    console.log(event);
  }

  getParamNames() {
    return this.getDefaultParamNames();
  }

  previouslyLogged() {
    return this._exposureLogged;
  }
}

// failure: will always return undefined
const try1 = new Repro({userid: 0});
console.log(try1.get('num_of_categories'));

// works
const try2 = new Repro({userid: '0'});
console.log(try2.get('num_of_categories'));

// works
const try3 = new Repro({userid: 1});
console.log(try3.get('num_of_categories'));

Call async/await inside log and previouslyLogged

If I call my own API inside the log() and previouslyLogged(), is it possible to use Promises or async/await to get the result of the API call? For example if I want to have an endpoint that writes the event to an external log inside log(), and then another endpoint that reads the log to determine if the event was previouslyLogged().

Tests don't pass on windows

I was informed that someone ran the build and test script on windows and a lot of the tests failed. This issue is for both finding out what the cause of the issue is on windows as well as fixing it

location.search breaks server-side usage

In https://github.com/HubSpot/PlanOut.js/blob/9a97c92ed29526ca810153321e442646c6f3f53f/es6/lib/utils.js#L14 location.search is accessed, which I assume is for overriding test parameters based on the URL.
Sadly this breaks usage on nodeJS since window.location is not available there.

For now I would propose to do a simple check whether it is available, but in the future it would probably be best if we could provide a hook for these overrides so that isomorphic applications can make use of this consistently.

Interpreted Experiments don't change `inExperiment()` return value

I was trying out the return false feature of the Planout language with the below script, and when I call inExperiment() on an interpreted experiment, it seems to always return the default value that the experiment is set at (false in Planout.js interpreter, but in the python version, it seems to be true. Haven't tried the python one to see if it exhibits the same behavior though /cc @eytan)

This is a simple example of allowing only people with language = "en" to be "inExperiment".

if (language != "en") {
  return false
}

welcome_text = uniformChoice(choices=["Welcome!", "Hello There"], unit=userid);

I made a table of my tests here: https://docs.google.com/spreadsheets/d/1r11sICfvNcILDYkUxVFX4Qq4_BkIYm6biWB9Jbpn7bw/edit#gid=0

I couldn't really find docs on inExperiment, so I'm not sure if I'm using it correctly. However, the "native javascript api" works as expected, when I use this code:

var EnglishOnly = function(args) {
  var experiment = new planout.Experiment(args);
  experiment.setup = function() { this.setSalt("Exp1"); }
  experiment.setup();
  experiment.assign = function(params, args) {
	if (args.language != "en") {
		return false;
	}
    params.set('welcome_text', new planout.Ops.Random.UniformChoice({ 'choices': ["Welcome!", "Hello there"], 'unit': args.userid}));
  };
  experiment.getParamNames = function() { return this.getDefaultParamNames(); }
  experiment.configureLogger = function() { return; }
  experiment.log = function(stuff) { return; }
  experiment.previouslyLogged = function() { return; }
  return experiment;
};

var inExperiment = new EnglishOnly({userid: 123, language: 'en'});
console.log(inExperiment.getParams()(;
console.log('inExperiment, should be true', inExperiment.inExperiment());
var notInExperiment = new EnglishOnly({userid: 123, language: 'es'});
console.log(notInExperiment.getParams());
console.log('notInExperiment, should be false', notInExperiment.inExperiment());

This, however, always returns false for an interpreted experiment:

var json = {
	"op": "seq",
	"seq": [
		{
			"op": "cond",
			"cond": [
				{
					"if": {
						"op": "not",
						"value": {
							"op": "equals",
							"left": {
								"op": "get",
								"var": "language"
							},
							"right": "en"
						}
					},
					"then": {
						"op": "seq",
						"seq": [
							{
								"op": "return",
								"value": false
							}
						]
					}
				}
			]
		},
		{
			"op": "set",
			"var": "welcome_text",
			"value": {
				"choices": {
					"op": "array",
					"values": [
						"Welcome!",
						"Hello There"
					]
				},
				"unit": {
					"op": "get",
					"var": "userid"
				},
				"op": "uniformChoice"
			}
		}
	]
};

var inExperiment = new planout.Interpreter(json, 'salt', {userid: 123, language: 'en'});
console.log(inExperiment.getParams());
console.log('inExperiment, should be true', inExperiment.inExperiment()); // returns false
var notInExperiment = new planout.Interpreter(json, 'salt', {userid: 123, language: 'es'});
console.log(notInExperiment.getParams());
console.log('notInExperiment, should be false', notInExperiment.inExperiment()); 

Is there a bug in Planout.js? Or should I be extending the interpreter and creating a custom _inExperiment setter?

.babelrc interferes with building

When using import PlanOut from 'planout'; and building with babel/webpack, babel tries to resolve the presets and plugins from Planout's .babelrc.

Since we are importing the transpiled code, this should not happen. While this can be circumvented using babelrc: false and extending to the parent project's .babelrc, it might be better to add the .babelrc to the command line used when testing the package.

default import stopped working with upgrade to v5

Code:

import PlanOut from 'planout';

export default class BaseExperiment extends PlanOut.Experiment {
...
}

Error is that PlanOut is undefined. Now that it is exported as a module, everything must be imported as something like import { Experiment } from 'planout'.

This is a breaking change; if intentional, it should be called out in the ReadMe.

Thanks for your work maintaining this!

Weird Installation Issues

I just attempted to set up the library on a new project and ran into some weird installation issues that should be resolved. Specifically, I needed to install the babel stage-0 plugin and the module transform plugin. The exact error Module build failed: ReferenceError: [BABEL] /Users/garidor/Desktop/damp-ocean-76383/hello/static/js/src_2/index.js: Using removed Babel 5 option: foreign.modules - Use the corresponding module transform plugin in the `plugins` option. Check out http://babeljs.io/docs/plugins/#modules.

This doesn't seem like stuff that should be spilling over to the user side and should be looked into at some point

inputs is undefined

Hey guys,

for a project at my university I need to implement PlanOut. Since my web app runs on angular (v.5.0.0) I try to use PlanOut.js to integrate it into my project, but It seems like I'm missing something.

When I extend my class MyExperiment from Experiment it does not seem to override the constructor properly. When I try to create an experiment like this:

var exp = new MyExperiment({userId: 12 });
this.btnText =  exp.get('foo');

it tells me, that the constructor of MyExperiment should be empty instead of containing e.g. the userId

I can't figure out how to pass this input value (e.g. userId) to the super class.

I'm fairly new to angular and javascript in general so please don't be too condemning ;)
I'm also aware that you guys actually created PlanOut.js to work well with React, but I thought since it is a javascript library it also should run on angular as well.

When I fake the userId in the MyExperiment class I'm able to log the event just fine.
If you want me to explain things in more detail, just let me know!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.