Giter Club home page Giter Club logo

alephbet's Introduction

AlephBet

AlephBet is a pure-javascript A/B (multivariate) testing framework for developers.

Key Features:

  • A choice of tracking backends:
    • AWS Lambda with Lamed (recommended)
    • Ruby on Rails with the alephbet rubygem (still experimental)
    • Gimel (no longer recommended)
  • Weighted variants. See #20
  • user-based / cross-device experiments. See #16
  • Pluggable backends: event tracking (defaults to Google Universal Analytics), and storage (defaults to localStorage)
  • Supports multiple variants and goals
  • Tracks unique visitors and goal completions
  • Flexible triggers
  • Ideal for use with page and fragment caching
  • Developer-friendly for both usage and contirbution (using npm / webpack)

What does AlephBet mean?

Aleph (אלף) Bet (בית) are the first two letters in the Hebrew alphabet. Similar to A and B.

Inspiration

AlephBet was heavily inspired by Optimizely (sans WYSIWYG and reporting) and Cohorts.js. The code structure and some code elements were taken from cohorts.js, with some notable changes to terminology and built-in support for unique goals and visitor tracking.

For more detailed info about the background and rationale for creating AlephBet, please check out this blog post

Screencast

AlephBet Screencast

code snippet used on the screencast

for more screencasts, tips and info, please check the wiki

Quick Start

  • Make sure your Google Universal analytics is set up.
  • Download and include alephbet.min.js in the head section of your HTML.
  • Or import it in your javascript code
import AlephBet from "alephbet" # ES6 module syntax
const AlephBet = require("alephbet") # commonJS syntax
  • Create an experiment:
import AlephBet from "alephbet";

const button_color_experiment = new AlephBet.Experiment({
  name: 'button color',  // the name of this experiment; required.
  variants: {  // variants for this experiment; required.
    blue: {
      activate: function() {  // activate function to execute if variant is selected
        $('#my-btn').attr('style', 'color: blue;');
      }
    },
    red: {
      activate: function() {
        $('#my-btn').attr('style', 'color: red;');
      }
    }
  },
});
  • Track goals for your experiment:
// creating a goal
const button_clicked_goal = new AlephBet.Goal('button clicked');
$('#my-btn').on('click', function() {
  // The chosen variant will be tied to the goal automatically
  button_clicked_goal.complete();
});

// adding experiment to the goal
button_clicked_goal.add_experiment(button_color_experiment);

// alternatively - add the goal to the experiment
button_color_experiment.add_goal(button_clicked_goal);

// tracking non-unique goals, e.g. page views
const page_views = new AlephBet.Goal('page view', {unique: false});
  • view results on your Google Analytics Event Tracking Section. The experiment name + variation will be assigned to actions, and Visitors or Goals to label. e.g.

    • action: button color | red, label: Visitors : unique count of visitors assigned to the red variant.
    • button color | blue, button clicked : unique visitors clicking on the button assigned to the blue variant.
    • button color | red, viewed page : count of pages viewed by all visitors (not-unique) after the experiment started.
  • important note: whilst Google Analytics is the easiest way to start playing with Alephbet, it's definitely not the best way to use it. GA starts sampling events after you reach a certain volume, and the built-in GA adapter does not support more advanced features like cross-device tracking. If you're serious about running A/B tests, I would urge you to consider using Lamed, Alephbet-Rails or another backend instead.

Advanced Usage

Recommended Usage Pattern

AlephBet was meant to be used across different pages, tracking multiple goals over simultaneous experiments. It is therefore recommended to keep all experiments in one javascript file, shared across all pages. This allows sharing goals across different experiments. Experiments can be triggered based on a set of conditions, allowing to fine-tune the audience for the experiments (e.g. mobile users, logged-in etc).

Triggers

Experiments automatically start by default. However, a trigger function can be provided, to limit the audience or the page(s) where the experiment "kicks-off".

import AlephBet from "alephbet";

const button_color_experiment = new Alephbet.Experiment({
  name: 'button color',
  trigger: () => {
    return window.location.href.match(/pricing/);
  },
  variants: { // ...
  },
});

// triggers can be assigned to a variable and shared / re-used
const logged_in_user = function() { return document.cookie.match(/__session/); };
const mobile_browser = function() { // test if mobile browser };

const big_header_experiment = new Alephbet.Experiment({
  name: 'big header',
  trigger: () => { return logged_in_user() && mobile_browser(); },
  // ...
});

NOTE: once a user participates in an experiment, the trigger is no longer checked. See #9

Sample size

You can specify a sample float (between 0.0 and 1.0) to limit the number of visitors participating in an experiment.

Weights

Whilst sample will limit the entire experiment to a subset of potential participants, weight allows you to apply a weighted-random selection of variants. This can be considered a first step (manual) way to implement Multi Armed Bandit testing.

NOTE: Weights can be any integer value. Do not use floats. You can use any number, but it's probably easiest to treat it as a percentage, e.g. use weights of 80, 20 to allocate ~80% to one variant vs. ~20% to the other.

import {Experiment} from "alephbet"

const button_color_experiment = new Experiment({
  name: 'button color',  // the name of this experiment; required.
  variants: {  // variants for this experiment; required.
    blue: {
      activate: function() {  // activate function to execute if variant is selected
        $('#my-btn').attr('style', 'color: blue;');
      },
      weight: 50 // optional, can be any integer value
    },
    red: {
      activate: function() {
        $('#my-btn').attr('style', 'color: red;');
      },
      weight: 50
    }
  },
});

Visitors

Visitors will be tracked once they participate in an experiment (and only once). Once a visitor participates in an experiment, the same variant will always be shown to them. If visitors are excluded from the sample, they will be permanently excluded from seeing the experiment. Triggers however will be checked more than once, to allow launching experiments under specific conditions for the same visitor.

User-based / Cross-device tracking

You can now pass a user_id to the experiment as an optional parameter. This allows experiment to work across devices on a per-user basis.

import AlephBet from "alephbet";

const button_color_experiment = new Alephbet.Experiment({
  name: 'button color',
  user_id: get_user_id(),  // pass over the unique user id bound to this experiment
  trigger: () => {
    // do not trigger this expeirment without a user_id
    return get_user_id() && other_condition();
  },
  variants: {  // variants for this experiment; required.
    blue: {
      activate: function() {  // activate function to execute if variant is selected
        $('#my-btn').attr('style', 'color: blue;');
      }
    },
    red: {
      activate: function() {
        $('#my-btn').attr('style', 'color: red;');
      }
    }
  },
});

// do not assign goals without a user_id
if (get_user_id()) {
  button_color_experiment.add_goal(my_goal);
}

Notes:

  • For user-based tracking, make sure you always have a user_id. Do not mix visitors (without an id) and users (with an id) in the same experiment.
  • Cross-device tracking only works with the Lamed or Alephbet-Rails tracking backends. It does not work with Google Analytics.

See this Wiki page for more information

Goals

Goals are uniquely tracked by default. i.e. if a goal is set to measure how many visitors clicked on a button, multiple clicks won't generate another goal completion. Only one per visitor. Non-unique goals can be set by passing unique: false to the goal when creating it.

Goals will only be tracked if the experiment was launched and a variant selected before. Tracking goals is therefore safe and idempotent (unless unique is false).

Here's a short sample of tracking multiple goals over multiple experiments:

import AlephBet from "alephbet";

// main goal - button click
const button_click_goal = new AlephBet.Goal('button click');
$('#my-btn').on('click', function() {
  button_clicked_goal.complete();
});

// engagement - any click on the page
const engagement = new AlephBet.Goal('engagement');
$('html').on('click', function() {
  engagement.complete();
});

const all_goals = [button_click_goal, engagement];

// experiments
const button_color_experiment = new AlephBet.Experiment({ /* ... */ });
const buy_button_cta_experiment = new AlephBet.Experiment({ /* ... */ });

// adding all goals to experiments
_(all_goals).each(function (goal) {
  button_color_experiment.add_goal(goal);
  buy_button_cta_experiment.add_goal(goal);
});

// alternatively, you can use the add_goals method and pass it an array of goals
button_color_experiment.add_goals(all_goals);
buy_button_cta_experiment.add_goals(all_goals);

Custom Tracking Adapter

AlephBet comes with a built-in Google Analytics adapter and several adapters with potentially better accuracy:

Persistent Queue GA Adapter

Persistent Queue Keen Adapter

Alephbet adapter - a generic adapter

Lamed adapter

Gimel adapter

Creating custom adapters is however very easy.

Here's an example for integrating an adapter for keen.io

(For a more complete implementation, you should use the built-in Alephbet.PersistentQueueKeenAdapter)

<script src="https://d26b395fwzu5fz.cloudfront.net/3.2.4/keen.min.js" type="text/javascript"></script>
<script src="alephbet.min.js"></script>
<script type="text/javascript">
    window.keen_client = new Keen({
        projectId: "ENTER YOUR PROJECT ID",
        writeKey: "ENTER YOUR WRITE KEY"
    });
    const tracking_adapter = {
        experiment_start: function(experiment, variant) {
            keen_client.addEvent(experiment.name, {variant: variant, event: 'participate'});
        },
        goal_complete: function(experiment, variant, event_name, _props) {
            keen_client.addEvent(experiment.name, {variant: variant, event: event_name});
        }
    };
    const my_experiment = new AlephBet.Experiment({
        name: 'my experiment',
        variants: { // ...
        },
        tracking_adapter: tracking_adapter,
        // ...
    });
</script>

Custom Storage Adapter

Similar to the tracking adapter, you can customize the storage adapter. AlephBet uses localStorage by default, but if you want to use cookies or customize how data is persisted on the client, creating an adapter is very easy.

Here's a simple example of a cookie storage adapter with expiry of 30 days, using js-cookie:

<script src="/path/to/js.cookie.js"></script>
<script type="text/javascript">
    // NOTE: using JSON stringify / parse to allow storing more complex values
    const storage_adapter = {
        set: function(key, value) {
            Cookies.set(key, JSON.stringify(value), {expires: 30});
        },
        get: function(key) {
            try { return JSON.parse(Cookies.get(key)); } catch(e) { return Cookies.get(key); }
        }
    };
    const my_experiment = new AlephBet.Experiment({
        name: 'my experiment',
        variants: { // ...
        },
        storage_adapter: storage_adapter,
        // ...
    });
</script> 

Debug mode

To set more verbose logging to the browser console, use

import Alephbet from "alephbet"

AlephBet.options.debug = true`

Other install options

  • download alephbet.min.js from the dist folder on github
  • npm install alephbet
  • bower install alephbet

Analyzing results

AlephBet is built for developers, so there's no fancy interface or WYSIWYG editors. The best way to analyze the results and determine the best variant from an experiment is to look at the raw data and calculate the statistical significance. A couple of recommended resources:

Development

Commands

  • yarn run build - to build distribution files
  • yarn run watch - will watch files and re-build using jest

License

AlephBet is distributed under the MIT license. All 3rd party libraries and components are distributed under their respective license terms.

Copyright (C) 2015 Yoav Aner

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

alephbet's People

Contributors

azd325 avatar dependabot[bot] avatar gingerlime avatar joker-777 avatar szymansd avatar zwacky 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

alephbet's Issues

Events not posted to GA

Can you please help me with the following set up? I tried creating a basic A/B test to evaluate Alephbet. I do not see aany errors in the code. However there are no events created in Google Analytics for the experiment.

I enabled debug mode, and see this in the console.
running with options: {"name":"button color","variants":{"green":{},"red":{}},"user_id":null,"sample":1}

I was also able to post a test event to Google Analytics, but the A/B test event fired by Alephbet does not show. Is there anything missing in the code? Thanks for checking.

Here is the complete page source code:

https://gist.github.com/shankarps/6dffaf3c98454fc73a21b93f2b67bcb2

Uncaught TypeError: AlephBet.Goal is not a constructor

Alephbet 0.21.0+

<html>
<head>
<script src="https://unpkg.com/[email protected]/dist/alephbet.min.js"></script>
<script type="text/javascript">

payment = new AlephBet.Goal('payment completed');
</script>
</head>
</html>

raises Uncaught TypeError: AlephBet.Goal is not a constructor

Alephbet 0.20.0

works fine

<html>
<head>
<script src="https://unpkg.com/[email protected]/dist/alephbet.min.js"></script>
<script type="text/javascript">

payment = new AlephBet.Goal('payment completed');
</script>
</head>
</html>

Store.js dependency is out of date (causing Alephbet to fail in Safari private mode)

Alephbet throws an error when local storage is not available (which actually caused our app to completely stop functioning). This error occurs in private browsing mode on Safari, due to the fact that the older version of store.js does not fall back to cookies. The 2.0 version of store.js does handle this case, however. It would be great if Alephbet updated this dependency, because it does not fail currently gracefully when localstorage is unavailable and the only alternative is to use a storage adapter that uses cookies when localstorage can't be used. If you need any more info, please let me know. Thank you!

link better the goal and the experiment

It feels like the goal is hanging away from the experiment, not sure why. I would find clearer to have

var button_color_experiment = new AlephBet.Experiment({ 
   name: 'button color',  // the name of this experiment; required.
   variants: {  // variants for this experiment... }
   goals: {  
     'button clicked': function (goal) { $('#my-btn').on("click", function() {goal.complete()}:}
  }
});

This shouldn't be instead of the existing syntax (being able to re-use a goal between experiment is probably a use case), but would help for the "usual" case to keep the goal(s) at the same place of the variants

minified version has similar size to non-minified ...

After releasing 0.18.0 I realised that the minified and non-minified versions have a very similar size somehow (around 250kb)... even though the minified version "looks" minified, so should be smaller... Not entirely sure what's going on

Uncaught TypeError: localStorage.set is not a function

Tested with a local web server (python -m http.server 9000) with this html file inside the root folder of the git repo

<html>
<head>
<script src="dist/alephbet.min.js"></script>
<script type="text/javascript">

payment = new AlephBet.Goal('payment completed');
track_url = "http://example.com";
namespace = "alephbet";

AlephBet.options.debug = true;

experiment = new AlephBet.Experiment({
  name: "my a/b test",
  tracking_adapter: new AlephBet.GimelAdapter(track_url, namespace),
  // trigger: function() { ... },  // optional trigger
  variants: {
    red: {
      activate: function() {
      // add your code here
      }
    },
    blue: {
      activate: function() {
      // add your code here
      }
    }
  }
});

experiment.add_goals([payment]);
payment.complete()

</script>
</head>
</html>

Going to http://localhost:9000/file.html produces the error

CleanShot 2021-01-08 at 08 25 42

Unfortunately it's hard to debug the minified code. If I try to use dist/alephbet.js instead, then it fails with a different error completely (Uncaught ReferenceError: exports is not defined)...

Any reason you haven't wired this up to Mixpanel?

I've read all of the tutorials and I think you've done an awesome job with this project and Gimel. After doing our own analysis, we were thinking of wiring this up to Mixpanel since we already use it and are comfortable with it. Have you experimented with this at all and did you find any reasons not to use it? I'd just like to avoid any pitfalls in the event you chose not to include it as a tracking adapter option.

Is it possible to force Alephbet to use a specific variant in development?

Hey,

I've created an experiment but I cannot see the altered version in my dev environment. The browser console shows the first variant being selected:

running with options: {"name":"my-test-name","tracking_adapter":{"url":"https://url.com/","namespace":"gimel-test","_queue":[]},"variants":{"1-Original":{},"2-Variant":{}},"user_id":null,"sample":1}
1-Original active

Is it possible to force a particular variant, e.g. the '2-Variant' test above, to be used in development?

Thanks!
Jon

user-based / cross-device tracking

problem

It's quite common today for users to use the same web app across different devices. Users might create an account on the desktop and do a few things. Then login again from their mobile or tablet and continue from where they left, or vice versa.

challenge

This creates a challenge for A/B testing for two main reasons:

  1. With an A/B test running, the same user might get a completely different experience when they switch between devices. This is confusing and potentially frustrating for users.
  2. Tracking conversions is inaccurate. A user might use variation A on one device, but then convert on another device where variation B is running. The conversion will therefore be wrongly attributed.

user-based or cross-device tracking can solve both those problems, as long as the user can be uniquely identified across all devices. This isn't particularly difficult given that most web apps require logging-in to use the same account.

method

With this in mind, it's possible to make Alephbet support user-based experiments. To do this, Alephbet needs to:

  1. Get a user identifier (user_id, email, any identifier that uniquely identifies the user)
  2. Use this identifier to:
    a. pick a variant that is psuedo-random, but consistent for the user_id. This can be done with deterministic assignment through hashing, instead of completely random. See planout
    b. generate a pseudo-uuid for goals. When goals are tracked, we already send a uuid. We can generate a pseudo-uuid for the user and experiment, so even if the user completes a goal across different devices, it will only be counted once

limitations

There are a couple of limitations with this approach:

  1. Some experiments are designed for visitors, before they register. This approach won't work in those cases. Experiments that employ user-based tracking must target only already-identified users.
  2. Not all backends support uuids. Gimel does. And so does Keen.io. Google Analytics internally counts unique events, so I'm not sure if it can work with a custom uuid(?)

Implementation

The implementation needs to change a couple of aspects of Alephbet:

  1. Allow users to specify a user_id, and use it to assign users to variants.
  2. Pass the user_id, as well as whether or not the goal is unique to the tracking adapters, so those can decide when to pass a consistent uuid (for unique goals), or a random one (for non-unique goals).

This might introduce some breaking changes, although it can be kept to a relative minimum, and most behaviour can be kept backwards-compatible if no user_id is specified (to also support visitor experiments).

Multiple tracking back-ends?

Hi!

I founds this project to be quite interesting for us, as it enables a very simple and intuitive way for us to get started with AB testing.

However, I would like to understand if it is possible to run multiple tracking backends?

For our specific use-case, we would want to run the Gimel backend for a simple-to-use, "just works", setup where we can evaluate tests that have clear goals. But, we would also want to send events to our own event pipeline to be able to perform more advanced analytics.

Is there a preferred way of enabling this in the current structure? I saw the section on adding a custom tracker, but I'm not sure what the best way of sending events to multiple backends would be.

I would assume that the best way would be to copy the Gimel Persistent Queue adapter and extend the code in the "_track" method to also push to the dataLayer (our in-house pipeline uses Google Tag Manager).

The alternative to this would be to extend the core to allow a list of tracking_adapter(s) and call "_track" on each one.

Do you have any specific opinion in this matter?

Cheers,
Patrik

Cannot find `lodash.custom` when used with mocha

When I test a file that uses alephbet with mocha, it complains that it cannot find module lodash.custom, which is linked through the browser field in your package.json. Have you encountered this before? Do you have any workarounds?

As a quick brain dump, the following might fix the problem:

  • A fork which uses vanilla lodash, for those of use that already use the entire lodash library and are using this with webpack
  • Use a relative path to grab lodash.custom instead of a full path

Thank you! Love the module.

trigger behaviour

The trigger function allows you to specify the condition(s) to participate in an experiment. This is very important to select the target audience.

Currently however, the trigger is checked every time the experiment is loaded, and the activate function won't be called unless the trigger evaluates to true. This means that when a user meets a certain criteria to participate in an experiment, but later no longer meets the criteria - their experience might be inconsistent. For example, if we want to trigger the experiment to visitors (non registered users), it might be odd if the experience changes once they register.

It might therefore make more sense to evaluate the trigger function only for determining initial participation in an experiment. Once "triggered", the participant stays on the experiment.

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.