implemented amplitude metrics (#1141)
This commit is contained in:
parent
1a483cad55
commit
9b37e92a81
26 changed files with 774 additions and 528 deletions
|
@ -1,4 +1,4 @@
|
|||
/* global LIMITS */
|
||||
/* global LIMITS DEFAULTS */
|
||||
import { blobStream, concatStream } from './streams';
|
||||
|
||||
function isDupe(newFile, array) {
|
||||
|
@ -17,6 +17,9 @@ function isDupe(newFile, array) {
|
|||
export default class Archive {
|
||||
constructor(files = []) {
|
||||
this.files = Array.from(files);
|
||||
this.timeLimit = DEFAULTS.EXPIRE_SECONDS;
|
||||
this.dlimit = 1;
|
||||
this.password = null;
|
||||
}
|
||||
|
||||
get name() {
|
||||
|
@ -73,5 +76,8 @@ export default class Archive {
|
|||
|
||||
clear() {
|
||||
this.files = [];
|
||||
this.dlimit = 1;
|
||||
this.timeLimit = DEFAULTS.EXPIRE_SECONDS;
|
||||
this.password = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global DEFAULTS LIMITS */
|
||||
/* global LIMITS */
|
||||
import FileSender from './fileSender';
|
||||
import FileReceiver from './fileReceiver';
|
||||
import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
|
||||
|
@ -50,37 +50,27 @@ export default function(state, emitter) {
|
|||
|
||||
emitter.on('logout', () => {
|
||||
state.user.logout();
|
||||
state.timeLimit = DEFAULTS.EXPIRE_SECONDS;
|
||||
state.downloadCount = 1;
|
||||
metrics.loggedOut({ trigger: 'button' });
|
||||
emitter.emit('pushState', '/');
|
||||
});
|
||||
|
||||
emitter.on('changeLimit', async ({ file, value }) => {
|
||||
const ok = await file.changeLimit(value, state.user);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
state.storage.writeFile(file);
|
||||
metrics.changedDownloadLimit(file);
|
||||
});
|
||||
|
||||
emitter.on('removeUpload', file => {
|
||||
state.archive.remove(file);
|
||||
render();
|
||||
});
|
||||
|
||||
emitter.on('delete', async ({ file, location }) => {
|
||||
emitter.on('delete', async ownedFile => {
|
||||
try {
|
||||
metrics.deletedUpload({
|
||||
size: file.size,
|
||||
time: file.time,
|
||||
speed: file.speed,
|
||||
type: file.type,
|
||||
ttl: file.expiresAt - Date.now(),
|
||||
size: ownedFile.size,
|
||||
time: ownedFile.time,
|
||||
speed: ownedFile.speed,
|
||||
type: ownedFile.type,
|
||||
ttl: ownedFile.expiresAt - Date.now(),
|
||||
location
|
||||
});
|
||||
state.storage.remove(file.id);
|
||||
await file.del();
|
||||
state.storage.remove(ownedFile.id);
|
||||
await ownedFile.del();
|
||||
} catch (e) {
|
||||
state.raven.captureException(e);
|
||||
}
|
||||
|
@ -100,20 +90,35 @@ export default function(state, emitter) {
|
|||
state.archive.addFiles(files, maxSize);
|
||||
} catch (e) {
|
||||
if (e.message === 'fileTooBig' && maxSize < LIMITS.MAX_FILE_SIZE) {
|
||||
state.modal = signupDialog();
|
||||
} else {
|
||||
state.modal = okDialog(
|
||||
state.translate(e.message, {
|
||||
size: bytes(maxSize),
|
||||
count: LIMITS.MAX_FILES_PER_ARCHIVE
|
||||
})
|
||||
);
|
||||
return emitter.emit('signup-cta', 'size');
|
||||
}
|
||||
state.modal = okDialog(
|
||||
state.translate(e.message, {
|
||||
size: bytes(maxSize),
|
||||
count: LIMITS.MAX_FILES_PER_ARCHIVE
|
||||
})
|
||||
);
|
||||
}
|
||||
render();
|
||||
});
|
||||
|
||||
emitter.on('upload', async ({ type, dlimit, password }) => {
|
||||
emitter.on('signup-cta', source => {
|
||||
state.modal = signupDialog(source);
|
||||
render();
|
||||
});
|
||||
|
||||
emitter.on('authenticate', async (code, oauthState) => {
|
||||
try {
|
||||
await state.user.finishLogin(code, oauthState);
|
||||
await state.user.syncFileList();
|
||||
emitter.emit('replaceState', '/');
|
||||
} catch (e) {
|
||||
emitter.emit('replaceState', '/error');
|
||||
setTimeout(render);
|
||||
}
|
||||
});
|
||||
|
||||
emitter.on('upload', async () => {
|
||||
if (state.storage.files.length >= LIMITS.MAX_ARCHIVES_PER_USER) {
|
||||
state.modal = okDialog(
|
||||
state.translate('tooManyArchives', {
|
||||
|
@ -122,8 +127,7 @@ export default function(state, emitter) {
|
|||
);
|
||||
return render();
|
||||
}
|
||||
const size = state.archive.size;
|
||||
if (!state.timeLimit) state.timeLimit = DEFAULTS.EXPIRE_SECONDS;
|
||||
const archive = state.archive;
|
||||
const sender = new FileSender();
|
||||
|
||||
sender.on('progress', updateProgress);
|
||||
|
@ -135,41 +139,38 @@ export default function(state, emitter) {
|
|||
|
||||
const links = openLinksInNewTab();
|
||||
await delay(200);
|
||||
const start = Date.now();
|
||||
try {
|
||||
metrics.startedUpload({ size, type });
|
||||
|
||||
const ownedFile = await sender.upload(
|
||||
state.archive,
|
||||
state.timeLimit,
|
||||
dlimit,
|
||||
state.user.bearerToken
|
||||
);
|
||||
ownedFile.type = type;
|
||||
const ownedFile = await sender.upload(archive, state.user.bearerToken);
|
||||
state.storage.totalUploads += 1;
|
||||
metrics.completedUpload(ownedFile);
|
||||
const duration = Date.now() - start;
|
||||
metrics.completedUpload(archive, duration);
|
||||
|
||||
state.storage.addFile(ownedFile);
|
||||
// TODO integrate password into /upload request
|
||||
if (password) {
|
||||
emitter.emit('password', { password, file: ownedFile });
|
||||
if (archive.password) {
|
||||
emitter.emit('password', {
|
||||
password: archive.password,
|
||||
file: ownedFile
|
||||
});
|
||||
}
|
||||
state.modal = copyDialog(ownedFile.name, ownedFile.url);
|
||||
} catch (err) {
|
||||
if (err.message === '0') {
|
||||
//cancelled. do nothing
|
||||
metrics.cancelledUpload({ size, type });
|
||||
const duration = Date.now() - start;
|
||||
metrics.cancelledUpload(archive, duration);
|
||||
render();
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
state.raven.captureException(err);
|
||||
metrics.stoppedUpload({ size, type, err });
|
||||
metrics.stoppedUpload(archive);
|
||||
emitter.emit('pushState', '/error');
|
||||
}
|
||||
} finally {
|
||||
openLinksInNewTab(links, false);
|
||||
state.archive.clear();
|
||||
state.password = '';
|
||||
archive.clear();
|
||||
state.uploading = false;
|
||||
state.transfer = null;
|
||||
await state.user.syncFileList();
|
||||
|
@ -183,7 +184,6 @@ export default function(state, emitter) {
|
|||
render();
|
||||
await file.setPassword(password);
|
||||
state.storage.writeFile(file);
|
||||
metrics.addedPassword({ size: file.size });
|
||||
await delay(1000);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -220,18 +220,20 @@ export default function(state, emitter) {
|
|||
state.transfer.on('complete', render);
|
||||
const links = openLinksInNewTab();
|
||||
const size = file.size;
|
||||
const start = Date.now();
|
||||
try {
|
||||
const start = Date.now();
|
||||
metrics.startedDownload({ size: file.size, ttl: file.ttl });
|
||||
const dl = state.transfer.download({
|
||||
stream: state.capabilities.streamDownload
|
||||
});
|
||||
render();
|
||||
await dl;
|
||||
const time = Date.now() - start;
|
||||
const speed = size / (time / 1000);
|
||||
state.storage.totalDownloads += 1;
|
||||
metrics.completedDownload({ size, time, speed });
|
||||
const duration = Date.now() - start;
|
||||
metrics.completedDownload({
|
||||
size,
|
||||
duration,
|
||||
password_protected: file.requiresPassword
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.message === '0') {
|
||||
// download cancelled
|
||||
|
@ -239,12 +241,16 @@ export default function(state, emitter) {
|
|||
render();
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
state.transfer = null;
|
||||
const location = err.message === '404' ? '/404' : '/error';
|
||||
if (location === '/error') {
|
||||
state.raven.captureException(err);
|
||||
metrics.stoppedDownload({ size, err });
|
||||
const duration = Date.now() - start;
|
||||
metrics.stoppedDownload({
|
||||
size,
|
||||
duration,
|
||||
password_protected: file.requiresPassword
|
||||
});
|
||||
}
|
||||
emitter.emit('pushState', location);
|
||||
}
|
||||
|
@ -253,9 +259,9 @@ export default function(state, emitter) {
|
|||
}
|
||||
});
|
||||
|
||||
emitter.on('copy', ({ url, location }) => {
|
||||
emitter.on('copy', ({ url }) => {
|
||||
copyToClipboard(url);
|
||||
metrics.copiedLink({ location });
|
||||
// metrics.copiedLink({ location });
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* global DEFAULTS */
|
||||
import Nanobus from 'nanobus';
|
||||
import OwnedFile from './ownedFile';
|
||||
import Keychain from './keychain';
|
||||
|
@ -42,29 +41,24 @@ export default class FileSender extends Nanobus {
|
|||
}
|
||||
}
|
||||
|
||||
async upload(
|
||||
file,
|
||||
timeLimit = DEFAULTS.EXPIRE_SECONDS,
|
||||
dlimit = 1,
|
||||
bearerToken
|
||||
) {
|
||||
async upload(archive, bearerToken) {
|
||||
const start = Date.now();
|
||||
if (this.cancelled) {
|
||||
throw new Error(0);
|
||||
}
|
||||
this.msg = 'encryptingFile';
|
||||
this.emit('encrypting');
|
||||
const totalSize = encryptedSize(file.size);
|
||||
const encStream = await this.keychain.encryptStream(file.stream);
|
||||
const metadata = await this.keychain.encryptMetadata(file);
|
||||
const totalSize = encryptedSize(archive.size);
|
||||
const encStream = await this.keychain.encryptStream(archive.stream);
|
||||
const metadata = await this.keychain.encryptMetadata(archive);
|
||||
const authKeyB64 = await this.keychain.authKeyB64();
|
||||
|
||||
this.uploadRequest = uploadWs(
|
||||
encStream,
|
||||
metadata,
|
||||
authKeyB64,
|
||||
timeLimit,
|
||||
dlimit,
|
||||
archive.timeLimit,
|
||||
archive.dlimit,
|
||||
bearerToken,
|
||||
p => {
|
||||
this.progress = [p, totalSize];
|
||||
|
@ -88,18 +82,18 @@ export default class FileSender extends Nanobus {
|
|||
const ownedFile = new OwnedFile({
|
||||
id: result.id,
|
||||
url: `${result.url}#${secretKey}`,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
manifest: file.manifest,
|
||||
name: archive.name,
|
||||
size: archive.size,
|
||||
manifest: archive.manifest,
|
||||
time: time,
|
||||
speed: file.size / (time / 1000),
|
||||
speed: archive.size / (time / 1000),
|
||||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + timeLimit * 1000,
|
||||
expiresAt: Date.now() + archive.timeLimit * 1000,
|
||||
secretKey: secretKey,
|
||||
nonce: this.keychain.nonce,
|
||||
ownerToken: result.ownerToken,
|
||||
dlimit,
|
||||
timeLimit: timeLimit
|
||||
dlimit: archive.dlimit,
|
||||
timeLimit: archive.timeLimit
|
||||
});
|
||||
|
||||
return ownedFile;
|
||||
|
|
348
app/metrics.js
348
app/metrics.js
|
@ -1,296 +1,172 @@
|
|||
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
|
||||
import storage from './storage';
|
||||
|
||||
let hasLocalStorage = false;
|
||||
try {
|
||||
hasLocalStorage = typeof localStorage !== 'undefined';
|
||||
} catch (e) {
|
||||
// when disabled, any mention of localStorage throws an error
|
||||
}
|
||||
|
||||
const analytics = new testPilotGA({
|
||||
an: 'Firefox Send',
|
||||
ds: 'web',
|
||||
tid: window.GOOGLE_ANALYTICS_ID
|
||||
});
|
||||
import { platform } from './utils';
|
||||
|
||||
let appState = null;
|
||||
let experiment = null;
|
||||
// let experiment = null;
|
||||
const HOUR = 1000 * 60 * 60;
|
||||
const events = [];
|
||||
let session_id = Date.now();
|
||||
const lang = document.querySelector('html').lang;
|
||||
|
||||
export default function initialize(state, emitter) {
|
||||
appState = state;
|
||||
if (!appState.user.firstAction) {
|
||||
appState.user.firstAction = appState.route === '/' ? 'upload' : 'download';
|
||||
}
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
addExitHandlers();
|
||||
experiment = storage.enrolled[0];
|
||||
sendEvent(category(), 'visit', {
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads
|
||||
// experiment = storage.enrolled[0];
|
||||
addEvent('client_visit', {
|
||||
entrypoint: appState.route === '/' ? 'upload' : 'download'
|
||||
});
|
||||
});
|
||||
emitter.on('exit', exitEvent);
|
||||
emitter.on('experiment', experimentEvent);
|
||||
window.addEventListener('unload', submitEvents);
|
||||
}
|
||||
|
||||
function category() {
|
||||
switch (appState.route) {
|
||||
case '/':
|
||||
case '/share/:id':
|
||||
return 'sender';
|
||||
case '/download/:id/:key':
|
||||
case '/download/:id':
|
||||
case '/completed':
|
||||
return 'recipient';
|
||||
default:
|
||||
return 'other';
|
||||
}
|
||||
function sizeOrder(n) {
|
||||
return Math.floor(Math.log10(n));
|
||||
}
|
||||
|
||||
function sendEvent() {
|
||||
const args = Array.from(arguments);
|
||||
if (experiment && args[2]) {
|
||||
args[2].xid = experiment[0];
|
||||
args[2].xvar = experiment[1];
|
||||
function submitEvents() {
|
||||
if (navigator.doNotTrack === '1') {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
hasLocalStorage && analytics.sendEvent.apply(analytics, args).catch(() => 0)
|
||||
const data = new Blob(
|
||||
[
|
||||
JSON.stringify({
|
||||
now: Date.now(),
|
||||
session_id,
|
||||
lang,
|
||||
platform: platform(),
|
||||
events
|
||||
})
|
||||
],
|
||||
{ type: 'application/json' }
|
||||
);
|
||||
}
|
||||
|
||||
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';
|
||||
case 'https://qsurvey.mozilla.com/s3/txp-firefox-send':
|
||||
return 'survey';
|
||||
case 'https://testpilot.firefox.com/':
|
||||
case 'https://testpilot.firefox.com/experiments/send':
|
||||
return 'testpilot';
|
||||
case 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com':
|
||||
return 'promo';
|
||||
default:
|
||||
return 'other';
|
||||
events.splice(0);
|
||||
if (!navigator.sendBeacon) {
|
||||
return;
|
||||
}
|
||||
navigator.sendBeacon('/api/metrics', data);
|
||||
}
|
||||
|
||||
function setReferrer(state) {
|
||||
if (category() === 'sender') {
|
||||
if (state) {
|
||||
storage.referrer = `${state}-upload`;
|
||||
}
|
||||
} else if (category() === 'recipient') {
|
||||
if (state) {
|
||||
storage.referrer = `${state}-download`;
|
||||
async function addEvent(event_type, event_properties) {
|
||||
const user_id = await appState.user.metricId();
|
||||
const device_id = await appState.user.deviceId();
|
||||
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 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('sender', 'upload-started', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length + 1,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd5: takeReferrer()
|
||||
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 cancelledUpload(params) {
|
||||
setReferrer('cancelled');
|
||||
return sendEvent('sender', 'upload-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd2: 'cancelled'
|
||||
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 completedUpload(params) {
|
||||
return sendEvent('sender', 'upload-stopped', {
|
||||
cm1: params.size,
|
||||
cm2: params.time,
|
||||
cm3: params.speed,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd2: 'completed'
|
||||
});
|
||||
}
|
||||
|
||||
function addedPassword(params) {
|
||||
return sendEvent('sender', 'password-added', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads
|
||||
});
|
||||
}
|
||||
|
||||
function startedDownload(params) {
|
||||
return sendEvent('recipient', 'download-started', {
|
||||
cm1: params.size,
|
||||
cm4: params.ttl,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads
|
||||
function stoppedUpload(archive) {
|
||||
return addEvent('client_upload', {
|
||||
download_limit: archive.dlimit,
|
||||
file_count: archive.numFiles,
|
||||
password_protected: !!archive.password,
|
||||
size: sizeOrder(archive.size),
|
||||
status: 'error',
|
||||
time_limit: archive.timeLimit
|
||||
});
|
||||
}
|
||||
|
||||
function stoppedDownload(params) {
|
||||
return sendEvent('recipient', 'download-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'errored',
|
||||
cd6: params.err
|
||||
});
|
||||
}
|
||||
|
||||
function cancelledDownload(params) {
|
||||
setReferrer('cancelled');
|
||||
return sendEvent('recipient', 'download-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'cancelled'
|
||||
});
|
||||
}
|
||||
|
||||
function stoppedUpload(params) {
|
||||
return sendEvent('sender', 'upload-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd2: 'errored',
|
||||
cd6: params.err
|
||||
});
|
||||
}
|
||||
|
||||
function changedDownloadLimit(params) {
|
||||
return sendEvent('sender', 'download-limit-changed', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cm8: params.dlimit
|
||||
return addEvent('client_download', {
|
||||
duration: sizeOrder(params.duration),
|
||||
password_protected: params.password_protected,
|
||||
size: sizeOrder(params.size),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
function completedDownload(params) {
|
||||
return sendEvent('recipient', 'download-stopped', {
|
||||
cm1: params.size,
|
||||
cm2: params.time,
|
||||
cm3: params.speed,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'completed'
|
||||
return addEvent('client_download', {
|
||||
duration: sizeOrder(params.duration),
|
||||
password_protected: params.password_protected,
|
||||
size: sizeOrder(params.size),
|
||||
status: 'ok'
|
||||
});
|
||||
}
|
||||
|
||||
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.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd4: params.location
|
||||
});
|
||||
}
|
||||
|
||||
function unsupported(params) {
|
||||
return sendEvent(category(), 'unsupported', {
|
||||
cd6: params.err
|
||||
});
|
||||
}
|
||||
|
||||
function copiedLink(params) {
|
||||
return sendEvent('sender', 'copied', {
|
||||
cd4: params.location
|
||||
});
|
||||
}
|
||||
|
||||
function exitEvent(target) {
|
||||
return sendEvent(category(), 'exited', {
|
||||
cd3: urlToMetric(target.currentTarget.href)
|
||||
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 sendEvent(category(), 'experiment', params);
|
||||
return addEvent('client_experiment', params);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function addExitHandlers() {
|
||||
const links = Array.from(document.querySelectorAll('a'));
|
||||
links.forEach(l => {
|
||||
if (/^http/.test(l.getAttribute('href'))) {
|
||||
l.addEventListener('click', exitEvent);
|
||||
}
|
||||
function submittedSignup(params) {
|
||||
return addEvent('client_login', {
|
||||
status: 'ok',
|
||||
trigger: params.trigger
|
||||
});
|
||||
}
|
||||
|
||||
function restart(state) {
|
||||
setReferrer(state);
|
||||
return sendEvent(category(), 'restarted', {
|
||||
cd2: state
|
||||
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 {
|
||||
copiedLink,
|
||||
startedUpload,
|
||||
cancelledUpload,
|
||||
stoppedUpload,
|
||||
completedUpload,
|
||||
changedDownloadLimit,
|
||||
deletedUpload,
|
||||
startedDownload,
|
||||
cancelledDownload,
|
||||
stoppedDownload,
|
||||
completedDownload,
|
||||
addedPassword,
|
||||
restart,
|
||||
unsupported
|
||||
submittedSignup,
|
||||
canceledSignup,
|
||||
loggedOut
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@ export default class OwnedFile {
|
|||
this.url = obj.url;
|
||||
this.name = obj.name;
|
||||
this.size = obj.size;
|
||||
this.type = obj.type;
|
||||
this.manifest = obj.manifest;
|
||||
this.time = obj.time;
|
||||
this.speed = obj.speed;
|
||||
|
@ -78,7 +77,6 @@ export default class OwnedFile {
|
|||
url: this.url,
|
||||
name: this.name,
|
||||
size: this.size,
|
||||
type: this.type,
|
||||
manifest: this.manifest,
|
||||
time: this.time,
|
||||
speed: this.speed,
|
||||
|
|
|
@ -11,14 +11,7 @@ module.exports = function(app = choo()) {
|
|||
app.route('/error', body(require('./ui/error')));
|
||||
app.route('/blank', body(require('./ui/blank')));
|
||||
app.route('/oauth', async function(state, emit) {
|
||||
try {
|
||||
await state.user.finishLogin(state.query.code, state.query.state);
|
||||
await state.user.syncFileList();
|
||||
emit('replaceState', '/');
|
||||
} catch (e) {
|
||||
emit('replaceState', '/error');
|
||||
setTimeout(() => emit('render'));
|
||||
}
|
||||
emit('authenticate', state.query.code, state.query.state);
|
||||
});
|
||||
app.route('*', body(require('./ui/notFound')));
|
||||
return app;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { isFile } from './utils';
|
||||
import { arrayToB64, isFile } from './utils';
|
||||
import OwnedFile from './ownedFile';
|
||||
|
||||
class Mem {
|
||||
|
@ -58,6 +58,15 @@ class Storage {
|
|||
return fs;
|
||||
}
|
||||
|
||||
get id() {
|
||||
let id = this.engine.getItem('device_id');
|
||||
if (!id) {
|
||||
id = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
|
||||
this.engine.setItem('device_id', id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
get totalDownloads() {
|
||||
return Number(this.engine.getItem('totalDownloads'));
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const html = require('choo/html');
|
||||
const Component = require('choo/component');
|
||||
const signupDialog = require('./signupDialog');
|
||||
|
||||
class Account extends Component {
|
||||
constructor(name, state, emit) {
|
||||
|
@ -27,8 +26,7 @@ class Account extends Component {
|
|||
|
||||
login(event) {
|
||||
event.preventDefault();
|
||||
this.state.modal = signupDialog();
|
||||
this.emit('render');
|
||||
this.emit('signup-cta', 'button');
|
||||
}
|
||||
|
||||
logout(event) {
|
||||
|
|
|
@ -34,7 +34,7 @@ function password(state) {
|
|||
<input
|
||||
id="add-password"
|
||||
type="checkbox"
|
||||
${state.password ? 'checked' : ''}
|
||||
${state.archive.password ? 'checked' : ''}
|
||||
autocomplete="off"
|
||||
onchange="${togglePasswordInput}"
|
||||
/>
|
||||
|
@ -44,7 +44,7 @@ function password(state) {
|
|||
</div>
|
||||
<input
|
||||
id="password-input"
|
||||
class="${state.password
|
||||
class="${state.archive.password
|
||||
? ''
|
||||
: 'invisible'} border rounded-sm focus:border-blue leading-normal my-2 py-1 px-2 h-8"
|
||||
autocomplete="off"
|
||||
|
@ -53,7 +53,7 @@ function password(state) {
|
|||
oninput="${inputChanged}"
|
||||
onfocus="${focused}"
|
||||
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||
value="${state.password || ''}"
|
||||
value="${state.archive.password || ''}"
|
||||
/>
|
||||
<label
|
||||
id="password-msg"
|
||||
|
@ -74,7 +74,7 @@ function password(state) {
|
|||
input.classList.add('invisible');
|
||||
input.value = '';
|
||||
document.getElementById('password-msg').textContent = '';
|
||||
state.password = null;
|
||||
state.archive.password = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@ function password(state) {
|
|||
} else {
|
||||
pwdmsg.textContent = '';
|
||||
}
|
||||
state.password = password;
|
||||
state.archive.password = password;
|
||||
}
|
||||
|
||||
function focused(event) {
|
||||
|
@ -219,7 +219,7 @@ module.exports = function(state, emit, archive) {
|
|||
|
||||
function del(event) {
|
||||
event.stopPropagation();
|
||||
emit('delete', { file: archive, location: 'success-screen' });
|
||||
emit('delete', archive);
|
||||
}
|
||||
|
||||
function share(event) {
|
||||
|
@ -279,11 +279,7 @@ module.exports.wip = function(state, emit) {
|
|||
event.preventDefault();
|
||||
event.target.disabled = true;
|
||||
if (!state.uploading) {
|
||||
emit('upload', {
|
||||
type: 'click',
|
||||
dlimit: state.downloadCount || 1,
|
||||
password: state.password
|
||||
});
|
||||
emit('upload');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -333,9 +329,9 @@ module.exports.uploading = function(state, emit) {
|
|||
</p>
|
||||
<div class="text-xs text-grey-dark w-full mt-2 mb-2">
|
||||
${expiryInfo(state.translate, {
|
||||
dlimit: state.downloadCount || 1,
|
||||
dlimit: state.archive.dlimit,
|
||||
dtotal: 0,
|
||||
expiresAt: Date.now() + 500 + state.timeLimit * 1000
|
||||
expiresAt: Date.now() + 500 + state.archive.timeLimit * 1000
|
||||
})}
|
||||
</div>
|
||||
<div class="text-blue text-sm font-medium mt-2">${progressPercent}</div>
|
||||
|
|
|
@ -3,7 +3,6 @@ const html = require('choo/html');
|
|||
const raw = require('choo/html/raw');
|
||||
const { secondsToL10nId } = require('../utils');
|
||||
const selectbox = require('./selectbox');
|
||||
const signupDialog = require('./signupDialog');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const el = html`
|
||||
|
@ -29,17 +28,17 @@ module.exports = function(state, emit) {
|
|||
const dlCountSelect = el.querySelector('#dlCount');
|
||||
el.replaceChild(
|
||||
selectbox(
|
||||
state.downloadCount || 1,
|
||||
state.archive.dlimit,
|
||||
counts,
|
||||
num => state.translate('downloadCount', { num }),
|
||||
value => {
|
||||
const max = state.user.maxDownloads;
|
||||
state.archive.dlimit = Math.min(value, max);
|
||||
if (value > max) {
|
||||
state.modal = signupDialog();
|
||||
value = max;
|
||||
emit('signup-cta', 'count');
|
||||
} else {
|
||||
emit('render');
|
||||
}
|
||||
state.downloadCount = value;
|
||||
emit('render');
|
||||
},
|
||||
'expire-after-dl-count-select'
|
||||
),
|
||||
|
@ -53,7 +52,7 @@ module.exports = function(state, emit) {
|
|||
const timeSelect = el.querySelector('#timespan');
|
||||
el.replaceChild(
|
||||
selectbox(
|
||||
state.timeLimit || 86400,
|
||||
state.archive.timeLimit,
|
||||
expires,
|
||||
num => {
|
||||
const l10n = secondsToL10nId(num);
|
||||
|
@ -61,12 +60,12 @@ module.exports = function(state, emit) {
|
|||
},
|
||||
value => {
|
||||
const max = state.user.maxExpireSeconds;
|
||||
state.archive.timeLimit = Math.min(value, max);
|
||||
if (value > max) {
|
||||
state.modal = signupDialog();
|
||||
value = max;
|
||||
emit('signup-cta', 'time');
|
||||
} else {
|
||||
emit('render');
|
||||
}
|
||||
state.timeLimit = value;
|
||||
emit('render');
|
||||
},
|
||||
'expire-after-time-select'
|
||||
),
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
/* global LIMITS */
|
||||
const html = require('choo/html');
|
||||
const { bytes, platform } = require('../utils');
|
||||
const { canceledSignup, submittedSignup } = require('../metrics');
|
||||
|
||||
module.exports = function() {
|
||||
module.exports = function(trigger) {
|
||||
return function(state, emit, close) {
|
||||
const hidden = platform() === 'android' ? 'hidden' : '';
|
||||
let submitting = false;
|
||||
|
@ -37,7 +38,7 @@ module.exports = function() {
|
|||
<button
|
||||
class="my-4 text-blue hover:text-blue-dark focus:text-blue-darker font-medium"
|
||||
title="${state.translate('deletePopupCancel')}"
|
||||
onclick=${close}>${state.translate('deletePopupCancel')}
|
||||
onclick=${cancel}>${state.translate('deletePopupCancel')}
|
||||
</button>
|
||||
</send-signup-dialog>`;
|
||||
|
||||
|
@ -50,6 +51,11 @@ module.exports = function() {
|
|||
return a.length === 2 && a.every(s => s.length > 0);
|
||||
}
|
||||
|
||||
function cancel(event) {
|
||||
canceledSignup({ trigger });
|
||||
close(event);
|
||||
}
|
||||
|
||||
function submitEmail(event) {
|
||||
event.preventDefault();
|
||||
if (submitting) {
|
||||
|
@ -59,6 +65,7 @@ module.exports = function() {
|
|||
|
||||
const el = document.getElementById('email-input');
|
||||
const email = el.value;
|
||||
submittedSignup({ trigger });
|
||||
emit('login', emailish(email) ? email : null);
|
||||
}
|
||||
};
|
||||
|
|
26
app/user.js
26
app/user.js
|
@ -9,6 +9,16 @@ import storage from './storage';
|
|||
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
const anonId = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
|
||||
|
||||
async function hashId(id) {
|
||||
const d = new Date();
|
||||
const month = d.getUTCMonth();
|
||||
const year = d.getUTCFullYear();
|
||||
const encoded = textEncoder.encode(`${id}:${year}:${month}`);
|
||||
const hash = await crypto.subtle.digest('SHA-256', encoded);
|
||||
return arrayToB64(new Uint8Array(hash.slice(16)));
|
||||
}
|
||||
|
||||
export default class User {
|
||||
constructor(storage) {
|
||||
|
@ -25,6 +35,14 @@ export default class User {
|
|||
this.storage.user = data;
|
||||
}
|
||||
|
||||
get firstAction() {
|
||||
return this.storage.get('firstAction');
|
||||
}
|
||||
|
||||
set firstAction(action) {
|
||||
this.storage.set('firstAction', action);
|
||||
}
|
||||
|
||||
get avatar() {
|
||||
const defaultAvatar = assets.get('user.svg');
|
||||
if (this.info.avatarDefault) {
|
||||
|
@ -63,6 +81,14 @@ export default class User {
|
|||
return this.loggedIn ? LIMITS.MAX_DOWNLOADS : LIMITS.ANON.MAX_DOWNLOADS;
|
||||
}
|
||||
|
||||
async metricId() {
|
||||
return this.loggedIn ? hashId(this.info.uid) : undefined;
|
||||
}
|
||||
|
||||
async deviceId() {
|
||||
return this.loggedIn ? hashId(this.storage.id) : hashId(anonId);
|
||||
}
|
||||
|
||||
async login(email) {
|
||||
const state = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
|
||||
storage.set('oauthState', state);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue