From b0d36529a19133bcce694b782af8803761c93618 Mon Sep 17 00:00:00 2001
From: Danny Coates <dannycoates@gmail.com>
Date: Sat, 5 Aug 2017 12:23:58 -0700
Subject: [PATCH] refactored metrics

---
 frontend/src/common.js    |  47 +-------
 frontend/src/download.js  |  78 +++----------
 frontend/src/metrics.js   | 234 ++++++++++++++++++++++++++++++++++++++
 frontend/src/upload.js    | 213 ++++++++++++----------------------
 server/server.js          |   4 +-
 views/download.handlebars |   6 +-
 views/index.handlebars    |   4 +-
 views/notfound.handlebars |   2 +-
 8 files changed, 330 insertions(+), 258 deletions(-)
 create mode 100644 frontend/src/metrics.js

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}}"}'></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>
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 @@
       <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>
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 @@
   <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>