Merge pull request #452 from mozilla/refactor-metrics

refactored metrics
This commit is contained in:
Danny Coates 2017-08-06 09:00:16 -07:00 committed by GitHub
commit 6d17b86d28
8 changed files with 330 additions and 258 deletions

View File

@ -1,61 +1,22 @@
const testPilotGA = require('testpilot-ga');
const Raven = require('raven-js');
const { unsupported } = require('./metrics');
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
}
const analytics = new testPilotGA({
an: 'Firefox Send',
ds: 'web',
tid: window.GOOGLE_ANALYTICS_ID
});
function sendEvent() {
return analytics.sendEvent.apply(analytics, arguments).catch(() => 0);
}
function findMetric(href) {
switch (href) {
case 'https://www.mozilla.org/':
return 'mozilla';
case 'https://www.mozilla.org/about/legal':
return 'legal';
case 'https://testpilot.firefox.com/about':
return 'about';
case 'https://testpilot.firefox.com/privacy':
return 'privacy';
case 'https://testpilot.firefox.com/terms':
return 'terms';
case 'https://www.mozilla.org/privacy/websites/#cookies':
return 'cookies';
case 'https://github.com/mozilla/send':
return 'github';
case 'https://twitter.com/FxTestPilot':
return 'twitter';
case 'https://www.mozilla.org/firefox/new/?scene=2':
return 'download-firefox';
default:
return 'other';
}
}
const ua = navigator.userAgent.toLowerCase();
if (
ua.indexOf('firefox') > -1 &&
parseInt(ua.match(/firefox\/*([^\n\r]*)\./)[1], 10) <= 49
) {
const isSender = !location.pathname.includes('/download');
const ec = isSender ? 'sender' : 'recipient';
sendEvent(ec, 'unsupported', {
cd6: new Error('Firefox is outdated.')
unsupported({
err: new Error('Firefox is outdated.')
}).then(() => {
location.replace('/unsupported/outdated');
});
}
module.exports = {
Raven,
sendEvent,
findMetric
Raven
};

View File

@ -1,10 +1,11 @@
const { Raven, findMetric, sendEvent } = require('./common');
const { Raven } = require('./common');
const FileReceiver = require('./fileReceiver');
const { notify, gcmCompliant } = require('./utils');
const bytes = require('bytes');
const Storage = require('./storage');
const storage = new Storage(localStorage);
const links = require('./links');
const metrics = require('./metrics');
const $ = require('jquery');
require('jquery-circle-progress');
@ -13,28 +14,13 @@ $(() => {
gcmCompliant()
.then(() => {
const $downloadBtn = $('#download-btn');
const $sendNew = $('.send-new');
const $dlProgress = $('#dl-progress');
const $progressText = $('.progress-text');
const $title = $('.title');
$sendNew.on('click', () => {
sendEvent('recipient', 'restarted', {
cd2: 'completed'
});
});
$('.legal-links a, .social-links a, #dl-firefox').on('click', function(target) {
const metric = findMetric(target.currentTarget.href);
// record exited event by recipient
sendEvent('recipient', 'exited', {
cd3: metric
});
});
const filename = $('#dl-filename').text();
const bytelength = Number($('#dl-bytelength').text());
const timeToExpiry = Number($('#dl-ttl').text());
const size = Number($('#dl-size').text());
const ttl = Number($('#dl-ttl').text());
//initiate progress bar
$dlProgress.circleProgress({
@ -50,22 +36,11 @@ $(() => {
$downloadBtn.attr('disabled', 'disabled');
links.setOpenInNewTab(true);
storage.totalDownloads += 1;
const fileReceiver = new FileReceiver();
const unexpiredFiles = storage.numFiles;
fileReceiver.on('progress', progress => {
window.onunload = function() {
storage.referrer = 'cancelled-download';
// record download-stopped (cancelled by tab close or reload)
sendEvent('recipient', 'download-stopped', {
cm1: bytelength,
cm5: storage.totalUploads,
cm6: unexpiredFiles,
cm7: storage.totalDownloads,
cd2: 'cancelled'
});
metrics.cancelledDownload({ size });
};
$('#download-page-one').attr('hidden', true);
@ -115,27 +90,12 @@ $(() => {
const startTime = Date.now();
// record download-started by recipient
sendEvent('recipient', 'download-started', {
cm1: bytelength,
cm4: timeToExpiry,
cm5: storage.totalUploads,
cm6: unexpiredFiles,
cm7: storage.totalDownloads
});
metrics.startedDownload({ size, ttl });
fileReceiver
.download()
.catch(err => {
// record download-stopped (errored) by recipient
sendEvent('recipient', 'download-stopped', {
cm1: bytelength,
cm5: storage.totalUploads,
cm6: unexpiredFiles,
cm7: storage.totalDownloads,
cd2: 'errored',
cd6: err
});
metrics.stoppedDownload({ size, err });
if (err.message === 'notfound') {
location.reload();
@ -150,21 +110,11 @@ $(() => {
})
.then(([decrypted, fname]) => {
const endTime = Date.now();
const totalTime = endTime - startTime;
const time = endTime - startTime;
const downloadTime = endTime - downloadEnd;
const downloadSpeed = bytelength / (downloadTime / 1000);
storage.referrer = 'completed-download';
// record download-stopped (completed) by recipient
sendEvent('recipient', 'download-stopped', {
cm1: bytelength,
cm2: totalTime,
cm3: downloadSpeed,
cm5: storage.totalUploads,
cm6: unexpiredFiles,
cm7: storage.totalDownloads,
cd2: 'completed'
});
const speed = size / (downloadTime / 1000);
storage.totalDownloads += 1;
metrics.completedDownload({ size, time, speed });
const dataView = new DataView(decrypted);
const blob = new Blob([dataView]);
@ -186,14 +136,12 @@ $(() => {
return Promise.reject(err);
})
.then(() => links.setOpenInNewTab(false));
}
};
$downloadBtn.on('click', download);
})
.catch(err => {
sendEvent('sender', 'unsupported', {
cd6: err
}).then(() => {
metrics.unsupported({ err }).then(() => {
location.replace('/unsupported/gcm');
});
});

234
frontend/src/metrics.js Normal file
View File

@ -0,0 +1,234 @@
const testPilotGA = require('testpilot-ga');
const Storage = require('./storage');
const storage = new Storage(localStorage);
const analytics = new testPilotGA({
an: 'Firefox Send',
ds: 'web',
tid: window.GOOGLE_ANALYTICS_ID
});
const category = location.pathname.includes('/download')
? 'recipient'
: 'sender';
document.addEventListener('DOMContentLoaded', function() {
addExitHandlers();
addRestartHandlers();
});
function sendEvent() {
return analytics.sendEvent.apply(analytics, arguments).catch(() => 0);
}
function urlToMetric(url) {
switch (url) {
case 'https://www.mozilla.org/':
return 'mozilla';
case 'https://www.mozilla.org/about/legal':
return 'legal';
case 'https://testpilot.firefox.com/about':
return 'about';
case 'https://testpilot.firefox.com/privacy':
return 'privacy';
case 'https://testpilot.firefox.com/terms':
return 'terms';
case 'https://www.mozilla.org/privacy/websites/#cookies':
return 'cookies';
case 'https://github.com/mozilla/send':
return 'github';
case 'https://twitter.com/FxTestPilot':
return 'twitter';
case 'https://www.mozilla.org/firefox/new/?scene=2':
return 'download-firefox';
default:
return 'other';
}
}
function setReferrer(state) {
if (category === 'sender') {
if (state) {
storage.referrer = `${state}-upload`;
}
} else if (category === 'recipient') {
if (state) {
storage.referrer = `${state}-download`;
}
}
}
function externalReferrer() {
if (/^https:\/\/testpilot\.firefox\.com/.test(document.referrer)) {
return 'testpilot';
}
return 'external';
}
function takeReferrer() {
const referrer = storage.referrer || externalReferrer();
storage.referrer = null;
return referrer;
}
function startedUpload(params) {
return sendEvent(category, 'upload-started', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.numFiles + 1,
cm7: storage.totalDownloads,
cd1: params.type,
cd5: takeReferrer()
});
}
function cancelledUpload(params) {
setReferrer('cancelled');
return sendEvent(category, 'upload-stopped', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.numFiles,
cm7: storage.totalDownloads,
cd1: params.type,
cd2: 'cancelled'
});
}
function completedUpload(params) {
return sendEvent(category, 'upload-stopped', {
cm1: params.size,
cm2: params.time,
cm3: params.speed,
cm5: storage.totalUploads,
cm6: storage.numFiles,
cm7: storage.totalDownloads,
cd1: params.type,
cd2: 'completed'
});
}
function startedDownload(params) {
return sendEvent(category, 'download-started', {
cm1: params.size,
cm4: params.ttl,
cm5: storage.totalUploads,
cm6: storage.numFiles,
cm7: storage.totalDownloads
});
}
function stoppedDownload(params) {
return sendEvent(category, 'download-stopped', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.numFiles,
cm7: storage.totalDownloads,
cd2: 'errored',
cd6: params.err
});
}
function cancelledDownload(params) {
setReferrer('cancelled');
return sendEvent(category, 'download-stopped', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.numFiles,
cm7: storage.totalDownloads,
cd2: 'cancelled'
});
}
function stoppedUpload(params) {
return sendEvent(category, 'upload-stopped', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.numFiles,
cm7: storage.totalDownloads,
cd1: params.type,
cd2: 'errored',
cd6: params.err
});
}
function completedDownload(params) {
return sendEvent(category, 'download-stopped', {
cm1: params.size,
cm2: params.time,
cm3: params.speed,
cm5: storage.totalUploads,
cm6: storage.numFiles,
cm7: storage.totalDownloads,
cd2: 'completed'
});
}
function deletedUpload(params) {
return sendEvent(category, 'upload-deleted', {
cm1: params.size,
cm2: params.time,
cm3: params.speed,
cm4: params.ttl,
cm5: storage.totalUploads,
cm6: storage.numFiles,
cm7: storage.totalDownloads,
cd1: params.type,
cd4: params.location
});
}
function unsupported(params) {
return sendEvent(category, 'unsupported', {
cd6: params.err
});
}
function copiedLink(params) {
return sendEvent(category, 'copied', {
cd4: params.location
});
}
function exitEvent(target) {
return sendEvent(category, 'exited', {
cd3: urlToMetric(target.currentTarget.href)
});
}
function addExitHandlers() {
const links = document.querySelectorAll('a');
links.forEach(l => {
if (/^http/.test(l.href)) {
l.addEventListener('click', exitEvent);
}
});
}
function restartEvent(state) {
setReferrer(state);
return sendEvent(category, 'restarted', {
cd2: state
});
}
function addRestartHandlers() {
const elements = document.querySelectorAll('.send-new');
elements.forEach(el => {
const state = el.getAttribute('data-state');
el.addEventListener('click', restartEvent.bind(null, state));
});
}
module.exports = {
copiedLink,
startedUpload,
cancelledUpload,
stoppedUpload,
completedUpload,
deletedUpload,
startedDownload,
cancelledDownload,
stoppedDownload,
completedDownload,
unsupported
};

View File

@ -1,5 +1,5 @@
/* global MAXFILESIZE EXPIRE_SECONDS */
const { Raven, findMetric, sendEvent } = require('./common');
const { Raven } = require('./common');
const FileSender = require('./fileSender');
const {
copyToClipboard,
@ -10,17 +10,11 @@ const {
const bytes = require('bytes');
const Storage = require('./storage');
const storage = new Storage(localStorage);
const metrics = require('./metrics');
const $ = require('jquery');
require('jquery-circle-progress');
if (storage.has('referrer')) {
window.referrer = storage.referrer;
storage.remove('referrer');
} else {
window.referrer = 'external';
}
const allowedCopy = () => {
const support = !!document.queryCommandSupported;
return support ? document.queryCommandSupported('copy') : false;
@ -28,7 +22,7 @@ const allowedCopy = () => {
$(() => {
gcmCompliant()
.then(function () {
.then(function() {
const $pageOne = $('#page-one');
const $copyBtn = $('#copy-btn');
const $link = $('#link');
@ -42,38 +36,13 @@ $(() => {
$pageOne.removeAttr('hidden');
$('#file-upload').on('change', onUpload);
$('.legal-links a, .social-links a, #dl-firefox').on('click', function(target) {
// record exited event by recipient
sendEvent('sender', 'exited', {
cd3: findMetric(target.currentTarget.href)
});
});
$('#send-new-completed').on('click', function() {
// record restarted event
storage.referrer = 'errored-upload';
sendEvent('sender', 'restarted', {
cd2: 'completed'
});
});
$('#send-new-error').on('click', function() {
// record restarted event
storage.referrer = 'errored-upload';
sendEvent('sender', 'restarted', {
cd2: 'errored'
});
});
$(document.body)
.on('dragover', allowDrop)
.on('drop', onUpload);
$(document.body).on('dragover', allowDrop).on('drop', onUpload);
// reset copy button
$copyBtn.attr({
disabled: !allowedCopy(),
'data-l10n-id': 'copyUrlFormButton'
})
});
$link.attr('disabled', false);
@ -84,7 +53,7 @@ $(() => {
} else {
$fileList.removeAttr('hidden');
}
}
};
const files = storage.files;
if (files.length === 0) {
@ -101,10 +70,7 @@ $(() => {
// copy link to clipboard
$copyBtn.on('click', () => {
if (allowedCopy() && copyToClipboard($link.attr('value'))) {
// record copied event from success screen
sendEvent('sender', 'copied', {
cd4: 'success-screen'
});
metrics.copiedLink({ location: 'success-screen' });
//disable button for 3s
$copyBtn.attr('disabled', true);
@ -122,12 +88,13 @@ $(() => {
}
});
$uploadWindow.on('dragover', () => {
$uploadWindow.addClass('ondrag');
})
.on('dragleave', () => {
$uploadWindow.removeClass('ondrag');
});
$uploadWindow
.on('dragover', () => {
$uploadWindow.addClass('ondrag');
})
.on('dragleave', () => {
$uploadWindow.removeClass('ondrag');
});
//initiate progress bar
$ulProgress.circleProgress({
@ -144,6 +111,7 @@ $(() => {
// on file upload by browse or drag & drop
function onUpload(event) {
event.preventDefault();
const clickOrDrop = event.type === 'drop' ? 'drop' : 'click';
// don't allow upload if not on upload page
if ($pageOne.attr('hidden')) {
@ -153,7 +121,7 @@ $(() => {
storage.totalUploads += 1;
let file = '';
if (event.type === 'drop') {
if (clickOrDrop === 'drop') {
if (!event.originalEvent.dataTransfer.files[0]) {
$uploadWindow.removeClass('ondrag');
return;
@ -193,16 +161,9 @@ $(() => {
const fileSender = new FileSender(file);
$('#cancel-upload').on('click', () => {
fileSender.cancel();
storage.referrer = 'cancelled-upload';
// record upload-stopped (cancelled) by sender
sendEvent('sender', 'upload-stopped', {
cm1: file.size,
cm5: storage.totalUploads,
cm6: unexpiredFiles,
cm7: storage.totalDownloads,
cd1: event.type === 'drop' ? 'drop' : 'click',
cd2: 'cancelled'
metrics.cancelledUpload({
size: file.size,
type: clickOrDrop
});
location.reload();
});
@ -245,18 +206,10 @@ $(() => {
let t;
const startTime = Date.now();
const unexpiredFiles = storage.numFiles + 1;
// record upload-started event by sender
sendEvent('sender', 'upload-started', {
cm1: file.size,
cm5: storage.totalUploads,
cm6: unexpiredFiles,
cm7: storage.totalDownloads,
cd1: event.type === 'drop' ? 'drop' : 'click',
cd5: window.referrer
metrics.startedUpload({
size: file.size,
type: clickOrDrop
});
// For large files we need to give the ui a tick to breathe and update
// before we kick off the FileSender
setTimeout(() => {
@ -264,21 +217,16 @@ $(() => {
.upload()
.then(info => {
const endTime = Date.now();
const totalTime = endTime - startTime;
const time = endTime - startTime;
const uploadTime = endTime - uploadStart;
const uploadSpeed = file.size / (uploadTime / 1000);
const speed = file.size / (uploadTime / 1000);
const expiration = EXPIRE_SECONDS * 1000;
// record upload-stopped (completed) by sender
sendEvent('sender', 'upload-stopped', {
cm1: file.size,
cm2: totalTime,
cm3: uploadSpeed,
cm5: storage.totalUploads,
cm6: unexpiredFiles,
cm7: storage.totalDownloads,
cd1: event.type === 'drop' ? 'drop' : 'click',
cd2: 'completed'
metrics.completedUpload({
size: file.size,
time,
speed,
type: clickOrDrop
});
const fileData = {
@ -290,9 +238,9 @@ $(() => {
deleteToken: info.deleteToken,
creationDate: new Date(),
expiry: expiration,
totalTime: totalTime,
typeOfUpload: event.type === 'drop' ? 'drop' : 'click',
uploadSpeed: uploadSpeed
totalTime: time,
typeOfUpload: clickOrDrop,
uploadSpeed: speed
};
storage.addFile(info.fileId, fileData);
@ -324,15 +272,10 @@ $(() => {
$uploadError.removeAttr('hidden');
window.clearTimeout(t);
// record upload-stopped (errored) by sender
sendEvent('sender', 'upload-stopped', {
cm1: file.size,
cm5: storage.totalUploads,
cm6: unexpiredFiles,
cm7: storage.totalDownloads,
cd1: event.type === 'drop' ? 'drop' : 'click',
cd2: 'errored',
cd6: err
metrics.stoppedUpload({
size: file.size,
type: clickOrDrop,
err
});
});
}, 10);
@ -363,7 +306,7 @@ $(() => {
}
//update file table with current files in storage
const populateFileList = (file) => {
const populateFileList = file => {
const row = document.createElement('tr');
const name = document.createElement('td');
const link = document.createElement('td');
@ -404,9 +347,7 @@ $(() => {
del.appendChild(delSpan);
const linkSpan = document.createElement('span');
$(linkSpan)
.addClass('icon-docs')
.attr('data-l10n-id', 'copyUrlHover');
$(linkSpan).addClass('icon-docs').attr('data-l10n-id', 'copyUrlHover');
link.appendChild(linkSpan);
link.style.color = '#0A8DFF';
@ -414,9 +355,7 @@ $(() => {
//copy link to clipboard when icon clicked
$copyIcon.on('click', () => {
// record copied event from upload list
sendEvent('sender', 'copied', {
cd4: 'upload-list'
});
metrics.copiedLink({ location: 'upload-list' });
copyToClipboard(url);
document.l10n.formatValue('copiedUrl').then(translated => {
link.innerHTML = translated;
@ -468,7 +407,7 @@ $(() => {
window.clearTimeout(t);
toggleHeader();
}
}
};
poll();
@ -496,59 +435,51 @@ $(() => {
row.appendChild(del);
$('tbody').append(row); //add row to table
const unexpiredFiles = storage.numFiles;
// delete file
$popupText.find('.popup-yes').on('click', e => {
FileSender.delete(file.fileId, file.deleteToken).then(() => {
$(e.target).parents('tr').remove();
const timeToExpiry =
const ttl =
ONE_DAY_IN_MS - (Date.now() - file.creationDate.getTime());
// record upload-deleted from file list
sendEvent('sender', 'upload-deleted', {
cm1: file.size,
cm2: file.totalTime,
cm3: file.uploadSpeed,
cm4: timeToExpiry,
cm5: storage.totalUploads,
cm6: unexpiredFiles,
cm7: storage.totalDownloads,
cd1: file.typeOfUpload,
cd4: 'upload-list'
}).then(() => {
storage.remove(file.fileId);
});
metrics
.deletedUpload({
size: file.size,
time: file.totalTime,
speed: file.uploadSpeed,
type: file.typeOfUpload,
location: 'upload-list',
ttl
})
.then(() => {
storage.remove(file.fileId);
});
toggleHeader();
});
});
$('#delete-file').on('click', () => {
FileSender.delete(file.fileId, file.deleteToken).then(() => {
const timeToExpiry =
const ttl =
ONE_DAY_IN_MS - (Date.now() - file.creationDate.getTime());
// record upload-deleted from success screen
sendEvent('sender', 'upload-deleted', {
cm1: file.size,
cm2: file.totalTime,
cm3: file.uploadSpeed,
cm4: timeToExpiry,
cm5: storage.totalUploads,
cm6: unexpiredFiles,
cm7: storage.totalDownloads,
cd1: file.typeOfUpload,
cd4: 'success-screen'
}).then(() => {
storage.remove(file.fileId);
location.reload();
});
metrics
.deletedUpload({
size: file.size,
time: file.totalTime,
speed: file.uploadSpeed,
type: file.typeOfUpload,
location: 'success-screen',
ttl
})
.then(() => {
storage.remove(file.fileId);
location.reload();
});
});
});
// show popup
$delIcon.on('click', () => {
$popupText
.addClass('show')
.focus();
$popupText.addClass('show').focus();
});
// hide popup
@ -567,12 +498,10 @@ $(() => {
});
toggleHeader();
}
};
})
.catch(err => {
sendEvent('sender', 'unsupported', {
cd6: err
}).then(() => {
metrics.unsupported({ err }).then(() => {
location.replace('/unsupported/gcm');
});
});

View File

@ -143,12 +143,12 @@ app.get('/download/:id', async (req, res) => {
try {
const filename = await storage.filename(id);
const contentLength = await storage.length(id);
const timeToExpiry = await storage.ttl(id);
const ttl = await storage.ttl(id);
res.render('download', {
filename: decodeURIComponent(filename),
filesize: bytes(contentLength),
sizeInBytes: contentLength,
timeToExpiry: timeToExpiry
ttl
});
} catch (e) {
res.status(404).render('notfound');

View File

@ -7,8 +7,8 @@
data-l10n-args='{"filename": "{{filename}}"}'></span>
<span data-l10n-id="downloadFileSize"
data-l10n-args='{"size": "{{filesize}}"}'></span>
<span id="dl-bytelength" hidden="true">{{sizeInBytes}}</span>
<span id="dl-ttl" hidden="true">{{timeToExpiry}}</span>
<span id="dl-size" hidden="true">{{sizeInBytes}}</span>
<span id="dl-ttl" hidden="true">{{ttl}}</span>
</div>
<div class="description" data-l10n-id="downloadMessage"></div>
<img src="/resources/illustration_download.svg" id="download-img" data-l10n-id="downloadAltText"/>
@ -35,5 +35,5 @@
</div>
</div>
<a class="send-new" data-l10n-id="sendYourFilesLink" href="/"></a>
<a class="send-new" data-state="completed" data-l10n-id="sendYourFilesLink" href="/"></a>
</div>

View File

@ -61,7 +61,7 @@
<button id="copy-btn" data-l10n-id="copyUrlFormButton"></button>
</div>
<button id="delete-file" data-l10n-id="deleteFileButton"></button>
<a class="send-new" id="send-new-completed" data-l10n-id="sendAnotherFileLink"></a>
<a class="send-new" data-state="completed" data-l10n-id="sendAnotherFileLink"></a>
</div>
</div>
@ -69,5 +69,5 @@
<div class="title" data-l10n-id="errorPageHeader"></div>
<div class="expired-description" data-l10n-id="errorPageMessage"></div>
<img id="upload-error-img" data-l10n-id="errorAltText" src="/resources/illustration_error.svg"/>
<a class="send-new" id="send-new-error" data-l10n-id="sendAnotherFileLink"></a>
<a class="send-new" data-state="errored" data-l10n-id="sendAnotherFileLink"></a>
</div>

View File

@ -4,5 +4,5 @@
<img src="/resources/illustration_expired.svg" id="expired-img" data-l10n-id="linkExpiredAlt"/>
</div>
<div class="expired-description" data-l10n-id="uploadPageExplainer"></div>
<a class="send-new" href="/" id="expired-send-new" data-l10n-id="sendYourFilesLink"></a>
<a class="send-new" href="/" data-state="notfound" data-l10n-id="sendYourFilesLink"></a>
</div>