Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
@marvelapp/react-ab-test
Advanced tools
A/B testing React components and debug tools. Isomorphic with a simple, universal interface. Well documented and lightweight. Tested in popular browsers and Node.js. Includes helpers for Mixpanel and Segment.com.
Wrap components in <Variant />
and nest in <Experiment />
. A variant is chosen randomly and saved to local storage.
<Experiment name="My Example">
<Variant name="A">
<div>Version A</div>
</Variant>
<Variant name="B">
<div>Version B</div>
</Variant>
</Experiment>
Report to your analytics provider using the emitter
. Helpers are available for Mixpanel and Segment.com.
emitter.addPlayListener((experimentName, variantName) => {
mixpanel.track('Start Experiment', {
name: experimentName,
variant: variantName,
});
});
Please ★ on GitHub!
<Experiment />
<Variant />
emitter
emitter.emitWin(experimentName)
emitter.addActiveVariantListener([experimentName, ] callback)
emitter.addPlayListener([experimentName, ] callback)
emitter.addWinListener([experimentName, ] callback)
emitter.defineVariants(experimentName, variantNames [, variantWeights])
emitter.setActiveVariant(experimentName, variantName)
emitter.getActiveVariant(experimentName)
emitter.calculateActiveVariant(experimentName [, userIdentifier, defaultVariantName])
emitter.getSortedVariants(experimentName)
emitter.setCustomDistributionAlgorithm(customAlgorithm)
Subscription
experimentDebugger
mixpanelHelper
segmentHelper
react-ab-test
is compatible with React >=0.14.x
yarn add @marvelapp/react-ab-test
Try it on JSFiddle
Using useExperiment Hook
import React from 'react';
import { useExperiment, emitter } from '@marvelapp/react-ab-test';
// Hook usage pattern requires registration of experiments
emitter.defineVariants("My Example", ["A", "B"]);
const App = () => {
const { selectVariant, emitWin } = useExperiment("My Example");
const variant = selectVariant({
A: <div>Section A</div>,
B: <div>Section B</div>
});
return (
<div>
{variant}
<button onClick={emitWin}>CTA</button>
</div>
);
};
Using Experiment Component
import React from 'react';
import { Experiment, Variant, emitter } from '@marvelapp/react-ab-test';
class App extends Component {
experimentRef = React.createRef();
onButtonClick(e) {
this.experimentRef.current.win();
}
render() {
return (
<div>
<Experiment ref={this.experimentRef} name="My Example">
<Variant name="A">
<div>Section A</div>
</Variant>
<Variant name="B">
<div>Section B</div>
</Variant>
</Experiment>
<button onClick={this.onButtonClick}>Emit a win</button>
</div>
);
}
}
// Called when the experiment is displayed to the user.
emitter.addPlayListener(function(experimentName, variantName) {
console.log(`Displaying experiment ${experimentName} variant ${variantName}`);
});
// Called when a 'win' is emitted, in this case by this.experimentRef.current.win()
emitter.addWinListener(function(experimentName, variantName) {
console.log(
`Variant ${variantName} of experiment ${experimentName} was clicked`
);
});
Try it on JSFiddle
import React from 'react';
import { Experiment, Variant, emitter } from '@marvelapp/react-ab-test';
// Define variants in advance.
emitter.defineVariants('My Example', ['A', 'B', 'C']);
function Component1 = () => {
return (
<Experiment name="My Example">
<Variant name="A">
<div>Section A</div>
</Variant>
<Variant name="B">
<div>Section B</div>
</Variant>
</Experiment>
);
};
const Component2 = () => {
return (
<Experiment name="My Example">
<Variant name="A">
<div>Subsection A</div>
</Variant>
<Variant name="B">
<div>Subsection B</div>
</Variant>
<Variant name="C">
<div>Subsection C</div>
</Variant>
</Experiment>
);
};
class Component3 extends React.Component {
onButtonClick(e) {
emitter.emitWin('My Example');
}
render() {
return <button onClick={this.onButtonClick}>Emit a win</button>;
}
}
const App = () => {
return (
<div>
<Component1 />
<Component2 />
<Component3 />
</div>
);
};
// Called when the experiment is displayed to the user.
emitter.addPlayListener(function(experimentName, variantName) {
console.log(`Displaying experiment ${experimentName} variant ${variantName}`);
});
// Called when a 'win' is emitted, in this case by emitter.emitWin('My Example')
emitter.addWinListener(function(experimentName, variantName) {
console.log(
`Variant ${variantName} of experiment ${experimentName} was clicked`
);
});
Try it on JSFiddle
Use emitter.defineVariants() to optionally define the ratios by which variants are chosen.
import React from 'react';
import { Experiment, Variant, emitter } from '@marvelapp/react-ab-test';
// Define variants and weights in advance.
emitter.defineVariants('My Example', ['A', 'B', 'C'], [10, 40, 40]);
const App = () => {
return (
<div>
<Experiment name="My Example">
<Variant name="A">
<div>Section A</div>
</Variant>
<Variant name="B">
<div>Section B</div>
</Variant>
<Variant name="C">
<div>Section C</div>
</Variant>
</Experiment>
</div>
);
}
There are some scenarios where you may want the active variant of an experiment to be calculated before the experiment is rendered. To do so, use emitter.calculateActiveVariant(). Note that this method must be called after emitter.defineVariants()
import { emitter } from '@marvelapp/react-ab-test';
// Define variants in advance
emitter.defineVariants('My Example', ['A', 'B', 'C']);
emitter.calculateActiveVariant('My Example', 'userId');
// Active variant will be defined even if the experiment is not rendered
const activeVariant = emitter.getActiveVariant('My Example');
The debugger attaches a fixed-position panel to the bottom of the <body>
element that displays mounted experiments and enables the user to change active variants in real-time.
The debugger is wrapped in a conditional if(process.env.NODE_ENV === "production") {...}
and will not display on production builds using envify.
Try it on JSFiddle
import React from 'react';
import { Experiment, Variant, experimentDebugger } from '@marvelapp/react-ab-test';
experimentDebugger.enable();
const App = () => {
return (
<div>
<Experiment name="My Example">
<Variant name="A">
<div>Section A</div>
</Variant>
<Variant name="B">
<div>Section B</div>
</Variant>
</Experiment>
</div>
);
}
A <Experiment />
with a userIdentifier
property will choose a consistent <Variant />
suitable for server side rendering.
See ./examples/isomorphic
for a working example.
The component in Component.jsx
:
var React = require('react');
var Experiment = require('react-ab-test/lib/Experiment');
var Variant = require('react-ab-test/lib/Variant');
module.exports = React.createClass({
propTypes: {
userIdentifier: React.PropTypes.string.isRequired,
},
render: function() {
return (
<div>
<Experiment
name="My Example"
userIdentifier={this.props.userIdentifier}
>
<Variant name="A">
<div>Section A</div>
</Variant>
<Variant name="B">
<div>Section B</div>
</Variant>
</Experiment>
</div>
);
},
});
We use a session ID for the userIdentifier
property in this example, although a long-lived user ID would be preferable. See server.js
:
require('babel/register')({ only: /jsx/ });
var express = require('express');
var session = require('express-session');
var React = require('react');
var ReactDOMServer = require('react-dom/server');
var Component = require('./Component.jsx');
var abEmitter = require('@marvelapp/react-ab-test/lib/emitter');
var app = express();
app.set('view engine', 'ejs');
app.use(
session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
})
);
app.get('/', function(req, res) {
var reactElement = React.createElement(Component, {
userIdentifier: req.sessionID,
});
var reactString = ReactDOMServer.renderToString(reactElement);
abEmitter.rewind();
res.render('template', {
sessionID: req.sessionID,
reactOutput: reactString,
});
});
app.use(express.static('www'));
app.listen(8080);
Remember to call abEmitter.rewind()
to prevent memory leaks.
An EJS template in template.ejs
:
<!doctype html>
<html>
<head>
<title>Isomorphic Rendering Example</title>
</head>
<script type="text/javascript">
var SESSION_ID = <%- JSON.stringify(sessionID) %>;
</script>
<body>
<div id="react-mount"><%- reactOutput %></div>
<script type="text/javascript" src="bundle.js"></script>
</body>
</html>
On the client in app.jsx
:
var React = require('react');
var ReactDOM = require('react-dom');
var Component = require('../Component.jsx');
var container = document.getElementById('react-mount');
ReactDOM.render(<Component userIdentifier={SESSION_ID} />, container);
Code from ./src
is written in JSX and transpiled into ./lib
using Babel. If your project uses Babel you may want to include files from ./src
directly.
Please let us know about alternate libraries not included here.
Please let us know about React A/B testing resources not included here.
<Experiment />
Experiment container component. Children must be of type Variant
.
name
- Name of the experiment.
string
"My Example"
userIdentifier
- Distinct user identifier. When defined, this value is hashed to choose a variant if defaultVariantName
or a stored value is not present. Useful for server side rendering.
string
"7cf61a4521f24507936a8977e1eee2d4"
defaultVariantName
- Name of the default variant. When defined, this value is used to choose a variant if a stored value is not present. This property may be useful for server side rendering but is otherwise not recommended.
string
"A"
<Variant />
Variant container component.
name
- Name of the variant.
string
"A"
emitter
Event emitter responsible for coordinating and reporting usage. Extended from facebook/emitter.
emitter.emitWin(experimentName)
Emit a win event.
experimentName
- Name of an experiment.
string
"My Example"
emitter.addActiveVariantListener([experimentName, ] callback)
Listen for the active variant specified by an experiment.
Subscription
experimentName
- Name of an experiment. If provided, the callback will only be called for the specified experiment.
string
"My Example"
callback
- Function to be called when a variant is chosen.
function
experimentName
- Name of the experiment.
string
variantName
- Name of the variant.
string
emitter.addPlayListener([experimentName, ] callback)
Listen for an experiment being displayed to the user. Trigged by the React componentWillMount lifecycle method.
Subscription
experimentName
- Name of an experiment. If provided, the callback will only be called for the specified experiment.
string
"My Example"
callback
- Function to be called when an experiment is displayed to the user.
function
experimentName
- Name of the experiment.
string
variantName
- Name of the variant.
string
emitter.addWinListener([experimentName, ] callback)
Listen for a successful outcome from the experiment. Trigged by the emitter.emitWin(experimentName) method.
Subscription
experimentName
- Name of an experiment. If provided, the callback will only be called for the specified experiment.
string
"My Example"
callback
- Function to be called when a win is emitted.
function
experimentName
- Name of the experiment.
string
variantName
- Name of the variant.
string
emitter.defineVariants(experimentName, variantNames [, variantWeights])
Define experiment variant names and weighting. Required when an experiment spans multiple components containing different sets of variants.
If variantWeights
are not specified variants will be chosen at equal rates.
The variants will be chosen according to the ratio of the numbers, for example variants ["A", "B", "C"]
with weights [20, 40, 40]
will be chosen 20%, 40%, and 40% of the time, respectively.
experimentName
- Name of the experiment.
string
"My Example"
variantNames
- Array of variant names.
Array.<string>
["A", "B", "C"]
variantWeights
- Array of variant weights.
Array.<number>
[20, 40, 40]
emitter.setActiveVariant(experimentName, variantName)
Set the active variant of an experiment.
experimentName
- Name of the experiment.
string
"My Example"
variantName
- Name of the variant.
string
"A"
emitter.getActiveVariant(experimentName)
Returns the variant name currently displayed by the experiment.
string
experimentName
- Name of the experiment.
string
"My Example"
emitter.calculateActiveVariant(experimentName [, userIdentifier, defaultVariantName])
Force calculation of active variant, even if the experiment is not displayed yet.
Note: This method must be called after emitter.defineVariants
string
experimentName
- Name of the experiment.
string
"My Example"
userIdentifier
- Distinct user identifier. When defined, this value is hashed to choose a variant if defaultVariantName
or a stored value is not present. Useful for server side rendering.
string
"7cf61a4521f24507936a8977e1eee2d4"
defaultVariantName
- Name of the default variant. When defined, this value is used to choose a variant if a stored value is not present. This property may be useful for server side rendering but is otherwise not recommended.
string
"A"
emitter.getSortedVariants(experimentName)
Returns a sorted array of variant names associated with the experiment.
Array.<string>
experimentName
- Name of the experiment.
string
"My Example"
emitter.setCustomDistributionAlgorithm(customAlgorithm)
Sets a custom function to use for calculating variants overriding the default. This can be usefull in cases when variants are expected from 3rd parties or when variants need to be in sync with other clients using ab test but different distribution algorithm.
customAlgorithm
- Function for calculating variant distribution.
function
experimentName
- Name of the experiment.
string
userIdentifier
- User's value which is used to calculate the variant
string
defaultVariantName
- Default variant passed from the experiment
string
Subscription
Returned by the emitter's add listener methods. More information available in the facebook/emitter documentation.
subscription.remove()
Removes the listener subscription and prevents future callbacks.
experimentDebugger
Debugging tool. Attaches a fixed-position panel to the bottom of the <body>
element that displays mounted experiments and enables the user to change active variants in real-time.
The debugger is wrapped in a conditional if(process.env.NODE_ENV === "production") {...}
and will not display on production builds using envify. This can be overriden by setDebuggerAvailable
experimentDebugger.setDebuggerAvailable(isAvailable)
Overrides process.env.NODE_ENV
check, so it can be decided if the debugger is available
or not at runtime. This allow, for instance, to enable the debugger in a testing environment but not in production.
Note that you require to explicitly call to .enable
even if you forced this to be truthy.
isAvailable
- Tells whether the debugger is available or not
boolean
experimentDebugger.enable()
Attaches the debugging panel to the <body>
element.
experimentDebugger.disable()
Removes the debugging panel from the <body>
element.
mixpanelHelper
Sends events to Mixpanel. Requires window.mixpanel
to be set using Mixpanel's embed snippet.
When the <Experiment />
is mounted, the helper sends an Experiment Play
event using mixpanel.track(...)
with Experiment
and Variant
properties.
When a win is emitted the helper sends an Experiment Win
event using mixpanel.track(...)
with Experiment
and Variant
properties.
Try it on JSFiddle
import React from 'react';
import { Experiment, Variant, mixpanelHelper } from '@marvelapp/react-ab-test';
// window.mixpanel has been set by Mixpanel's embed snippet.
mixpanelHelper.enable();
class App extends React.Component {
experimentRef = React.createRef();
onButtonClick(e) {
this.experimentRef.current.win();
// mixpanelHelper sends the 'Experiment Win' event, equivalent to:
// mixpanel.track('Experiment Win', {Experiment: "My Example", Variant: "A"})
}
componentWillMount() {
// mixpanelHelper sends the 'Experiment Play' event, equivalent to:
// mixpanel.track('Experiment Play', {Experiment: "My Example", Variant: "A"})
}
render() {
return (
<div>
<Experiment ref={this.experimentRef} name="My Example">
<Variant name="A">
<div>Section A</div>
</Variant>
<Variant name="B">
<div>Section B</div>
</Variant>
</Experiment>
<button onClick={this.onButtonClick}>Emit a win</button>
</div>
);
}
}
mixpanelHelper.enable()
Add listeners to win
and play
events and report results to Mixpanel.
mixpanelHelper.disable()
Remove win
and play
listeners and stop reporting results to Mixpanel.
segmentHelper
Sends events to Segment. Requires window.analytics
to be set using Segment's embed snippet.
When the <Experiment />
is mounted, the helper sends an Experiment Viewed
event using segment.track(...)
with experimentName
and variationName
properties.
When a win is emitted the helper sends an Experiment Won
event using segment.track(...)
with experimentName
and variationName
properties.
Try it on JSFiddle
import React from 'react';
import { Experiment, Variant, segmentHelper } from '@marvelapp/react-ab-test';
// window.analytics has been set by Segment's embed snippet.
segmentHelper.enable();
class App extends React.Component {
experimentRef = React.createRef();
onButtonClick(e) {
this.experimentRef.current.win();
// segmentHelper sends the 'Experiment Won' event, equivalent to:
// segment.track('Experiment Won', {experimentName: "My Example", variationName: "A"})
}
componentWillMount() {
// segmentHelper sends the 'Experiment Viewed' event, equivalent to:
// segment.track('Experiment Viewed, {experimentName: "My Example", variationName: "A"})
}
render() {
return (
<div>
<Experiment ref={this.experimentRef} name="My Example">
<Variant name="A">
<div>Section A</div>
</Variant>
<Variant name="B">
<div>Section B</div>
</Variant>
</Experiment>
<button onClick={this.onButtonClick}>Emit a win</button>
</div>
);
}
}
segmentHelper.enable()
Add listeners to win
and play
events and report results to Segment.
segmentHelper.disable()
Remove win
and play
listeners and stop reporting results to Segment.
Before contribuiting you need:
Then you can:
yarn build
yarn test
yarn lint
yarn test
FAQs
A/B testing React components and debug tools. Isomorphic with a simple, universal interface. Well documented and lightweight. Tested in popular browsers and Node.js. Includes helpers for Mixpanel and Segment.com.
We found that @marvelapp/react-ab-test demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 13 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.