diff --git a/frontend/src/common.js b/frontend/src/common.js index c993ed7a..361a81d9 100644 --- a/frontend/src/common.js +++ b/frontend/src/common.js @@ -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 }; diff --git a/frontend/src/download.js b/frontend/src/download.js index 230a2062..98ac2ce9 100644 --- a/frontend/src/download.js +++ b/frontend/src/download.js @@ -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'); }); }); diff --git a/frontend/src/metrics.js b/frontend/src/metrics.js new file mode 100644 index 00000000..afa2f5cf --- /dev/null +++ b/frontend/src/metrics.js @@ -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 +}; diff --git a/frontend/src/upload.js b/frontend/src/upload.js index 1423f6d4..8dd315b5 100644 --- a/frontend/src/upload.js +++ b/frontend/src/upload.js @@ -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'); @@ -385,7 +328,7 @@ $(() => { const cellText = document.createTextNode(file.name); const url = file.url.trim() + `#${file.secretKey}`.trim(); - + $link.attr('value', url); $('#copy-text') .attr('data-l10n-args', `{"filename": "${file.name}"}`) @@ -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'); }); }); diff --git a/server/server.js b/server/server.js index 8415f061..674d541d 100644 --- a/server/server.js +++ b/server/server.js @@ -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'); diff --git a/views/download.handlebars b/views/download.handlebars index dc7b0085..ede45fdd 100644 --- a/views/download.handlebars +++ b/views/download.handlebars @@ -7,8 +7,8 @@ data-l10n-args='{"filename": "{{filename}}"}'> - {{sizeInBytes}} - {{timeToExpiry}} + {{sizeInBytes}} + {{ttl}}
@@ -35,5 +35,5 @@ - + diff --git a/views/index.handlebars b/views/index.handlebars index 49a849bb..76c37dd6 100644 --- a/views/index.handlebars +++ b/views/index.handlebars @@ -61,7 +61,7 @@ - + @@ -69,5 +69,5 @@ - + diff --git a/views/notfound.handlebars b/views/notfound.handlebars index 9f89c95a..56ce2632 100644 --- a/views/notfound.handlebars +++ b/views/notfound.handlebars @@ -4,5 +4,5 @@ - +