Merge pull request #18140 from atom/migrate-exception-reporting-package

➡️ Migrate core package 'exception-reporting' into ./packages
This commit is contained in:
David Wilson
2018-09-28 09:48:48 -07:00
committed by GitHub
10 changed files with 856 additions and 25 deletions

3
package-lock.json generated
View File

@@ -2045,8 +2045,7 @@
}
},
"exception-reporting": {
"version": "https://www.atom.io/api/packages/exception-reporting/versions/0.43.1/tarball",
"integrity": "sha512-IYDPs9MNXcbKJv+G/WH6FqkbikiaP9VBskWatMjCj6ca7aey0bbK/qEQwaMogzEGwh9C+n7f+4fAarpgWNKpsw==",
"version": "file:packages/exception-reporting",
"requires": {
"fs-plus": "^3.0.0",
"node-uuid": "~1.4.7",

View File

@@ -55,7 +55,7 @@
"encoding-selector": "https://www.atom.io/api/packages/encoding-selector/versions/0.23.9/tarball",
"etch": "^0.12.6",
"event-kit": "^2.5.1",
"exception-reporting": "https://www.atom.io/api/packages/exception-reporting/versions/0.43.1/tarball",
"exception-reporting": "file:packages/exception-reporting",
"find-and-replace": "https://www.atom.io/api/packages/find-and-replace/versions/0.215.14/tarball",
"find-parent-dir": "^0.3.0",
"first-mate": "7.1.3",
@@ -199,7 +199,7 @@
"deprecation-cop": "0.56.9",
"dev-live-reload": "0.48.1",
"encoding-selector": "0.23.9",
"exception-reporting": "0.43.1",
"exception-reporting": "file:./packages/exception-reporting",
"find-and-replace": "0.215.14",
"fuzzy-finder": "1.8.2",
"github": "0.20.0",

View File

@@ -8,17 +8,17 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| Package | Where to find it | Migration issue |
|---------|------------------|-----------------|
| **about** | [`./packages/about`](./about) | [#17832](https://github.com/atom/atom/issues/17832) |
| **atom-dark-syntax** | [`./packages/atom-dark-syntax`][./atom-dark-syntax] | [#17849](https://github.com/atom/atom/issues/17849) |
| **atom-dark-ui** | [`./packages/atom-dark-ui`][./atom-dark-ui] | [#17850](https://github.com/atom/atom/issues/17850) |
| **atom-light-syntax** | [`./packages/atom-light-syntax`][./atom-light-syntax] | [#17851](https://github.com/atom/atom/issues/17851) |
| **atom-light-ui** | [`./packages/atom-light-ui`][./atom-light-ui] | [#17852](https://github.com/atom/atom/issues/17852) |
| **about** | [`./about`](./about) | [#17832](https://github.com/atom/atom/issues/17832) |
| **atom-dark-syntax** | [`./atom-dark-syntax`](./atom-dark-syntax) | [#17849](https://github.com/atom/atom/issues/17849) |
| **atom-dark-ui** | [`./atom-dark-ui`](./atom-dark-ui) | [#17850](https://github.com/atom/atom/issues/17850) |
| **atom-light-syntax** | [`./atom-light-syntax`](./atom-light-syntax) | [#17851](https://github.com/atom/atom/issues/17851) |
| **atom-light-ui** | [`./atom-light-ui`](./atom-light-ui) | [#17852](https://github.com/atom/atom/issues/17852) |
| **autocomplete-atom-api** | [`atom/autocomplete-atom-api`][autocomplete-atom-api] | |
| **autocomplete-css** | [`atom/autocomplete-css`][autocomplete-css] | |
| **autocomplete-html** | [`atom/autocomplete-html`][autocomplete-html] | |
| **autocomplete-plus** | [`atom/autocomplete-plus`][autocomplete-plus] | |
| **autocomplete-snippets** | [`atom/autocomplete-snippets`][autocomplete-snippets] | |
| **autoflow** | [`atom/autoflow`][./autoflow] | [#17833](https://github.com/atom/atom/issues/17833) |
| **autoflow** | [`atom/autoflow`](./autoflow) | [#17833](https://github.com/atom/atom/issues/17833) |
| **autosave** | [`atom/autosave`][autosave] | [#17834](https://github.com/atom/atom/issues/17834) |
| **background-tips** | [`atom/background-tips`][background-tips] | [#17835](https://github.com/atom/atom/issues/17835) |
| **base16-tomorrow-dark-theme** | [`atom/base16-tomorrow-dark-theme`][base16-tomorrow-dark-theme] | [#17836](https://github.com/atom/atom/issues/17836) |
@@ -30,15 +30,15 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| **deprecation-cop** | [`atom/deprecation-cop`][deprecation-cop] | [#17839](https://github.com/atom/atom/issues/17839) |
| **dev-live-reload** | [`atom/dev-live-reload`][dev-live-reload] | [#17840](https://github.com/atom/atom/issues/17840) |
| **encoding-selector** | [`atom/encoding-selector`][encoding-selector] | [#17841](https://github.com/atom/atom/issues/17841) |
| **exception-reporting** | [`atom/exception-reporting`][exception-reporting] | [#17842](https://github.com/atom/atom/issues/17842) |
| **exception-reporting** | [`./exception-reporting`](./exception-reporting) | [#17842](https://github.com/atom/atom/issues/17842) |
| **find-and-replace** | [`atom/find-and-replace`][find-and-replace] | |
| **fuzzy-finder** | [`atom/fuzzy-finder`][fuzzy-finder] | |
| **github** | [`atom/github`][github] | |
| **git-diff** | [`./packages/git-diff`](./git-diff) | [#17843](https://github.com/atom/atom/issues/17843) |
| **git-diff** | [`./git-diff`](./git-diff) | [#17843](https://github.com/atom/atom/issues/17843) |
| **go-to-line** | [`atom/go-to-line`][go-to-line] | [#17844](https://github.com/atom/atom/issues/17844) |
| **grammar-selector** | [`atom/grammar-selector`][grammar-selector] | [#17845](https://github.com/atom/atom/issues/17845) |
| **image-view** | [`atom/image-view`][image-view] | |
| **incompatible-packages** | [`./packages/incompatible-packages`][./incompatible-packages] | [#17846](https://github.com/atom/atom/issues/17846) |
| **incompatible-packages** | [`./incompatible-packages`](./incompatible-packages) | [#17846](https://github.com/atom/atom/issues/17846) |
| **keybinding-resolver** | [`atom/keybinding-resolver`][keybinding-resolver] | |
| **language-c** | [`atom/language-c`][language-c] | |
| **language-clojure** | [`atom/language-clojure`][language-clojure] | |
@@ -74,14 +74,14 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| **language-xml** | [`atom/language-xml`][language-xml] | |
| **language-yaml** | [`atom/language-yaml`][language-yaml] | |
| **line-ending-selector** | [`atom/line-ending-selector`][line-ending-selector] | [#17847](https://github.com/atom/atom/issues/17847) |
| **link** | [`./packages/link`][./link] | [#17848](https://github.com/atom/atom/issues/17848) |
| **link** | [`./link`](./link) | [#17848](https://github.com/atom/atom/issues/17848) |
| **markdown-preview** | [`atom/markdown-preview`][markdown-preview] | |
| **metrics** | [`atom/metrics`][metrics] | |
| **notifications** | [`atom/notifications`][notifications] | |
| **one-dark-syntax** | [`./packages/one-dark-syntax`](./one-dark-syntax) | [#17853](https://github.com/atom/atom/issues/17853) |
| **one-dark-ui** | [`./packages/one-dark-ui`](./one-dark-ui) | [#17854](https://github.com/atom/atom/issues/17854) |
| **one-light-syntax** | [`./packages/one-light-syntax`](./one-light-syntax) | [#17855](https://github.com/atom/atom/issues/17855) |
| **one-light-ui** | [`./packages/one-light-ui`](./one-light-ui) | [#17856](https://github.com/atom/atom/issues/17856) |
| **one-dark-syntax** | [`./one-dark-syntax`](./one-dark-syntax) | [#17853](https://github.com/atom/atom/issues/17853) |
| **one-dark-ui** | [`./one-dark-ui`](./one-dark-ui) | [#17854](https://github.com/atom/atom/issues/17854) |
| **one-light-syntax** | [`./one-light-syntax`](./one-light-syntax) | [#17855](https://github.com/atom/atom/issues/17855) |
| **one-light-ui** | [`./one-light-ui`](./one-light-ui) | [#17856](https://github.com/atom/atom/issues/17856) |
| **open-on-github** | [`atom/open-on-github`][open-on-github] | |
| **package-generator** | [`atom/package-generator`][package-generator] | |
| **settings-view** | [`atom/settings-view`][settings-view] | |
@@ -101,10 +101,6 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| **wrap-guide** | [`atom/wrap-guide`][wrap-guide] | |
[archive-view]: https://github.com/atom/archive-view
[atom-dark-syntax]: https://github.com/atom/atom-dark-syntax
[atom-dark-ui]: https://github.com/atom/atom-dark-ui
[atom-light-syntax]: https://github.com/atom/atom-light-syntax
[atom-light-ui]: https://github.com/atom/atom-light-ui
[autocomplete-atom-api]: https://github.com/atom/autocomplete-atom-api
[autocomplete-css]: https://github.com/atom/autocomplete-css
[autocomplete-html]: https://github.com/atom/autocomplete-html
@@ -121,14 +117,12 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
[deprecation-cop]: https://github.com/atom/deprecation-cop
[dev-live-reload]: https://github.com/atom/dev-live-reload
[encoding-selector]: https://github.com/atom/encoding-selector
[exception-reporting]: https://github.com/atom/exception-reporting
[find-and-replace]: https://github.com/atom/find-and-replace
[fuzzy-finder]: https://github.com/atom/fuzzy-finder
[github]: https://github.com/atom/github
[go-to-line]: https://github.com/atom/go-to-line
[grammar-selector]: https://github.com/atom/grammar-selector
[image-view]: https://github.com/atom/image-view
[incompatible-packages]: https://github.com/atom/incompatible-packages
[keybinding-resolver]: https://github.com/atom/keybinding-resolver
[language-c]: https://github.com/atom/language-c
[language-clojure]: https://github.com/atom/language-clojure
@@ -164,7 +158,6 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
[language-xml]: https://github.com/atom/language-xml
[language-yaml]: https://github.com/atom/language-yaml
[line-ending-selector]: https://github.com/atom/line-ending-selector
[link]: https://github.com/atom/link
[markdown-preview]: https://github.com/atom/markdown-preview
[metrics]: https://github.com/atom/metrics
[notifications]: https://github.com/atom/notifications

View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,20 @@
Copyright (c) 2014 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,3 @@
## Exception Reporting package
Reports uncaught exceptions in Atom to [bugsnag](https://bugsnag.com).

View File

@@ -0,0 +1,49 @@
/** @babel */
import {CompositeDisposable} from 'atom'
let reporter
function getReporter () {
if (!reporter) {
const Reporter = require('./reporter')
reporter = new Reporter()
}
return reporter
}
export default {
activate() {
this.subscriptions = new CompositeDisposable()
if (!atom.config.get('exception-reporting.userId')) {
atom.config.set('exception-reporting.userId', require('node-uuid').v4())
}
this.subscriptions.add(atom.onDidThrowError(({message, url, line, column, originalError}) => {
try {
getReporter().reportUncaughtException(originalError)
} catch (secondaryException) {
try {
console.error("Error reporting uncaught exception", secondaryException)
getReporter().reportUncaughtException(secondaryException)
} catch (error) { }
}
})
)
if (atom.onDidFailAssertion != null) {
this.subscriptions.add(atom.onDidFailAssertion(error => {
try {
getReporter().reportFailedAssertion(error)
} catch (secondaryException) {
try {
console.error("Error reporting assertion failure", secondaryException)
getReporter().reportUncaughtException(secondaryException)
} catch (error) {}
}
})
)
}
}
}

View File

@@ -0,0 +1,267 @@
/** @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

View File

@@ -0,0 +1,20 @@
{
"name": "exception-reporting",
"main": "./lib/main",
"version": "0.43.1",
"description": "Reports uncaught Atom exceptions to the Atom team via bugsnag.com",
"repository": "https://github.com/atom/atom",
"license": "MIT",
"engines": {
"atom": ">0.48.0"
},
"dependencies": {
"fs-plus": "^3.0.0",
"node-uuid": "~1.4.7",
"stack-trace": "0.0.9",
"underscore-plus": "1.x"
},
"devDependencies": {
"semver": "^5.3.0"
}
}

View File

@@ -0,0 +1,479 @@
const Reporter = require('../lib/reporter')
const semver = require('semver')
const os = require('os')
const path = require('path')
const fs = require('fs-plus')
let osVersion = `${os.platform()}-${os.arch()}-${os.release()}`
let getReleaseChannel = version => {
return (version.indexOf('beta') > -1)
? 'beta'
: (version.indexOf('dev') > -1)
? 'dev'
: 'stable'
}
describe("Reporter", () => {
let reporter, requests, initialStackTraceLimit, initialFsGetHomeDirectory, mockActivePackages
beforeEach(() => {
reporter = new Reporter({
request: (url, options) => requests.push(Object.assign({url}, options)),
alwaysReport: true,
reportPreviousErrors: false
})
requests = []
mockActivePackages = []
spyOn(atom.packages, 'getActivePackages').andCallFake(() => mockActivePackages)
initialStackTraceLimit = Error.stackTraceLimit
Error.stackTraceLimit = 1
initialFsGetHomeDirectory = fs.getHomeDirectory
})
afterEach(() => {
fs.getHomeDirectory = initialFsGetHomeDirectory
Error.stackTraceLimit = initialStackTraceLimit
})
describe(".reportUncaughtException(error)", () => {
it("posts errors originated inside Atom Core to BugSnag", () => {
const repositoryRootPath = path.join(__dirname, '..')
reporter = new Reporter({
request: (url, options) => requests.push(Object.assign({url}, options)),
alwaysReport: true,
reportPreviousErrors: false,
resourcePath: repositoryRootPath
})
let error = new Error()
Error.captureStackTrace(error)
reporter.reportUncaughtException(error)
let [lineNumber, columnNumber] = error.stack.match(/.js:(\d+):(\d+)/).slice(1).map(s => parseInt(s))
expect(requests.length).toBe(1)
let [request] = requests
expect(request.method).toBe("POST")
expect(request.url).toBe("https://notify.bugsnag.com")
expect(request.headers.get("Content-Type")).toBe("application/json")
let body = JSON.parse(request.body)
// Delete `inProject` field because tests may fail when run as part of Atom core
// (i.e. when this test file will be located under `node_modules/exception-reporting/spec`)
delete body.events[0].exceptions[0].stacktrace[0].inProject
expect(body).toEqual({
"apiKey": Reporter.API_KEY,
"notifier": {
"name": "Atom",
"version": Reporter.LIB_VERSION,
"url": "https://www.atom.io"
},
"events": [
{
"payloadVersion": "2",
"exceptions": [
{
"errorClass": "Error",
"message": "",
"stacktrace": [
{
"method": semver.gt(process.versions.electron, '1.6.0') ? 'Spec.it' : 'it',
"file": "spec/reporter-spec.js",
"lineNumber": lineNumber,
"columnNumber": columnNumber
}
]
}
],
"severity": "error",
"user": {},
"app": {
"version": atom.getVersion(),
"releaseStage": getReleaseChannel(atom.getVersion())
},
"device": {
"osVersion": osVersion
}
}
]
});})
it("posts errors originated outside Atom Core to BugSnag", () => {
fs.getHomeDirectory = () => path.join(__dirname, '..', '..')
let error = new Error()
Error.captureStackTrace(error)
reporter.reportUncaughtException(error)
let [lineNumber, columnNumber] = error.stack.match(/.js:(\d+):(\d+)/).slice(1).map(s => parseInt(s))
expect(requests.length).toBe(1)
let [request] = requests
expect(request.method).toBe("POST")
expect(request.url).toBe("https://notify.bugsnag.com")
expect(request.headers.get("Content-Type")).toBe("application/json")
let body = JSON.parse(request.body)
// Delete `inProject` field because tests may fail when run as part of Atom core
// (i.e. when this test file will be located under `node_modules/exception-reporting/spec`)
delete body.events[0].exceptions[0].stacktrace[0].inProject
expect(body).toEqual({
"apiKey": Reporter.API_KEY,
"notifier": {
"name": "Atom",
"version": Reporter.LIB_VERSION,
"url": "https://www.atom.io"
},
"events": [
{
"payloadVersion": "2",
"exceptions": [
{
"errorClass": "Error",
"message": "",
"stacktrace": [
{
"method": semver.gt(process.versions.electron, '1.6.0') ? 'Spec.it' : 'it',
"file": '~/exception-reporting/spec/reporter-spec.js',
"lineNumber": lineNumber,
"columnNumber": columnNumber
}
]
}
],
"severity": "error",
"user": {},
"app": {
"version": atom.getVersion(),
"releaseStage": getReleaseChannel(atom.getVersion())
},
"device": {
"osVersion": osVersion
}
}
]
});})
describe("when the error object has `privateMetadata` and `privateMetadataDescription` fields", () => {
let [error, notification] = []
beforeEach(() => {
atom.notifications.clear()
spyOn(atom.notifications, 'addInfo').andCallThrough()
error = new Error()
Error.captureStackTrace(error)
error.metadata = {foo: "bar"}
error.privateMetadata = {baz: "quux"}
error.privateMetadataDescription = "The contents of baz"
})
it("posts a notification asking for consent", () => {
reporter.reportUncaughtException(error)
expect(atom.notifications.addInfo).toHaveBeenCalled()
})
it("submits the error with the private metadata if the user consents", () => {
spyOn(reporter, 'reportUncaughtException').andCallThrough()
reporter.reportUncaughtException(error)
reporter.reportUncaughtException.reset()
notification = atom.notifications.getNotifications()[0]
let notificationOptions = atom.notifications.addInfo.argsForCall[0][1]
expect(notificationOptions.buttons[1].text).toMatch(/Yes/)
notificationOptions.buttons[1].onDidClick()
expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error)
expect(reporter.reportUncaughtException.callCount).toBe(1)
expect(error.privateMetadata).toBeUndefined()
expect(error.privateMetadataDescription).toBeUndefined()
expect(error.metadata).toEqual({foo: "bar", baz: "quux"})
expect(notification.isDismissed()).toBe(true)
})
it("submits the error without the private metadata if the user does not consent", () => {
spyOn(reporter, 'reportUncaughtException').andCallThrough()
reporter.reportUncaughtException(error)
reporter.reportUncaughtException.reset()
notification = atom.notifications.getNotifications()[0]
let notificationOptions = atom.notifications.addInfo.argsForCall[0][1]
expect(notificationOptions.buttons[0].text).toMatch(/No/)
notificationOptions.buttons[0].onDidClick()
expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error)
expect(reporter.reportUncaughtException.callCount).toBe(1)
expect(error.privateMetadata).toBeUndefined()
expect(error.privateMetadataDescription).toBeUndefined()
expect(error.metadata).toEqual({foo: "bar"})
expect(notification.isDismissed()).toBe(true)
})
it("submits the error without the private metadata if the user dismisses the notification", () => {
spyOn(reporter, 'reportUncaughtException').andCallThrough()
reporter.reportUncaughtException(error)
reporter.reportUncaughtException.reset()
notification = atom.notifications.getNotifications()[0]
notification.dismiss()
expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error)
expect(reporter.reportUncaughtException.callCount).toBe(1)
expect(error.privateMetadata).toBeUndefined()
expect(error.privateMetadataDescription).toBeUndefined()
expect(error.metadata).toEqual({foo: "bar"});});})
it('treats packages located in atom.packages.getPackageDirPaths as user packages', () => {
mockActivePackages = [
{name: 'user-1', path: '/Users/user/.atom/packages/user-1', metadata: {version: '1.0.0'}},
{name: 'user-2', path: '/Users/user/.atom/packages/user-2', metadata: {version: '1.2.0'}},
{name: 'bundled-1', path: '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-1', metadata: {version: '1.0.0'}},
{name: 'bundled-2', path: '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-2', metadata: {version: '1.2.0'}},
]
const packageDirPaths = ['/Users/user/.atom/packages']
spyOn(atom.packages, 'getPackageDirPaths').andReturn(packageDirPaths)
let error = new Error()
Error.captureStackTrace(error)
reporter.reportUncaughtException(error)
expect(error.metadata.userPackages).toEqual({
'user-1': '1.0.0',
'user-2': '1.2.0'
})
expect(error.metadata.bundledPackages).toEqual({
'bundled-1': '1.0.0',
'bundled-2': '1.2.0'
})
})
it('adds previous error messages and assertion failures to the reported metadata', () => {
reporter.reportPreviousErrors = true
reporter.reportUncaughtException(new Error('A'))
reporter.reportUncaughtException(new Error('B'))
reporter.reportFailedAssertion(new Error('X'))
reporter.reportFailedAssertion(new Error('Y'))
reporter.reportUncaughtException(new Error('C'))
expect(requests.length).toBe(5)
const lastRequest = requests[requests.length - 1]
const body = JSON.parse(lastRequest.body)
console.log(body);
expect(body.events[0].metaData.previousErrors).toEqual(['A', 'B'])
expect(body.events[0].metaData.previousAssertionFailures).toEqual(['X', 'Y'])
})
})
describe(".reportFailedAssertion(error)", () => {
it("posts warnings to bugsnag", () => {
fs.getHomeDirectory = () => path.join(__dirname, '..', '..')
let error = new Error()
Error.captureStackTrace(error)
reporter.reportFailedAssertion(error)
let [lineNumber, columnNumber] = error.stack.match(/.js:(\d+):(\d+)/).slice(1).map(s => parseInt(s))
expect(requests.length).toBe(1)
let [request] = requests
expect(request.method).toBe("POST")
expect(request.url).toBe("https://notify.bugsnag.com")
expect(request.headers.get("Content-Type")).toBe("application/json")
let body = JSON.parse(request.body)
// Delete `inProject` field because tests may fail when run as part of Atom core
// (i.e. when this test file will be located under `node_modules/exception-reporting/spec`)
delete body.events[0].exceptions[0].stacktrace[0].inProject
expect(body).toEqual({
"apiKey": Reporter.API_KEY,
"notifier": {
"name": "Atom",
"version": Reporter.LIB_VERSION,
"url": "https://www.atom.io"
},
"events": [
{
"payloadVersion": "2",
"exceptions": [
{
"errorClass": "Error",
"message": "",
"stacktrace": [
{
"method": semver.gt(process.versions.electron, '1.6.0') ? 'Spec.it' : 'it',
"file": '~/exception-reporting/spec/reporter-spec.js',
"lineNumber": lineNumber,
"columnNumber": columnNumber
}
]
}
],
"severity": "warning",
"user": {},
"app": {
"version": atom.getVersion(),
"releaseStage": getReleaseChannel(atom.getVersion())
},
"device": {
"osVersion": osVersion
}
}
]
});})
describe("when the error object has `privateMetadata` and `privateMetadataDescription` fields", () => {
let [error, notification] = []
beforeEach(() => {
atom.notifications.clear()
spyOn(atom.notifications, 'addInfo').andCallThrough()
error = new Error()
Error.captureStackTrace(error)
error.metadata = {foo: "bar"}
error.privateMetadata = {baz: "quux"}
error.privateMetadataDescription = "The contents of baz"
})
it("posts a notification asking for consent", () => {
reporter.reportFailedAssertion(error)
expect(atom.notifications.addInfo).toHaveBeenCalled()
})
it("submits the error with the private metadata if the user consents", () => {
spyOn(reporter, 'reportFailedAssertion').andCallThrough()
reporter.reportFailedAssertion(error)
reporter.reportFailedAssertion.reset()
notification = atom.notifications.getNotifications()[0]
let notificationOptions = atom.notifications.addInfo.argsForCall[0][1]
expect(notificationOptions.buttons[1].text).toMatch(/Yes/)
notificationOptions.buttons[1].onDidClick()
expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error)
expect(reporter.reportFailedAssertion.callCount).toBe(1)
expect(error.privateMetadata).toBeUndefined()
expect(error.privateMetadataDescription).toBeUndefined()
expect(error.metadata).toEqual({foo: "bar", baz: "quux"})
expect(notification.isDismissed()).toBe(true)
})
it("submits the error without the private metadata if the user does not consent", () => {
spyOn(reporter, 'reportFailedAssertion').andCallThrough()
reporter.reportFailedAssertion(error)
reporter.reportFailedAssertion.reset()
notification = atom.notifications.getNotifications()[0]
let notificationOptions = atom.notifications.addInfo.argsForCall[0][1]
expect(notificationOptions.buttons[0].text).toMatch(/No/)
notificationOptions.buttons[0].onDidClick()
expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error)
expect(reporter.reportFailedAssertion.callCount).toBe(1)
expect(error.privateMetadata).toBeUndefined()
expect(error.privateMetadataDescription).toBeUndefined()
expect(error.metadata).toEqual({foo: "bar"})
expect(notification.isDismissed()).toBe(true)
})
it("submits the error without the private metadata if the user dismisses the notification", () => {
spyOn(reporter, 'reportFailedAssertion').andCallThrough()
reporter.reportFailedAssertion(error)
reporter.reportFailedAssertion.reset()
notification = atom.notifications.getNotifications()[0]
notification.dismiss()
expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error)
expect(reporter.reportFailedAssertion.callCount).toBe(1)
expect(error.privateMetadata).toBeUndefined()
expect(error.privateMetadataDescription).toBeUndefined()
expect(error.metadata).toEqual({foo: "bar"})
})
it("only notifies the user once for a given 'privateMetadataRequestName'", () => {
let fakeStorage = {}
spyOn(global.localStorage, 'setItem').andCallFake((key, value) => fakeStorage[key] = value)
spyOn(global.localStorage, 'getItem').andCallFake(key => fakeStorage[key])
error.privateMetadataRequestName = 'foo'
reporter.reportFailedAssertion(error)
expect(atom.notifications.addInfo).toHaveBeenCalled()
atom.notifications.addInfo.reset()
reporter.reportFailedAssertion(error)
expect(atom.notifications.addInfo).not.toHaveBeenCalled()
let error2 = new Error()
Error.captureStackTrace(error2)
error2.privateMetadataDescription = 'Something about you'
error2.privateMetadata = {baz: 'quux'}
error2.privateMetadataRequestName = 'bar'
reporter.reportFailedAssertion(error2)
expect(atom.notifications.addInfo).toHaveBeenCalled()
})
})
it('treats packages located in atom.packages.getPackageDirPaths as user packages', () => {
mockActivePackages = [
{name: 'user-1', path: '/Users/user/.atom/packages/user-1', metadata: {version: '1.0.0'}},
{name: 'user-2', path: '/Users/user/.atom/packages/user-2', metadata: {version: '1.2.0'}},
{name: 'bundled-1', path: '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-1', metadata: {version: '1.0.0'}},
{name: 'bundled-2', path: '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-2', metadata: {version: '1.2.0'}},
]
const packageDirPaths = ['/Users/user/.atom/packages']
spyOn(atom.packages, 'getPackageDirPaths').andReturn(packageDirPaths)
let error = new Error()
Error.captureStackTrace(error)
reporter.reportFailedAssertion(error)
expect(error.metadata.userPackages).toEqual({
'user-1': '1.0.0',
'user-2': '1.2.0'
})
expect(error.metadata.bundledPackages).toEqual({
'bundled-1': '1.0.0',
'bundled-2': '1.2.0'
})
})
it('adds previous error messages and assertion failures to the reported metadata', () => {
reporter.reportPreviousErrors = true
reporter.reportUncaughtException(new Error('A'))
reporter.reportUncaughtException(new Error('B'))
reporter.reportFailedAssertion(new Error('X'))
reporter.reportFailedAssertion(new Error('Y'))
reporter.reportFailedAssertion(new Error('C'))
expect(requests.length).toBe(5)
const lastRequest = requests[requests.length - 1]
const body = JSON.parse(lastRequest.body)
expect(body.events[0].metaData.previousErrors).toEqual(['A', 'B'])
expect(body.events[0].metaData.previousAssertionFailures).toEqual(['X', 'Y'])
})
})
})