diff --git a/README.md b/README.md index b6291288..7344f963 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Thanks [Mozilla][mozilla] for building this amazing tool! --- -**Docs:** [FAQ](docs/faq.md), [Encryption](docs/encryption.md), [Build](docs/build.md), [Docker](docs/docker.md), [Metrics](docs/metrics.md), [More](docs/) +**Docs:** [FAQ](docs/faq.md), [Encryption](docs/encryption.md), [Build](docs/build.md), [Docker](docs/docker.md), [More](docs/) --- diff --git a/android/android.js b/android/android.js index 033c0581..0cbdefe5 100644 --- a/android/android.js +++ b/android/android.js @@ -4,7 +4,6 @@ import html from 'choo/html'; import * as Sentry from '@sentry/browser'; import { setApiUrlPrefix, getConstants } from '../app/api'; -import metrics from '../app/metrics'; //import assets from '../common/assets'; import Archive from '../app/archive'; import Header from '../app/ui/header'; @@ -83,7 +82,6 @@ function body(main) { state.user = new User(storage, LIMITS); state.sentry = Sentry; }); - app.use(metrics); app.route('/', body(home)); app.route('/upload', upload); app.route('/share/:id', share); diff --git a/app/api.js b/app/api.js index a36d2228..f271ea2e 100644 --- a/app/api.js +++ b/app/api.js @@ -420,17 +420,6 @@ export async function setFileList(bearerToken, kid, data) { return response.ok; } -export function sendMetrics(blob) { - if (!navigator.sendBeacon) { - return; - } - try { - navigator.sendBeacon(getApiUrl('/api/metrics'), blob); - } catch (e) { - console.error(e); - } -} - export async function getConstants() { const response = await fetch(getApiUrl('/config')); diff --git a/app/controller.js b/app/controller.js index c7c0769c..b4b6dbe7 100644 --- a/app/controller.js +++ b/app/controller.js @@ -1,4 +1,3 @@ -import * as metrics from './metrics'; import FileReceiver from './fileReceiver'; import FileSender from './fileSender'; import copyDialog from './ui/copyDialog'; @@ -54,7 +53,6 @@ export default function(state, emitter) { emitter.on('logout', async () => { await state.user.logout(); - metrics.loggedOut({ trigger: 'button' }); emitter.emit('pushState', '/'); }); @@ -68,14 +66,6 @@ export default function(state, emitter) { emitter.on('delete', async ownedFile => { try { - metrics.deletedUpload({ - size: ownedFile.size, - time: ownedFile.time, - speed: ownedFile.speed, - type: ownedFile.type, - ttl: ownedFile.expiresAt - Date.now(), - location - }); state.storage.remove(ownedFile.id); await ownedFile.del(); } catch (e) { @@ -123,7 +113,7 @@ export default function(state, emitter) { source: query.utm_source, term: query.utm_term }); - state.modal = signupDialog(source); + state.modal = signupDialog(); render(); }); @@ -159,12 +149,9 @@ export default function(state, emitter) { const links = openLinksInNewTab(); await delay(200); - const start = Date.now(); try { const ownedFile = await sender.upload(archive, state.user.bearerToken); state.storage.totalUploads += 1; - const duration = Date.now() - start; - metrics.completedUpload(archive, duration); faviconProgressbar.updateFavicon(0); state.storage.addFile(ownedFile); @@ -181,7 +168,6 @@ export default function(state, emitter) { } catch (err) { if (err.message === '0') { //cancelled. do nothing - metrics.cancelledUpload(archive, err.duration); render(); } else if (err.message === '401') { const refreshed = await state.user.refresh(); @@ -197,7 +183,6 @@ export default function(state, emitter) { scope.setExtra('size', err.size); state.sentry.captureException(err); }); - metrics.stoppedUpload(archive, err.duration); emitter.emit('pushState', '/error'); } } finally { @@ -249,13 +234,11 @@ export default function(state, emitter) { render(); }); - emitter.on('download', async file => { + emitter.on('download', async () => { state.transfer.on('progress', updateProgress); state.transfer.on('decrypting', render); state.transfer.on('complete', render); const links = openLinksInNewTab(); - const size = file.size; - const start = Date.now(); try { const dl = state.transfer.download({ stream: state.capabilities.streamDownload @@ -263,12 +246,6 @@ export default function(state, emitter) { render(); await dl; state.storage.totalDownloads += 1; - const duration = Date.now() - start; - metrics.completedDownload({ - size, - duration, - password_protected: file.requiresPassword - }); faviconProgressbar.updateFavicon(0); } catch (err) { if (err.message === '0') { @@ -286,12 +263,6 @@ export default function(state, emitter) { scope.setExtra('progress', err.progress); state.sentry.captureException(err); }); - const duration = Date.now() - start; - metrics.stoppedDownload({ - size, - duration, - password_protected: file.requiresPassword - }); } emitter.emit('pushState', location); } @@ -302,7 +273,6 @@ export default function(state, emitter) { emitter.on('copy', ({ url }) => { copyToClipboard(url); - // metrics.copiedLink({ location }); }); emitter.on('closeModal', () => { diff --git a/app/main.js b/app/main.js index 09853c18..469dfccb 100644 --- a/app/main.js +++ b/app/main.js @@ -10,7 +10,6 @@ import controller from './controller'; import dragManager from './dragManager'; import pasteManager from './pasteManager'; import storage from './storage'; -import metrics from './metrics'; import experiments from './experiments'; import * as Sentry from '@sentry/browser'; import './main.css'; @@ -68,7 +67,6 @@ if (process.env.NODE_ENV === 'production') { // eslint-disable-next-line require-atomic-updates window.app = app; app.use(experiments); - app.use(metrics); app.use(controller); app.use(dragManager); app.use(pasteManager); diff --git a/app/metrics.js b/app/metrics.js deleted file mode 100644 index 759866df..00000000 --- a/app/metrics.js +++ /dev/null @@ -1,186 +0,0 @@ -import storage from './storage'; -import { platform, locale } from './utils'; -import { sendMetrics } from './api'; - -let appState = null; -let experiment = null; -const HOUR = 1000 * 60 * 60; -const events = []; -let session_id = Date.now(); -const lang = locale(); - -export default function initialize(state, emitter) { - appState = state; - - emitter.on('DOMContentLoaded', () => { - experiment = storage.enrolled; - if (!appState.user.firstAction) { - appState.user.firstAction = - appState.route === '/' ? 'upload' : 'download'; - } - const query = appState.query; - addEvent('client_visit', { - entrypoint: appState.route === '/' ? 'upload' : 'download', - referrer: document.referrer, - utm_campaign: query.utm_campaign, - utm_content: query.utm_content, - utm_medium: query.utm_medium, - utm_source: query.utm_source, - utm_term: query.utm_term - }); - }); - emitter.on('experiment', experimentEvent); - window.addEventListener('unload', submitEvents); -} - -function sizeOrder(n) { - return Math.floor(Math.log10(n)); -} - -function submitEvents() { - if (navigator.doNotTrack === '1') { - return; - } - sendMetrics( - new Blob( - [ - JSON.stringify({ - now: Date.now(), - session_id, - lang, - platform: platform(), - events - }) - ], - { type: 'text/plain' } // see http://crbug.com/490015 - ) - ); - events.splice(0); -} - -async function addEvent(event_type, event_properties) { - const user_id = await appState.user.metricId(); - const device_id = await appState.user.deviceId(); - const ab_id = Object.keys(experiment)[0]; - if (ab_id) { - event_properties.experiment = ab_id; - event_properties.variant = experiment[ab_id]; - } - events.push({ - device_id, - event_properties, - event_type, - time: Date.now(), - user_id, - user_properties: { - anonymous: !appState.user.loggedIn, - first_action: appState.user.firstAction, - active_count: storage.files.length - } - }); - if (events.length === 25) { - submitEvents(); - } -} - -function cancelledUpload(archive, duration) { - return addEvent('client_upload', { - download_limit: archive.dlimit, - duration: sizeOrder(duration), - file_count: archive.numFiles, - password_protected: !!archive.password, - size: sizeOrder(archive.size), - status: 'cancel', - time_limit: archive.timeLimit - }); -} - -function completedUpload(archive, duration) { - return addEvent('client_upload', { - download_limit: archive.dlimit, - duration: sizeOrder(duration), - file_count: archive.numFiles, - password_protected: !!archive.password, - size: sizeOrder(archive.size), - status: 'ok', - time_limit: archive.timeLimit - }); -} - -function stoppedUpload(archive, duration = 0) { - return addEvent('client_upload', { - download_limit: archive.dlimit, - duration: sizeOrder(duration), - file_count: archive.numFiles, - password_protected: !!archive.password, - size: sizeOrder(archive.size), - status: 'error', - time_limit: archive.timeLimit - }); -} - -function stoppedDownload(params) { - return addEvent('client_download', { - duration: sizeOrder(params.duration), - password_protected: params.password_protected, - size: sizeOrder(params.size), - status: 'error' - }); -} - -function completedDownload(params) { - return addEvent('client_download', { - duration: sizeOrder(params.duration), - password_protected: params.password_protected, - size: sizeOrder(params.size), - status: 'ok' - }); -} - -function deletedUpload(ownedFile) { - return addEvent('client_delete', { - age: Math.floor((Date.now() - ownedFile.createdAt) / HOUR), - downloaded: ownedFile.dtotal > 0, - status: 'ok' - }); -} - -function experimentEvent(params) { - return addEvent('client_experiment', params); -} - -function submittedSignup(params) { - return addEvent('client_login', { - status: 'ok', - trigger: params.trigger - }); -} - -function canceledSignup(params) { - return addEvent('client_login', { - status: 'cancel', - trigger: params.trigger - }); -} - -function loggedOut(params) { - addEvent('client_logout', { - status: 'ok', - trigger: params.trigger - }); - // flush events and start new anon session - submitEvents(); - session_id = Date.now(); -} - -export { - cancelledUpload, - stoppedUpload, - completedUpload, - deletedUpload, - stoppedDownload, - completedDownload, - submittedSignup, - canceledSignup, - loggedOut -}; diff --git a/app/ui/archiveTile.js b/app/ui/archiveTile.js index 1592f882..a78f07fe 100644 --- a/app/ui/archiveTile.js +++ b/app/ui/archiveTile.js @@ -580,7 +580,7 @@ module.exports.preview = function(state, emit) { function download(event) { event.preventDefault(); event.target.disabled = true; - emit('download', archive); + emit('download'); } }; diff --git a/app/ui/noStreams.js b/app/ui/noStreams.js index 52cb7d05..b570c3b4 100644 --- a/app/ui/noStreams.js +++ b/app/ui/noStreams.js @@ -100,7 +100,7 @@ module.exports = function(state, emit) { ); break; case 'download': - emit('download', archive); + emit('download'); break; } return false; diff --git a/app/ui/signupDialog.js b/app/ui/signupDialog.js index 01be324f..506c9645 100644 --- a/app/ui/signupDialog.js +++ b/app/ui/signupDialog.js @@ -1,9 +1,8 @@ const html = require('choo/html'); const assets = require('../../common/assets'); const { bytes } = require('../utils'); -const { canceledSignup, submittedSignup } = require('../metrics'); -module.exports = function(trigger) { +module.exports = function() { return function(state, emit, close) { const DAYS = Math.floor(state.LIMITS.MAX_EXPIRE_SECONDS / 86400); let submitting = false; @@ -72,7 +71,6 @@ module.exports = function(trigger) { } function cancel(event) { - canceledSignup({ trigger }); close(event); } @@ -85,7 +83,6 @@ module.exports = function(trigger) { const el = document.getElementById('email-input'); const email = el.value; - submittedSignup({ trigger }); emit('login', emailish(email) ? email : null); } }; diff --git a/app/user.js b/app/user.js index 6e6bbacd..d1b28e3e 100644 --- a/app/user.js +++ b/app/user.js @@ -109,27 +109,8 @@ export default class User { async startAuthFlow(trigger, utms = {}) { this.utms = utms; this.trigger = trigger; - try { - const params = new URLSearchParams({ - entrypoint: `send-${trigger}`, - form_type: 'email', - utm_source: utms.source || 'send', - utm_campaign: utms.campaign || 'none' - }); - const res = await fetch( - `${this.authConfig.issuer}/metrics-flow?${params.toString()}`, - { - mode: 'cors' - } - ); - const { flowId, flowBeginTime } = await res.json(); - this.flowId = flowId; - this.flowBeginTime = flowBeginTime; - } catch (e) { - console.error(e); - this.flowId = null; - this.flowBeginTime = null; - } + this.flowId = null; + this.flowBeginTime = null; } async login(email) { diff --git a/docs/metrics.md b/docs/metrics.md deleted file mode 100644 index 432e00f4..00000000 --- a/docs/metrics.md +++ /dev/null @@ -1,128 +0,0 @@ -# Send V2 Metrics Definitions - -## Key Value Prop - -Quickly and privately transfer large files from any device to any device. - -## Key Business Question to Answer - -Is the value proposition of a large encrypted file transfer service enough to drive Firefox Account relationships for non-Firefox users. - -## Hypotheses to Test - -### Primary - In support of Relationships KPI - -We believe that a privacy-respecting file transfer service can drive Firefox Accounts beyond the Firefox Browser. - -We will know this to be true when we see 250k Firefox Account creations from non-Firefox contexts w/in six months of launch. - -### Secondary - In support of Revenue KPI - -We believe that a privacy respecting service accessible beyond the reach of Firefox will provide a valuable platform to research, communicate with, and market to conscious choosers we have traditionally found hard to reach. - -We will know this to be true when we can conduct six research tasks (surveys, A/B tests, fake doors, etc) in support of premium services KPIs in the first six months after launch. - -## Overview of Key Measures - -* Number of people using the service to send and receive files - * Why: measure of service size. Important for understanding addressable market size -* Percent of users who have or create an FxAccount via Send - * Why: representation of % of any service users who might be amenable to an upsell -* % of downloaders who convert into uploaders - * Why: represents a measure of our key growth-loop potential -* Count of uploads and size - * Why: Represents cost of service on a running basis - -## Key Funnels -* App Open or Visit `--- DESIRED OUTCOME --->` Successful Upload -* Download UI Visit `--- DESIRED OUTCOME --->` Successful Download -* FxA UI Engagement `--- DESIRED OUTCOME --->` Authenticate -* **STRETCH** App Open or Visit `--- DESIRED OUTCOME --->` Successful Download - -## Amplitude Schema - -Please see, **See Amplitude HTTP API**(https://amplitude.zendesk.com/hc/en-us/articles/204771828) for HTTP API reference. - -## Metric Events - -In support of our KPIs we collect events from two separate contexts, server and client. The events are designed to have minimal correlation between contexts. - -Server events collect lifecycle information about individual uploads but no user information; also time precision is truncated to hour increments. Client events collect information about how users interact with the UI but no upload identifiers. - -### Server Events - -Server events allow us to aggregate data about file lifecycle without collecting data about individual users. In this context `user_id` and `user_properties` describe the uploaded archive. - -* `session_id` -1 (not part of a session) -* `user_id` hash of (archive_id + owner_id) -* `app_version` package.json version -* `time` timestamp truncated to hour precision -* `country` -* `region` -* `event_type` [server_upload | server_download | server_delete] -* `user_properties` - * `download_limit` set number of downloads - * `time_limit` set expiry duration - * `size` approximate size (log10) - * `anonymous` true if anonymous, false if fxa -* `event_properties` - * `download_count` downloads completed - * `ttl` time remaining before expiry truncated to hour - * `agent` the browser name or first 6 characters of the user agent that made the request - -### Client Events - -Client events allow us to aggregate data about how the user interface is being used without tracking the lifecycle of individual files. In this context `user_id` and `user_properties` describe the user. The `user_id` and `device_id` change for all users at the beginning of each month. - -* `session_id` timestamp -* `user_id` hash of (fxa_id + Date.year + Date.month) -* `device_id` hash of (localStorage random id + Date.year + Date.month) -* `platform` [web | android] -* `country` -* `region` -* `language` -* `time` timestamp -* `os_name` -* `event_type` [client_visit | client_upload | client_download | client_delete | client_login | client_logout] -* `event_properties` - * `browser` - * `browser_version` - * `status` [ ok | error | cancel ] - * Event specific properties (see below) -* `user_properties` - * `active_count` number of active uploads - * `anonymous` true if anonymous, false if fxa - * `experiments` list of experiment ids the user is participating in - * `first_action` how this use came to Send the first time [ upload | download ] - -#### Visit Event - - * `entrypoint` [ upload | download ] - -#### Upload Event - - * `download_limit` download limit - * `file_count` number of files - * `password_protected` boolean - * `size` approximate size (log10) - * `time_limit` time limit - * `duration` approximate transfer duration (log10) - -#### Download Event - - * `password_protected` boolean - * `size` approximate size (log10) - * `duration` approximate transfer duration (log10) - -#### Delete Event - - * `age` hours since uploaded - * `downloaded` downloaded at least once - -#### Login Event - - * `trigger` [button | time | count | size] - -#### Logout Event - - * `trigger` [button | timeout] diff --git a/server/amplitude.js b/server/amplitude.js deleted file mode 100644 index a69717fd..00000000 --- a/server/amplitude.js +++ /dev/null @@ -1,171 +0,0 @@ -const crypto = require('crypto'); -const fetch = require('node-fetch'); -const config = require('./config'); -const pkg = require('../package.json'); - -const HOUR = 1000 * 60 * 60; - -function truncateToHour(timestamp) { - return Math.floor(timestamp / HOUR) * HOUR; -} - -function orderOfMagnitude(n) { - return Math.floor(Math.log10(n)); -} - -function userId(fileId, ownerId) { - const hash = crypto.createHash('sha256'); - hash.update(fileId); - hash.update(ownerId); - return hash.digest('hex').substring(32); -} - -function statUploadEvent(data) { - const event = { - session_id: -1, - country: data.country, - region: data.state, - user_id: userId(data.id, data.owner), - app_version: pkg.version, - time: truncateToHour(Date.now()), - event_type: 'server_upload', - user_properties: { - download_limit: data.dlimit, - time_limit: data.timeLimit, - size: orderOfMagnitude(data.size), - anonymous: data.anonymous - }, - event_properties: { - agent: data.agent - }, - event_id: 0 - }; - return sendBatch([event]); -} - -function statDownloadEvent(data) { - const event = { - session_id: -1, - country: data.country, - region: data.state, - user_id: userId(data.id, data.owner), - app_version: pkg.version, - time: truncateToHour(Date.now()), - event_type: 'server_download', - event_properties: { - agent: data.agent, - download_count: data.download_count, - ttl: data.ttl - }, - event_id: data.download_count - }; - return sendBatch([event]); -} - -function statDeleteEvent(data) { - const event = { - session_id: -1, - country: data.country, - region: data.state, - user_id: userId(data.id, data.owner), - app_version: pkg.version, - time: truncateToHour(Date.now()), - event_type: 'server_delete', - event_properties: { - agent: data.agent, - download_count: data.download_count, - ttl: data.ttl - }, - event_id: data.download_count + 1 - }; - return sendBatch([event]); -} - -function clientEvent( - event, - ua, - language, - session_id, - deltaT, - platform, - country, - state -) { - const ep = event.event_properties || {}; - const up = event.user_properties || {}; - const event_properties = { - browser: ua.browser.name, - browser_version: ua.browser.version, - status: ep.status, - - age: ep.age, - downloaded: ep.downloaded, - download_limit: ep.download_limit, - duration: ep.duration, - entrypoint: ep.entrypoint, - file_count: ep.file_count, - password_protected: ep.password_protected, - referrer: ep.referrer, - size: ep.size, - time_limit: ep.time_limit, - trigger: ep.trigger, - ttl: ep.ttl, - utm_campaign: ep.utm_campaign, - utm_content: ep.utm_content, - utm_medium: ep.utm_medium, - utm_source: ep.utm_source, - utm_term: ep.utm_term, - experiment: ep.experiment, - variant: ep.variant - }; - const user_properties = { - active_count: up.active_count, - anonymous: up.anonymous, - experiments: up.experiments, - first_action: up.first_action - }; - return { - app_version: pkg.version, - country: country, - device_id: event.device_id, - event_properties, - event_type: event.event_type, - language, - os_name: ua.os.name, - os_version: ua.os.version, - platform, - region: state, - session_id, - time: event.time + deltaT, - user_id: event.user_id, - user_properties - }; -} - -async function sendBatch(events, timeout = 1000) { - if (!config.amplitude_id) { - return 200; - } - try { - const result = await fetch('https://api.amplitude.com/batch', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - api_key: config.amplitude_id, - events - }), - timeout - }); - return result.status; - } catch (e) { - return 500; - } -} - -module.exports = { - statUploadEvent, - statDownloadEvent, - statDeleteEvent, - clientEvent, - sendBatch -}; diff --git a/server/config.js b/server/config.js index 21a46c9e..2c799673 100644 --- a/server/config.js +++ b/server/config.js @@ -100,16 +100,6 @@ const conf = convict({ arg: 'port', env: 'PORT' }, - amplitude_id: { - format: String, - default: '', - env: 'AMPLITUDE_ID' - }, - analytics_id: { - format: String, - default: '', - env: 'GOOGLE_ANALYTICS_ID' - }, sentry_id: { format: String, default: '', diff --git a/server/routes/delete.js b/server/routes/delete.js index e909e90a..fca5f230 100644 --- a/server/routes/delete.js +++ b/server/routes/delete.js @@ -1,23 +1,10 @@ const storage = require('../storage'); -const { statDeleteEvent } = require('../amplitude'); module.exports = async function(req, res) { try { const id = req.params.id; - const meta = req.meta; - const ttl = await storage.ttl(id); await storage.del(id); res.sendStatus(200); - statDeleteEvent({ - id, - ip: req.ip, - country: req.geo.country, - state: req.geo.state, - owner: meta.owner, - download_count: meta.dl, - ttl, - agent: req.ua.browser.name || req.ua.ua.substring(0, 6) - }); } catch (e) { res.sendStatus(404); } diff --git a/server/routes/download.js b/server/routes/download.js index 63de2146..1c319a7f 100644 --- a/server/routes/download.js +++ b/server/routes/download.js @@ -1,7 +1,6 @@ const storage = require('../storage'); const mozlog = require('../log'); const log = mozlog('send.download'); -const { statDownloadEvent } = require('../amplitude'); module.exports = async function(req, res) { const id = req.params.id; @@ -27,17 +26,6 @@ module.exports = async function(req, res) { const dl = meta.dl + 1; const dlimit = meta.dlimit; - const ttl = await storage.ttl(id); - statDownloadEvent({ - id, - ip: req.ip, - country: req.geo.country, - state: req.geo.state, - owner: meta.owner, - download_count: dl, - ttl, - agent: req.ua.browser.name || req.ua.ua.substring(0, 6) - }); try { if (dl >= dlimit) { await storage.del(id); diff --git a/server/routes/index.js b/server/routes/index.js index c12b3faa..80d25707 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -112,7 +112,6 @@ module.exports = function(app) { require('./params') ); app.post(`/api/info/:id${ID_REGEX}`, auth.owner, require('./info')); - app.post('/api/metrics', require('./metrics')); app.get('/__version__', function(req, res) { // eslint-disable-next-line node/no-missing-require res.sendFile(require.resolve('../../dist/version.json')); diff --git a/server/routes/metrics.js b/server/routes/metrics.js deleted file mode 100644 index 0f6f64aa..00000000 --- a/server/routes/metrics.js +++ /dev/null @@ -1,24 +0,0 @@ -const { sendBatch, clientEvent } = require('../amplitude'); - -module.exports = async function(req, res) { - try { - const data = JSON.parse(req.body); // see http://crbug.com/490015 - const deltaT = Date.now() - data.now; - const events = data.events.map(e => - clientEvent( - e, - req.ua, - data.lang, - data.session_id + deltaT, - deltaT, - data.platform, - req.geo.country, - req.geo.state - ) - ); - const status = await sendBatch(events); - res.sendStatus(status); - } catch (e) { - res.sendStatus(500); - } -}; diff --git a/server/routes/ws.js b/server/routes/ws.js index c32b63a9..7e89a180 100644 --- a/server/routes/ws.js +++ b/server/routes/ws.js @@ -4,7 +4,6 @@ const config = require('../config'); const mozlog = require('../log'); const Limiter = require('../limiter'); const fxa = require('../fxa'); -const { statUploadEvent } = require('../amplitude'); const { encryptedSize } = require('../../app/utils'); const { Transform } = require('stream'); @@ -108,18 +107,6 @@ module.exports = function(ws, req) { // in order to avoid having to check socket state and clean // up storage, possibly with an exception that we can catch. ws.send(JSON.stringify({ ok: true })); - statUploadEvent({ - id: newId, - ip: req.ip, - country: req.geo.country, - state: req.geo.state, - owner, - dlimit, - timeLimit, - anonymous: !user, - size: limiter.length, - agent: req.ua.browser.name || req.ua.ua.substring(0, 6) - }); } } catch (e) { log.error('upload', e);