mirror of
https://gitlab.com/timvisee/send.git
synced 2024-11-08 20:22:45 +01:00
added first A/B experiment
This commit is contained in:
parent
14e21988b2
commit
17e61bb09d
76
app/experiments.js
Normal file
76
app/experiments.js
Normal file
@ -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 storage from './storage';
|
||||
import metrics from './metrics';
|
||||
import experiments from './experiments';
|
||||
import Raven from 'raven-js';
|
||||
|
||||
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||
@ -22,6 +23,10 @@ app.use((state, emitter) => {
|
||||
state.translate = locale.getTranslator();
|
||||
state.storage = storage;
|
||||
state.raven = Raven;
|
||||
state.config = {
|
||||
uploadWindowStyle: 'upload-window',
|
||||
uploadButtonStyle: 'browse btn'
|
||||
};
|
||||
emitter.on('DOMContentLoaded', async () => {
|
||||
const ok = await canHasSend(assets.get('cryptofill.js'));
|
||||
if (!ok) {
|
||||
@ -34,5 +39,6 @@ app.use((state, emitter) => {
|
||||
app.use(metrics);
|
||||
app.use(fileManager);
|
||||
app.use(dragManager);
|
||||
app.use(experiments);
|
||||
|
||||
app.mount('#page-one');
|
||||
|
@ -15,23 +15,44 @@ const analytics = new testPilotGA({
|
||||
});
|
||||
|
||||
let appState = null;
|
||||
let experiment = null;
|
||||
|
||||
export default function initialize(state, emitter) {
|
||||
appState = state;
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
addExitHandlers();
|
||||
experiment = storage.enrolled[0];
|
||||
sendEvent(category(), 'visit', {
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads
|
||||
});
|
||||
//TODO restart handlers... somewhere
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
const args = Array.from(arguments);
|
||||
if (experiment && args[2]) {
|
||||
args[2].xid = experiment[0];
|
||||
args[2].xvar = experiment[1];
|
||||
}
|
||||
return (
|
||||
hasLocalStorage &&
|
||||
analytics.sendEvent.apply(analytics, arguments).catch(() => 0)
|
||||
hasLocalStorage && analytics.sendEvent.apply(analytics, args).catch(() => 0)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,11 @@ class Storage {
|
||||
const k = this.engine.key(i);
|
||||
if (isFile(k)) {
|
||||
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) {
|
||||
// obviously you're not a golfer
|
||||
this.engine.removeItem(k);
|
||||
@ -70,6 +74,18 @@ class Storage {
|
||||
set 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() {
|
||||
return this._files;
|
||||
@ -83,6 +99,10 @@ class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
get(id) {
|
||||
return this.engine.getItem(id);
|
||||
}
|
||||
|
||||
remove(property) {
|
||||
if (isFile(property)) {
|
||||
this._files.splice(this._files.findIndex(f => f.id === property), 1);
|
||||
|
@ -13,7 +13,8 @@ module.exports = function(state, emit) {
|
||||
'uploadPageLearnMore'
|
||||
)}</a>
|
||||
</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(
|
||||
'upload.svg'
|
||||
)}" title="${state.translate('uploadSvgAlt')}"/></div>
|
||||
@ -22,9 +23,10 @@ module.exports = function(state, emit) {
|
||||
'uploadPageSizeMessage'
|
||||
)}</em></span>
|
||||
<form method="post" action="upload" enctype="multipart/form-data">
|
||||
<label for="file-upload" id="browse" class="btn">${state.translate(
|
||||
'uploadPageBrowseButton1'
|
||||
)}</label>
|
||||
<label for="file-upload" id="browse" class="${state.config
|
||||
.uploadButtonStyle}">${state.translate(
|
||||
'uploadPageBrowseButton1'
|
||||
)}</label>
|
||||
<input id="file-upload" type="file" name="fileUploaded" onchange=${upload} />
|
||||
</form>
|
||||
</div>
|
||||
|
@ -231,6 +231,14 @@ a {
|
||||
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 {
|
||||
color: #0094fb;
|
||||
text-decoration: none;
|
||||
@ -247,7 +255,7 @@ a {
|
||||
font-family: 'SF Pro Text', sans-serif;
|
||||
}
|
||||
|
||||
#browse {
|
||||
.browse {
|
||||
background: #0297f8;
|
||||
border-radius: 5px;
|
||||
font-size: 15px;
|
||||
@ -261,10 +269,15 @@ a {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#browse:hover {
|
||||
.browse:hover {
|
||||
background-color: #0287e8;
|
||||
}
|
||||
|
||||
.browse-b {
|
||||
height: 60px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -10466,6 +10466,12 @@
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"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": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||
|
@ -81,6 +81,7 @@
|
||||
"rimraf": "^2.6.1",
|
||||
"selenium-webdriver": "^3.5.0",
|
||||
"sinon": "^3.2.1",
|
||||
"string-hash": "^1.1.3",
|
||||
"stylelint-config-standard": "^17.0.0",
|
||||
"stylelint-no-unsupported-browser-features": "^1.0.0",
|
||||
"supertest": "^3.0.0",
|
||||
|
@ -15,6 +15,10 @@ module.exports = function(req) {
|
||||
storage: {
|
||||
files: []
|
||||
},
|
||||
config: {
|
||||
uploadWindowStyle: 'upload-window',
|
||||
uploadButtonStyle: 'browse btn'
|
||||
},
|
||||
layout
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user