Files
atom/packages/exception-reporting/lib/reporter.js
2018-09-28 07:46:03 -07:00

268 lines
8.2 KiB
JavaScript

/** @babel */
import _ from 'underscore-plus'
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