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'); } 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() }; 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; } /* * 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, 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, 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 };