Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

experiments

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

experiments - npm Package Compare versions

Comparing version 0.1.0 to 0.2.0

bower.json

18

package.json
{
"name": "experiments",
"version": "0.1.0",
"version": "0.2.0",
"author": "Matt Chadburn <matt.chadburn@ft.com>",
"description": "Simple split testing",
"contributors": [
"Matt Chadburn",
"Guardian News & Media Ltd"
"Matt Chadburn"
],
"repository": {
"type": "git",
"url": "git@github.com:commuterjoy/experiments.git"
},
"engines": {
"node": "*"
},
"keywords": [
"ab testing",
"split testing"
],
"dependencies": {
"seedrandom": "2.3.4"
},
"devDependencies": {

@@ -47,3 +41,3 @@ "browserify": "*",

},
"license": "Apache-2.0"
"license": "MIT"
}

@@ -9,7 +9,8 @@ A JavaScript AB testing framework, ported from [http://www.github.com/guardian/frontend](guardian/frontend).

- Basic segmentation of audience.
- Fixed duration tests - that automatically close and delete themselves.
- Deterministic segmentation - allowing allocation of users in to tests based on some external key (Eg, user name)
- Fixed duration tests - that automatically close and delete their footprint.
- Isolation of each test audience - so a user can not accidently be in several tests at once.
- Agnostic of where the data is logged - most companies have their own customer data repoisitories.
- Minimal payload - ~1kb (minified + gzip) with no additional cookie overhead created.
### Experiment profiles

@@ -16,0 +17,0 @@

@@ -1,150 +0,258 @@

/*global module*/
/*global module,require */
require('seedrandom');
/**
* Represents a single Ab test
* @type {Function}
* @param {Object} profile
* @param {Object} opts (optional)
*/
var Ab = function (profile, opts) {
"use strict";
this.opts = opts || {};
this.profile = profile;
this.storagePrefix += profile.id;
// All users need to be allocated to a test percentile
this.allocateId();
"use strict";
if (!this.idValidator.test(this.profile.id)) {
throw new Error('Invalid test profile id');
}
// if a variant is supplied then force the user in to that test
if (!!this.opts.variant) {
this.addParticipation(this.profile.id, this.opts.variant);
}
this.opts = opts || {};
this.seed = this.opts.seed;
this.profile = profile;
this.storagePrefix += profile.id;
// All users need to be allocated to a test percentile
this.allocateId();
if (!this.idValidator.test(this.profile.id)) {
throw new Error('Invalid test profile id');
}
// if a variant is supplied then force the user in to that test
if (!!this.opts.variant) {
this.addParticipation(this.profile.id, this.opts.variant);
}
return this;
};
/**
* Represents the minimum end of the range of a user's ID. Ie. Allocation
* of user ID's should be evenly distributed between min and max to ensure
* the segmention is fair.
* @type {Number}
*/
Ab.prototype.min = 0;
Ab.prototype.max = 1000;
/**
* Represents the upper end of the range of a user's ID.
* @type {Number}
*/
Ab.prototype.max = 1000000;
/**
* The localStorage key of the user id.
* @type {String}
*/
Ab.prototype.uidKey = 'ab__uid';
/**
* If set, the allocation of the uidKey is generated with a seeded random
* number. This makes the uid deterministic, while making the allocation evenly
* distributed from 0 to 1 across all test subjects. A good idea is to seed the
* number with a persistant string or integer held externally, for example a
* logged in user account. This ensures the user gets allocated in to the same
* test bucket and variant across devices, sessions etc. even if the
* localStorage data is disgarded.
*
* Ref: https://github.com/davidbau/seedrandom
*
* @type {String}
*/
Ab.prototype.seed = undefined;
/**
* The localstorage key prefix for each AB test.
* @type {String}
*/
Ab.prototype.storagePrefix = 'ab__';
/**
* The test profile.
* @type {Object}
*/
Ab.prototype.profile = {};
/**
* Each test can be marked as complete (AKA. a conversion), so this property
* represents the state of the test for ecah user.
* @type {Boolean}
*/
Ab.prototype.isComplete = false;
/**
* The valid pattern for each AB test's ID.
* @type {RegExp}
*/
Ab.prototype.idValidator = /^[a-z0-9-]{1,10}$/;
/**
* Gets the state of the user for this experiment
* @return {Object} An object representing that state
*/
Ab.prototype.getParticipation = function () {
"use strict";
var db = localStorage.getItem(this.storagePrefix);
return (db) ? JSON.parse(db) : {};
"use strict";
var db = localStorage.getItem(this.storagePrefix);
return (db) ? JSON.parse(db) : {};
};
/**
* Remove a user's participation from an experiment
* @return {Boolean}
*/
Ab.prototype.removeParticipation = function () {
"use strict";
return localStorage.removeItem(this.storagePrefix);
"use strict";
return localStorage.removeItem(this.storagePrefix);
};
/**
* Allow a user to join an experiment
*/
Ab.prototype.addParticipation = function(test, variantId) {
"use strict";
localStorage.setItem(this.storagePrefix, JSON.stringify({
"id": test,
"variant": variantId
}));
"use strict";
localStorage.setItem(this.storagePrefix, JSON.stringify({
"id": test,
"variant": variantId
}));
};
/**
* Tests whether the experiment has expired
* @return {Boolean}
*/
Ab.prototype.hasExpired = function () {
"use strict";
return (new Date() > this.profile.expiry);
"use strict";
return (new Date() > this.profile.expiry);
};
/**
* Remove a user's participation from an experiment
* @return {Object}
*/
Ab.prototype.clean = function () {
"use strict";
this.removeParticipation();
"use strict";
this.removeParticipation();
return this;
};
/**
* Puts the user in a test variant
* @return {Object}
*/
Ab.prototype.segment = function () {
"use strict";
"use strict";
var smallestTestId = this.max * this.profile.audienceOffset,
largestTestId = smallestTestId + this.max * this.profile.audience;
var smallestTestId = this.max * this.profile.audienceOffset,
largestTestId = smallestTestId + this.max * this.profile.audience;
// deterministically allocate the user in to a test variant
var allocateVariant = function (id, profile) {
return profile.variants[id % profile.variants.length].id;
};
// check if not already a member of this experiment
if (this.getParticipation().id === this.profile.id) {
return false;
}
// deterministically allocate the user in to a test variant
var allocateVariant = function (id, profile) {
return profile.variants[id % profile.variants.length].id;
};
// check if not already a member of this experiment
if (this.getParticipation().id === this.profile.id) {
return this;
}
// check the test has not passed it's expiry date
if (this.hasExpired()) {
return false;
}
// check the test has not passed it's expiry date
if (this.hasExpired()) {
return this;
}
// check the test can be exectuted in this context
if (!this.profile.canRun.call(this)) {
return false;
}
// check the test can be exectuted in this context
if (!this.profile.canRun.call(this)) {
return this;
}
if (smallestTestId <= this.getId() && largestTestId > this.getId()) {
var variant = allocateVariant(this.getId(), this.profile);
this.addParticipation(this.profile.id, variant);
return variant;
} else {
this.addParticipation(this.profile.id, 'not-in-test');
}
if (smallestTestId <= this.getId() && largestTestId > this.getId()) {
var variant = allocateVariant(this.getId(), this.profile);
this.addParticipation(this.profile.id, variant);
} else {
this.addParticipation(this.profile.id, 'not-in-test');
}
return this;
};
/**
* Has the user been allocated a permanent AB test ID
* @return {Boolean}
*/
Ab.prototype.hasId = function () {
"use strict";
return !!localStorage.getItem(this.uidKey);
"use strict";
return !!localStorage.getItem(this.uidKey);
};
/**
* Get the user's AB test ID
* @return {Boolean}
*/
Ab.prototype.getId = function () {
"use strict";
return parseInt(localStorage.getItem(this.uidKey));
"use strict";
return parseInt(localStorage.getItem(this.uidKey));
};
/**
* Set the user's AB test ID
* @return {String}
*/
Ab.prototype.setId = function (n) {
"use strict";
localStorage.setItem(this.uidKey, n);
return n;
"use strict";
localStorage.setItem(this.uidKey, n);
return n;
};
/**
* Allocate the user a permanent test ID
* @return {Object}
*/
Ab.prototype.allocateId = function () {
"use strict";
// TODO for signed in people we should create a key off their user ids, I.e. deterministic
var generateRandomInteger = function(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
};
"use strict";
switch (this.hasId()) {
case true:
return this.getId();
default:
return this.setId(generateRandomInteger(this.min, this.max));
}
var generateRandomInteger = function(min, max, seed) {
var rng = (seed) ? new Math.seedrandom(seed) : Math.random;
return Math.floor(rng() * (max - min + 1) + min);
};
switch (this.hasId()) {
case true:
return this.getId();
default:
return this.setId(generateRandomInteger(this.min, this.max, this.seed));
}
};
/**
* Run the AB test
* @return {Object}
*/
Ab.prototype.run = function () {
"use strict";
var belongsTo = this.getParticipation().variant;
this.profile.variants.forEach(function (v) {
if (v.id === belongsTo) {
v.test.call();
}
});
"use strict";
var belongsTo = this.getParticipation().variant;
this.profile.variants.forEach(function (v) {
if (v.id === belongsTo) {
v.test.call();
}
});
return this;
};
// a conversion
/**
* Mark the AB test as complete, IE. a successful conversion
* @return {Object}
*/
Ab.prototype.complete = function () {
"use strict";
this.isComplete = true;
"use strict";
this.isComplete = true;
return this;
};
module.exports = Ab;

@@ -9,2 +9,3 @@ /*global require,describe,beforeEach,it,expect,spyOn*/

// mock test
var test = {

@@ -53,4 +54,10 @@ id: 'foo',

var a = new Ab(test);
expect(a.getId()).toEqual(852);
expect(a.getId()).toEqual(851230);
});
it("segmentation should be optionally deterministic", function() {
var a = new Ab(test, { seed: 'abc' }).complete();
expect(a.getId()).toEqual(731943);
});

@@ -64,4 +71,3 @@ it('should not reassign the user to audience segment if one already exists', function() {

it('should allocate the user to a test variant', function() {
var a = new Ab(test);
expect(a.segment()).toEqual('A');
new Ab(test).segment();
expect(localStorage.getItem('ab__foo')).toEqual('{"id":"foo","variant":"A"}');

@@ -72,4 +78,3 @@ });

var t = Object.create(test, { audienceOffset: { value: 0.3 } });
var a = new Ab(t);
a.segment();
new Ab(t).segment();
expect(localStorage.getItem('ab__foo')).toEqual('{"id":"foo","variant":"not-in-test"}');

@@ -81,4 +86,3 @@ });

localStorage.setItem('ab__foo', t);
var a = new Ab(test);
a.segment();
new Ab(test).segment();
expect(localStorage.getItem('ab__foo')).toEqual(t);

@@ -92,4 +96,3 @@ });

}});
var a = new Ab(t);
a.segment();
new Ab(t).segment();
expect(localStorage.getItem('ab__foo')).toBeNull();

@@ -99,4 +102,3 @@ });

it("Mark the test as complete", function() {
var a = new Ab(test);
a.complete();
var a = new Ab(test).complete();
expect(a.isComplete).toBeTruthy();

@@ -107,4 +109,3 @@ });

var t = Object.create(test, { audienceOffset: { value: 0.3 } });
var a = new Ab(t, { variant: 'B' });
a.segment();
new Ab(t, { variant: 'B' }).segment();
expect(localStorage.getItem('ab__foo')).toEqual('{"id":"foo","variant":"B"}');

@@ -116,4 +117,3 @@ });

var t = Object.create(test, { expiry: { value: dateInThePast }});
var a = new Ab(t);
a.segment();
new Ab(t).segment();
expect(localStorage.getItem('ab__foo')).toBeNull();

@@ -123,4 +123,3 @@ });

it('should remove participation from a test', function () {
var a = new Ab(test);
a.segment();
var a = new Ab(test).segment();
expect(localStorage.getItem('ab__foo')).toEqual('{"id":"foo","variant":"A"}');

@@ -130,3 +129,3 @@ a.clean();

});
});

@@ -139,5 +138,3 @@

spyOn(variant, 'test');
var a = new Ab(test);
a.segment();
a.run();
new Ab(test).segment().run();
expect(variant.test.calls.count()).toBe(1);

@@ -151,4 +148,3 @@ });

it('should return the variant of a test that current user is participating in', function () {
var ab = new Ab(test);
ab.segment();
var ab = new Ab(test).segment();
expect(ab.getParticipation().variant).toBe('A');

@@ -155,0 +151,0 @@ });

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc