@honeybadger-io/core
Version:
JavaScript error notifier for Honeybadger.io
448 lines • 20.8 kB
JavaScript
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = void 0;
var util_1 = require("./util");
var store_1 = require("./store");
var throttled_events_logger_1 = require("./throttled_events_logger");
var defaults_1 = require("./defaults");
// Split at commas and spaces
var TAG_SEPARATOR = /,|\s+/;
// Checks for non-blank characters
var NOT_BLANK = /\S/;
var Client = /** @class */ (function () {
function Client(opts, transport) {
if (opts === void 0) { opts = {}; }
this.__pluginsLoaded = false;
this.__store = null;
this.__beforeNotifyHandlers = [];
this.__afterNotifyHandlers = [];
this.__notifier = {
name: '@honeybadger-io/core',
url: 'https://github.com/honeybadger-io/honeybadger-js/tree/master/packages/core',
version: '__VERSION__'
};
this.config = __assign(__assign({}, defaults_1.CONFIG), opts);
this.__initStore();
this.__transport = transport;
this.__eventsLogger = new throttled_events_logger_1.ThrottledEventsLogger(this.config, this.__transport);
this.logger = (0, util_1.logger)(this);
}
Client.prototype.getVersion = function () {
return this.__notifier.version;
};
Client.prototype.getNotifier = function () {
return this.__notifier;
};
/**
* CAREFUL: When adding a new notifier or updating the name of an existing notifier,
* the Honeybadger rails project may need its mappings updated.
* See https://github.com/honeybadger-io/honeybadger/blob/master/app/presenters/breadcrumbs_presenter.rb
* https://github.com/honeybadger-io/honeybadger/blob/master/app/models/parser/java_script.rb
* https://github.com/honeybadger-io/honeybadger/blob/master/app/models/language.rb
**/
Client.prototype.setNotifier = function (notifier) {
this.__notifier = notifier;
};
Client.prototype.configure = function (opts) {
if (opts === void 0) { opts = {}; }
for (var k in opts) {
this.config[k] = opts[k];
}
this.__eventsLogger.configure(this.config);
this.loadPlugins();
return this;
};
Client.prototype.loadPlugins = function () {
var _this = this;
var pluginsToLoad = this.__pluginsLoaded
? this.config.__plugins.filter(function (plugin) { return plugin.shouldReloadOnConfigure; })
: this.config.__plugins;
pluginsToLoad.forEach(function (plugin) { return plugin.load(_this); });
this.__pluginsLoaded = true;
};
Client.prototype.__initStore = function () {
this.__store = new store_1.GlobalStore({ context: {}, breadcrumbs: [] }, this.config.maxBreadcrumbs);
};
Client.prototype.beforeNotify = function (handler) {
this.__beforeNotifyHandlers.push(handler);
return this;
};
Client.prototype.afterNotify = function (handler) {
this.__afterNotifyHandlers.push(handler);
return this;
};
Client.prototype.setContext = function (context) {
if (typeof context === 'object' && context != null) {
this.__store.setContext(context);
}
return this;
};
Client.prototype.resetContext = function (context) {
this.logger.warn('Deprecation warning: `Honeybadger.resetContext()` has been deprecated; please use `Honeybadger.clear()` instead.');
this.__store.clear();
if (typeof context === 'object' && context !== null) {
this.__store.setContext(context);
}
return this;
};
Client.prototype.clear = function () {
this.__store.clear();
return this;
};
Client.prototype.notify = function (noticeable, name, extra) {
var _this = this;
if (name === void 0) { name = undefined; }
if (extra === void 0) { extra = undefined; }
var notice = this.makeNotice(noticeable, name, extra);
// we need to have the source file data before the beforeNotifyHandlers,
// in case they modify them
var sourceCodeData = notice && notice.backtrace ? notice.backtrace.map(function (trace) { return (0, util_1.shallowClone)(trace); }) : null;
var preConditionsResult = this.__runPreconditions(notice);
if (preConditionsResult instanceof Error) {
(0, util_1.runAfterNotifyHandlers)(notice, this.__afterNotifyHandlers, preConditionsResult);
return false;
}
if (preConditionsResult instanceof Promise) {
preConditionsResult.then(function (result) {
if (result instanceof Error) {
(0, util_1.runAfterNotifyHandlers)(notice, _this.__afterNotifyHandlers, result);
return false;
}
return _this.__send(notice, sourceCodeData);
});
return true;
}
this.__send(notice, sourceCodeData).catch(function (_err) { });
return true;
};
/**
* An async version of {@link notify} that resolves only after the notice has been reported to Honeybadger.
* Implemented using the {@link afterNotify} hook.
* Rejects if for any reason the report failed to be reported.
* Useful in serverless environments (AWS Lambda).
*/
Client.prototype.notifyAsync = function (noticeable, name, extra) {
var _this = this;
if (name === void 0) { name = undefined; }
if (extra === void 0) { extra = undefined; }
return new Promise(function (resolve, reject) {
var applyAfterNotify = function (partialNotice) {
var originalAfterNotify = partialNotice.afterNotify;
partialNotice.afterNotify = function (err) {
originalAfterNotify === null || originalAfterNotify === void 0 ? void 0 : originalAfterNotify.call(_this, err);
if (err) {
return reject(err);
}
resolve();
};
};
// We have to respect any afterNotify hooks that come from the arguments
var objectToOverride;
if (noticeable.afterNotify) {
objectToOverride = noticeable;
}
else if (name && name.afterNotify) {
objectToOverride = name;
}
else if (extra && extra.afterNotify) {
objectToOverride = extra;
}
else if (name && typeof name === 'object') {
objectToOverride = name;
}
else if (extra) {
objectToOverride = extra;
}
else {
objectToOverride = name = {};
}
applyAfterNotify(objectToOverride);
_this.notify(noticeable, name, extra);
});
};
Client.prototype.makeNotice = function (noticeable, name, extra) {
if (name === void 0) { name = undefined; }
if (extra === void 0) { extra = undefined; }
var notice = (0, util_1.makeNotice)(noticeable);
if (name && !(typeof name === 'object')) {
var n = String(name);
name = { name: n };
}
if (name) {
notice = (0, util_1.mergeNotice)(notice, name);
}
if (typeof extra === 'object' && extra !== null) {
notice = (0, util_1.mergeNotice)(notice, extra);
}
if ((0, util_1.objectIsEmpty)(notice)) {
return null;
}
var context = this.__store.getContents('context');
var noticeTags = this.__constructTags(notice.tags);
var contextTags = this.__constructTags(context['tags']);
var configTags = this.__constructTags(this.config.tags);
// Turning into a Set will remove duplicates
var tags = noticeTags.concat(contextTags).concat(configTags);
var uniqueTags = tags.filter(function (item, index) { return tags.indexOf(item) === index; });
notice = (0, util_1.merge)(notice, {
name: notice.name || 'Error',
context: (0, util_1.merge)(context, notice.context),
projectRoot: notice.projectRoot || this.config.projectRoot,
environment: notice.environment || this.config.environment,
component: notice.component || this.config.component,
action: notice.action || this.config.action,
revision: notice.revision || this.config.revision,
tags: uniqueTags,
});
// If we're passed a custom backtrace array, use it
// Otherwise we make one.
if (!Array.isArray(notice.backtrace) || !notice.backtrace.length) {
if (typeof notice.stack !== 'string' || !notice.stack.trim()) {
notice.stack = (0, util_1.generateStackTrace)();
notice.backtrace = (0, util_1.makeBacktrace)(notice.stack, true, this.logger);
}
else {
notice.backtrace = (0, util_1.makeBacktrace)(notice.stack, false, this.logger);
}
}
return notice;
};
Client.prototype.addBreadcrumb = function (message, opts) {
if (!this.config.breadcrumbsEnabled) {
return;
}
opts = opts || {};
var metadata = (0, util_1.shallowClone)(opts.metadata);
var category = opts.category || 'custom';
var timestamp = new Date().toISOString();
this.__store.addBreadcrumb({
category: category,
message: message,
metadata: metadata,
timestamp: timestamp
});
return this;
};
/**
* @deprecated Use {@link event} instead.
*/
Client.prototype.logEvent = function (data) {
(0, util_1.logDeprecatedMethod)(this.logger, 'Honeybadger.logEvent', 'Honeybadger.event');
this.event('log', data);
};
Client.prototype.event = function (type, data) {
var _a;
if (typeof type === 'object') {
data = type;
type = (_a = type['event_type']) !== null && _a !== void 0 ? _a : undefined;
}
this.__eventsLogger.log(__assign({ event_type: type, ts: new Date().toISOString() }, data));
};
/**
* This method currently flushes the event (Insights) queue.
* In the future, it should also flush the error queue (assuming an error throttler is implemented).
*/
Client.prototype.flushAsync = function () {
return this.__eventsLogger.flushAsync();
};
Client.prototype.__getBreadcrumbs = function () {
return this.__store.getContents('breadcrumbs').slice();
};
Client.prototype.__getContext = function () {
return this.__store.getContents('context');
};
Client.prototype.__developmentMode = function () {
if (this.config.reportData === true) {
return false;
}
return (this.config.environment && this.config.developmentEnvironments.includes(this.config.environment));
};
Client.prototype.__buildPayload = function (notice) {
var headers = (0, util_1.filter)(notice.headers, this.config.filters) || {};
var cgiData = (0, util_1.filter)(__assign(__assign({}, notice.cgiData), (0, util_1.formatCGIData)(headers, 'HTTP_')), this.config.filters);
return {
notifier: this.__notifier,
breadcrumbs: {
enabled: !!this.config.breadcrumbsEnabled,
trail: notice.__breadcrumbs || []
},
error: {
class: notice.name,
message: notice.message,
backtrace: notice.backtrace,
fingerprint: notice.fingerprint,
tags: notice.tags,
causes: (0, util_1.getCauses)(notice, this.logger),
},
request: {
url: (0, util_1.filterUrl)(notice.url, this.config.filters),
component: notice.component,
action: notice.action,
context: notice.context,
cgi_data: cgiData,
params: (0, util_1.filter)(notice.params, this.config.filters) || {},
session: (0, util_1.filter)(notice.session, this.config.filters) || {}
},
server: {
project_root: notice.projectRoot,
environment_name: notice.environment,
revision: notice.revision,
hostname: this.config.hostname,
time: new Date().toUTCString()
},
details: notice.details || {}
};
};
Client.prototype.__constructTags = function (tags) {
if (!tags) {
return [];
}
return tags.toString().split(TAG_SEPARATOR).filter(function (tag) { return NOT_BLANK.test(tag); });
};
Client.prototype.__runPreconditions = function (notice) {
var _this = this;
var preConditionError = null;
if (!notice) {
this.logger.debug('failed to build error report');
preConditionError = new Error('failed to build error report');
}
if (this.config.reportData === false) {
this.logger.debug('skipping error report: honeybadger.js is disabled', notice);
preConditionError = new Error('honeybadger.js is disabled');
}
if (this.__developmentMode()) {
this.logger.log('honeybadger.js is in development mode; the following error report will be sent in production.', notice);
preConditionError = new Error('honeybadger.js is in development mode');
}
if (!this.config.apiKey) {
this.logger.warn('could not send error report: no API key has been configured', notice);
preConditionError = new Error('missing API key');
}
var beforeNotifyResult = (0, util_1.runBeforeNotifyHandlers)(notice, this.__beforeNotifyHandlers);
if (!preConditionError && !beforeNotifyResult.result) {
this.logger.debug('skipping error report: one or more beforeNotify handlers returned false', notice);
preConditionError = new Error('beforeNotify handlers returned false');
}
if (beforeNotifyResult.results.length && beforeNotifyResult.results.some(function (result) { return result instanceof Promise; })) {
return Promise.allSettled(beforeNotifyResult.results)
.then(function (results) {
if (!preConditionError && (results.some(function (result) { return result.status === 'rejected' || result.value === false; }))) {
_this.logger.debug('skipping error report: one or more beforeNotify handlers returned false', notice);
preConditionError = new Error('beforeNotify handlers (async) returned false');
}
if (preConditionError) {
return preConditionError;
}
});
}
return preConditionError;
};
Client.prototype.__send = function (notice, originalBacktrace) {
var _this = this;
if (this.config.breadcrumbsEnabled) {
this.addBreadcrumb('Honeybadger Notice', {
category: 'notice',
metadata: {
message: notice.message,
name: notice.name,
stack: notice.stack
}
});
notice.__breadcrumbs = this.__store.getContents('breadcrumbs');
}
else {
notice.__breadcrumbs = [];
}
return (0, util_1.getSourceForBacktrace)(originalBacktrace, this.__getSourceFileHandler)
.then(function (sourcePerTrace) { return __awaiter(_this, void 0, void 0, function () {
var payload;
return __generator(this, function (_a) {
sourcePerTrace.forEach(function (source, index) {
notice.backtrace[index].source = source;
});
payload = this.__buildPayload(notice);
return [2 /*return*/, this.__transport
.send({
headers: {
'X-API-Key': this.config.apiKey,
'Content-Type': 'application/json',
'Accept': 'text/json, application/json'
},
method: 'POST',
endpoint: (0, util_1.endpoint)(this.config.endpoint, '/v1/notices/js'),
maxObjectDepth: this.config.maxObjectDepth,
logger: this.logger,
}, payload)];
});
}); })
.then(function (res) {
if (res.statusCode !== 201) {
(0, util_1.runAfterNotifyHandlers)(notice, _this.__afterNotifyHandlers, new Error("Bad HTTP response: ".concat(res.statusCode)));
_this.logger.warn("Error report failed: unknown response from server. code=".concat(res.statusCode));
return false;
}
var uuid = JSON.parse(res.body).id;
(0, util_1.runAfterNotifyHandlers)((0, util_1.merge)(notice, {
id: uuid
}), _this.__afterNotifyHandlers);
var noticeUrl = (0, util_1.endpoint)(_this.config.appEndpoint, "notice/".concat(uuid));
_this.logger.info("Error report sent \u26A1 ".concat(noticeUrl));
return true;
})
.catch(function (err) {
_this.logger.error('Error report failed: an unknown error occurred.', "message=".concat(err.message));
(0, util_1.runAfterNotifyHandlers)(notice, _this.__afterNotifyHandlers, err);
return false;
});
};
return Client;
}());
exports.Client = Client;
//# sourceMappingURL=client.js.map