Telemetry
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||
@@ -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
139
telemetry.js
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user