From c3686417e3a625586f06ea997248287df6a90d1a Mon Sep 17 00:00:00 2001 From: DC Date: Sun, 19 Jun 2016 15:19:08 -0700 Subject: [PATCH] Telemetry --- bin/check-deps.js | 43 +++++++++++- config.js | 7 +- renderer/main.js | 29 +++++++- renderer/views/player.js | 1 + telemetry.js | 139 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 telemetry.js diff --git a/bin/check-deps.js b/bin/check-deps.js index 6f212295..89403f63 100755 --- a/bin/check-deps.js +++ b/bin/check-deps.js @@ -3,7 +3,48 @@ var fs = require('fs') var cp = require('child_process') -var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path'] +// We can't use `builtin-modules` here since our TravisCI +// setup expects this file to run with no dependencies +var BUILT_IN_NODE_MODULES = [ + 'assert', + 'buffer', + 'child_process', + 'cluster', + 'console', + 'constants', + 'crypto', + 'dgram', + 'dns', + 'domain', + 'events', + 'fs', + 'http', + 'https', + 'module', + 'net', + 'os', + 'path', + 'process', + 'punycode', + 'querystring', + 'readline', + 'repl', + 'stream', + 'string_decoder', + 'timers', + 'tls', + 'tty', + 'url', + 'util', + 'v8', + 'vm', + 'zlib' +] + +var BUILT_IN_ELECTRON_MODULES = [ 'electron' ] + +var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES) + var EXECUTABLE_DEPS = ['gh-release', 'standard'] main() diff --git a/config.js b/config.js index b1c75fa1..d01b5390 100644 --- a/config.js +++ b/config.js @@ -10,6 +10,9 @@ var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings module.exports = { ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement', + AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update', + CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report', + TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry', APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM, APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'), @@ -19,10 +22,6 @@ module.exports = { APP_VERSION: APP_VERSION, APP_WINDOW_TITLE: APP_NAME + ' (BETA)', - AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update', - - CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report', - CONFIG_PATH: getConfigPath(), DEFAULT_TORRENTS: [ diff --git a/renderer/main.js b/renderer/main.js index 39bcc30a..7b6085fb 100644 --- a/renderer/main.js +++ b/renderer/main.js @@ -14,8 +14,9 @@ var createElement = require('virtual-dom/create-element') var diff = require('virtual-dom/diff') var patch = require('virtual-dom/patch') -var App = require('./views/app') var config = require('../config') +var telemetry = require('../telemetry') +var App = require('./views/app') var errors = require('./lib/errors') var sound = require('./lib/sound') var State = require('./lib/state') @@ -103,6 +104,7 @@ function onState (err, _state) { function delayedInit () { lazyLoadCast() sound.preload() + telemetry.init(state) } // Lazily loads Chromecast and Airplay support @@ -254,6 +256,7 @@ function dispatch (action, ...args) { } if (action === 'mediaError') { if (state.location.url() === 'player') { + state.playing.result = 'error' state.playing.location = 'error' ipcRenderer.send('checkForVLC') ipcRenderer.once('checkForVLC', function (e, isInstalled) { @@ -265,6 +268,9 @@ function dispatch (action, ...args) { }) } } + if (action === 'mediaSuccess') { + state.playing.result = 'success' + } if (action === 'mediaTimeUpdate') { state.playing.lastTimeUpdate = new Date().getTime() state.playing.isStalled = false @@ -978,10 +984,13 @@ function openPlayer (infoHash, index, cb) { // update UI to show pending playback if (torrentSummary.progress !== 1) sound.play('PLAY') + // TODO: remove torrentSummary.playStatus torrentSummary.playStatus = 'requested' update() var timeout = setTimeout(function () { + telemetry.logPlayAttempt('timeout') + // TODO: remove torrentSummary.playStatus torrentSummary.playStatus = 'timeout' /* no seeders available? */ sound.play('ERROR') cb(new Error('Playback timed out. Try again.')) @@ -1047,25 +1056,39 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) { } function closePlayer (cb) { + // Quit any external players, like Chromecast/Airplay/etc or VLC if (isCasting()) { Cast.close() } if (state.playing.location === 'vlc') { ipcRenderer.send('vlcQuit') } - state.window.title = config.APP_WINDOW_TITLE - // Lets save volume for later + + // Save volume (this session only, not in state.saved) state.previousVolume = state.playing.volume + // Telemetry: track what happens after the user clicks play + var result = state.playing.result // 'success' or 'error' + if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed + else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc + else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user exited before first frame + else console.error('Unknown state.playing.result', state.playing.result) + + // Reset the window contents back to the home screen + state.window.title = config.APP_WINDOW_TITLE state.playing = State.getDefaultPlayState() state.server = null + // Reset the window size and location back to where it was if (state.window.isFullScreen) { dispatch('toggleFullScreen', false) } restoreBounds() + // Tell the WebTorrent process to kill the torrent-to-HTTP server ipcRenderer.send('wt-stop-server') + + // Tell the OS we're no longer playing media, laptops allowed to sleep again ipcRenderer.send('unblockPowerSave') ipcRenderer.send('onPlayerClose') diff --git a/renderer/views/player.js b/renderer/views/player.js index b25b87bc..606105ef 100644 --- a/renderer/views/player.js +++ b/renderer/views/player.js @@ -143,6 +143,7 @@ function renderMedia (state) { } else if (elem.webkitAudioDecodedByteCount === 0) { dispatch('mediaError', 'Audio codec unsupported') } else { + dispatch('mediaSuccess') elem.play() } } diff --git a/telemetry.js b/telemetry.js new file mode 100644 index 00000000..946fd490 --- /dev/null +++ b/telemetry.js @@ -0,0 +1,139 @@ +// Collects anonymous usage stats and uncaught errors +// Reports back so that we can improve WebTorrent Desktop +module.exports = { + init, + logUncaughtError, + logPlayAttempt, + getSummary +} + +const crypto = require('crypto') +const electron = require('electron') +const https = require('https') +const os = require('os') +const url = require('url') + +const config = require('./config') + +var telemetry + +function init (state) { + telemetry = state.saved.telemetry + if (!telemetry) { + telemetry = state.saved.telemetry = createSummary() + reset() + } + + var now = new Date() + telemetry.timestamp = now.toISOString() + telemetry.localTime = now.toTimeString() + telemetry.screens = getScreenInfo() + telemetry.system = getSystemInfo() + telemetry.approxNumTorrents = getApproxNumTorrents(state) + + postToServer(telemetry) +} + +function reset () { + telemetry.uncaughtErrors = [] + telemetry.playAttempts = { + total: 0, + success: 0, + timeout: 0, + error: 0, + abandoned: 0 + } +} + +function postToServer () { + // Serialize the telemetry summary + return console.log(JSON.stringify(telemetry, null, 2)) + var payload = new Buffer(JSON.stringify(telemetry), 'utf8') + + // POST to our server + var options = url.parse(config.TELEMETRY_URL) + options.method = 'POST' + options.headers = { + 'Content-Type': 'application/json', + 'Content-Length': payload.length + } + + var req = https.request(options, function (res) { + if (res.statusCode === 200) { + console.log('Successfully posted telemetry summary') + reset() + } else { + console.error('Couldn\'t post telemetry summary, got HTTP ' + res.statusCode) + } + }) + req.on('error', function (e) { + console.error('Couldn\'t post telemetry summary', e) + }) + req.write(payload) + req.end() +} + +// Creates a new telemetry summary. Gives the user a unique ID, +// collects screen resolution, etc +function createSummary () { + // Make a 256-bit random unique ID + var userID = crypto.randomBytes(32).toString('hex') + return { userID } +} + +// Track screen resolution +function getScreenInfo () { + return electron.screen.getAllDisplays().map((screen) => ({ + width: screen.size.width, + height: screen.size.height, + scaleFactor: screen.scaleFactor + })) +} + +// Track basic system info like OS version and amount of RAM +function getSystemInfo () { + return { + osPlatform: process.platform, + osRelease: os.type() + ' ' + os.release(), + architecture: os.arch(), + totalMemoryMB: os.totalmem() / (1 << 20), + numCores: os.cpus().length + } +} + +// Get the number of torrents, rounded to the nearest power of two +function getApproxNumTorrents (state) { + var exactNum = state.saved.torrents.length + if (exactNum === 0) return 0 + // Otherwise, return 1, 2, 4, 8, etc by rounding in log space + var log2 = Math.log(exactNum) / Math.log(2) + return 1 << Math.round(log2) +} + +// An uncaught error happened in the main process or one in one of the windows +function logUncaughtError (err) { + var errString + if (typeof err === 'string') { + errString = err + } else { + errString = err.message + '\n' + err.stack + } + telemetry.uncaughtErrors.push(errString) +} + +// The user pressed play. It either worked, timed out, or showed the +// "Play in VLC" codec error +function logPlayAttempt (result) { + if (!['success', 'timeout', 'error', 'abandoned'].includes(result)) { + return console.error('Unknown play attempt result', result) + } + + var attempts = telemetry.playAttempts + attempts.total = (attempts.total || 0) + 1 + attempts[result] = (attempts[result] || 0) + 1 +} + +// Gets a summary JSON object to send to the server +function getSummary () { + return telemetry +}