Skip to content

Commit 0a30248

Browse files
author
Guy Aridor
committed
Merge pull request #11 from HubSpot/guy_higher_order_component
enable higher order component for assigning experiment params
2 parents b8f83f8 + 41afe75 commit 0a30248

15 files changed

+261
-97
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Changes in 2.1
2+
- Added the ```parametrize``` function that takes in experiment information and a component and parametrizes the component with the experiment parameters as props.
3+
- Added the requirement to pass in an array of experiment parameters as props to the Parametrize component and removed the experimentName prop.

README.md

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,49 +11,43 @@ npm install react-experiments
1111

1212
# Usage
1313

14-
react-experiments was built to work with [PlanOut.js](https://www.github.com/HubSpot/PlanOut.js) and most of its constructs are inspired by the structure of PlanOut.js. This library will work out of the box if you pass it an instantiated PlanOut Namespace or Experiment class, but if you want to use your own methods of assigning experiment parameters and logging exposure then you can extend the base [experiment class](https://github.com/HubSpot/react-experiments/blob/master/src/experimentClass.js) and pass that as the experiment prop to the Experiment class components.
14+
react-experiments was built to work with [PlanOut.js](https://www.github.com/HubSpot/PlanOut.js) and most of its constructs are inspired by the structure of PlanOut.js. This library will work out of the box if you pass it an instantiated PlanOut Namespace or Experiment class, but if you want to use your own methods of assigning experiment parameters and logging exposure then you can extend the base [experiment class](https://github.com/HubSpot/react-experiments/blob/master/src/experimentClass.js) and pass that as the experiment class prop.
1515

1616

17-
## Implementing an experiment
17+
## Implementing a simple experiment
1818

1919
This library serves as a way to declaratively implement UI experiments that are defined via PlanOut. The standard usage of this library is as follows:
2020

21-
1) Define experiment via PlanOut script / API. The PlanOut parameters that you set should map to the props on which you want to run an experiment. Let's use the [sample PlanOut.js experiment](https://github.com/HubSpot/PlanOut.js/blob/master/examples/sample_planout_es5.js#L41) as an example, which is effectively:
21+
1) Define experiment via PlanOut script / API. The PlanOut parameters that you set should map to the props on which you want to run an experiment. Let's use the [sample PlanOut.js experiment](https://github.com/HubSpot/PlanOut.js/blob/master/examples/sample_planout_es5.js#L41) as an example, which is effectively:
2222

2323
```
2424
signupText = uniformChoice(choices=['Signup', 'Join now'])
2525
```
2626

27-
2) Wrap the component where you want to implement your UI experiment with the Parametrize component provided by the library. As an example,
27+
2) Wrap the component where you want to implement your UI experiment with the parametrize function provided by the library along with an instantiated experiment class and the specific parameter names that you want to parametrize the component with. As an example,
2828

2929
```
30-
<Parametrize experiment={DummyExperiment} experimentName='SampleExperiment'>
31-
<Signup />
32-
</Parametrize>
33-
```
34-
35-
3) Suppose your Signup component looks something like this:
36-
```javascript
30+
const Signup = parametrize(DummyExperiment, ['signupText'], React.createClass({
3731
render() {
3832
return (
3933
<div>
4034
{this.props.signupText}
4135
</div>
4236
);
4337
}
44-
```
45-
46-
Now, you can just use the ```WithExperimentParams``` component provided by the library and wrap the Signup component with it.
47-
```
48-
Signup = withExperimentParams(Signup);
38+
}
39+
}));
4940
```
5041

5142
Now you should be all set to run the sample experiment. The Signup component will render 'Sign up' or 'Join now' based on the randomized parameter assigned by PlanOut.js.
5243

5344
To put it all together,
5445

5546
```javascript
56-
let Signup = React.createClass({
47+
48+
exp = new DummyExperiment({ id: 'this_is_the_user_id'});
49+
50+
let Signup = parametrize(exp, ['signupText'], React.createClass({
5751
render() {
5852
return (
5953
<div>
@@ -63,42 +57,83 @@ let Signup = React.createClass({
6357
}
6458
});
6559

66-
Signup = withExperimentParams(Signup);
67-
exp = new DummyExperiment({ id: 'this_is_the_user_id'});
68-
6960
let Parent = React.createClass({
7061
render() {
7162
...
72-
<Parametrize experiment={exp} experimentName='SampleExperiment'>
73-
<Signup />
74-
</Parametrize>
63+
<Signup />
7564
}
7665
});
7766
```
7867
79-
###Parametrize component
8068
81-
The following are the props for the Parametrize component:
69+
## Base Parametrize Component
70+
71+
The implementation of all the components provided by this library are wrappers around a base ```Parametrize``` component. The ```Parametrize``` component allows for parametrizing a given component with experiment parameters. The following are the props that the ```Parametrize``` component takes:
8272
8373
**experiment**: This is an instance of a PlanOut.js experiment / namespace class or the base experimentClass. [REQUIRED]
8474
85-
**experimentName**: This is the name of the experiment. It is particularly important if you're using a PlanOut.js namespace, since this corresponds to the name of the experiment WITHIN the namespace, not the name of the namespace itself. This is required so that exposure gets logged correctly. [REQUIRED]
75+
76+
**params**: This is the list of experiment parameters that you want to use to parametrize the component. They should correspond to the parameter names defined in your PlanOut script / experiment definition. [REQUIRED]
8677
8778
[any arbitrary prop]: You can pass arbitrary props to the Parametrize component and they will be available via context.experimentProps in all descendants of the Parametrize component.
8879
80+
### Higher-order Parametrization Components
81+
82+
There are two primary higher-order components to use for parametrization.
83+
84+
**parametrize**: The ```parametrize``` function takes an instantiated experiment class, either an experiment name or a list of params, and a React component. It takes the given component and sets the deterministically and randomly assigned experiment parameters of the experiment class as props.
8985
90-
### ABTest component:
86+
```parametrize(exp, ['signupText'], React.createClass({..}));```
87+
88+
**withExperimentParams**: The ```withExperimentParams``` function is used in combination with the base ```Parametrize``` component. It is useful when running an experiment with nested components, but generally the ```parametrize``` function should be preferred.
89+
90+
91+
```javascript
92+
const Parent = React.createClass({
93+
render() {
94+
return (
95+
<Parametrize experiment={exp} params=['signup_form_text', 'signup_nav_text']>
96+
<SignupHeader />
97+
<SignupForm />
98+
</Parametrize>
99+
);
100+
}
101+
});
102+
103+
const SignupHeader = withExperimentParams(React.createClass({
104+
render() {
105+
return (
106+
<div>
107+
{this.props.signup_nav_text}
108+
</div>
109+
);
110+
}
111+
});
112+
113+
const SignupForm = withExperimentParams(React.createClass({
114+
render() {
115+
return (
116+
<div>
117+
{this.props.signup_form_text}
118+
</div>
119+
);
120+
}
121+
});
122+
```
123+
124+
125+
## Running A/B Variation experiments:
91126
92127
There are two common types of experimental parameters:
93128
94129
1) Parameters that correspond to parametrizations of existing variables and components. For instance, if one is running an experiment to test which shade of blue optimizes the click rate of the button, then the values to which your experiment parameters map would correspond to something such as the different hex codes for the different shades of blue.
95130
96131
2) "Branching" parameters where the parameter values correspond to different "variations" of the experiment. For instance, if one is testing two completely different user interfaces then it wouldn't make sense to parametrize every aspect that has changed, but rather to bin users into either 'Variation A' or 'Variation B'.
97132
98-
While the core component of this library focuses on the first type of parameter, it also includes some convenience components built around the Parametrize component for running "branching" experiments.
133+
While the core component of this library focuses on the first type of parameter, it also includes some convenience components built around the Parametrize component for running "branching" experiments using the ```ABTest``` component.
99134
100135
```javascript
101-
<ABTest on='foo' experiment={TestNamespace} experimentName='SimpleExperiment' shouldEnroll={this.shouldEnroll()}>
136+
<ABTest on='foo' experiment={TestNamespace} shouldEnroll={this.shouldEnroll()}>
102137
<When value='foobar'>
103138
variation 1
104139
</When>
@@ -122,8 +157,6 @@ The ABTest component takes the following as props:
122157
123158
**on** - the parameter name to "branch" off [REQUIRED]
124159
125-
**experimentName** - the name of the experiment with which the component corresponds. This is particularly important if you are passing in a namespace class. If you are passing in a namespace class then experimentName should correspond to the name of the experiment within the namespace that this component should handle. Use this if you want your component to deal with any arbitrary number of parameters. [REQUIRED]
126-
127160
**shouldEnroll** - this determines whether or not the user should be enrolled in the experiment or not. It defaults to true. If false is passed, nothing is returned and no exposure is logged. [OPTIONAL]
128161
129162
## Customized Experiment Components
@@ -132,7 +165,7 @@ If you want to create your own experiment component you can extend the base Para
132165
133166
## Logging
134167
135-
react-experiments logs an exposure event when it determines that a user should be enrolled in an experiment (i.e. the shouldEnroll prop is not false).
168+
react-experiments deals with logging experiment exposure. Using the base ```Parametrize``` component always triggers an exposure log when the component is mounted. The ```ABTest``` component also does the same thing unless the ```shouldEnroll``` prop is false.
136169
137170
## Development
138171
@@ -141,4 +174,3 @@ This project is written using ES6 and all build steps / transpilation are done b
141174
To test API changes locally, open the examples/index.html file locally after building with your most recent changes. The index.html file contains a simple example of using this library paired with the [PlanOut.js sample experiment](https://github.com/HubSpot/PlanOut.js/blob/master/examples/sample_planout_es5.js).
142175
143176
Please be sure to add tests to this project when making changes. This project uses [Jest](https://facebook.github.io/jest/) for running tests. Tests can be run either by building the project using build.sh or by using ```npm test```.
144-

__tests__/testExperiment.js renamed to __tests__/testAbTest.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('Test experiment component', () => {
1717

1818
it('renders only one, correct variation', () => {
1919
const experimentComponent = TestUtils.renderIntoDocument(
20-
<ReactExperiments.ABTest on='foo' experiment={exp} experimentName={exp.getName()}>
20+
<ReactExperiments.ABTest on='foo' experiment={exp}>
2121
<ReactExperiments.When value='Variation A'>
2222
<span className='variation-a'>
2323
foo
@@ -48,7 +48,7 @@ describe('Test experiment component', () => {
4848

4949
it('renders the default variation when needed', () => {
5050
const experimentComponent = TestUtils.renderIntoDocument(
51-
<ReactExperiments.ABTest experiment={exp} experimentName='SampleExperiment' on='foo'>
51+
<ReactExperiments.ABTest experiment={exp} on='foo'>
5252
<ReactExperiments.When value='foo'>
5353
foo
5454
</ReactExperiments.When>
@@ -67,7 +67,7 @@ describe('Test experiment component', () => {
6767

6868
it('renders nothing with no default variation', () => {
6969
const experimentComponent = TestUtils.renderIntoDocument(
70-
<ReactExperiments.ABTest experiment={exp} on='foob' experimentName='SampleExperiment'>
70+
<ReactExperiments.ABTest experiment={exp} on='foob' >
7171
<ReactExperiments.When value = 'Variation B'>
7272
<div className='foobar'>
7373
test
@@ -83,7 +83,7 @@ describe('Test experiment component', () => {
8383

8484
it('handles enrollment properly', () => {
8585
const experimentComponent = TestUtils.renderIntoDocument(
86-
<ReactExperiments.ABTest on='foo' experiment={exp} shouldEnroll={false} experimentName='SampleExperiment'>
86+
<ReactExperiments.ABTest on='foo' experiment={exp} shouldEnroll={false}>
8787
<ReactExperiments.When value='Variation B'>
8888
<div className='foo'>
8989
test
@@ -97,7 +97,7 @@ describe('Test experiment component', () => {
9797
).length).toBe(0);
9898

9999
const experimentComponent2 = TestUtils.renderIntoDocument(
100-
<ReactExperiments.ABTest on='foo'experiment={exp} experimentName='SampleExperiment' shouldEnroll={true}>
100+
<ReactExperiments.ABTest on='foo'experiment={exp} shouldEnroll={true}>
101101
<ReactExperiments.When value='Variation B'>
102102
<div className='foo'>
103103
test

__tests__/testExperimentClass.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ let logs = [];
55
const paramKey = 'foo';
66
const paramVal = 'bar';
77
class experiment extends ReactExperiments.experimentClass {
8-
getParams() {
9-
let ret = {};
10-
ret[paramKey] = paramVal;
11-
return ret;
8+
get(param) {
9+
if (param === paramKey) {
10+
return paramVal;
11+
}
1212
}
1313

1414
logExposure(data) {
@@ -47,7 +47,7 @@ describe('Test experiment', () => {
4747
});
4848

4949
const parametrized = TestUtils.renderIntoDocument(
50-
<ReactExperiments.Parametrize experiment={expClass}>
50+
<ReactExperiments.Parametrize experiment={expClass} params={[paramKey]}>
5151
<Comp />
5252
</ReactExperiments.Parametrize>
5353
);

__tests__/testNamespace.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('Test that experiment component works with namespaces', () => {
1212
it('works when the user is enrolled in the namespace', () => {
1313
const namespace = new DefaultNamespace(expInitializeObject);
1414
const experimentComponent = TestUtils.renderIntoDocument(
15-
<ReactExperiments.ABTest on='foo' experimentName='SampleExperiment' experiment={namespace}>
15+
<ReactExperiments.ABTest on='foo' experiment={namespace}>
1616
<ReactExperiments.When value='Variation A'>
1717
<span className='variation-a'>
1818
foo
@@ -43,7 +43,7 @@ describe('Test that experiment component works with namespaces', () => {
4343
it('default component works when the user is not enrolled in a namespace', () => {
4444
const emptyNamespace = new DefaultEmptyNamespace(expInitializeObject);
4545
const experimentComponent = TestUtils.renderIntoDocument(
46-
<ReactExperiments.ABTest on='foo' experimentName='SampleExperiment' experiment={emptyNamespace}>
46+
<ReactExperiments.ABTest on='foo' experiment={emptyNamespace}>
4747
<ReactExperiments.When value='Variation A'>
4848
<span className='variation-a'>
4949
foo
@@ -92,7 +92,7 @@ describe('Test that experiment component works with namespaces', () => {
9292
});
9393
SampleComponent = ReactExperiments.withExperimentParams(SampleComponent);
9494
const experimentComponent = TestUtils.renderIntoDocument(
95-
<ReactExperiments.Parametrize experimentName='SampleExperiment' experiment={namespace}>
95+
<ReactExperiments.Parametrize params={['foo']} experiment={namespace}>
9696
<SampleComponent />
9797
</ReactExperiments.Parametrize>
9898
);
@@ -111,7 +111,7 @@ describe('Test that experiment component works with namespaces', () => {
111111
expect(getLogLength()).toEqual(1);
112112

113113
const experimentComponent2 = TestUtils.renderIntoDocument(
114-
<ReactExperiments.Parametrize experimentName='SimpleExperiment' experiment={namespace}>
114+
<ReactExperiments.Parametrize params={['fz']} experiment={namespace}>
115115
<SampleComponent />
116116
</ReactExperiments.Parametrize>
117117
);
@@ -148,7 +148,7 @@ describe('Test that experiment component works with namespaces', () => {
148148
});
149149
SampleComponent = ReactExperiments.withExperimentParams(SampleComponent);
150150
const experimentComponent = TestUtils.renderIntoDocument(
151-
<ReactExperiments.Parametrize experimentName='SampleExperiment' experiment={emptyNamespace}>
151+
<ReactExperiments.Parametrize params={['foo']} experiment={emptyNamespace}>
152152
<SampleComponent />
153153
</ReactExperiments.Parametrize>
154154
);
@@ -184,7 +184,7 @@ describe('Test that experiment component works with namespaces', () => {
184184
});
185185
SampleComponent = ReactExperiments.withExperimentParams(SampleComponent);
186186
const experimentComponent = TestUtils.renderIntoDocument(
187-
<ReactExperiments.Parametrize experimentName='SampleExperiment2' experiment={namespace}>
187+
<ReactExperiments.Parametrize params={['foobar']} experiment={namespace}>
188188
<SampleComponent />
189189
</ReactExperiments.Parametrize>
190190
);

0 commit comments

Comments
 (0)