2025-01-24 06:58:00 +01:00
|
|
|
// Collects anonymous usage stats and uncaught errors
|
|
|
|
// Reports back so that we can improve WebTorrent Desktop
|
|
|
|
module.exports = {
|
|
|
|
init,
|
|
|
|
send,
|
|
|
|
logUncaughtError,
|
|
|
|
logPlayAttempt
|
|
|
|
}
|
|
|
|
|
|
|
|
const remote = require('@electron/remote')
|
|
|
|
|
|
|
|
const config = require('../../config')
|
|
|
|
|
|
|
|
let telemetry
|
|
|
|
|
|
|
|
function init (state) {
|
|
|
|
telemetry = state.saved.telemetry
|
|
|
|
|
|
|
|
// First app run
|
|
|
|
if (!telemetry) {
|
|
|
|
const crypto = require('crypto')
|
|
|
|
telemetry = state.saved.telemetry = {
|
|
|
|
userID: crypto.randomBytes(32).toString('hex') // 256-bit random ID
|
|
|
|
}
|
|
|
|
reset()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function send (state) {
|
2025-01-24 06:11:46 +00:00
|
|
|
//Disable telemetry from webtorrent
|
|
|
|
return
|
2025-01-24 06:58:00 +01:00
|
|
|
const now = new Date()
|
|
|
|
telemetry.version = config.APP_VERSION
|
|
|
|
telemetry.timestamp = now.toISOString()
|
|
|
|
telemetry.localTime = now.toTimeString()
|
|
|
|
telemetry.screens = getScreenInfo()
|
|
|
|
telemetry.system = getSystemInfo()
|
|
|
|
telemetry.torrentStats = getTorrentStats(state)
|
|
|
|
telemetry.approxNumTorrents = telemetry.torrentStats.approxCount
|
|
|
|
|
|
|
|
if (!config.IS_PRODUCTION) {
|
|
|
|
// Development: telemetry used only for local debugging
|
|
|
|
// Empty uncaught errors, etc at the start of every run
|
|
|
|
return reset()
|
|
|
|
}
|
|
|
|
|
|
|
|
const get = require('simple-get')
|
|
|
|
|
|
|
|
const opts = {
|
|
|
|
url: config.TELEMETRY_URL,
|
|
|
|
body: telemetry,
|
|
|
|
json: true
|
|
|
|
}
|
|
|
|
|
|
|
|
get.post(opts, (err, res) => {
|
|
|
|
if (err) return console.error('Error sending telemetry', err)
|
|
|
|
if (res.statusCode !== 200) {
|
|
|
|
return console.error(`Error sending telemetry, status code: ${res.statusCode}`)
|
|
|
|
}
|
|
|
|
console.log('Sent telemetry')
|
|
|
|
reset()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
function reset () {
|
|
|
|
telemetry.uncaughtErrors = []
|
|
|
|
telemetry.playAttempts = {
|
|
|
|
minVersion: config.APP_VERSION,
|
|
|
|
total: 0,
|
|
|
|
success: 0,
|
|
|
|
error: 0,
|
|
|
|
external: 0,
|
|
|
|
abandoned: 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Track screen resolution
|
|
|
|
function getScreenInfo () {
|
|
|
|
return remote.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 () {
|
|
|
|
const os = require('os')
|
|
|
|
return {
|
|
|
|
osPlatform: process.platform,
|
|
|
|
osRelease: os.type() + ' ' + os.release(),
|
|
|
|
architecture: os.arch(),
|
|
|
|
systemArchitecture: config.OS_SYSARCH,
|
|
|
|
totalMemoryMB: roundPow2(os.totalmem() / (1 << 20)),
|
|
|
|
numCores: os.cpus().length
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get stats like the # of torrents currently active, # in list, total size
|
|
|
|
function getTorrentStats (state) {
|
|
|
|
const count = state.saved.torrents.length
|
|
|
|
let sizeMB = 0
|
|
|
|
const byStatus = {
|
|
|
|
new: { count: 0, sizeMB: 0 },
|
|
|
|
downloading: { count: 0, sizeMB: 0 },
|
|
|
|
seeding: { count: 0, sizeMB: 0 },
|
|
|
|
paused: { count: 0, sizeMB: 0 }
|
|
|
|
}
|
|
|
|
|
|
|
|
// First, count torrents & total file size
|
|
|
|
for (const torrent of state.saved.torrents) {
|
|
|
|
const stat = byStatus[torrent.status]
|
|
|
|
if (!torrent || !torrent.files || !stat) continue
|
|
|
|
stat.count++
|
|
|
|
|
|
|
|
for (const file of torrent.files) {
|
|
|
|
if (!file || !file.length) continue
|
|
|
|
const fileSizeMB = file.length / (1 << 20)
|
|
|
|
sizeMB += fileSizeMB
|
|
|
|
stat.sizeMB += fileSizeMB
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then, round all the counts and sums to the nearest power of 2
|
|
|
|
const ret = roundTorrentStats({ count, sizeMB })
|
|
|
|
ret.byStatus = {
|
|
|
|
new: roundTorrentStats(byStatus.new),
|
|
|
|
downloading: roundTorrentStats(byStatus.downloading),
|
|
|
|
seeding: roundTorrentStats(byStatus.seeding),
|
|
|
|
paused: roundTorrentStats(byStatus.paused)
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
function roundTorrentStats (stats) {
|
|
|
|
return {
|
|
|
|
approxCount: roundPow2(stats.count),
|
|
|
|
approxSizeMB: roundPow2(stats.sizeMB)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rounds to the nearest power of 2, for privacy and easy bucketing.
|
|
|
|
// Rounds 35 to 32, 70 to 64, 5 to 4, 1 to 1, 0 to 0.
|
|
|
|
// Supports nonnegative numbers only.
|
|
|
|
function roundPow2 (n) {
|
|
|
|
if (n <= 0) return 0
|
|
|
|
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
|
|
|
const log2 = Math.log(n) / Math.log(2)
|
|
|
|
return 2 ** Math.round(log2)
|
|
|
|
}
|
|
|
|
|
|
|
|
// An uncaught error happened in the main process or in one of the windows
|
|
|
|
function logUncaughtError (procName, e) {
|
|
|
|
// Not initialized yet? Ignore.
|
|
|
|
// Hopefully uncaught errors immediately on startup are fixed in dev
|
|
|
|
if (!telemetry) return
|
|
|
|
|
|
|
|
let message
|
|
|
|
let stack = ''
|
|
|
|
if (e == null) {
|
|
|
|
message = 'Unexpected undefined error'
|
|
|
|
} else if (e.error) {
|
|
|
|
// Uncaught Javascript errors (window.onerror), err is an ErrorEvent
|
|
|
|
if (!e.error.message) {
|
|
|
|
message = 'Unexpected ErrorEvent.error: ' + Object.keys(e.error).join(' ')
|
|
|
|
} else {
|
|
|
|
message = e.error.message
|
|
|
|
stack = e.error.stack
|
|
|
|
}
|
|
|
|
} else if (e.message) {
|
|
|
|
// err is either an Error or a plain object {message, stack}
|
|
|
|
message = e.message
|
|
|
|
stack = e.stack
|
|
|
|
} else {
|
|
|
|
// Resource errors (captured element.onerror), err is an Event
|
|
|
|
if (!e.target) {
|
|
|
|
message = 'Unexpected unknown error'
|
|
|
|
} else if (!e.target.error) {
|
|
|
|
message = 'Unexpected resource loading error: ' + getElemString(e.target)
|
|
|
|
} else {
|
|
|
|
message = 'Resource error ' + getElemString(e.target) + ': ' + e.target.error.code
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof stack !== 'string') stack = 'Unexpected stack: ' + stack
|
|
|
|
if (typeof message !== 'string') message = 'Unexpected message: ' + message
|
|
|
|
|
|
|
|
// Remove the first part of each file path in the stack trace.
|
|
|
|
// - Privacy: remove personal info like C:\Users\<full name>
|
|
|
|
// - Aggregation: this lets us find which stacktraces occur often
|
|
|
|
stack = stack.replace(/\(.*app.asar/g, '(...')
|
|
|
|
stack = stack.replace(/at .*app.asar/g, 'at ...')
|
|
|
|
|
|
|
|
// We need to POST the telemetry object, make sure it stays < 100kb
|
|
|
|
if (telemetry.uncaughtErrors.length > 20) return
|
|
|
|
if (message.length > 1000) message = message.substring(0, 1000)
|
|
|
|
if (stack.length > 1000) stack = stack.substring(0, 1000)
|
|
|
|
|
|
|
|
// Log the app version *at the time of the error*
|
|
|
|
const version = config.APP_VERSION
|
|
|
|
|
|
|
|
telemetry.uncaughtErrors.push({ process: procName, message, stack, version })
|
|
|
|
}
|
|
|
|
|
|
|
|
// Turns a DOM element into a string, eg "DIV.my-class.visible"
|
|
|
|
function getElemString (elem) {
|
|
|
|
let ret = elem.tagName
|
|
|
|
try {
|
|
|
|
ret += '.' + Array.from(elem.classList).join('.')
|
|
|
|
} catch (err) {}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
// The user pressed play. Did it work, display an error,
|
|
|
|
// open an external player or did user abandon the attempt?
|
|
|
|
function logPlayAttempt (result) {
|
|
|
|
if (!['success', 'error', 'external', 'abandoned'].includes(result)) {
|
|
|
|
return console.error('Unknown play attempt result', result)
|
|
|
|
}
|
|
|
|
|
|
|
|
const attempts = telemetry.playAttempts
|
|
|
|
attempts.total = (attempts.total || 0) + 1
|
|
|
|
attempts[result] = (attempts[result] || 0) + 1
|
|
|
|
}
|