decentraland-experiments
Advanced tools
Comparing version
@@ -132,3 +132,3 @@ "use strict"; | ||
Experiment.prototype.setState = function (patchState) { | ||
if (this.state !== undefined) { | ||
if (patchState && this.isActive() && !this.isCompleted()) { | ||
this.state = Object.assign({}, this.state, patchState); | ||
@@ -135,0 +135,0 @@ } |
@@ -12,3 +12,3 @@ /// <reference types="segment-analytics" /> | ||
private storage; | ||
private analytics; | ||
private _analytics; | ||
/** | ||
@@ -31,3 +31,4 @@ * Semaphore to handle localStorage or sessionStorage changes | ||
*/ | ||
constructor(experiments: ExperimentMap, storage?: Storage, analytics?: SegmentAnalytics.AnalyticsJS | undefined); | ||
constructor(experiments: ExperimentMap, storage: Storage, _analytics: SegmentAnalytics.AnalyticsJS | null | undefined); | ||
readonly analytics: SegmentAnalytics.AnalyticsJS | null; | ||
/** | ||
@@ -34,0 +35,0 @@ * Persist methods |
@@ -53,9 +53,8 @@ "use strict"; | ||
*/ | ||
function Experiments(experiments, storage, analytics) { | ||
function Experiments(experiments, storage, _analytics) { | ||
var _this = this; | ||
if (storage === void 0) { storage = window_1.default.localStorage; } | ||
if (analytics === void 0) { analytics = window_1.default.analytics; } | ||
this.experiments = experiments; | ||
this.storage = storage; | ||
this.analytics = analytics; | ||
this._analytics = _analytics; | ||
/** | ||
@@ -88,2 +87,5 @@ * Semaphore to handle localStorage or sessionStorage changes | ||
} | ||
else { | ||
console.warn("Analytics is not present in the project, experiments framework will not generate any report. Follow this guide to include it: https://segment.com/docs/sources/website/analytics.js/quickstart/"); | ||
} | ||
this.loadPersisted(); | ||
@@ -94,2 +96,9 @@ if (this.isBrowserStorage()) { | ||
} | ||
Object.defineProperty(Experiments.prototype, "analytics", { | ||
get: function () { | ||
return this._analytics || window_1.default.analytics || null; | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
/** | ||
@@ -211,3 +220,3 @@ * Persist methods | ||
if (this.analytics) { | ||
var experimentState = experiment.state || {}; | ||
var experimentState = experiment.state; | ||
this.analytics.track('experiment_conversion', __assign({ experiment: experiment.name, variation: experiment.variant.name }, experimentState)); | ||
@@ -214,0 +223,0 @@ } |
@@ -22,7 +22,9 @@ "use strict"; | ||
var SIGN_UP_EVENT = 'sign_up_event'; | ||
function createExperiments(storage) { | ||
function createExperiments(storage, segment) { | ||
if (storage === void 0) { storage = window_1.default.localStorage; } | ||
if (segment === void 0) { segment = analytics_1.default; } | ||
return new index_1.Experiments({ | ||
avatar_sign_up_test: new index_1.Experiment({ | ||
name: 'sign_up_vs_send', | ||
initialState: function () { return ({ calls: 0 }); }, | ||
variants: [ | ||
@@ -34,2 +36,5 @@ new index_1.Variant('sign_up', 0.5, 'Sign up'), | ||
if (event.type === 'track' && event.name === SIGN_UP_EVENT) { | ||
currentExperiment.setState({ | ||
calls: currentExperiment.state.calls + 1 | ||
}); | ||
currentExperiment.complete(); | ||
@@ -54,3 +59,3 @@ } | ||
}) | ||
}, storage, analytics_1.default); | ||
}, storage, segment); | ||
} | ||
@@ -79,7 +84,2 @@ describe("src/Experiments", function () { | ||
}); | ||
test("must listen for analytics event", function () { | ||
var experiments = createExperiments(); | ||
expect(analytics_1.on.mock.calls.length).toEqual(1); | ||
expect(analytics_1.on.mock.calls[0]).toEqual(['track', experiments.handleTrackEvent]); | ||
}); | ||
test("must listen for storage event", function () { | ||
@@ -93,6 +93,20 @@ var experiments = createExperiments(); | ||
}); | ||
test("must listen for analytics event", function () { | ||
var experiments = createExperiments(); | ||
expect(analytics_1.on.mock.calls.length).toEqual(1); | ||
expect(analytics_1.on.mock.calls[0]).toEqual(['track', experiments.handleTrackEvent]); | ||
}); | ||
test("must listen for window.analytics event", function () { | ||
window_1.default.analytics = analytics_1.default; | ||
var experiments = createExperiments(window_1.default.localStorage, null); | ||
expect(analytics_1.on.mock.calls.length).toEqual(1); | ||
expect(analytics_1.on.mock.calls[0]).toEqual(['track', experiments.handleTrackEvent]); | ||
delete window_1.default.analytics; | ||
}); | ||
test("must not fail if analytics isn't present", function () { | ||
createExperiments(window_1.default.localStorage, null); | ||
expect(analytics_1.on.mock.calls.length).toEqual(0); | ||
expect(console_1.warn.mock.calls.length).toEqual(1); | ||
}); | ||
}); | ||
describe(".emit()", function () { | ||
test("", function () { }); | ||
}); | ||
describe(".detach()", function () { | ||
@@ -215,3 +229,3 @@ test("must remove all analytics event", function () { | ||
'experiment_conversion', | ||
{ experiment: 'sign_up_vs_send', variation: 'sign_up' } | ||
{ experiment: 'sign_up_vs_send', variation: 'sign_up', calls: 1 } | ||
]); | ||
@@ -218,0 +232,0 @@ }); |
{ | ||
"name": "decentraland-experiments", | ||
"version": "1.0.0", | ||
"version": "1.0.1", | ||
"description": "Experiment Tracking Tool (A/B Testing)", | ||
@@ -8,3 +8,2 @@ "main": "dist", | ||
"devDependencies": { | ||
"@semantic-release/changelog": "^3.0.4", | ||
"@types/jest": "^24.0.18", | ||
@@ -15,6 +14,5 @@ "@types/node": "^12.7.5", | ||
"jest": "^24.9.0", | ||
"jest-date-mock": "^1.0.7", | ||
"jest-mock-random": "^1.0.2", | ||
"semantic-release": "^15.13.24", | ||
"ts-jest": "^24.0.2", | ||
"tslint-language-service": "^0.9.9", | ||
"typescript": "^3.6.3" | ||
@@ -21,0 +19,0 @@ }, |
166
README.md
@@ -7,1 +7,167 @@ # decentraland-experiments | ||
🛠 Experiment Tracking Tool (A/B Testing) | ||
> Implemented from [RFC](RCF.md) ([#54](https://github.com/decentraland/decentraland-dapps/issues/54)) | ||
## Index | ||
- [Installation](#installation) | ||
- [Usage](#usage) | ||
- [Vanilla JS](#vanilla-js) | ||
- [React](#react) | ||
- [React+Context](#react--context) | ||
- [Testing](#testing-with-jest) | ||
## Installation | ||
```bash | ||
npm install -s decentraland-experiments | ||
``` | ||
## Usage | ||
### Vanilla JS | ||
Instantiate all experiments | ||
```typescript | ||
import { Experiments, Experiment, Variant } from 'decentraland-experiments'; | ||
const experiments = new Experiments({ avatar_signup_test: ... }, localStorage, analytics) | ||
``` | ||
Retrieve and use the test value | ||
```typescript | ||
// if there are any test for `avatar_signup_test` it will be activate | ||
const value = experiments.getCurrentValueFor('avatar_signup_test', 'Sing Up') | ||
``` | ||
Track segments events | ||
```typescript | ||
analytics.track(SIGNUP_EVENT) | ||
``` | ||
### React | ||
Instantiate all experiments | ||
```typescript | ||
import { Experiments, Experiment, Variant } from 'decentraland-experiments'; | ||
const experiments = new Experiments({ avatar_signup_test: ... }, localStorage, analytics) | ||
``` | ||
Retrieve and use the test value | ||
```jsx | ||
import { experiments } from 'path/to/experiments' | ||
export default class SignUpButton extends React.PureComponent<Props, State> { | ||
render() { | ||
const text = experiments.getCurrentValueFor('avatar_signup_test', 'avatars.form.signup') | ||
<Button>{t(text)}</Button> | ||
} | ||
} | ||
``` | ||
Track segments events | ||
```jsx | ||
import { experiments } from 'path/to/experiments' | ||
export default class SignUpButton extends React.PureComponent<Props, State> { | ||
handleClick = (event: React.MouseEvent<HTMLElement>) => { | ||
// ... | ||
analytics.track(SIGNUP_EVENT) | ||
} | ||
render() { | ||
const text = experiments.getCurrentValueFor('avatar_signup_test', 'avatars.form.signup') | ||
<Button onClick={this.handleClick}>{t(text)}</Button> | ||
} | ||
} | ||
``` | ||
### React + Context | ||
Create the new Context without experiments | ||
```typescript | ||
import { Experiments } from 'decentraland-experiments' | ||
const ExperimentsContext = React.createContext(new Experiments({})) | ||
``` | ||
Instantiate all experiments | ||
```typescript | ||
import { Experiments, Experiment, Variant } from 'decentraland-experiments'; | ||
const experiments = new Experiments({ avatar_signup_test: ... }, localStorage, analytics) | ||
``` | ||
Add `Context.Provider` to initial render and set the experiments instance as value property | ||
```jsx | ||
import ExperimentsContext from 'path/to/context' | ||
ReactDOM.render( | ||
<ExperimentsContext.Provider value={experiments}> | ||
{/* ... */} | ||
</ExperimentsContext.Provider>, | ||
document.getElementById('root') | ||
) | ||
``` | ||
Add `Context` to the testing element and retrieve the test value | ||
```jsx | ||
import ExperimentsContext from 'path/to/context'; | ||
export default class SignUpButton extends React.PureComponent<Props, State> { | ||
static contextType = ExperimentsContext | ||
render() { | ||
const text = this.context.getCurrentValueFor('avatar_signup_test', 'avatars.form.signup') | ||
<Button>{t(text)}</Button> | ||
} | ||
} | ||
``` | ||
Track segments events | ||
```jsx | ||
import ExperimentsContext from 'path/to/context'; | ||
export default class SignUpButton extends React.PureComponent<Props, State> { | ||
handleClick = (event: React.MouseEvent<HTMLElement>) => { | ||
// ... | ||
analytics.track(SIGNUP_EVENT) | ||
} | ||
render() { | ||
const text = this.context.getCurrentValueFor('avatar_signup_test', 'avatars.form.signup') | ||
<Button>{t(text)}</Button> | ||
} | ||
} | ||
``` | ||
## Testing (with Jest) | ||
Retrieve all values and ensure its types | ||
```typescript | ||
import { experiments } from 'path/to/experiments' | ||
test(`all experiment value for avatar_signup_test are not empty strings`, () => { | ||
for (const value of experiments.getAllValuesFor('avatar_signup_test')) { | ||
expect(typeof value).toBe('string') | ||
expect(value.length).toBeGreaterThanOrEqual(5) | ||
} | ||
}) | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
9
-18.18%173
2371.43%92281
-11.57%27
-40%1179
-19.8%1
Infinity%