Telemetry

This commit is contained in:
DC
2016-06-19 15:19:08 -07:00
parent 746e10c025
commit c3686417e3
5 changed files with 211 additions and 8 deletions

View File

@@ -3,7 +3,48 @@
var fs = require('fs') var fs = require('fs')
var cp = require('child_process') 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'] var EXECUTABLE_DEPS = ['gh-release', 'standard']
main() main()

View File

@@ -10,6 +10,9 @@ var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings
module.exports = { module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement', 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_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'), APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
@@ -19,10 +22,6 @@ module.exports = {
APP_VERSION: APP_VERSION, APP_VERSION: APP_VERSION,
APP_WINDOW_TITLE: APP_NAME + ' (BETA)', 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(), CONFIG_PATH: getConfigPath(),
DEFAULT_TORRENTS: [ DEFAULT_TORRENTS: [

View File

@@ -14,8 +14,9 @@ var createElement = require('virtual-dom/create-element')
var diff = require('virtual-dom/diff') var diff = require('virtual-dom/diff')
var patch = require('virtual-dom/patch') var patch = require('virtual-dom/patch')
var App = require('./views/app')
var config = require('../config') var config = require('../config')
var telemetry = require('../telemetry')
var App = require('./views/app')
var errors = require('./lib/errors') var errors = require('./lib/errors')
var sound = require('./lib/sound') var sound = require('./lib/sound')
var State = require('./lib/state') var State = require('./lib/state')
@@ -103,6 +104,7 @@ function onState (err, _state) {
function delayedInit () { function delayedInit () {
lazyLoadCast() lazyLoadCast()
sound.preload() sound.preload()
telemetry.init(state)
} }
// Lazily loads Chromecast and Airplay support // Lazily loads Chromecast and Airplay support
@@ -254,6 +256,7 @@ function dispatch (action, ...args) {
} }
if (action === 'mediaError') { if (action === 'mediaError') {
if (state.location.url() === 'player') { if (state.location.url() === 'player') {
state.playing.result = 'error'
state.playing.location = 'error' state.playing.location = 'error'
ipcRenderer.send('checkForVLC') ipcRenderer.send('checkForVLC')
ipcRenderer.once('checkForVLC', function (e, isInstalled) { ipcRenderer.once('checkForVLC', function (e, isInstalled) {
@@ -265,6 +268,9 @@ function dispatch (action, ...args) {
}) })
} }
} }
if (action === 'mediaSuccess') {
state.playing.result = 'success'
}
if (action === 'mediaTimeUpdate') { if (action === 'mediaTimeUpdate') {
state.playing.lastTimeUpdate = new Date().getTime() state.playing.lastTimeUpdate = new Date().getTime()
state.playing.isStalled = false state.playing.isStalled = false
@@ -978,10 +984,13 @@ function openPlayer (infoHash, index, cb) {
// update UI to show pending playback // update UI to show pending playback
if (torrentSummary.progress !== 1) sound.play('PLAY') if (torrentSummary.progress !== 1) sound.play('PLAY')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'requested' torrentSummary.playStatus = 'requested'
update() update()
var timeout = setTimeout(function () { var timeout = setTimeout(function () {
telemetry.logPlayAttempt('timeout')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'timeout' /* no seeders available? */ torrentSummary.playStatus = 'timeout' /* no seeders available? */
sound.play('ERROR') sound.play('ERROR')
cb(new Error('Playback timed out. Try again.')) cb(new Error('Playback timed out. Try again.'))
@@ -1047,25 +1056,39 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
} }
function closePlayer (cb) { function closePlayer (cb) {
// Quit any external players, like Chromecast/Airplay/etc or VLC
if (isCasting()) { if (isCasting()) {
Cast.close() Cast.close()
} }
if (state.playing.location === 'vlc') { if (state.playing.location === 'vlc') {
ipcRenderer.send('vlcQuit') 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 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.playing = State.getDefaultPlayState()
state.server = null state.server = null
// Reset the window size and location back to where it was
if (state.window.isFullScreen) { if (state.window.isFullScreen) {
dispatch('toggleFullScreen', false) dispatch('toggleFullScreen', false)
} }
restoreBounds() restoreBounds()
// Tell the WebTorrent process to kill the torrent-to-HTTP server
ipcRenderer.send('wt-stop-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('unblockPowerSave')
ipcRenderer.send('onPlayerClose') ipcRenderer.send('onPlayerClose')

View File

@@ -143,6 +143,7 @@ function renderMedia (state) {
} else if (elem.webkitAudioDecodedByteCount === 0) { } else if (elem.webkitAudioDecodedByteCount === 0) {
dispatch('mediaError', 'Audio codec unsupported') dispatch('mediaError', 'Audio codec unsupported')
} else { } else {
dispatch('mediaSuccess')
elem.play() elem.play()
} }
} }

139
telemetry.js Normal file
View File

@@ -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
}