Skip to content
Permalink
Browse files

feat: implement components for managing active experiments

BREAKING CHANGE: drop ability to set variant using <Experiment />
  • Loading branch information...
danhayden committed May 4, 2018
1 parent 72ef519 commit b2903838ae3f28e21eb59f2b9661ccd55218b66b
Showing with 430 additions and 163 deletions.
  1. +9 −3 .eslintrc.js
  2. +0 −5 .prettierrc
  3. +1 −2 LICENSE.md
  4. +133 −26 README.md
  5. +25 −20 package.json
  6. +45 −27 rollup.config.js
  7. +7 −7 src/pick-by-weight.js
  8. +171 −51 src/react-simple-experiment.js
  9. +5 −5 src/react-simple-experiment.stories.js
  10. +34 −17 src/storage.js
@@ -1,11 +1,17 @@
module.exports = {
parser: 'babel-eslint',
extends: ['unobtrusive', 'unobtrusive/react'],
parser: "babel-eslint",
extends: [
"plugin:react/recommended",
"standard",
"prettier",
"prettier/react",
"prettier/standard"
],
env: {
browser: true,
node: true
},
globals: {
Promise: true
}
}
};

This file was deleted.

Oops, something went wrong.
@@ -1,5 +1,4 @@
The MIT License (MIT)
=====================
# The MIT License (MIT)

Copyright © 2017 Dan Hayden

159 README.md
@@ -1,56 +1,163 @@
# React Simple Experiment

[![version][version]](http://npm.im/react-simple-experiment)
[![MIT License][MIT License]](http://opensource.org/licenses/MIT)
[![Standard Version][Standard Version]](https://github.com/conventional-changelog/standard-version)
[![Size][Size]](https://unpkg.com/react-simple-experiment)
[![Size gzip][Size gzip]](https://unpkg.com/react-simple-experiment)
[![MIT License][mit license]](http://opensource.org/licenses/MIT)
[![Standard Version][standard version]](https://github.com/conventional-changelog/standard-version)
[![Size][size]](https://unpkg.com/react-simple-experiment)
[![Size gzip][size gzip]](https://unpkg.com/react-simple-experiment)

Simple A/B testing for React

## Usage

### Experiment & Variant

```js
import React from 'react'
import {Experiment, Variant} from 'react-simple-experiment'
import React from "react";
import { Experiment, Variant } from "react-simple-experiment";
export default function MyABTest () {
export default function MyABTest() {
return (
<Experiment
name='Experiment Name'
name="Experiment Name"
onLoad={(name, variant) => console.log(name, variant)}
>
<Variant name='Variant 1 Name' weight={60}>
<div>
Variant 1
</div>
<Variant name="Variant 1 Name" weight={60}>
<div>Variant 1</div>
</Variant>
<Variant name='Variant 2 Name' weight={20}>
<div>
Variant 2
</div>
<Variant name="Variant 2 Name" weight={20}>
<div>Variant 2</div>
</Variant>
<Variant name='Variant 3 Name' weight={20}>
<div>
Variant 3
</div>
<Variant name="Variant 3 Name" weight={20}>
<div>Variant 3</div>
</Variant>
</Experiment>
)
);
}
```

Once a variant has been chosen the choice is persisted to browser storage (using [localForage]) to ensure the same variant is shown on return visits

### ExperimentQueryString

If you use react-router-dom in your project `<ExperimentQueryString />` component allows you can set experimant variants using a query string, like so:

Experiment query strings can now be formatted as below:
`?exp=experimentId1||variantId&exp=experimentId2||variantId`

```js
import { ExperimentQueryString } from "react-simple-experiment";
import { Route } from "react-router-dom";
<Route component={ExperimentQueryString} />;
```

you can customise `querystringName` and `querystringDivider` using props:

```js
import { ExperimentQueryString } from "react-simple-experiment";
import { Route } from "react-router-dom";
<Route
render={({ location }) => (
<ExperimentQueryString
querystringName="variant"
querystringDivider="__"
location={location}
/>
)}
/>;
```

Experiment query strings can now be formatted as below:
`?variant=experimentId1__variantId&variant=experimentId2__variantId`

### ExperimentManager

If you'd like to manipulate active experiment varaints within a component, perhaps for debugging, you can use the `<ExperimentManager/ >` component which takes a function for its children prop.
The render function exposes the following four functions:

```js
getExperiments().then(experiments => {
console.log(experiments);
});
// { experimentId1: variantId, experimentId2: variantId, ... }
getExperiment(experimentId).then(variantId => {
console.log(variantId);
});
// variantId
setExperiment(experimentId, variantId).then(variantId => {
console.log(variantId);
});
// variantId
removeExperiment(experimentId).then(undefined => {});
```

and can be used within a component like so:

```js
import { ExperimentManager } from "react-simple-experiment";
export default function MyExperimentManager() {
return (
<ExperimentManager>
{({ getExperiments, getExperiment, setExperiment, removeExperiment }) => (
<div>
<button
onClick={() => {
getExperiments().then(data => {
console.log(data);
});
}}
>
Get Experiments
</button>
<button
onClick={() => {
getExperiment("test").then(data => {
console.log(data);
});
}}
>
Get "test" experiment
</button>
<button
onClick={() => {
setExperiment("test", "control").then(data => {
console.log(data);
});
}}
>
Set "test" experiment to "control"
</button>
<button
onClick={() => {
removeExperiment("test").then(data => {
console.log(data);
});
}}
>
Remove "test" experiment
</button>
</div>
)}
</ExperimentManager>
);
}
```

### License

MIT

[version]: https://img.shields.io/npm/v/react-simple-experiment.svg
[MIT License]: https://img.shields.io/npm/l/react-simple-experiment.svg
[Standard Version]: https://img.shields.io/badge/release-standard%20version-brightgreen.svg
[Size]: https://badges.herokuapp.com/size/npm/react-simple-experiment
[Size gzip]: https://badges.herokuapp.com/size/npm/react-simple-experiment?gzip=true
[localForage]: https://github.com/localForage/localForage
[mit license]: https://img.shields.io/npm/l/react-simple-experiment.svg
[standard version]: https://img.shields.io/badge/release-standard%20version-brightgreen.svg
[size]: https://badges.herokuapp.com/size/npm/react-simple-experiment
[size gzip]: https://badges.herokuapp.com/size/npm/react-simple-experiment?gzip=true
[localforage]: https://github.com/localForage/localForage
@@ -20,7 +20,7 @@
"lint": "prettier 'src/**/*.{js,json}' --write",
"build": "rollup -c",
"dev": "rollup -c -w",
"changelog": "standard-version",
"release": "standard-version",
"size": "npx gzip-size-cli ./dist/react-simple-experiment.js ",
"precommit": "lint-staged",
"prepush": "npm test",
@@ -39,33 +39,38 @@
"dependencies": {
"babel-plugin-external-helpers": "6.22.0",
"is-finite": "1.0.2",
"localforage": "1.5.5",
"prop-types": "15.6.0",
"localforage": "1.7.1",
"prop-types": "15.6.1",
"query-string": "5.0.1"
},
"devDependencies": {
"@storybook/react": "3.2.17",
"babel-eslint": "8.0.3",
"@storybook/react": "3.4.3",
"babel-core": "6.26.3",
"babel-eslint": "8.2.3",
"babel-preset-env": "1.6.1",
"babel-preset-react": "6.24.1",
"babel-preset-stage-2": "6.24.1",
"eslint": "4.13.0",
"eslint-config-unobtrusive": "1.2.0",
"eslint-plugin-react": "7.5.1",
"eslint": "4.19.1",
"eslint-config-prettier": "2.9.0",
"eslint-config-standard": "11.0.0",
"eslint-plugin-import": "2.11.0",
"eslint-plugin-node": "6.0.1",
"eslint-plugin-promise": "3.7.0",
"eslint-plugin-react": "7.7.0",
"eslint-plugin-standard": "3.1.0",
"husky": "0.14.3",
"lint-staged": "6.0.0",
"prettier": "1.9.1",
"prettier-eslint-cli": "4.4.2",
"react": "16.2.0",
"react-dom": "16.2.0",
"rollup": "0.52.1",
"rollup-plugin-babel": "3.0.2",
"rollup-plugin-commonjs": "8.2.6",
"rollup-plugin-node-resolve": "3.0.0",
"rollup-plugin-uglify": "2.0.1",
"lint-staged": "7.0.5",
"prettier": "1.12.1",
"react": "16.3.2",
"react-dom": "16.3.2",
"rollup": "0.58.2",
"rollup-plugin-babel": "3.0.4",
"rollup-plugin-commonjs": "9.1.3",
"rollup-plugin-node-resolve": "3.3.0",
"rollup-plugin-uglify": "3.0.0",
"rollup-watch": "4.3.1",
"standard-version": "4.2.0",
"uglify-es": "3.2.1"
"standard-version": "4.3.0",
"uglify-es": "3.3.9"
},
"repository": {
"type": "git",
@@ -1,46 +1,64 @@
import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import babel from 'rollup-plugin-babel'
import uglify from 'rollup-plugin-uglify'
import {minify} from 'uglify-es'
import fs from 'fs'
const pkg = JSON.parse(fs.readFileSync('./package.json'))
import resolve from "rollup-plugin-node-resolve";
import commonjs from "rollup-plugin-commonjs";
import babel from "rollup-plugin-babel";
import uglify from "rollup-plugin-uglify";
import { minify } from "uglify-es";
import fs from "fs";
const pkg = JSON.parse(fs.readFileSync("./package.json"));

const name = "react-simple-experiment";
const globals = {
react: "React",
"prop-types": "PropTypes",
"pick-one-by-weight": "pickOneByWeight",
localforage: "localforage"
};

export default {
name: 'ReactSimpleExperiment',
input: 'src/react-simple-experiment.js',
output: {
name: "ReactSimpleExperiment",
globals: {
react: "React",
"prop-types": "PropTypes",
"pick-one-by-weight": "pickOneByWeight",
localforage: "localforage"
}
},
input: "src/react-simple-experiment.js",
output: [
{format: 'es', file: pkg.module},
{format: 'cjs', file: pkg.main},
{format: 'umd', file: pkg['umd:main']}
{ name, globals, format: "es", file: pkg.module },
{ name, globals, format: "cjs", file: pkg.main },
{ name, globals, format: "umd", file: pkg["umd:main"] }
],
external: ['react', 'prop-types', 'pick-one-by-weight', 'localforage'],
globals: {
react: 'React',
'prop-types': 'PropTypes',
'pick-one-by-weight': 'pickOneByWeight',
localforage: 'localforage'
},
external: ["react", "prop-types", "pick-one-by-weight", "localforage"],
plugins: [
resolve(),
commonjs({exclude: 'src/**'}),
commonjs({ exclude: "src/**" }),
babel({
babelrc: false,
exclude: 'node_modules/**',
exclude: "node_modules/**",
presets: [
[
'babel-preset-env',
"babel-preset-env",
{
modules: false,
loose: true,
targets: {browsers: ['last 2 versions', '> 1%']}
targets: {
browsers: [
"last 2 Chrome versions",
"last 2 Edge versions",
"last 2 Firefox versions",
"last 2 Safari versions",
"Explorer 11"
]
}
}
],
'babel-preset-react',
'babel-preset-stage-2'
"babel-preset-react",
"babel-preset-stage-2"
],
plugins: ['external-helpers']
plugins: ["external-helpers"]
}),
uglify({}, minify)
]
}
};
Oops, something went wrong.

0 comments on commit b290383

Please sign in to comment.
You can’t perform that action at this time.