Merge branch 'remove-metrics' into 'master'
Remove metrics Closes #4 See merge request timvisee/send!11
This commit is contained in:
commit
d3f9b82672
|
@ -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/)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import html from 'choo/html';
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
|
|
||||||
import { setApiUrlPrefix, getConstants } from '../app/api';
|
import { setApiUrlPrefix, getConstants } from '../app/api';
|
||||||
import metrics from '../app/metrics';
|
|
||||||
//import assets from '../common/assets';
|
//import assets from '../common/assets';
|
||||||
import Archive from '../app/archive';
|
import Archive from '../app/archive';
|
||||||
import Header from '../app/ui/header';
|
import Header from '../app/ui/header';
|
||||||
|
@ -83,7 +82,6 @@ function body(main) {
|
||||||
state.user = new User(storage, LIMITS);
|
state.user = new User(storage, LIMITS);
|
||||||
state.sentry = Sentry;
|
state.sentry = Sentry;
|
||||||
});
|
});
|
||||||
app.use(metrics);
|
|
||||||
app.route('/', body(home));
|
app.route('/', body(home));
|
||||||
app.route('/upload', upload);
|
app.route('/upload', upload);
|
||||||
app.route('/share/:id', share);
|
app.route('/share/:id', share);
|
||||||
|
|
11
app/api.js
11
app/api.js
|
@ -420,17 +420,6 @@ export async function setFileList(bearerToken, kid, data) {
|
||||||
return response.ok;
|
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() {
|
export async function getConstants() {
|
||||||
const response = await fetch(getApiUrl('/config'));
|
const response = await fetch(getApiUrl('/config'));
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import * as metrics from './metrics';
|
|
||||||
import FileReceiver from './fileReceiver';
|
import FileReceiver from './fileReceiver';
|
||||||
import FileSender from './fileSender';
|
import FileSender from './fileSender';
|
||||||
import copyDialog from './ui/copyDialog';
|
import copyDialog from './ui/copyDialog';
|
||||||
|
@ -54,7 +53,6 @@ export default function(state, emitter) {
|
||||||
|
|
||||||
emitter.on('logout', async () => {
|
emitter.on('logout', async () => {
|
||||||
await state.user.logout();
|
await state.user.logout();
|
||||||
metrics.loggedOut({ trigger: 'button' });
|
|
||||||
emitter.emit('pushState', '/');
|
emitter.emit('pushState', '/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -68,14 +66,6 @@ export default function(state, emitter) {
|
||||||
|
|
||||||
emitter.on('delete', async ownedFile => {
|
emitter.on('delete', async ownedFile => {
|
||||||
try {
|
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);
|
state.storage.remove(ownedFile.id);
|
||||||
await ownedFile.del();
|
await ownedFile.del();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -123,7 +113,7 @@ export default function(state, emitter) {
|
||||||
source: query.utm_source,
|
source: query.utm_source,
|
||||||
term: query.utm_term
|
term: query.utm_term
|
||||||
});
|
});
|
||||||
state.modal = signupDialog(source);
|
state.modal = signupDialog();
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -159,12 +149,9 @@ export default function(state, emitter) {
|
||||||
|
|
||||||
const links = openLinksInNewTab();
|
const links = openLinksInNewTab();
|
||||||
await delay(200);
|
await delay(200);
|
||||||
const start = Date.now();
|
|
||||||
try {
|
try {
|
||||||
const ownedFile = await sender.upload(archive, state.user.bearerToken);
|
const ownedFile = await sender.upload(archive, state.user.bearerToken);
|
||||||
state.storage.totalUploads += 1;
|
state.storage.totalUploads += 1;
|
||||||
const duration = Date.now() - start;
|
|
||||||
metrics.completedUpload(archive, duration);
|
|
||||||
faviconProgressbar.updateFavicon(0);
|
faviconProgressbar.updateFavicon(0);
|
||||||
|
|
||||||
state.storage.addFile(ownedFile);
|
state.storage.addFile(ownedFile);
|
||||||
|
@ -181,7 +168,6 @@ export default function(state, emitter) {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message === '0') {
|
if (err.message === '0') {
|
||||||
//cancelled. do nothing
|
//cancelled. do nothing
|
||||||
metrics.cancelledUpload(archive, err.duration);
|
|
||||||
render();
|
render();
|
||||||
} else if (err.message === '401') {
|
} else if (err.message === '401') {
|
||||||
const refreshed = await state.user.refresh();
|
const refreshed = await state.user.refresh();
|
||||||
|
@ -197,7 +183,6 @@ export default function(state, emitter) {
|
||||||
scope.setExtra('size', err.size);
|
scope.setExtra('size', err.size);
|
||||||
state.sentry.captureException(err);
|
state.sentry.captureException(err);
|
||||||
});
|
});
|
||||||
metrics.stoppedUpload(archive, err.duration);
|
|
||||||
emitter.emit('pushState', '/error');
|
emitter.emit('pushState', '/error');
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -249,13 +234,11 @@ export default function(state, emitter) {
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('download', async file => {
|
emitter.on('download', async () => {
|
||||||
state.transfer.on('progress', updateProgress);
|
state.transfer.on('progress', updateProgress);
|
||||||
state.transfer.on('decrypting', render);
|
state.transfer.on('decrypting', render);
|
||||||
state.transfer.on('complete', render);
|
state.transfer.on('complete', render);
|
||||||
const links = openLinksInNewTab();
|
const links = openLinksInNewTab();
|
||||||
const size = file.size;
|
|
||||||
const start = Date.now();
|
|
||||||
try {
|
try {
|
||||||
const dl = state.transfer.download({
|
const dl = state.transfer.download({
|
||||||
stream: state.capabilities.streamDownload
|
stream: state.capabilities.streamDownload
|
||||||
|
@ -263,12 +246,6 @@ export default function(state, emitter) {
|
||||||
render();
|
render();
|
||||||
await dl;
|
await dl;
|
||||||
state.storage.totalDownloads += 1;
|
state.storage.totalDownloads += 1;
|
||||||
const duration = Date.now() - start;
|
|
||||||
metrics.completedDownload({
|
|
||||||
size,
|
|
||||||
duration,
|
|
||||||
password_protected: file.requiresPassword
|
|
||||||
});
|
|
||||||
faviconProgressbar.updateFavicon(0);
|
faviconProgressbar.updateFavicon(0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message === '0') {
|
if (err.message === '0') {
|
||||||
|
@ -286,12 +263,6 @@ export default function(state, emitter) {
|
||||||
scope.setExtra('progress', err.progress);
|
scope.setExtra('progress', err.progress);
|
||||||
state.sentry.captureException(err);
|
state.sentry.captureException(err);
|
||||||
});
|
});
|
||||||
const duration = Date.now() - start;
|
|
||||||
metrics.stoppedDownload({
|
|
||||||
size,
|
|
||||||
duration,
|
|
||||||
password_protected: file.requiresPassword
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
emitter.emit('pushState', location);
|
emitter.emit('pushState', location);
|
||||||
}
|
}
|
||||||
|
@ -302,7 +273,6 @@ export default function(state, emitter) {
|
||||||
|
|
||||||
emitter.on('copy', ({ url }) => {
|
emitter.on('copy', ({ url }) => {
|
||||||
copyToClipboard(url);
|
copyToClipboard(url);
|
||||||
// metrics.copiedLink({ location });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('closeModal', () => {
|
emitter.on('closeModal', () => {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import controller from './controller';
|
||||||
import dragManager from './dragManager';
|
import dragManager from './dragManager';
|
||||||
import pasteManager from './pasteManager';
|
import pasteManager from './pasteManager';
|
||||||
import storage from './storage';
|
import storage from './storage';
|
||||||
import metrics from './metrics';
|
|
||||||
import experiments from './experiments';
|
import experiments from './experiments';
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
|
@ -68,7 +67,6 @@ if (process.env.NODE_ENV === 'production') {
|
||||||
// eslint-disable-next-line require-atomic-updates
|
// eslint-disable-next-line require-atomic-updates
|
||||||
window.app = app;
|
window.app = app;
|
||||||
app.use(experiments);
|
app.use(experiments);
|
||||||
app.use(metrics);
|
|
||||||
app.use(controller);
|
app.use(controller);
|
||||||
app.use(dragManager);
|
app.use(dragManager);
|
||||||
app.use(pasteManager);
|
app.use(pasteManager);
|
||||||
|
|
186
app/metrics.js
186
app/metrics.js
|
@ -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
|
|
||||||
};
|
|
|
@ -580,7 +580,7 @@ module.exports.preview = function(state, emit) {
|
||||||
function download(event) {
|
function download(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.target.disabled = true;
|
event.target.disabled = true;
|
||||||
emit('download', archive);
|
emit('download');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -100,7 +100,7 @@ module.exports = function(state, emit) {
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'download':
|
case 'download':
|
||||||
emit('download', archive);
|
emit('download');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
const assets = require('../../common/assets');
|
const assets = require('../../common/assets');
|
||||||
const { bytes } = require('../utils');
|
const { bytes } = require('../utils');
|
||||||
const { canceledSignup, submittedSignup } = require('../metrics');
|
|
||||||
|
|
||||||
module.exports = function(trigger) {
|
module.exports = function() {
|
||||||
return function(state, emit, close) {
|
return function(state, emit, close) {
|
||||||
const DAYS = Math.floor(state.LIMITS.MAX_EXPIRE_SECONDS / 86400);
|
const DAYS = Math.floor(state.LIMITS.MAX_EXPIRE_SECONDS / 86400);
|
||||||
let submitting = false;
|
let submitting = false;
|
||||||
|
@ -72,7 +71,6 @@ module.exports = function(trigger) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel(event) {
|
function cancel(event) {
|
||||||
canceledSignup({ trigger });
|
|
||||||
close(event);
|
close(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +83,6 @@ module.exports = function(trigger) {
|
||||||
|
|
||||||
const el = document.getElementById('email-input');
|
const el = document.getElementById('email-input');
|
||||||
const email = el.value;
|
const email = el.value;
|
||||||
submittedSignup({ trigger });
|
|
||||||
emit('login', emailish(email) ? email : null);
|
emit('login', emailish(email) ? email : null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
19
app/user.js
19
app/user.js
|
@ -109,28 +109,9 @@ export default class User {
|
||||||
async startAuthFlow(trigger, utms = {}) {
|
async startAuthFlow(trigger, utms = {}) {
|
||||||
this.utms = utms;
|
this.utms = utms;
|
||||||
this.trigger = trigger;
|
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.flowId = null;
|
||||||
this.flowBeginTime = null;
|
this.flowBeginTime = null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async login(email) {
|
async login(email) {
|
||||||
const state = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
|
const state = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
|
||||||
|
|
128
docs/metrics.md
128
docs/metrics.md
|
@ -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]
|
|
|
@ -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
|
|
||||||
};
|
|
|
@ -100,16 +100,6 @@ const conf = convict({
|
||||||
arg: 'port',
|
arg: 'port',
|
||||||
env: 'PORT'
|
env: 'PORT'
|
||||||
},
|
},
|
||||||
amplitude_id: {
|
|
||||||
format: String,
|
|
||||||
default: '',
|
|
||||||
env: 'AMPLITUDE_ID'
|
|
||||||
},
|
|
||||||
analytics_id: {
|
|
||||||
format: String,
|
|
||||||
default: '',
|
|
||||||
env: 'GOOGLE_ANALYTICS_ID'
|
|
||||||
},
|
|
||||||
sentry_id: {
|
sentry_id: {
|
||||||
format: String,
|
format: String,
|
||||||
default: '',
|
default: '',
|
||||||
|
|
|
@ -1,23 +1,10 @@
|
||||||
const storage = require('../storage');
|
const storage = require('../storage');
|
||||||
const { statDeleteEvent } = require('../amplitude');
|
|
||||||
|
|
||||||
module.exports = async function(req, res) {
|
module.exports = async function(req, res) {
|
||||||
try {
|
try {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
const meta = req.meta;
|
|
||||||
const ttl = await storage.ttl(id);
|
|
||||||
await storage.del(id);
|
await storage.del(id);
|
||||||
res.sendStatus(200);
|
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) {
|
} catch (e) {
|
||||||
res.sendStatus(404);
|
res.sendStatus(404);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
const storage = require('../storage');
|
const storage = require('../storage');
|
||||||
const mozlog = require('../log');
|
const mozlog = require('../log');
|
||||||
const log = mozlog('send.download');
|
const log = mozlog('send.download');
|
||||||
const { statDownloadEvent } = require('../amplitude');
|
|
||||||
|
|
||||||
module.exports = async function(req, res) {
|
module.exports = async function(req, res) {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
|
@ -27,17 +26,6 @@ module.exports = async function(req, res) {
|
||||||
|
|
||||||
const dl = meta.dl + 1;
|
const dl = meta.dl + 1;
|
||||||
const dlimit = meta.dlimit;
|
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 {
|
try {
|
||||||
if (dl >= dlimit) {
|
if (dl >= dlimit) {
|
||||||
await storage.del(id);
|
await storage.del(id);
|
||||||
|
|
|
@ -112,7 +112,6 @@ module.exports = function(app) {
|
||||||
require('./params')
|
require('./params')
|
||||||
);
|
);
|
||||||
app.post(`/api/info/:id${ID_REGEX}`, auth.owner, require('./info'));
|
app.post(`/api/info/:id${ID_REGEX}`, auth.owner, require('./info'));
|
||||||
app.post('/api/metrics', require('./metrics'));
|
|
||||||
app.get('/__version__', function(req, res) {
|
app.get('/__version__', function(req, res) {
|
||||||
// eslint-disable-next-line node/no-missing-require
|
// eslint-disable-next-line node/no-missing-require
|
||||||
res.sendFile(require.resolve('../../dist/version.json'));
|
res.sendFile(require.resolve('../../dist/version.json'));
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -4,7 +4,6 @@ const config = require('../config');
|
||||||
const mozlog = require('../log');
|
const mozlog = require('../log');
|
||||||
const Limiter = require('../limiter');
|
const Limiter = require('../limiter');
|
||||||
const fxa = require('../fxa');
|
const fxa = require('../fxa');
|
||||||
const { statUploadEvent } = require('../amplitude');
|
|
||||||
const { encryptedSize } = require('../../app/utils');
|
const { encryptedSize } = require('../../app/utils');
|
||||||
|
|
||||||
const { Transform } = require('stream');
|
const { Transform } = require('stream');
|
||||||
|
@ -108,18 +107,6 @@ module.exports = function(ws, req) {
|
||||||
// in order to avoid having to check socket state and clean
|
// in order to avoid having to check socket state and clean
|
||||||
// up storage, possibly with an exception that we can catch.
|
// up storage, possibly with an exception that we can catch.
|
||||||
ws.send(JSON.stringify({ ok: true }));
|
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) {
|
} catch (e) {
|
||||||
log.error('upload', e);
|
log.error('upload', e);
|
||||||
|
|
Loading…
Reference in New Issue