Files
atom/packages/exception-reporting/lib/reporter.js
2019-02-25 12:50:18 +01:00

303 lines
8.5 KiB
JavaScript

/** @babel */
import os from 'os'
import stackTrace from 'stack-trace'
import fs from 'fs-plus'
import path from 'path'
const API_KEY = '7ddca14cb60cbd1cd12d1b252473b076'
const LIB_VERSION = require('../package.json')['version']
const StackTraceCache = new WeakMap()
export default class Reporter {
constructor (params = {}) {
this.request = params.request || window.fetch
this.alwaysReport = params.hasOwnProperty('alwaysReport')
? params.alwaysReport
: false
this.reportPreviousErrors = params.hasOwnProperty('reportPreviousErrors')
? params.reportPreviousErrors
: true
this.resourcePath = this.normalizePath(
params.resourcePath || process.resourcesPath
)
this.reportedErrors = []
this.reportedAssertionFailures = []
}
buildNotificationJSON (error, params) {
return {
apiKey: API_KEY,
notifier: {
name: 'Atom',
version: LIB_VERSION,
url: 'https://www.atom.io'
},
events: [
{
payloadVersion: '2',
exceptions: [this.buildExceptionJSON(error, params.projectRoot)],
severity: params.severity,
user: {
id: params.userId
},
app: {
version: params.appVersion,
releaseStage: params.releaseStage
},
device: {
osVersion: params.osVersion
},
metaData: error.metadata
}
]
}
}
buildExceptionJSON (error, projectRoot) {
return {
errorClass: error.constructor.name,
message: error.message,
stacktrace: this.buildStackTraceJSON(error, projectRoot)
}
}
buildStackTraceJSON (error, projectRoot) {
return this.parseStackTrace(error).map(callSite => {
return {
file: this.scrubPath(callSite.getFileName()),
method:
callSite.getMethodName() || callSite.getFunctionName() || 'none',
lineNumber: callSite.getLineNumber(),
columnNumber: callSite.getColumnNumber(),
inProject: !/node_modules/.test(callSite.getFileName())
}
})
}
normalizePath (pathToNormalize) {
return pathToNormalize
.replace('file:///', '') // Sometimes it's a uri
.replace(/\\/g, '/') // Unify path separators across Win/macOS/Linux
}
scrubPath (pathToScrub) {
const absolutePath = this.normalizePath(pathToScrub)
if (this.isBundledFile(absolutePath)) {
return this.normalizePath(path.relative(this.resourcePath, absolutePath))
} else {
return absolutePath
.replace(this.normalizePath(fs.getHomeDirectory()), '~') // Remove users home dir
.replace(/.*(\/packages\/.*)/, '$1') // Remove everything before app.asar or packages
}
}
getDefaultNotificationParams () {
return {
userId: atom.config.get('exception-reporting.userId'),
appVersion: atom.getVersion(),
releaseStage: this.getReleaseChannel(atom.getVersion()),
projectRoot: atom.getLoadSettings().resourcePath,
osVersion: `${os.platform()}-${os.arch()}-${os.release()}`
}
}
getReleaseChannel (version) {
return version.indexOf('beta') > -1
? 'beta'
: version.indexOf('dev') > -1
? 'dev'
: 'stable'
}
performRequest (json) {
this.request.call(null, 'https://notify.bugsnag.com', {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(json)
})
}
shouldReport (error) {
if (this.alwaysReport) return true // Used in specs
if (atom.config.get('core.telemetryConsent') !== 'limited') return false
if (atom.inDevMode()) return false
const topFrame = this.parseStackTrace(error)[0]
const fileName = topFrame ? topFrame.getFileName() : null
return (
fileName &&
(this.isBundledFile(fileName) || this.isTeletypeFile(fileName))
)
}
parseStackTrace (error) {
let callSites = StackTraceCache.get(error)
if (callSites) {
return callSites
} else {
callSites = stackTrace.parse(error)
StackTraceCache.set(error, callSites)
return callSites
}
}
requestPrivateMetadataConsent (error, message, reportFn) {
let notification, dismissSubscription
function reportWithoutPrivateMetadata () {
if (dismissSubscription) {
dismissSubscription.dispose()
}
delete error.privateMetadata
delete error.privateMetadataDescription
reportFn(error)
if (notification) {
notification.dismiss()
}
}
function reportWithPrivateMetadata () {
if (error.metadata == null) {
error.metadata = {}
}
for (let key in error.privateMetadata) {
let value = error.privateMetadata[key]
error.metadata[key] = value
}
reportWithoutPrivateMetadata()
}
const name = error.privateMetadataRequestName
if (name != null) {
if (localStorage.getItem(`private-metadata-request:${name}`)) {
return reportWithoutPrivateMetadata(error)
} else {
localStorage.setItem(`private-metadata-request:${name}`, true)
}
}
notification = atom.notifications.addInfo(message, {
detail: error.privateMetadataDescription,
description:
'Are you willing to submit this information to a private server for debugging purposes?',
dismissable: true,
buttons: [
{
text: 'No',
onDidClick: reportWithoutPrivateMetadata
},
{
text: 'Yes, Submit for Debugging',
onDidClick: reportWithPrivateMetadata
}
]
})
dismissSubscription = notification.onDidDismiss(
reportWithoutPrivateMetadata
)
}
addPackageMetadata (error) {
let activePackages = atom.packages.getActivePackages()
const availablePackagePaths = atom.packages.getPackageDirPaths()
if (activePackages.length > 0) {
let userPackages = {}
let bundledPackages = {}
for (let pack of atom.packages.getActivePackages()) {
if (availablePackagePaths.includes(path.dirname(pack.path))) {
userPackages[pack.name] = pack.metadata.version
} else {
bundledPackages[pack.name] = pack.metadata.version
}
}
if (error.metadata == null) {
error.metadata = {}
}
error.metadata.bundledPackages = bundledPackages
error.metadata.userPackages = userPackages
}
}
addPreviousErrorsMetadata (error) {
if (!this.reportPreviousErrors) return
if (!error.metadata) error.metadata = {}
error.metadata.previousErrors = this.reportedErrors.map(
error => error.message
)
error.metadata.previousAssertionFailures = this.reportedAssertionFailures.map(
error => error.message
)
}
reportUncaughtException (error) {
if (!this.shouldReport(error)) return
this.addPackageMetadata(error)
this.addPreviousErrorsMetadata(error)
if (
error.privateMetadata != null &&
error.privateMetadataDescription != null
) {
this.requestPrivateMetadataConsent(
error,
'The Atom team would like to collect the following information to resolve this error:',
error => this.reportUncaughtException(error)
)
return
}
let params = this.getDefaultNotificationParams()
params.severity = 'error'
this.performRequest(this.buildNotificationJSON(error, params))
this.reportedErrors.push(error)
}
reportFailedAssertion (error) {
if (!this.shouldReport(error)) return
this.addPackageMetadata(error)
this.addPreviousErrorsMetadata(error)
if (
error.privateMetadata != null &&
error.privateMetadataDescription != null
) {
this.requestPrivateMetadataConsent(
error,
'The Atom team would like to collect some information to resolve an unexpected condition:',
error => this.reportFailedAssertion(error)
)
return
}
let params = this.getDefaultNotificationParams()
params.severity = 'warning'
this.performRequest(this.buildNotificationJSON(error, params))
this.reportedAssertionFailures.push(error)
}
// Used in specs
setRequestFunction (requestFunction) {
this.request = requestFunction
}
isBundledFile (fileName) {
return this.normalizePath(fileName).indexOf(this.resourcePath) === 0
}
isTeletypeFile (fileName) {
const teletypePath = atom.packages.resolvePackagePath('teletype')
return (
teletypePath && this.normalizePath(fileName).indexOf(teletypePath) === 0
)
}
}
Reporter.API_KEY = API_KEY
Reporter.LIB_VERSION = LIB_VERSION