packages/exception-reporting/lib/reporter.js
/** @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;