added first A/B experiment
This commit is contained in:
parent
14e21988b2
commit
17e61bb09d
|
@ -0,0 +1,76 @@
|
||||||
|
import hash from 'string-hash';
|
||||||
|
|
||||||
|
const experiments = {
|
||||||
|
'5YHCzn2CQTmBwWwTmZupBA': {
|
||||||
|
id: '5YHCzn2CQTmBwWwTmZupBA',
|
||||||
|
run: function(variant, state, emitter) {
|
||||||
|
state.experiment = {
|
||||||
|
xid: this.id,
|
||||||
|
xvar: variant
|
||||||
|
};
|
||||||
|
// Beefy UI
|
||||||
|
if (variant === 1) {
|
||||||
|
state.config.uploadWindowStyle = 'upload-window upload-window-b';
|
||||||
|
state.config.uploadButtonStyle = 'btn browse browse-b';
|
||||||
|
} else {
|
||||||
|
state.config.uploadWindowStyle = 'upload-window';
|
||||||
|
state.config.uploadButtonStyle = 'btn browse';
|
||||||
|
}
|
||||||
|
emitter.emit('render');
|
||||||
|
},
|
||||||
|
eligible: function(state) {
|
||||||
|
return this.luckyNumber(state) >= 0.5;
|
||||||
|
},
|
||||||
|
variant: function(state) {
|
||||||
|
return this.luckyNumber(state) < 0.5 ? 0 : 1;
|
||||||
|
},
|
||||||
|
luckyNumber: function(state) {
|
||||||
|
return luckyNumber(
|
||||||
|
`${this.id}:${state.storage.get('testpilot_ga__cid')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Returns a number between 0 and 1
|
||||||
|
function luckyNumber(str) {
|
||||||
|
return hash(str) / 0xffffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkExperiments(state, emitter) {
|
||||||
|
const all = Object.keys(experiments);
|
||||||
|
const id = all.find(id => experiments[id].eligible(state));
|
||||||
|
if (id) {
|
||||||
|
const variant = experiments[id].variant(state);
|
||||||
|
state.storage.enroll(id, variant);
|
||||||
|
experiments[id].run(variant, state, emitter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function initialize(state, emitter) {
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
const xp = experiments[state.query.x];
|
||||||
|
if (xp) {
|
||||||
|
xp.run(state.query.v, state, emitter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!state.storage.get('testpilot_ga__cid')) {
|
||||||
|
// first ever visit. check again after cid is assigned.
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
checkExperiments(state, emitter);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const enrolled = state.storage.enrolled;
|
||||||
|
enrolled.forEach(([id, variant]) => {
|
||||||
|
const xp = experiments[id];
|
||||||
|
if (xp) {
|
||||||
|
xp.run(variant, state, emitter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// single experiment per session for now
|
||||||
|
if (enrolled.length === 0) {
|
||||||
|
checkExperiments(state, emitter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import { canHasSend } from './utils';
|
||||||
import assets from '../common/assets';
|
import assets from '../common/assets';
|
||||||
import storage from './storage';
|
import storage from './storage';
|
||||||
import metrics from './metrics';
|
import metrics from './metrics';
|
||||||
|
import experiments from './experiments';
|
||||||
import Raven from 'raven-js';
|
import Raven from 'raven-js';
|
||||||
|
|
||||||
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||||
|
@ -22,6 +23,10 @@ app.use((state, emitter) => {
|
||||||
state.translate = locale.getTranslator();
|
state.translate = locale.getTranslator();
|
||||||
state.storage = storage;
|
state.storage = storage;
|
||||||
state.raven = Raven;
|
state.raven = Raven;
|
||||||
|
state.config = {
|
||||||
|
uploadWindowStyle: 'upload-window',
|
||||||
|
uploadButtonStyle: 'browse btn'
|
||||||
|
};
|
||||||
emitter.on('DOMContentLoaded', async () => {
|
emitter.on('DOMContentLoaded', async () => {
|
||||||
const ok = await canHasSend(assets.get('cryptofill.js'));
|
const ok = await canHasSend(assets.get('cryptofill.js'));
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
|
@ -34,5 +39,6 @@ app.use((state, emitter) => {
|
||||||
app.use(metrics);
|
app.use(metrics);
|
||||||
app.use(fileManager);
|
app.use(fileManager);
|
||||||
app.use(dragManager);
|
app.use(dragManager);
|
||||||
|
app.use(experiments);
|
||||||
|
|
||||||
app.mount('#page-one');
|
app.mount('#page-one');
|
||||||
|
|
|
@ -15,23 +15,44 @@ const analytics = new testPilotGA({
|
||||||
});
|
});
|
||||||
|
|
||||||
let appState = null;
|
let appState = null;
|
||||||
|
let experiment = null;
|
||||||
|
|
||||||
export default function initialize(state, emitter) {
|
export default function initialize(state, emitter) {
|
||||||
appState = state;
|
appState = state;
|
||||||
emitter.on('DOMContentLoaded', () => {
|
emitter.on('DOMContentLoaded', () => {
|
||||||
addExitHandlers();
|
addExitHandlers();
|
||||||
|
experiment = storage.enrolled[0];
|
||||||
|
sendEvent(category(), 'visit', {
|
||||||
|
cm5: storage.totalUploads,
|
||||||
|
cm6: storage.files.length,
|
||||||
|
cm7: storage.totalDownloads
|
||||||
|
});
|
||||||
//TODO restart handlers... somewhere
|
//TODO restart handlers... somewhere
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function category() {
|
function category() {
|
||||||
return appState.route === '/' ? 'sender' : 'recipient';
|
switch (appState.route) {
|
||||||
|
case '/':
|
||||||
|
case '/share/:id':
|
||||||
|
return 'sender';
|
||||||
|
case '/download/:id/:key':
|
||||||
|
case '/download/:id':
|
||||||
|
case '/completed':
|
||||||
|
return 'recipient';
|
||||||
|
default:
|
||||||
|
return 'other';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendEvent() {
|
function sendEvent() {
|
||||||
|
const args = Array.from(arguments);
|
||||||
|
if (experiment && args[2]) {
|
||||||
|
args[2].xid = experiment[0];
|
||||||
|
args[2].xvar = experiment[1];
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
hasLocalStorage &&
|
hasLocalStorage && analytics.sendEvent.apply(analytics, args).catch(() => 0)
|
||||||
analytics.sendEvent.apply(analytics, arguments).catch(() => 0)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,11 @@ class Storage {
|
||||||
const k = this.engine.key(i);
|
const k = this.engine.key(i);
|
||||||
if (isFile(k)) {
|
if (isFile(k)) {
|
||||||
try {
|
try {
|
||||||
fs.push(JSON.parse(this.engine.getItem(k)));
|
const f = JSON.parse(this.engine.getItem(k));
|
||||||
|
if (!f.id) {
|
||||||
|
f.id = f.fileId;
|
||||||
|
}
|
||||||
|
fs.push(f);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// obviously you're not a golfer
|
// obviously you're not a golfer
|
||||||
this.engine.removeItem(k);
|
this.engine.removeItem(k);
|
||||||
|
@ -70,6 +74,18 @@ class Storage {
|
||||||
set referrer(str) {
|
set referrer(str) {
|
||||||
this.engine.setItem('referrer', str);
|
this.engine.setItem('referrer', str);
|
||||||
}
|
}
|
||||||
|
get enrolled() {
|
||||||
|
return JSON.parse(this.engine.getItem('experiments') || '[]');
|
||||||
|
}
|
||||||
|
|
||||||
|
enroll(id, variant) {
|
||||||
|
const enrolled = this.enrolled;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
if (!enrolled.find(([i, v]) => i === id)) {
|
||||||
|
enrolled.push([id, variant]);
|
||||||
|
this.engine.setItem('experiments', JSON.stringify(enrolled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get files() {
|
get files() {
|
||||||
return this._files;
|
return this._files;
|
||||||
|
@ -83,6 +99,10 @@ class Storage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get(id) {
|
||||||
|
return this.engine.getItem(id);
|
||||||
|
}
|
||||||
|
|
||||||
remove(property) {
|
remove(property) {
|
||||||
if (isFile(property)) {
|
if (isFile(property)) {
|
||||||
this._files.splice(this._files.findIndex(f => f.id === property), 1);
|
this._files.splice(this._files.findIndex(f => f.id === property), 1);
|
||||||
|
|
|
@ -13,7 +13,8 @@ module.exports = function(state, emit) {
|
||||||
'uploadPageLearnMore'
|
'uploadPageLearnMore'
|
||||||
)}</a>
|
)}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="upload-window" ondragover=${dragover} ondragleave=${dragleave}>
|
<div class="${state.config
|
||||||
|
.uploadWindowStyle}" ondragover=${dragover} ondragleave=${dragleave}>
|
||||||
<div id="upload-img"><img src="${assets.get(
|
<div id="upload-img"><img src="${assets.get(
|
||||||
'upload.svg'
|
'upload.svg'
|
||||||
)}" title="${state.translate('uploadSvgAlt')}"/></div>
|
)}" title="${state.translate('uploadSvgAlt')}"/></div>
|
||||||
|
@ -22,9 +23,10 @@ module.exports = function(state, emit) {
|
||||||
'uploadPageSizeMessage'
|
'uploadPageSizeMessage'
|
||||||
)}</em></span>
|
)}</em></span>
|
||||||
<form method="post" action="upload" enctype="multipart/form-data">
|
<form method="post" action="upload" enctype="multipart/form-data">
|
||||||
<label for="file-upload" id="browse" class="btn">${state.translate(
|
<label for="file-upload" id="browse" class="${state.config
|
||||||
'uploadPageBrowseButton1'
|
.uploadButtonStyle}">${state.translate(
|
||||||
)}</label>
|
'uploadPageBrowseButton1'
|
||||||
|
)}</label>
|
||||||
<input id="file-upload" type="file" name="fileUploaded" onchange=${upload} />
|
<input id="file-upload" type="file" name="fileUploaded" onchange=${upload} />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -231,6 +231,14 @@ a {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-window-b {
|
||||||
|
border: 3px dashed rgba(0, 148, 251, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-window-b.ondrag {
|
||||||
|
border: 5px dashed rgba(0, 148, 251, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
color: #0094fb;
|
color: #0094fb;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -247,7 +255,7 @@ a {
|
||||||
font-family: 'SF Pro Text', sans-serif;
|
font-family: 'SF Pro Text', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#browse {
|
.browse {
|
||||||
background: #0297f8;
|
background: #0297f8;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
@ -261,10 +269,15 @@ a {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#browse:hover {
|
.browse:hover {
|
||||||
background-color: #0287e8;
|
background-color: #0287e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.browse-b {
|
||||||
|
height: 60px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10466,6 +10466,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
||||||
},
|
},
|
||||||
|
"string-hash": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz",
|
||||||
|
"integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"string-width": {
|
"string-width": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||||
|
|
|
@ -81,6 +81,7 @@
|
||||||
"rimraf": "^2.6.1",
|
"rimraf": "^2.6.1",
|
||||||
"selenium-webdriver": "^3.5.0",
|
"selenium-webdriver": "^3.5.0",
|
||||||
"sinon": "^3.2.1",
|
"sinon": "^3.2.1",
|
||||||
|
"string-hash": "^1.1.3",
|
||||||
"stylelint-config-standard": "^17.0.0",
|
"stylelint-config-standard": "^17.0.0",
|
||||||
"stylelint-no-unsupported-browser-features": "^1.0.0",
|
"stylelint-no-unsupported-browser-features": "^1.0.0",
|
||||||
"supertest": "^3.0.0",
|
"supertest": "^3.0.0",
|
||||||
|
|
|
@ -15,6 +15,10 @@ module.exports = function(req) {
|
||||||
storage: {
|
storage: {
|
||||||
files: []
|
files: []
|
||||||
},
|
},
|
||||||
|
config: {
|
||||||
|
uploadWindowStyle: 'upload-window',
|
||||||
|
uploadButtonStyle: 'browse btn'
|
||||||
|
},
|
||||||
layout
|
layout
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue