diff --git a/docs/metrics.md b/docs/metrics.md index 035a57fa..54d27deb 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -112,6 +112,7 @@ Fired whenever a user deletes a file they’ve uploaded. - `cm6` - `cm7` - `cd1` +- `cd4` #### `copied` Fired whenever a user copies the URL of an upload file. diff --git a/frontend/src/download.js b/frontend/src/download.js index 5aa951eb..c8a82d0d 100644 --- a/frontend/src/download.js +++ b/frontend/src/download.js @@ -1,15 +1,51 @@ const FileReceiver = require('./fileReceiver'); -const { notify } = require('./utils'); +const { notify, findMetric, sendEvent } = require('./utils'); const bytes = require('bytes'); +const Storage = require('./storage'); +const storage = new Storage(localStorage); + const $ = require('jquery'); require('jquery-circle-progress'); const Raven = window.Raven; + $(document).ready(function() { //link back to homepage $('.send-new').attr('href', window.location.origin); + if (location.pathname.toString().includes('download')) { + $('.send-new').click(function(target) { + target.preventDefault(); + sendEvent('recipient', 'restarted', { + cd2: 'completed' + }) + .then(() => { + location.href = target.currentTarget.href; + }); + }) + + + $('.legal-links a, .social-links a, #dl-firefox').click(function(target) { + target.preventDefault(); + const metric = findMetric(target.currentTarget.href); + // record exited event by recipient + sendEvent('recipient', 'exited', { + cd3: metric + }) + .then(() => { + location.href = target.currentTarget.href; + }); + }) + + $('#expired-send-new').click(function() { + storage.referrer = 'errored-download'; + }) + + } + const filename = $('#dl-filename').html(); + const bytelength = Number($('#dl-bytelength').text()); + const timeToExpiry = Number($('#dl-ttl').text()); //initiate progress bar $('#dl-progress').circleProgress({ @@ -21,9 +57,26 @@ $(document).ready(function() { }); $('#download-btn').click(download); function download() { + 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' + }) + } + $('#download-page-one').attr('hidden', true); $('#download-progress').removeAttr('hidden'); const percent = progress[0] / progress[1]; @@ -39,15 +92,18 @@ $(document).ready(function() { notify(translated[0]); $('.title').html(translated[1]); }); + window.onunload = null; } }); + let downloadEnd; fileReceiver.on('decrypting', isStillDecrypting => { // The file is being decrypted if (isStillDecrypting) { console.log('Decrypting'); } else { console.log('Done decrypting'); + downloadEnd = Date.now(); } }); @@ -60,9 +116,30 @@ $(document).ready(function() { } }); + 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 + }); + fileReceiver .download() - .catch(() => { + .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 + }); + document.l10n.formatValue('expiredPageHeader') .then(translated => { $('.title').text(translated); @@ -73,6 +150,23 @@ $(document).ready(function() { return; }) .then(([decrypted, fname]) => { + const endTime = Date.now(); + const totalTime = 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 dataView = new DataView(decrypted); const blob = new Blob([dataView]); const downloadUrl = URL.createObjectURL(blob); diff --git a/frontend/src/main.js b/frontend/src/main.js index 12c05a38..ae7641d9 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,5 +1,13 @@ window.Raven = require('raven-js'); window.Raven.config(window.dsn).install(); window.dsn = undefined; + +const testPilotGA = require('testpilot-ga'); +window.analytics = new testPilotGA({ + an: 'Firefox Send', + ds: 'web', + tid: window.trackerId +}) + require('./upload'); require('./download'); diff --git a/frontend/src/storage.js b/frontend/src/storage.js new file mode 100644 index 00000000..c7a9aa85 --- /dev/null +++ b/frontend/src/storage.js @@ -0,0 +1,66 @@ +const { isFile } = require('./utils'); + +class Storage { + constructor(engine) { + this.engine = engine + } + + get totalDownloads() { + return Number(this.engine.getItem('totalDownloads')); + } + set totalDownloads(n) { + this.engine.setItem('totalDownloads', n); + } + get totalUploads() { + return Number(this.engine.getItem('totalUploads')); + } + set totalUploads(n) { + this.engine.setItem('totalUploads', n); + } + get referrer() { + return this.engine.getItem('referrer'); + } + set referrer(str) { + this.engine.setItem('referrer', str); + } + + get files() { + const fs = []; + for (let i = 0; i < this.engine.length; i++) { + const k = this.engine.key(i); + if (isFile(k)) { + fs.push(JSON.parse(this.engine.getItem(k))); // parse or whatever else + } + } + return fs; + } + + get numFiles() { + let length = 0; + for (let i = 0; i < this.engine.length; i++) { + const k = this.engine.key(i); + if (isFile(k)) { + length += 1; + } + } + return length; + } + + getFileById(id) { + return this.engine.getItem(id); + } + + has(property) { + return this.engine.hasOwnProperty(property); + } + + remove(property) { + this.engine.removeItem(property); + } + + addFile(id, file) { + this.engine.setItem(id, JSON.stringify(file)); + } +} + +module.exports = Storage; \ No newline at end of file diff --git a/frontend/src/upload.js b/frontend/src/upload.js index 3a78a853..6a6e8038 100644 --- a/frontend/src/upload.js +++ b/frontend/src/upload.js @@ -1,76 +1,140 @@ /* global MAXFILESIZE */ const FileSender = require('./fileSender'); -const { notify, gcmCompliant } = require('./utils'); +const { notify, gcmCompliant, findMetric, sendEvent, ONE_DAY_IN_MS } = require('./utils'); const bytes = require('bytes'); +const Storage = require('./storage'); +const storage = new Storage(localStorage); + const $ = require('jquery'); require('jquery-circle-progress'); const Raven = window.Raven; +if (storage.has('referrer')) { + window.referrer = storage.referrer; + storage.remove('referrer'); +} else { + window.referrer = 'external'; +} + $(document).ready(function() { - gcmCompliant().catch(err => { - $('#page-one').attr('hidden', true); - $('#unsupported-browser').removeAttr('hidden'); - }); + if (!location.pathname.toString().includes('download')) { + gcmCompliant() + .catch(err => { + $('#page-one').attr('hidden', true); + $('#unsupported-browser').removeAttr('hidden'); + // record unsupported event + sendEvent('sender', 'unsupported', { + cd6: err + }); + }); - $('#file-upload').change(onUpload); - $('body').on('dragover', allowDrop).on('drop', onUpload); - // reset copy button - const $copyBtn = $('#copy-btn'); - $copyBtn.attr('disabled', false); - $('#link').attr('disabled', false); - $copyBtn.attr('data-l10n-id', 'copyUrlFormButton'); + $('#file-upload').change(onUpload); - if (localStorage.length === 0) { - toggleHeader(); - } else { - for (let i = 0; i < localStorage.length; i++) { - const id = localStorage.key(i); - //check if file exists before adding to list - checkExistence(id, true); + $('.legal-links a, .social-links a, #dl-firefox').click(function(target) { + target.preventDefault(); + const metric = findMetric(target.currentTarget.href); + // record exited event by recipient + sendEvent('sender', 'exited', { + cd3: metric + }) + .then(() => { + location.href = target.currentTarget.href; + }); + }) + + $('#send-new-completed').click(function(target) { + target.preventDefault(); + // record restarted event + sendEvent('sender', 'restarted', { + cd2: 'completed' + }) + .then(() => { + storage.referrer = 'completed-upload'; + location.href = target.currentTarget.href; + }); + }) + + $('#send-new-error').click(function(target) { + target.preventDefault(); + // record restarted event + sendEvent('sender', 'restarted', { + cd2: 'errored' + }) + .then(() => { + storage.referrer = 'errored-upload'; + location.href = target.currentTarget.href; + }); + }) + + $('body').on('dragover', allowDrop).on('drop', onUpload); + // reset copy button + const $copyBtn = $('#copy-btn'); + $copyBtn.attr('disabled', false); + $('#link').attr('disabled', false); + $copyBtn.attr('data-l10n-id', 'copyUrlFormButton'); + + const files = storage.files; + console.log(files); + if (files.length === 0) { + toggleHeader(); + } else { + for (const index in files) { + const id = files[index].fileId; + //check if file still exists before adding to list + checkExistence(id, files[index], true); + } } + + + // copy link to clipboard + $copyBtn.click(() => { + // record copied event from success screen + sendEvent('sender', 'copied', { + cd4: 'success-screen' + }); + const aux = document.createElement('input'); + aux.setAttribute('value', $('#link').attr('value')); + document.body.appendChild(aux); + aux.select(); + document.execCommand('copy'); + document.body.removeChild(aux); + //disable button for 3s + $copyBtn.attr('disabled', true); + $('#link').attr('disabled', true); + $copyBtn.html(''); + window.setTimeout(() => { + $copyBtn.attr('disabled', false); + $('#link').attr('disabled', false); + $copyBtn.attr('data-l10n-id', 'copyUrlFormButton'); + }, 3000); + }); + + $('.upload-window').on('dragover', () => { + $('.upload-window').addClass('ondrag'); + }); + $('.upload-window').on('dragleave', () => { + $('.upload-window').removeClass('ondrag'); + }); + //initiate progress bar + $('#ul-progress').circleProgress({ + value: 0.0, + startAngle: -Math.PI / 2, + fill: '#3B9DFF', + size: 158, + animation: { duration: 300 } + }); } - // copy link to clipboard - $copyBtn.click(() => { - const aux = document.createElement('input'); - aux.setAttribute('value', $('#link').attr('value')); - document.body.appendChild(aux); - aux.select(); - document.execCommand('copy'); - document.body.removeChild(aux); - //disable button for 3s - $copyBtn.attr('disabled', true); - $('#link').attr('disabled', true); - $copyBtn.html(''); - window.setTimeout(() => { - $copyBtn.attr('disabled', false); - $('#link').attr('disabled', false); - $copyBtn.attr('data-l10n-id', 'copyUrlFormButton'); - }, 3000); - }); - - $('.upload-window').on('dragover', () => { - $('.upload-window').addClass('ondrag'); - }); - $('.upload-window').on('dragleave', () => { - $('.upload-window').removeClass('ondrag'); - }); - //initiate progress bar - $('#ul-progress').circleProgress({ - value: 0.0, - startAngle: -Math.PI / 2, - fill: '#3B9DFF', - size: 158, - animation: { duration: 300 } - }); - //link back to homepage $('.send-new').attr('href', window.location); // on file upload by browse or drag & drop function onUpload(event) { event.preventDefault(); + + storage.totalUploads += 1; + let file = ''; if (event.type === 'drop') { if (event.originalEvent.dataTransfer.files.length > 1 || event.originalEvent.dataTransfer.files[0].size === 0){ @@ -105,6 +169,17 @@ $(document).ready(function() { .then(str => { notify(str); }); + 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' + }); }); fileSender.on('progress', progress => { @@ -135,28 +210,66 @@ $(document).ready(function() { } }); + let uploadStart; fileSender.on('encrypting', isStillEncrypting => { // The file is being encrypted if (isStillEncrypting) { console.log('Encrypting'); } else { console.log('Finished encrypting'); + uploadStart = Date.now(); } }); - let t = ''; + + 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 + }); + fileSender .upload() .then(info => { + const endTime = Date.now(); + const totalTime = endTime - startTime; + const uploadTime = endTime - uploadStart; + const uploadSpeed = file.size / (uploadTime / 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' + }); + const fileData = { name: file.name, + size: file.size, fileId: info.fileId, url: info.url, secretKey: info.secretKey, deleteToken: info.deleteToken, creationDate: new Date(), - expiry: expiration + expiry: expiration, + totalTime: totalTime, + typeOfUpload: event.type === 'drop' ? 'drop' : 'click', + uploadSpeed: uploadSpeed }; - localStorage.setItem(info.fileId, JSON.stringify(fileData)); + + storage.addFile(info.fileId, fileData); $('#upload-filename').attr('data-l10n-id', 'uploadSuccessConfirmHeader'); t = window.setTimeout(() => { $('#page-one').attr('hidden', true); @@ -165,7 +278,7 @@ $(document).ready(function() { $('#share-link').removeAttr('hidden'); }, 1000); - populateFileList(JSON.stringify(fileData)); + populateFileList(fileData); document.l10n.formatValue('notifyUploadDone') .then(str => { notify(str); @@ -178,6 +291,17 @@ $(document).ready(function() { $('#upload-progress').attr('hidden', true); $('#upload-error').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 + }); }); } @@ -185,16 +309,19 @@ $(document).ready(function() { ev.preventDefault(); } - function checkExistence(id, populate) { + function checkExistence(id, file, populate) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { if (populate) { - populateFileList(localStorage.getItem(id)); + populateFileList(file); } } else if (xhr.status === 404) { - localStorage.removeItem(id); + storage.remove(id); + if (storage.numFiles === 0) { + toggleHeader(); + } } } }; @@ -202,14 +329,8 @@ $(document).ready(function() { xhr.send(); } - //update file table with current files in localStorage + //update file table with current files in storage function populateFileList(file) { - try { - file = JSON.parse(file); - } catch (e) { - return; - } - const row = document.createElement('tr'); const name = document.createElement('td'); const link = document.createElement('td'); @@ -252,6 +373,10 @@ $(document).ready(function() { //copy link to clipboard when icon clicked $copyIcon.click(function() { + // record copied event from upload list + sendEvent('sender', 'copied', { + cd4: 'upload-list' + }); const aux = document.createElement('input'); aux.setAttribute('value', url); document.body.appendChild(aux); @@ -277,7 +402,7 @@ $(document).ready(function() { future.setTime(file.creationDate.getTime() + file.expiry); let countdown = 0; - countdown = future.getTime() - new Date().getTime(); + countdown = future.getTime() - Date.now(); let minutes = Math.floor(countdown / 1000 / 60); let hours = Math.floor(minutes / 60); let seconds = Math.floor(countdown / 1000 % 60); @@ -285,7 +410,7 @@ $(document).ready(function() { poll(); function poll() { - countdown = future.getTime() - new Date().getTime(); + countdown = future.getTime() - Date.now(); minutes = Math.floor(countdown / 1000 / 60); hours = Math.floor(minutes / 60); seconds = Math.floor(countdown / 1000 % 60); @@ -304,7 +429,7 @@ $(document).ready(function() { } //remove from list when expired if (countdown <= 0) { - localStorage.removeItem(file.fileId); + storage.remove(file.fileId); $(expiry).parents('tr').remove(); window.clearTimeout(t); toggleHeader(); @@ -328,7 +453,6 @@ $(document).ready(function() { popupNvmSpan ]); - // add data cells to table row row.appendChild(name); $(link).append($copyIcon); @@ -340,18 +464,51 @@ $(document).ready(function() { row.appendChild(del); $('tbody').append(row); //add row to table + const unexpiredFiles = storage.numFiles; + // delete file $popupText.find('.del-file').click(e => { FileSender.delete(file.fileId, file.deleteToken).then(() => { $(e.target).parents('tr').remove(); - localStorage.removeItem(file.fileId); + const timeToExpiry = 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); + }) toggleHeader(); }); }); + document.getElementById('delete-file').onclick = () => { FileSender.delete(file.fileId, file.deleteToken).then(() => { - localStorage.removeItem(file.fileId); - location.reload(); + const timeToExpiry = 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(); + }) }); }; // show popup diff --git a/frontend/src/utils.js b/frontend/src/utils.js index aff37488..f8d6f833 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -69,9 +69,54 @@ function gcmCompliant() { } } +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/en-US/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 isFile(id) { + return !['referrer', + 'totalDownloads', + 'totalUploads', + 'testpilot_ga__cid'].includes(id); +} + +function sendEvent() { + return window.analytics + .sendEvent + .apply(window.analytics, arguments) + .catch(() => 0); +} + +const ONE_DAY_IN_MS = 86400000; + module.exports = { arrayToHex, hexToArray, notify, - gcmCompliant + gcmCompliant, + findMetric, + isFile, + sendEvent, + ONE_DAY_IN_MS }; diff --git a/package-lock.json b/package-lock.json index 5878105a..e1b7f4ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -208,9 +208,21 @@ "dev": true }, "aws-sdk": { - "version": "2.77.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.77.0.tgz", - "integrity": "sha1-gJCQu4dNj0//ysUxZilYdjjnhlw=" + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.87.0.tgz", + "integrity": "sha1-lW+Ey48yah0j/ioJ1JVlbhobato=", + "dependencies": { + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } + } }, "babel-code-frame": { "version": "6.22.0", @@ -384,7 +396,8 @@ "buffer": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.0.6.tgz", - "integrity": "sha1-LqZp9+7Atu2gWwj4tf9mGyhXNYg=" + "integrity": "sha1-LqZp9+7Atu2gWwj4tf9mGyhXNYg=", + "dev": true }, "buffer-xor": { "version": "1.0.3", @@ -526,12 +539,24 @@ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, + "color-convert": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", + "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", + "dev": true + }, "color-diff": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/color-diff/-/color-diff-0.1.7.tgz", "integrity": "sha1-bbeM2UgqjkWdQIIer0tQMoPcuOI=", "dev": true }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, "colorguard": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/colorguard/-/colorguard-1.2.0.tgz", @@ -682,18 +707,11 @@ "version": "https://registry.npmjs.org/varify/-/varify-0.2.0.tgz", "integrity": "sha1-GR2p/p3EzWjQ0USY1OKpEP9OZRY=", "optional": true, - "requires": { - "redeyed": "https://registry.npmjs.org/redeyed/-/redeyed-1.0.1.tgz", - "through": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" - }, "dependencies": { "redeyed": { "version": "https://registry.npmjs.org/redeyed/-/redeyed-1.0.1.tgz", "integrity": "sha1-6WwZO0DAgWsArshCaY5hGF5VSYo=", "optional": true, - "requires": { - "esprima": "https://registry.npmjs.org/esprima/-/esprima-3.0.0.tgz" - }, "dependencies": { "esprima": { "version": "https://registry.npmjs.org/esprima/-/esprima-3.0.0.tgz", @@ -1074,11 +1092,17 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.1.0.tgz", - "integrity": "sha1-u7VaKCIO4Itp2pVU1FprLr/X2RM=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.2.0.tgz", + "integrity": "sha1-orMYQRGxmOAunH88ymJaXgHFaz0=", "dev": true, "dependencies": { + "ajv": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.2.tgz", + "integrity": "sha1-R8aNaehvXZUxA7AHSpQw3GPaXjk=", + "dev": true + }, "concat-stream": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", @@ -1110,9 +1134,9 @@ "dev": true }, "readable-stream": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.2.tgz", - "integrity": "sha1-WgTfBeT1f+Pw3Gj90R3FyXx+b00=", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", "dev": true }, "string_decoder": { @@ -1130,9 +1154,9 @@ "dev": true }, "eslint-plugin-node": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-5.0.0.tgz", - "integrity": "sha512-9xERRx9V/8ciUHlTDlz9S4JiTL6Dc5oO+jKTy2mvQpxjhycpYZXzTT1t90IXjf+nAYw6/8sDnZfkeixJHxromA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-5.1.1.tgz", + "integrity": "sha512-3xdoEbPyyQNyGhhqttjgSO3cU/non8QDBJF8ttGaHM2h8CaY5zFIngtqW6ZbLEIvhpoFPDVwiQg61b8zanx5zQ==", "dev": true }, "eslint-plugin-security": { @@ -1205,8 +1229,7 @@ "events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", - "dev": true + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" }, "evp_bytestokey": { "version": "1.0.0", @@ -1272,6 +1295,12 @@ "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", "dev": true }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", + "dev": true + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -2186,18 +2215,6 @@ "integrity": "sha1-szmUr0V6gRVwDUEPMXczy+egkEs=", "dev": true }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true - }, "get-stdin": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", @@ -2356,12 +2373,6 @@ "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.0.0.tgz", "integrity": "sha1-pSI0xgcN7PIUsra3C7FE0H5Hdsc=" }, - "html-tags": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-1.2.0.tgz", - "integrity": "sha1-x43mW1Zjqll5id0rerSSANfk25g=", - "dev": true - }, "htmlescape": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", @@ -2471,6 +2482,9 @@ "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", "dev": true }, + "intl-pluralrules": { + "version": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b" + }, "ipaddr.js": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.3.0.tgz", @@ -2565,12 +2579,6 @@ "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true }, - "is-my-json-valid": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", - "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", - "dev": true - }, "is-number": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", @@ -2619,12 +2627,6 @@ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", "dev": true }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -2729,6 +2731,12 @@ "integrity": "sha1-KqEH8UKvQSHRRWWdRPUIMJYeaZo=", "dev": true }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "json-stable-stringify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", @@ -2802,12 +2810,6 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, "JSONStream": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", @@ -3555,9 +3557,9 @@ "dev": true }, "prettier": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.4.4.tgz", - "integrity": "sha512-GuuPazIvjW1DG26yLQgO+nagmRF/h9M4RaCtZWqu/eFW7csdZkQEwPJUeXX10d+LzmCnR9DuIZndqIOn3p2YoA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.5.3.tgz", + "integrity": "sha1-WdrcaDNF7GuI+IuU7Urn4do5S/4=", "dev": true }, "process": { @@ -3702,9 +3704,9 @@ } }, "raven-js": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/raven-js/-/raven-js-3.16.0.tgz", - "integrity": "sha1-p5naT90ExjlD9n3rk9qg7P4QHqs=" + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/raven-js/-/raven-js-3.17.0.tgz", + "integrity": "sha1-d5RXrHkQUSw8LMm7bQqe61mpaew=" }, "raw-body": { "version": "2.2.0", @@ -4067,9 +4069,9 @@ "dev": true }, "sinon": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.3.5.tgz", - "integrity": "sha1-mi/A/41SbacW8wlTqixl1RiRf2w=", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.3.8.tgz", + "integrity": "sha1-Md4G/tj7o6Zx5XbdltClhjeW8lw=", "dev": true, "dependencies": { "path-to-regexp": { @@ -4352,17 +4354,29 @@ } }, "stylelint": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-7.11.1.tgz", - "integrity": "sha1-yBbGWLr32eXRZ9gic/6tN8l65J0=", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-7.13.0.tgz", + "integrity": "sha1-ER+Xttpy53XICADWu29fhpmXeF0=", "dev": true, "dependencies": { + "ansi-styles": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.1.0.tgz", + "integrity": "sha1-CcIC1ckX7CMYjKpcnLkXnNlUd1A=", + "dev": true + }, "balanced-match": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", "dev": true }, + "chalk": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.0.1.tgz", + "integrity": "sha512-Mp+FXEI+FrwY/XYV45b2YD3E8i3HwnEAoFcM0qlZzq/RZ9RwWitt2Y/c7cqRAz70U7hfekqx6qNYthuKFO6K0g==", + "dev": true + }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -4375,11 +4389,29 @@ "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "dev": true }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=", + "dev": true + }, "resolve-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", "dev": true + }, + "supports-color": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.0.tgz", + "integrity": "sha512-Ts0Mu/A1S1aZxEJNG88I4Oc9rcZSBFNac5e27yh4j2mqbhZSSzR1Ah79EYwSn9Zuh7lrlGD2cVGzw1RKGzyLSg==", + "dev": true } } }, @@ -4470,6 +4502,11 @@ "integrity": "sha1-qBFsEz+sLGH0pCCrbN9cTWHw5DU=", "dev": true }, + "testpilot-ga": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/testpilot-ga/-/testpilot-ga-0.3.0.tgz", + "integrity": "sha512-z4PJbw3KK0R0iflA+u/3BhWZrtsLHLu+0rMviMd8H1wp8qPV0rggNBjsKckBJCcXq4uEjXETGZzApHH7Tovpzw==" + }, "text-encoding": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", diff --git a/package.json b/package.json index 027de5a5..26eab242 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.2.0", "author": "Mozilla (https://mozilla.org)", "dependencies": { - "aws-sdk": "^2.62.0", + "aws-sdk": "^2.87.0", "body-parser": "^1.17.2", "bytes": "^2.5.0", "connect-busboy": "0.0.2", @@ -18,25 +18,26 @@ "l20n": "^5.0.0", "mozlog": "^2.1.1", "raven": "^2.1.0", - "raven-js": "^3.16.0", + "raven-js": "^3.17.0", "redis": "^2.7.1", "selenium-webdriver": "^3.4.0", "supertest": "^3.0.0", + "testpilot-ga": "^0.3.0", "uglify-es": "3.0.19" }, "devDependencies": { "browserify": "^14.4.0", - "eslint": "^4.0.0", + "eslint": "^4.2.0", "eslint-plugin-mocha": "^4.11.0", - "eslint-plugin-node": "^5.0.0", + "eslint-plugin-node": "^5.1.1", "eslint-plugin-security": "^1.4.0", "git-rev-sync": "^1.9.1", "mocha": "^3.4.2", "npm-run-all": "^4.0.2", - "prettier": "^1.4.4", + "prettier": "^1.5.3", "proxyquire": "^1.8.0", - "sinon": "^2.3.5", - "stylelint": "^7.11.0", + "sinon": "^2.3.8", + "stylelint": "^7.13.0", "stylelint-config-standard": "^16.0.0", "watchify": "^3.9.0" }, diff --git a/server/server.js b/server/server.js index 6d0039a0..bca56c96 100644 --- a/server/server.js +++ b/server/server.js @@ -112,12 +112,16 @@ app.get('/download/:id', (req, res) => { storage .length(id) .then(contentLength => { - res.render('download', { - filename: decodeURIComponent(filename), - filesize: bytes(contentLength), - trackerId: conf.analytics_id, - dsn: conf.sentry_id - }); + storage + .ttl(id) + .then(timeToExpiry => { + res.render('download', { + filename: decodeURIComponent(filename), + filesize: bytes(contentLength), + sizeInBytes: contentLength, + timeToExpiry: timeToExpiry + }); + }) }) .catch(() => { res.render('download'); diff --git a/server/storage.js b/server/storage.js index 6e101d78..d92de791 100644 --- a/server/storage.js +++ b/server/storage.js @@ -23,6 +23,7 @@ if (conf.s3_bucket) { module.exports = { filename: filename, exists: exists, + ttl: ttl, length: awsLength, get: awsGet, set: awsSet, @@ -39,6 +40,7 @@ if (conf.s3_bucket) { module.exports = { filename: filename, exists: exists, + ttl: ttl, length: localLength, get: localGet, set: localSet, @@ -73,6 +75,18 @@ function metadata(id) { }); } +function ttl(id) { + return new Promise((resolve, reject) => { + redis_client.ttl(id, (err, reply) => { + if (!err) { + resolve(reply * 1000); + } else { + reject(err); + } + }) + }) +} + function filename(id) { return new Promise((resolve, reject) => { redis_client.hget(id, 'filename', (err, reply) => { diff --git a/views/download.handlebars b/views/download.handlebars index 94cfeef8..0d288c4c 100644 --- a/views/download.handlebars +++ b/views/download.handlebars @@ -7,6 +7,8 @@ data-l10n-args='{"filename": "{{filename}}"}'> + +
@@ -41,6 +43,6 @@
- + {{/if}} diff --git a/views/index.handlebars b/views/index.handlebars index 30ae6a97..2c268e08 100644 --- a/views/index.handlebars +++ b/views/index.handlebars @@ -60,7 +60,7 @@ - + @@ -68,7 +68,7 @@
- +