rollbar
Version:
Effortlessly track and debug errors in your JavaScript applications with Rollbar. This package includes advanced error tracking features and an intuitive interface to help you identify and fix issues more quickly.
851 lines (777 loc) • 20.2 kB
JavaScript
var merge = require('./merge');
var RollbarJSON = {};
function setupJSON(polyfillJSON) {
if (isFunction(RollbarJSON.stringify) && isFunction(RollbarJSON.parse)) {
return;
}
if (isDefined(JSON)) {
// If polyfill is provided, prefer it over existing non-native shims.
if (polyfillJSON) {
if (isNativeFunction(JSON.stringify)) {
RollbarJSON.stringify = JSON.stringify;
}
if (isNativeFunction(JSON.parse)) {
RollbarJSON.parse = JSON.parse;
}
} else {
// else accept any interface that is present.
if (isFunction(JSON.stringify)) {
RollbarJSON.stringify = JSON.stringify;
}
if (isFunction(JSON.parse)) {
RollbarJSON.parse = JSON.parse;
}
}
}
if (!isFunction(RollbarJSON.stringify) || !isFunction(RollbarJSON.parse)) {
polyfillJSON && polyfillJSON(RollbarJSON);
}
}
/*
* isType - Given a Javascript value and a string, returns true if the type of the value matches the
* given string.
*
* @param x - any value
* @param t - a lowercase string containing one of the following type names:
* - undefined
* - null
* - error
* - number
* - boolean
* - string
* - symbol
* - function
* - object
* - array
* @returns true if x is of type t, otherwise false
*/
function isType(x, t) {
return t === typeName(x);
}
/*
* typeName - Given a Javascript value, returns the type of the object as a string
*/
function typeName(x) {
var name = typeof x;
if (name !== 'object') {
return name;
}
if (!x) {
return 'null';
}
if (x instanceof Error) {
return 'error';
}
return {}.toString
.call(x)
.match(/\s([a-zA-Z]+)/)[1]
.toLowerCase();
}
/* isFunction - a convenience function for checking if a value is a function
*
* @param f - any value
* @returns true if f is a function, otherwise false
*/
function isFunction(f) {
return isType(f, 'function');
}
/* isNativeFunction - a convenience function for checking if a value is a native JS function
*
* @param f - any value
* @returns true if f is a native JS function, otherwise false
*/
function isNativeFunction(f) {
var reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
var funcMatchString = Function.prototype.toString
.call(Object.prototype.hasOwnProperty)
.replace(reRegExpChar, '\\$&')
.replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?');
var reIsNative = RegExp('^' + funcMatchString + '$');
return isObject(f) && reIsNative.test(f);
}
/* isObject - Checks if the argument is an object
*
* @param value - any value
* @returns true is value is an object function is an object)
*/
function isObject(value) {
var type = typeof value;
return value != null && (type == 'object' || type == 'function');
}
/* isString - Checks if the argument is a string
*
* @param value - any value
* @returns true if value is a string
*/
function isString(value) {
return typeof value === 'string' || value instanceof String;
}
/**
* isFiniteNumber - determines whether the passed value is a finite number
*
* @param {*} n - any value
* @returns true if value is a finite number
*/
function isFiniteNumber(n) {
return Number.isFinite(n);
}
/*
* isDefined - a convenience function for checking if a value is not equal to undefined
*
* @param u - any value
* @returns true if u is anything other than undefined
*/
function isDefined(u) {
return !isType(u, 'undefined');
}
/*
* isIterable - convenience function for checking if a value can be iterated, essentially
* whether it is an object or an array.
*
* @param i - any value
* @returns true if i is an object or an array as determined by `typeName`
*/
function isIterable(i) {
var type = typeName(i);
return type === 'object' || type === 'array';
}
/*
* isError - convenience function for checking if a value is of an error type
*
* @param e - any value
* @returns true if e is an error
*/
function isError(e) {
// Detect both Error and Firefox Exception type
return isType(e, 'error') || isType(e, 'exception');
}
/* isPromise - a convenience function for checking if a value is a promise
*
* @param p - any value
* @returns true if f is a function, otherwise false
*/
function isPromise(p) {
return isObject(p) && isType(p.then, 'function');
}
/**
* isBrowser - a convenience function for checking if the code is running in a browser
*
* @returns true if the code is running in a browser environment
*/
function isBrowser() {
return typeof window !== 'undefined';
}
function redact() {
return '********';
}
// from http://stackoverflow.com/a/8809472/1138191
function uuid4() {
var d = now();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
/[xy]/g,
function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x7) | 0x8).toString(16);
},
);
return uuid;
}
var LEVELS = {
debug: 0,
info: 1,
warning: 2,
error: 3,
critical: 4,
};
function sanitizeUrl(url) {
var baseUrlParts = parseUri(url);
if (!baseUrlParts) {
return '(unknown)';
}
// remove a trailing # if there is no anchor
if (baseUrlParts.anchor === '') {
baseUrlParts.source = baseUrlParts.source.replace('#', '');
}
url = baseUrlParts.source.replace('?' + baseUrlParts.query, '');
return url;
}
var parseUriOptions = {
strictMode: false,
key: [
'source',
'protocol',
'authority',
'userInfo',
'user',
'password',
'host',
'port',
'relative',
'path',
'directory',
'file',
'query',
'anchor',
],
q: {
name: 'queryKey',
parser: /(?:^|&)([^&=]*)=?([^&]*)/g,
},
parser: {
strict:
/^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
loose:
/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,
},
};
function parseUri(str) {
if (!isType(str, 'string')) {
return undefined;
}
var o = parseUriOptions;
var m = o.parser[o.strictMode ? 'strict' : 'loose'].exec(str);
var uri = {};
for (var i = 0, l = o.key.length; i < l; ++i) {
uri[o.key[i]] = m[i] || '';
}
uri[o.q.name] = {};
uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
if ($1) {
uri[o.q.name][$1] = $2;
}
});
return uri;
}
function addParamsAndAccessTokenToPath(accessToken, options, params) {
params = params || {};
params.access_token = accessToken;
var paramsArray = [];
var k;
for (k in params) {
if (Object.prototype.hasOwnProperty.call(params, k)) {
paramsArray.push([k, params[k]].join('='));
}
}
var query = '?' + paramsArray.sort().join('&');
options = options || {};
options.path = options.path || '';
var qs = options.path.indexOf('?');
var h = options.path.indexOf('#');
var p;
if (qs !== -1 && (h === -1 || h > qs)) {
p = options.path;
options.path = p.substring(0, qs) + query + '&' + p.substring(qs + 1);
} else {
if (h !== -1) {
p = options.path;
options.path = p.substring(0, h) + query + p.substring(h);
} else {
options.path = options.path + query;
}
}
}
function formatUrl(u, protocol) {
protocol = protocol || u.protocol;
if (!protocol && u.port) {
if (u.port === 80) {
protocol = 'http:';
} else if (u.port === 443) {
protocol = 'https:';
}
}
protocol = protocol || 'https:';
if (!u.hostname) {
return null;
}
var result = protocol + '//' + u.hostname;
if (u.port) {
result = result + ':' + u.port;
}
if (u.path) {
result = result + u.path;
}
return result;
}
function stringify(obj, backup) {
var value, error;
try {
value = RollbarJSON.stringify(obj);
} catch (jsonError) {
if (backup && isFunction(backup)) {
try {
value = backup(obj);
} catch (backupError) {
error = backupError;
}
} else {
error = jsonError;
}
}
return { error: error, value: value };
}
function maxByteSize(string) {
// The transport will use utf-8, so assume utf-8 encoding.
//
// This minimal implementation will accurately count bytes for all UCS-2 and
// single code point UTF-16. If presented with multi code point UTF-16,
// which should be rare, it will safely overcount, not undercount.
//
// While robust utf-8 encoders exist, this is far smaller and far more performant.
// For quickly counting payload size for truncation, smaller is better.
var count = 0;
var length = string.length;
for (var i = 0; i < length; i++) {
var code = string.charCodeAt(i);
if (code < 128) {
// up to 7 bits
count = count + 1;
} else if (code < 2048) {
// up to 11 bits
count = count + 2;
} else if (code < 65536) {
// up to 16 bits
count = count + 3;
}
}
return count;
}
function jsonParse(s) {
var value, error;
try {
value = RollbarJSON.parse(s);
} catch (e) {
error = e;
}
return { error: error, value: value };
}
function makeUnhandledStackInfo(
message,
url,
lineno,
colno,
error,
mode,
backupMessage,
errorParser,
) {
var location = {
url: url || '',
line: lineno,
column: colno,
};
location.func = errorParser.guessFunctionName(location.url, location.line);
location.context = errorParser.gatherContext(location.url, location.line);
var href =
typeof document !== 'undefined' &&
document &&
document.location &&
document.location.href;
var useragent =
typeof window !== 'undefined' &&
window &&
window.navigator &&
window.navigator.userAgent;
return {
mode: mode,
message: error ? String(error) : message || backupMessage,
url: href,
stack: [location],
useragent: useragent,
};
}
function wrapCallback(logger, f) {
return function (err, resp) {
try {
f(err, resp);
} catch (e) {
logger.error(e);
}
};
}
function nonCircularClone(obj) {
var seen = [obj];
function clone(obj, seen) {
var value,
name,
newSeen,
result = {};
try {
for (name in obj) {
value = obj[name];
if (value && (isType(value, 'object') || isType(value, 'array'))) {
if (seen.includes(value)) {
result[name] = 'Removed circular reference: ' + typeName(value);
} else {
newSeen = seen.slice();
newSeen.push(value);
result[name] = clone(value, newSeen);
}
continue;
}
result[name] = value;
}
} catch (e) {
result = 'Failed cloning custom data: ' + e.message;
}
return result;
}
return clone(obj, seen);
}
function createItem(args, logger, notifier, requestKeys, lambdaContext) {
var message, err, custom, callback, request;
var arg;
var extraArgs = [];
var diagnostic = {};
var argTypes = [];
for (var i = 0, l = args.length; i < l; ++i) {
arg = args[i];
var typ = typeName(arg);
argTypes.push(typ);
switch (typ) {
case 'undefined':
break;
case 'string':
message ? extraArgs.push(arg) : (message = arg);
break;
case 'function':
callback = wrapCallback(logger, arg);
break;
case 'date':
extraArgs.push(arg);
break;
case 'error':
case 'domexception':
case 'exception': // Firefox Exception type
err ? extraArgs.push(arg) : (err = arg);
break;
case 'object':
case 'array':
if (
arg instanceof Error ||
(typeof DOMException !== 'undefined' && arg instanceof DOMException)
) {
err ? extraArgs.push(arg) : (err = arg);
break;
}
if (requestKeys && typ === 'object' && !request) {
for (var j = 0, len = requestKeys.length; j < len; ++j) {
if (arg[requestKeys[j]] !== undefined) {
request = arg;
break;
}
}
if (request) {
break;
}
}
custom ? extraArgs.push(arg) : (custom = arg);
break;
default:
if (
arg instanceof Error ||
(typeof DOMException !== 'undefined' && arg instanceof DOMException)
) {
err ? extraArgs.push(arg) : (err = arg);
break;
}
extraArgs.push(arg);
}
}
// if custom is an array this turns it into an object with integer keys
if (custom) custom = nonCircularClone(custom);
if (extraArgs.length > 0) {
if (!custom) custom = nonCircularClone({});
custom.extraArgs = nonCircularClone(extraArgs);
}
var item = {
message: message,
err: err,
custom: custom,
timestamp: now(),
callback: callback,
notifier: notifier,
diagnostic: diagnostic,
uuid: uuid4(),
};
item.data = item.data || {};
setCustomItemKeys(item, custom);
if (requestKeys && request) {
item.request = request;
}
if (lambdaContext) {
item.lambdaContext = lambdaContext;
}
item._originalArgs = args;
item.diagnostic.original_arg_types = argTypes;
return item;
}
function setCustomItemKeys(item, custom) {
if (custom && custom.level !== undefined) {
item.level = custom.level;
delete custom.level;
}
if (custom && custom.skipFrames !== undefined) {
item.skipFrames = custom.skipFrames;
delete custom.skipFrames;
}
}
function addErrorContext(item, errors) {
var custom = item.data.custom || {};
var contextAdded = false;
try {
for (var i = 0; i < errors.length; ++i) {
if (errors[i].hasOwnProperty('rollbarContext')) {
custom = merge(custom, nonCircularClone(errors[i].rollbarContext));
contextAdded = true;
}
}
// Avoid adding an empty object to the data.
if (contextAdded) {
item.data.custom = custom;
}
} catch (e) {
item.diagnostic.error_context = 'Failed: ' + e.message;
}
}
var TELEMETRY_TYPES = [
'log',
'network',
'dom',
'navigation',
'error',
'manual',
];
var TELEMETRY_LEVELS = ['critical', 'error', 'warning', 'info', 'debug'];
function arrayIncludes(arr, val) {
for (var k = 0; k < arr.length; ++k) {
if (arr[k] === val) {
return true;
}
}
return false;
}
function createTelemetryEvent(args) {
var type, metadata, level;
var arg;
for (var i = 0, l = args.length; i < l; ++i) {
arg = args[i];
var typ = typeName(arg);
switch (typ) {
case 'string':
if (!type && arrayIncludes(TELEMETRY_TYPES, arg)) {
type = arg;
} else if (!level && arrayIncludes(TELEMETRY_LEVELS, arg)) {
level = arg;
}
break;
case 'object':
metadata = arg;
break;
default:
break;
}
}
var event = {
type: type || 'manual',
metadata: metadata || {},
level: level,
};
return event;
}
function addItemAttributes(itemData, attributes) {
itemData.attributes = itemData.attributes || [];
for (const a of attributes) {
if (a.value === undefined) {
continue;
}
itemData.attributes.push(a);
}
}
function getItemAttribute(itemData, key) {
const attributes = itemData.attributes || [];
for (let i = 0; i < attributes.length; ++i) {
if (attributes[i].key === key) {
return attributes[i].value;
}
}
return undefined;
}
/*
* get - given an obj/array and a keypath, return the value at that keypath or
* undefined if not possible.
*
* @param obj - an object or array
* @param path - a string of keys separated by '.' such as 'plugin.jquery.0.message'
* which would correspond to 42 in `{plugin: {jquery: [{message: 42}]}}`
*/
function get(obj, path) {
if (!obj) {
return undefined;
}
var keys = path.split('.');
var result = obj;
try {
for (var i = 0, len = keys.length; i < len; ++i) {
result = result[keys[i]];
}
} catch (e) {
result = undefined;
}
return result;
}
function set(obj, path, value) {
if (!obj) {
return;
}
var keys = path.split('.');
var len = keys.length;
if (len < 1) {
return;
}
if (len === 1) {
obj[keys[0]] = value;
return;
}
try {
var temp = obj[keys[0]] || {};
var replacement = temp;
for (var i = 1; i < len - 1; ++i) {
temp[keys[i]] = temp[keys[i]] || {};
temp = temp[keys[i]];
}
temp[keys[len - 1]] = value;
obj[keys[0]] = replacement;
} catch (e) {
return;
}
}
function formatArgsAsString(args) {
var i, len, arg;
var result = [];
for (i = 0, len = args.length; i < len; ++i) {
arg = args[i];
switch (typeName(arg)) {
case 'object':
arg = stringify(arg);
arg = arg.error || arg.value;
if (arg.length > 500) {
arg = arg.substr(0, 497) + '...';
}
break;
case 'null':
arg = 'null';
break;
case 'undefined':
arg = 'undefined';
break;
case 'symbol':
arg = arg.toString();
break;
}
result.push(arg);
}
return result.join(' ');
}
function now() {
if (Date.now) {
return +Date.now();
}
return +new Date();
}
function filterIp(requestData, captureIp) {
if (!requestData || !requestData['user_ip'] || captureIp === true) {
return;
}
var newIp = requestData['user_ip'];
if (!captureIp) {
newIp = null;
} else {
try {
var parts;
if (newIp.indexOf('.') !== -1) {
parts = newIp.split('.');
parts.pop();
parts.push('0');
newIp = parts.join('.');
} else if (newIp.indexOf(':') !== -1) {
parts = newIp.split(':');
if (parts.length > 2) {
var beginning = parts.slice(0, 3);
var slashIdx = beginning[2].indexOf('/');
if (slashIdx !== -1) {
beginning[2] = beginning[2].substring(0, slashIdx);
}
var terminal = '0000:0000:0000:0000:0000';
newIp = beginning.concat(terminal).join(':');
}
} else {
newIp = null;
}
} catch (e) {
newIp = null;
}
}
requestData['user_ip'] = newIp;
}
function handleOptions(current, input, payload, logger) {
var result = merge(current, input, payload);
result = updateDeprecatedOptions(result, logger);
if (!input || input.overwriteScrubFields) {
return result;
}
if (input.scrubFields) {
result.scrubFields = (current.scrubFields || []).concat(input.scrubFields);
}
return result;
}
function updateDeprecatedOptions(options, logger) {
if (options.hostWhiteList && !options.hostSafeList) {
options.hostSafeList = options.hostWhiteList;
options.hostWhiteList = undefined;
logger && logger.log('hostWhiteList is deprecated. Use hostSafeList.');
}
if (options.hostBlackList && !options.hostBlockList) {
options.hostBlockList = options.hostBlackList;
options.hostBlackList = undefined;
logger && logger.log('hostBlackList is deprecated. Use hostBlockList.');
}
return options;
}
module.exports = {
addParamsAndAccessTokenToPath: addParamsAndAccessTokenToPath,
createItem: createItem,
addErrorContext: addErrorContext,
createTelemetryEvent: createTelemetryEvent,
addItemAttributes: addItemAttributes,
getItemAttribute: getItemAttribute,
filterIp: filterIp,
formatArgsAsString: formatArgsAsString,
formatUrl: formatUrl,
get: get,
handleOptions: handleOptions,
isError: isError,
isFiniteNumber: isFiniteNumber,
isFunction: isFunction,
isIterable: isIterable,
isNativeFunction: isNativeFunction,
isObject: isObject,
isString: isString,
isType: isType,
isPromise: isPromise,
isBrowser: isBrowser,
jsonParse: jsonParse,
LEVELS: LEVELS,
makeUnhandledStackInfo: makeUnhandledStackInfo,
merge: merge,
now: now,
redact: redact,
RollbarJSON: RollbarJSON,
sanitizeUrl: sanitizeUrl,
set: set,
setupJSON: setupJSON,
stringify: stringify,
maxByteSize: maxByteSize,
typeName: typeName,
uuid4: uuid4,
};