Added experiment for firefox download promo

This commit is contained in:
Danny Coates 2017-11-07 15:54:42 -08:00
parent d0d41b743a
commit e7fdf76120
No known key found for this signature in database
GPG Key ID: 4C442633C62E00CB
20 changed files with 222 additions and 106 deletions

View File

@ -1,6 +1,28 @@
import hash from 'string-hash'; import hash from 'string-hash';
const experiments = {}; const experiments = {
'SyI-hI7gT9agiH-f3f0BYg': {
id: 'SyI-hI7gT9agiH-f3f0BYg',
run: function(variant, state, emitter) {
state.promo = variant === 1 ? 'body' : 'header';
emitter.emit('render');
},
eligible: function() {
return (
!/firefox/i.test(navigator.userAgent) &&
document.querySelector('html').lang === 'en-US'
);
},
variant: function(state) {
return this.luckyNumber(state) > 0.5 ? 1 : 0;
},
luckyNumber: function(state) {
return luckyNumber(
`${this.id}:${state.storage.get('testpilot_ga__cid')}`
);
}
}
};
//Returns a number between 0 and 1 //Returns a number between 0 and 1
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
@ -32,12 +54,12 @@ export default function initialize(state, emitter) {
checkExperiments(state, emitter); checkExperiments(state, emitter);
}); });
} else { } else {
const enrolled = state.storage.enrolled; const enrolled = state.storage.enrolled.filter(([id, variant]) => {
enrolled.forEach(([id, variant]) => {
const xp = experiments[id]; const xp = experiments[id];
if (xp) { if (xp) {
xp.run(variant, state, emitter); xp.run(variant, state, emitter);
} }
return !!xp;
}); });
// single experiment per session for now // single experiment per session for now
if (enrolled.length === 0) { if (enrolled.length === 0) {

View File

@ -47,4 +47,4 @@ app.use(fileManager);
app.use(dragManager); app.use(dragManager);
app.use(experiments); app.use(experiments);
app.mount('#page-one'); app.mount('body');

View File

@ -20,7 +20,7 @@ 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]; experiment = storage.enrolled[0];
sendEvent(category(), 'visit', { sendEvent(category(), 'visit', {
cm5: storage.totalUploads, cm5: storage.totalUploads,
@ -29,6 +29,9 @@ export default function initialize(state, emitter) {
}); });
//TODO restart handlers... somewhere //TODO restart handlers... somewhere
}); });
emitter.on('exit', evt => {
exitEvent(evt);
});
} }
function category() { function category() {
@ -81,6 +84,8 @@ function urlToMetric(url) {
case 'https://testpilot.firefox.com/': case 'https://testpilot.firefox.com/':
case 'https://testpilot.firefox.com/experiments/send': case 'https://testpilot.firefox.com/experiments/send':
return 'testpilot'; return 'testpilot';
case 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com':
return 'promo';
default: default:
return 'other'; return 'other';
} }
@ -244,6 +249,7 @@ function exitEvent(target) {
}); });
} }
// eslint-disable-next-line no-unused-vars
function addExitHandlers() { function addExitHandlers() {
const links = Array.from(document.querySelectorAll('a')); const links = Array.from(document.querySelectorAll('a'));
links.forEach(l => { links.forEach(l => {

View File

@ -1,17 +1,43 @@
const choo = require('choo'); const choo = require('choo');
const html = require('choo/html');
const download = require('./download'); const download = require('./download');
const header = require('../templates/header');
const footer = require('../templates/footer');
const fxPromo = require('../templates/fxPromo');
const app = choo(); const app = choo();
app.route('/', require('./home')); function body(template) {
app.route('/share/:id', require('../templates/share')); return function(state, emit) {
app.route('/download/:id', download); const b = html`<body>
app.route('/download/:id/:key', download); ${state.promo === 'header' ? fxPromo(state, emit) : ''}
app.route('/completed', require('../templates/completed')); ${header(state)}
app.route('/unsupported/:reason', require('../templates/unsupported')); <div class="all">
app.route('/legal', require('../templates/legal')); <noscript>
app.route('/error', require('../templates/error')); <h2>Firefox Send requires JavaScript</h2>
app.route('/blank', require('../templates/blank')); <p><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">Why does Firefox Send require JavaScript?</a></p>
app.route('*', require('../templates/notFound')); <p>Please enable JavaScript and try again.</p>
</noscript>
${template(state, emit)}
</div>
${footer(state)}
</body>`;
if (state.layout) {
return state.layout(state, b);
}
return b;
};
}
app.route('/', body(require('./home')));
app.route('/share/:id', body(require('../templates/share')));
app.route('/download/:id', body(download));
app.route('/download/:id/:key', body(download));
app.route('/completed', body(require('../templates/completed')));
app.route('/unsupported/:reason', body(require('../templates/unsupported')));
app.route('/legal', body(require('../templates/legal')));
app.route('/error', body(require('../templates/error')));
app.route('/blank', body(require('../templates/blank')));
app.route('*', body(require('../templates/notFound')));
module.exports = app; module.exports = app;

View File

@ -1,9 +1,6 @@
const html = require('choo/html'); const html = require('choo/html');
module.exports = function(state) { module.exports = function() {
const div = html`<div id="page-one"></div>`; const div = html`<div id="page-one"></div>`;
if (state.layout) {
return state.layout(state, div);
}
return div; return div;
}; };

View File

@ -1,9 +1,11 @@
const html = require('choo/html'); const html = require('choo/html');
const progress = require('./progress'); const progress = require('./progress');
const { fadeOut } = require('../utils'); const { fadeOut } = require('../utils');
const fxPromo = require('./fxPromo');
module.exports = function(state, emit) { module.exports = function(state, emit) {
const div = html` const div = html`
<div id="page-one">
<div id="download" class="fadeIn"> <div id="download" class="fadeIn">
<div id="download-progress"> <div id="download-progress">
<div id="dl-title" class="title">${state.translate( <div id="dl-title" class="title">${state.translate(
@ -19,6 +21,8 @@ module.exports = function(state, emit) {
sendNew sendNew
}>${state.translate('sendYourFilesLink')}</a> }>${state.translate('sendYourFilesLink')}</a>
</div> </div>
${state.promo === 'body' ? fxPromo(state, emit) : ''}
</div>
`; `;
async function sendNew(e) { async function sendNew(e) {

View File

@ -1,10 +1,12 @@
const html = require('choo/html'); const html = require('choo/html');
const progress = require('./progress'); const progress = require('./progress');
const { bytes } = require('../utils'); const { bytes } = require('../utils');
const fxPromo = require('./fxPromo');
module.exports = function(state) { module.exports = function(state, emit) {
const transfer = state.transfer; const transfer = state.transfer;
const div = html` const div = html`
<div id="page-one">
<div id="download-progress" class="fadeIn"> <div id="download-progress" class="fadeIn">
<div id="dl-title" class="title">${state.translate( <div id="dl-title" class="title">${state.translate(
'downloadingPageProgress', 'downloadingPageProgress',
@ -21,6 +23,8 @@ module.exports = function(state) {
transfer.sizes transfer.sizes
)}</div> )}</div>
</div> </div>
</div>
${state.promo === 'body' ? fxPromo(state, emit) : ''}
</div> </div>
`; `;

31
app/templates/footer.js Normal file
View File

@ -0,0 +1,31 @@
const html = require('choo/html');
const assets = require('../../common/assets');
module.exports = function(state) {
return html`<div class="footer">
<div class="legal-links">
<a href="https://www.mozilla.org" role="presentation"><img class="mozilla-logo" src="${assets.get(
'mozilla-logo.svg'
)}" alt="mozilla"/></a>
<a href="https://www.mozilla.org/about/legal">${state.translate(
'footerLinkLegal'
)}</a>
<a href="https://testpilot.firefox.com/about">${state.translate(
'footerLinkAbout'
)}</a>
<a href="/legal">${state.translate('footerLinkPrivacy')}</a>
<a href="/legal">${state.translate('footerLinkTerms')}</a>
<a href="https://www.mozilla.org/privacy/websites/#cookies">${state.translate(
'footerLinkCookies'
)}</a>
</div>
<div class="social-links">
<a href="https://github.com/mozilla/send" role="presentation"><img class="github" src="${assets.get(
'github-icon.svg'
)}" alt="github"/></a>
<a href="https://twitter.com/FxTestPilot" role="presentation"><img class="twitter" src="${assets.get(
'twitter-icon.svg'
)}" alt="twitter"/></a>
</div>
</div>`;
};

44
app/templates/fxPromo.js Normal file
View File

@ -0,0 +1,44 @@
const html = require('choo/html');
const assets = require('../../common/assets');
// function replaceLinks(str, urls) {
// let i = -1;
// const s = str.replace(/<a>([^<]+)<\/a>/g, (m, v) => {
// i++;
// return `<a class="link" href="${urls[i]}">${v}</a>`;
// });
// return [`<span>${s}</span>`];
// }
module.exports = function(state, emit) {
// function close() {
// document.querySelector('.banner').remove();
// }
function clicked(evt) {
emit('exit', evt);
}
return html`
<div class="banner">
<div>
<img
src="${assets.get('firefox_logo-only.svg')}"
class="firefox-logo-small"
alt="Firefox"/>
<span>Send is brought to you by the all-new Firefox.
<a
class="link"
href="https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com"
onclick=${clicked}
>Download Firefox now </a></span>
</div>
</div>`;
};
/*
<img
src="${assets.get('close-16.svg')}"
class="icon-delete"
onclick=${close}>
*/

21
app/templates/header.js Normal file
View File

@ -0,0 +1,21 @@
const html = require('choo/html');
const assets = require('../../common/assets');
module.exports = function(state) {
return html`<header class="header">
<div class="send-logo">
<a href="/">
<img src="${assets.get(
'send_logo.svg'
)}" alt="Send"/><h1 class="site-title">Send</h1>
</a>
<div class="site-subtitle">
<a href="https://testpilot.firefox.com">Firefox Test Pilot</a>
<div>${state.translate('siteSubtitle')}</div>
</div>
</div>
<a href="https://qsurvey.mozilla.com/s3/txp-firefox-send" rel="noreferrer noopener" class="feedback" target="_blank">${state.translate(
'siteFeedback'
)}</a>
</header>`;
};

View File

@ -30,9 +30,5 @@ module.exports = function(state) {
</div> </div>
</div> </div>
`; `;
if (state.layout) {
return state.layout(state, div);
}
return div; return div;
}; };

View File

@ -17,9 +17,5 @@ module.exports = function(state) {
)}</a> )}</a>
</div> </div>
</div>`; </div>`;
if (state.layout) {
return state.layout(state, div);
}
return div; return div;
}; };

View File

@ -3,6 +3,7 @@ const assets = require('../../common/assets');
const notFound = require('./notFound'); const notFound = require('./notFound');
const downloadPassword = require('./downloadPassword'); const downloadPassword = require('./downloadPassword');
const { bytes } = require('../utils'); const { bytes } = require('../utils');
const fxPromo = require('./fxPromo');
function getFileFromDOM() { function getFileFromDOM() {
const el = document.getElementById('dl-file'); const el = document.getElementById('dl-file');
@ -61,6 +62,7 @@ module.exports = function(state, emit) {
</div> </div>
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a> <a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
</div> </div>
${state.promo === 'body' ? fxPromo(state, emit) : ''}
</div> </div>
`; `;
@ -68,9 +70,5 @@ module.exports = function(state, emit) {
event.preventDefault(); event.preventDefault();
emit('download', fileInfo); emit('download', fileInfo);
} }
if (state.layout) {
return state.layout(state, div);
}
return div; return div;
}; };

View File

@ -42,9 +42,5 @@ module.exports = function(state) {
)}</div> )}</div>
</div>`; </div>`;
const div = html`<div id="page-one">${msg}</div>`; const div = html`<div id="page-one">${msg}</div>`;
if (state.layout) {
return state.layout(state, div);
}
return div; return div;
}; };

View File

@ -1,6 +1,7 @@
const html = require('choo/html'); const html = require('choo/html');
const assets = require('../../common/assets'); const assets = require('../../common/assets');
const fileList = require('./fileList'); const fileList = require('./fileList');
const fxPromo = require('./fxPromo');
const { fadeOut } = require('../utils'); const { fadeOut } = require('../utils');
module.exports = function(state, emit) { module.exports = function(state, emit) {
@ -35,6 +36,7 @@ module.exports = function(state, emit) {
title="${state.translate('uploadPageBrowseButton1')}"> title="${state.translate('uploadPageBrowseButton1')}">
${state.translate('uploadPageBrowseButton1')}</label> ${state.translate('uploadPageBrowseButton1')}</label>
</div> </div>
${state.promo === 'body' ? fxPromo(state, emit) : ''}
${fileList(state, emit)} ${fileList(state, emit)}
</div> </div>
`; `;
@ -67,9 +69,5 @@ module.exports = function(state, emit) {
await fadeOut('page-one'); await fadeOut('page-one');
emit('upload', { file, type: 'click' }); emit('upload', { file, type: 'click' });
} }
if (state.layout) {
return state.layout(state, div);
}
return div; return div;
}; };

View File

@ -108,7 +108,8 @@ function bytes(num) {
let nStr = n.toFixed(1); let nStr = n.toFixed(1);
if (LOCALIZE_NUMBERS) { if (LOCALIZE_NUMBERS) {
try { try {
nStr = n.toLocaleString(navigator.languages.map(l => l.split(';')[0]), { const locale = document.querySelector('html').lang;
nStr = n.toLocaleString(locale, {
minimumFractionDigits: 1, minimumFractionDigits: 1,
maximumFractionDigits: 1 maximumFractionDigits: 1
}); });
@ -122,10 +123,8 @@ function bytes(num) {
function percent(ratio) { function percent(ratio) {
if (LOCALIZE_NUMBERS) { if (LOCALIZE_NUMBERS) {
try { try {
return ratio.toLocaleString( const locale = document.querySelector('html').lang;
navigator.languages.map(l => l.split(';')[0]), return ratio.toLocaleString(locale, { style: 'percent' });
{ style: 'percent' }
);
} catch (e) { } catch (e) {
// fall through // fall through
} }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -8,7 +8,6 @@ html {
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center top; background-position: center top;
height: 100%; height: 100%;
max-width: 1440px;
margin: auto; margin: auto;
} }
@ -130,7 +129,7 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
max-width: 630px; max-width: 650px;
margin: 0 auto; margin: 0 auto;
padding: 0 20px; padding: 0 20px;
box-sizing: border-box; box-sizing: border-box;
@ -213,7 +212,7 @@ a {
.upload-window { .upload-window {
border: 3px dashed rgba(0, 148, 251, 0.5); border: 3px dashed rgba(0, 148, 251, 0.5);
margin: 0 auto; margin: 0 auto 10px;
height: 255px; height: 255px;
border-radius: 4px; border-radius: 4px;
display: flex; display: flex;
@ -709,6 +708,10 @@ tbody {
width: 70px; width: 70px;
} }
.firefox-logo-small {
width: 24px;
}
#dl-firefox, #dl-firefox,
#update-firefox { #update-firefox {
margin-bottom: 181px; margin-bottom: 181px;
@ -766,7 +769,7 @@ tbody {
} }
#download { #download {
margin: 0 auto; margin: 0 auto 30px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -955,6 +958,29 @@ tbody {
background-position: 2px 1px; background-position: 2px 1px;
} }
.banner {
padding: 0 15px;
height: 48px;
background-color: #efeff1;
color: #4a4a4f;
font-size: 13px;
display: flex;
flex-direction: row;
align-content: center;
align-items: center;
justify-content: center;
}
.banner > div {
display: flex;
align-items: center;
margin: 0 auto;
}
.banner > div > span {
margin-left: 10px;
}
@media (max-device-width: 992px), (max-width: 992px) { @media (max-device-width: 992px), (max-width: 992px) {
.popup .popuptext { .popup .popuptext {
left: auto; left: auto;

View File

@ -112,6 +112,7 @@
"en-US", "en-US",
"ast", "ast",
"az", "az",
"bs",
"ca", "ca",
"cak", "cak",
"cs", "cs",
@ -132,6 +133,7 @@
"id", "id",
"it", "it",
"ja", "ja",
"ka",
"kab", "kab",
"ko", "ko",
"ms", "ms",
@ -145,6 +147,7 @@
"sl", "sl",
"sr", "sr",
"sv-SE", "sv-SE",
"tl",
"tr", "tr",
"uk", "uk",
"vi", "vi",

View File

@ -8,7 +8,7 @@ module.exports = function(state, body = '') {
: ''; : '';
return html` return html`
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="${state.locale}">
<head> <head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8" /> <meta charset="utf-8" />
@ -31,7 +31,7 @@ module.exports = function(state, body = '') {
<title>${state.title}</title> <title>${state.title}</title>
<link rel="stylesheet" type="text/css" href="${assets.get('main.css')}" /> <link rel="stylesheet" type="text/css" href="${assets.get('main.css')}" />
<!-- generic favicons --> <!-- generic favicons -->
<link rel="icon" href="${assets.get('favicon-32.png')}" sizes="32x32"> <link rel="icon" href="${assets.get('favicon-32.png')}" sizes="32x32">
<link rel="icon" href="${assets.get('favicon-96.png')}" sizes="96x96"> <link rel="icon" href="${assets.get('favicon-96.png')}" sizes="96x96">
@ -62,8 +62,8 @@ module.exports = function(state, body = '') {
<! Windows 8.1 + IE11 and above > <! Windows 8.1 + IE11 and above >
<meta name="msapplication-config" content="/browserconfig.xml"/> <meta name="msapplication-config" content="/browserconfig.xml"/>
${firaTag} ${firaTag}
<script defer src="/jsconfig.js"></script> <script defer src="/jsconfig.js"></script>
<script defer src="${assets.get('runtime.js')}"></script> <script defer src="${assets.get('runtime.js')}"></script>
@ -71,58 +71,7 @@ module.exports = function(state, body = '') {
<script defer src="${locales.get(state.locale)}"></script> <script defer src="${locales.get(state.locale)}"></script>
<script defer src="${assets.get('app.js')}"></script> <script defer src="${assets.get('app.js')}"></script>
</head> </head>
<body> ${body}
<header class="header">
<div class="send-logo">
<a href="/">
<img src="${assets.get(
'send_logo.svg'
)}" alt="Send"/><h1 class="site-title">Send</h1>
</a>
<div class="site-subtitle">
<a href="https://testpilot.firefox.com">Firefox Test Pilot</a>
<div>${state.translate('siteSubtitle')}</div>
</div>
</div>
<a href="https://qsurvey.mozilla.com/s3/txp-firefox-send" rel="noreferrer noopener" class="feedback" target="_blank">${state.translate(
'siteFeedback'
)}</a>
</header>
<div class="all">
<noscript>
<h2>Firefox Send requires JavaScript</h2>
<p><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">Why does Firefox Send require JavaScript?</a></p>
<p>Please enable JavaScript and try again.</p>
</noscript>
${body}
</div>
<div class="footer">
<div class="legal-links">
<a href="https://www.mozilla.org" role="presentation"><img class="mozilla-logo" src="${assets.get(
'mozilla-logo.svg'
)}" alt="mozilla"/></a>
<a href="https://www.mozilla.org/about/legal">${state.translate(
'footerLinkLegal'
)}</a>
<a href="https://testpilot.firefox.com/about">${state.translate(
'footerLinkAbout'
)}</a>
<a href="/legal">${state.translate('footerLinkPrivacy')}</a>
<a href="/legal">${state.translate('footerLinkTerms')}</a>
<a href="https://www.mozilla.org/privacy/websites/#cookies">${state.translate(
'footerLinkCookies'
)}</a>
</div>
<div class="social-links">
<a href="https://github.com/mozilla/send" role="presentation"><img class="github" src="${assets.get(
'github-icon.svg'
)}" alt="github"/></a>
<a href="https://twitter.com/FxTestPilot" role="presentation"><img class="twitter" src="${assets.get(
'twitter-icon.svg'
)}" alt="twitter"/></a>
</div>
</div>
</body>
</html> </html>
`; `;
}; };