diff --git a/assets/javascripts/vendor/raven.js b/assets/javascripts/vendor/raven.js index a4d42a9b..7ed8e6bd 100644 --- a/assets/javascripts/vendor/raven.js +++ b/assets/javascripts/vendor/raven.js @@ -1,10 +1,10 @@ -/*! Raven.js 3.20.1 (42adaf5) | github.com/getsentry/raven-js */ +/*! Raven.js 3.25.2 (30b6d4e) | github.com/getsentry/raven-js */ /* * Includes TraceKit * https://github.com/getsentry/TraceKit * - * Copyright 2017 Matt Robenolt and other contributors + * Copyright 2018 Matt Robenolt and other contributors * Released under the BSD license * https://github.com/getsentry/raven-js/blob/master/LICENSE * @@ -77,6 +77,8 @@ ], 2: [ function (_dereq_, module, exports) { + var utils = _dereq_(5); + var wrapMethod = function (console, level, callback) { var originalConsoleLevel = console[level]; var originalConsole = console; @@ -90,7 +92,7 @@ console[level] = function () { var args = [].slice.call(arguments); - var msg = "" + args.join(" "); + var msg = utils.safeJoin(args, " "); var data = { level: sentryLevel, logger: "console", @@ -102,7 +104,7 @@ // Default browsers message msg = "Assertion failed: " + - (args.slice(1).join(" ") || "console.assert"); + (utils.safeJoin(args.slice(1), " ") || "console.assert"); data.extra.arguments = args.slice(1); callback && callback(msg, data); } @@ -127,7 +129,7 @@ wrapMethod: wrapMethod, }; }, - {}, + { 5: 5 }, ], 3: [ function (_dereq_, module, exports) { @@ -136,12 +138,16 @@ var TraceKit = _dereq_(6); var stringify = _dereq_(7); + var md5 = _dereq_(8); var RavenConfigError = _dereq_(1); var utils = _dereq_(5); + var isErrorEvent = utils.isErrorEvent; + var isDOMError = utils.isDOMError; + var isDOMException = utils.isDOMException; var isError = utils.isError; var isObject = utils.isObject; - var isErrorEvent = utils.isErrorEvent; + var isPlainObject = utils.isPlainObject; var isUndefined = utils.isUndefined; var isFunction = utils.isFunction; var isString = utils.isString; @@ -160,6 +166,11 @@ var isSameStacktrace = utils.isSameStacktrace; var parseUrl = utils.parseUrl; var fill = utils.fill; + var supportsFetch = utils.supportsFetch; + var supportsReferrerPolicy = utils.supportsReferrerPolicy; + var serializeKeysForMessage = utils.serializeKeysForMessage; + var serializeException = utils.serializeException; + var sanitize = utils.sanitize; var wrapConsoleMethod = _dereq_(2).wrapMethod; @@ -207,20 +218,33 @@ this._globalProject = null; this._globalContext = {}; this._globalOptions = { + // SENTRY_RELEASE can be injected by https://github.com/getsentry/sentry-webpack-plugin + release: _window.SENTRY_RELEASE && _window.SENTRY_RELEASE.id, logger: "javascript", ignoreErrors: [], ignoreUrls: [], whitelistUrls: [], includePaths: [], + headers: null, collectWindowErrors: true, + captureUnhandledRejections: true, maxMessageLength: 0, - // By default, truncates URL values to 250 chars maxUrlLength: 250, stackTraceLimit: 50, autoBreadcrumbs: true, instrument: true, sampleRate: 1, + sanitizeKeys: [], + }; + this._fetchDefaults = { + method: "POST", + keepalive: true, + // Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default + // https://caniuse.com/#feat=referrer-policy + // It doesn't. And it throw exception instead of ignoring this parameter... + // REF: https://github.com/getsentry/raven-js/issues/1233 + referrerPolicy: supportsReferrerPolicy() ? "origin" : "", }; this._ignoreOnError = 0; this._isRavenInstalled = false; @@ -257,7 +281,7 @@ // webpack (using a build step causes webpack #1617). Grunt verifies that // this value matches package.json during build. // See: https://github.com/getsentry/raven-js/issues/465 - VERSION: "3.20.1", + VERSION: "3.25.2", debug: false, @@ -376,6 +400,10 @@ self._handleOnErrorStackInfo.apply(self, arguments); }); + if (self._globalOptions.captureUnhandledRejections) { + self._attachPromiseRejectionHandler(); + } + self._patchFunctionToString(); if ( @@ -538,7 +566,7 @@ return wrapped; }, - /* + /** * Uninstalls the global error handler. * * @return {Raven} @@ -546,8 +574,10 @@ uninstall: function () { TraceKit.report.uninstall(); + this._detachPromiseRejectionHandler(); this._unpatchFunctionToString(); this._restoreBuiltIns(); + this._restoreConsole(); Error.stackTraceLimit = this._originalErrorStackTraceLimit; this._isRavenInstalled = false; @@ -555,7 +585,58 @@ return this; }, - /* + /** + * Callback used for `unhandledrejection` event + * + * @param {PromiseRejectionEvent} event An object containing + * promise: the Promise that was rejected + * reason: the value with which the Promise was rejected + * @return void + */ + _promiseRejectionHandler: function (event) { + this._logDebug( + "debug", + "Raven caught unhandled promise rejection:", + event, + ); + this.captureException(event.reason, { + extra: { + unhandledPromiseRejection: true, + }, + }); + }, + + /** + * Installs the global promise rejection handler. + * + * @return {raven} + */ + _attachPromiseRejectionHandler: function () { + this._promiseRejectionHandler = + this._promiseRejectionHandler.bind(this); + _window.addEventListener && + _window.addEventListener( + "unhandledrejection", + this._promiseRejectionHandler, + ); + return this; + }, + + /** + * Uninstalls the global promise rejection handler. + * + * @return {raven} + */ + _detachPromiseRejectionHandler: function () { + _window.removeEventListener && + _window.removeEventListener( + "unhandledrejection", + this._promiseRejectionHandler, + ); + return this; + }, + + /** * Manually capture an exception and send it over to Sentry * * @param {error} ex An exception to be logged @@ -563,30 +644,60 @@ * @return {Raven} */ captureException: function (ex, options) { - // Cases for sending ex as a message, rather than an exception - var isNotError = !isError(ex); - var isNotErrorEvent = !isErrorEvent(ex); - var isErrorEventWithoutError = isErrorEvent(ex) && !ex.error; + options = objectMerge( + { trimHeadFrames: 0 }, + options ? options : {}, + ); - if ( - (isNotError && isNotErrorEvent) || - isErrorEventWithoutError - ) { + if (isErrorEvent(ex) && ex.error) { + // If it is an ErrorEvent with `error` property, extract it to get actual Error + ex = ex.error; + } else if (isDOMError(ex) || isDOMException(ex)) { + // If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers) + // then we just extract the name and message, as they don't provide anything else + // https://developer.mozilla.org/en-US/docs/Web/API/DOMError + // https://developer.mozilla.org/en-US/docs/Web/API/DOMException + var name = + ex.name || (isDOMError(ex) ? "DOMError" : "DOMException"); + var message = ex.message ? name + ": " + ex.message : name; + + return this.captureMessage( + message, + objectMerge(options, { + // neither DOMError or DOMException provide stack trace and we most likely wont get it this way as well + // but it's barely any overhead so we may at least try + stacktrace: true, + trimHeadFrames: options.trimHeadFrames + 1, + }), + ); + } else if (isError(ex)) { + // we have a real Error object + ex = ex; + } else if (isPlainObject(ex)) { + // If it is plain Object, serialize it manually and extract options + // This will allow us to group events based on top-level keys + // which is much better than creating new group when any key/value change + options = this._getCaptureExceptionOptionsFromPlainObject( + options, + ex, + ); + ex = new Error(options.message); + } else { + // If none of previous checks were valid, then it means that + // it's not a DOMError/DOMException + // it's not a plain Object + // it's not a valid ErrorEvent (one with an error property) + // it's not an Error + // So bail out and capture it as a simple message: return this.captureMessage( ex, - objectMerge( - { - trimHeadFrames: 1, - stacktrace: true, // if we fall back to captureMessage, default to attempting a new trace - }, - options, - ), + objectMerge(options, { + stacktrace: true, // if we fall back to captureMessage, default to attempting a new trace + trimHeadFrames: options.trimHeadFrames + 1, + }), ); } - // Get actual Error from ErrorEvent - if (isErrorEvent(ex)) ex = ex.error; - // Store the raw exception object for potential debugging and introspection this._lastCapturedException = ex; @@ -607,6 +718,23 @@ return this; }, + _getCaptureExceptionOptionsFromPlainObject: function ( + currentOptions, + ex, + ) { + var exKeys = Object.keys(ex).sort(); + var options = objectMerge(currentOptions, { + message: + "Non-Error exception captured with keys: " + + serializeKeysForMessage(exKeys), + fingerprint: [md5(exKeys)], + extra: currentOptions.extra || {}, + }); + options.extra.__serialized__ = serializeException(ex); + + return options; + }, + /* * Manually send a message to Sentry * @@ -626,10 +754,11 @@ } options = options || {}; + msg = msg + ""; // Make sure it's actually a string var data = objectMerge( { - message: msg + "", // Make sure it's actually a string + message: msg, }, options, ); @@ -651,6 +780,17 @@ // stack[0] is `throw new Error(msg)` call itself, we are interested in the frame that was just before that, stack[1] var initialCall = isArray(stack.stack) && stack.stack[1]; + + // if stack[1] is `Raven.captureException`, it means that someone passed a string to it and we redirected that call + // to be handled by `captureMessage`, thus `initialCall` is the 3rd one, not 2nd + // initialCall => captureException(string) => captureMessage(string) + if ( + initialCall && + initialCall.func === "Raven.captureException" + ) { + initialCall = stack.stack[2]; + } + var fileurl = (initialCall && initialCall.url) || ""; if ( @@ -671,18 +811,21 @@ this._globalOptions.stacktrace || (options && options.stacktrace) ) { + // fingerprint on msg, not stack trace (legacy behavior, could be revisited) + data.fingerprint = + data.fingerprint == null ? msg : data.fingerprint; + options = objectMerge( { - // fingerprint on msg, not stack trace (legacy behavior, could be - // revisited) - fingerprint: msg, - // since we know this is a synthetic trace, the top N-most frames - // MUST be from Raven.js, so mark them as in_app later by setting - // trimHeadFrames - trimHeadFrames: (options.trimHeadFrames || 0) + 1, + trimHeadFrames: 0, }, options, ); + // Since we know this is a synthetic trace, the top frame (this function call) + // MUST be from Raven.js, so mark it for trimming + // We add to the trim counter so that callers can choose to trim extra frames, such + // as utility functions. + options.trimHeadFrames += 1; var frames = this._prepareFrames(stack, options); data.stacktrace = { @@ -691,6 +834,13 @@ }; } + // Make sure that fingerprint is always wrapped in an array + if (data.fingerprint) { + data.fingerprint = isArray(data.fingerprint) + ? data.fingerprint + : [data.fingerprint]; + } + // Fire away! this._send(data); @@ -976,7 +1126,7 @@ }, _triggerEvent: function (eventType, options) { - // NOTE: `event` is a native browser thing, so let's avoid conflicting with it + // NOTE: `event` is a native browser thing, so let's avoid conflicting wiht it var evt, key; if (!this._hasDocument) return; @@ -1290,7 +1440,7 @@ fill(_window, "setTimeout", wrapTimeFn, wrappedBuiltIns); fill(_window, "setInterval", wrapTimeFn, wrappedBuiltIns); - if (_requestAnimationFrame) { + if (_window.requestAnimationFrame) { fill( _window, "requestAnimationFrame", @@ -1365,7 +1515,8 @@ } if (autoBreadcrumbs.xhr && "XMLHttpRequest" in _window) { - var xhrproto = XMLHttpRequest.prototype; + var xhrproto = + _window.XMLHttpRequest && _window.XMLHttpRequest.prototype; fill( xhrproto, "open", @@ -1395,7 +1546,7 @@ xhrproto, "send", function (origSend) { - return function (data) { + return function () { // preserve arity var xhr = this; @@ -1450,12 +1601,12 @@ ); } - if (autoBreadcrumbs.xhr && "fetch" in _window) { + if (autoBreadcrumbs.xhr && supportsFetch()) { fill( _window, "fetch", function (origFetch) { - return function (fn, t) { + return function () { // preserve arity // Make a copy of the arguments to prevent deoptimization // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments @@ -1482,6 +1633,11 @@ url = "" + fetchInput; } + // if Sentry key appears in URL, don't capture, as it's our own request + if (url.indexOf(self._globalKey) !== -1) { + return origFetch.apply(this, args); + } + if (args[1] && args[1].method) { method = args[1].method; } @@ -1492,18 +1648,29 @@ status_code: null, }; - self.captureBreadcrumb({ - type: "http", - category: "fetch", - data: fetchData, - }); - return origFetch .apply(this, args) .then(function (response) { fetchData.status_code = response.status; + self.captureBreadcrumb({ + type: "http", + category: "fetch", + data: fetchData, + }); + return response; + }) + ["catch"](function (err) { + // if there is an error performing the request + self.captureBreadcrumb({ + type: "http", + category: "fetch", + data: fetchData, + level: "error", + }); + + throw err; }); }; }, @@ -1525,7 +1692,7 @@ self._keypressEventHandler(), false, ); - } else { + } else if (_document.attachEvent) { // IE8 Compatibility _document.attachEvent( "onclick", @@ -1548,8 +1715,8 @@ var hasPushAndReplaceState = !isChromePackagedApp && _window.history && - history.pushState && - history.replaceState; + _window.history.pushState && + _window.history.replaceState; if (autoBreadcrumbs.location && hasPushAndReplaceState) { // TODO: remove onpopstate handler on uninstall() var oldOnPopState = _window.onpopstate; @@ -1579,13 +1746,13 @@ }; fill( - history, + _window.history, "pushState", historyReplacementFunction, wrappedBuiltIns, ); fill( - history, + _window.history, "replaceState", historyReplacementFunction, wrappedBuiltIns, @@ -1629,6 +1796,14 @@ } }, + _restoreConsole: function () { + // eslint-disable-next-line guard-for-in + for (var method in this._originalConsoleMethods) { + this._originalConsole[method] = + this._originalConsoleMethods[method]; + } + }, + _drainPlugins: function () { var self = this; @@ -1822,7 +1997,7 @@ }, ], }, - culprit: fileurl, + transaction: fileurl, }, options, ); @@ -1905,18 +2080,18 @@ if (this._hasNavigator && _navigator.userAgent) { httpData.headers = { - "User-Agent": navigator.userAgent, + "User-Agent": _navigator.userAgent, }; } - if (this._hasDocument) { - if (_document.location && _document.location.href) { - httpData.url = _document.location.href; - } - if (_document.referrer) { - if (!httpData.headers) httpData.headers = {}; - httpData.headers.Referer = _document.referrer; - } + // Check in `window` instead of `document`, as we may be in ServiceWorker environment + if (_window.location && _window.location.href) { + httpData.url = _window.location.href; + } + + if (this._hasDocument && _document.referrer) { + if (!httpData.headers) httpData.headers = {}; + httpData.headers.Referer = _document.referrer; } return httpData; @@ -1949,7 +2124,7 @@ if ( !last || current.message !== last.message || // defined for captureMessage - current.culprit !== last.culprit // defined for captureException/onerror + current.transaction !== last.transaction // defined for captureException/onerror ) return false; @@ -1982,8 +2157,14 @@ try { // If Retry-After is not in Access-Control-Expose-Headers, most // browsers will throw an exception trying to access it - retry = request.getResponseHeader("Retry-After"); - retry = parseInt(retry, 10) * 1000; // Retry-After is returned in seconds + if (supportsFetch()) { + retry = request.headers.get("Retry-After"); + } else { + retry = request.getResponseHeader("Retry-After"); + } + + // Retry-After is returned in seconds + retry = parseInt(retry, 10) * 1000; } catch (e) { /* eslint no-empty:0 */ } @@ -2037,9 +2218,6 @@ }; } - // If there are no tags/extra, strip the key from the payload alltogther. - if (isEmptyObject(data.tags)) delete data.tags; - if (this._globalContext.user) { // sentry.interfaces.User data.user = this._globalContext.user; @@ -2056,6 +2234,19 @@ if (globalOptions.serverName) data.server_name = globalOptions.serverName; + data = this._sanitizeData(data); + + // Cleanup empty properties before sending them to the server + Object.keys(data).forEach(function (key) { + if ( + data[key] == null || + data[key] === "" || + isEmptyObject(data[key]) + ) { + delete data[key]; + } + }); + if (isFunction(globalOptions.dataCallback)) { data = globalOptions.dataCallback(data) || data; } @@ -2093,6 +2284,10 @@ } }, + _sanitizeData: function (data) { + return sanitize(data, this._globalOptions.sanitizeKeys); + }, + _getUuid: function () { return uuid4(); }, @@ -2197,6 +2392,61 @@ }, _makeRequest: function (opts) { + // Auth is intentionally sent as part of query string (NOT as custom HTTP header) to avoid preflight CORS requests + var url = opts.url + "?" + urlencode(opts.auth); + + var evaluatedHeaders = null; + var evaluatedFetchParameters = {}; + + if (opts.options.headers) { + evaluatedHeaders = this._evaluateHash(opts.options.headers); + } + + if (opts.options.fetchParameters) { + evaluatedFetchParameters = this._evaluateHash( + opts.options.fetchParameters, + ); + } + + if (supportsFetch()) { + evaluatedFetchParameters.body = stringify(opts.data); + + var defaultFetchOptions = objectMerge( + {}, + this._fetchDefaults, + ); + var fetchOptions = objectMerge( + defaultFetchOptions, + evaluatedFetchParameters, + ); + + if (evaluatedHeaders) { + fetchOptions.headers = evaluatedHeaders; + } + + return _window + .fetch(url, fetchOptions) + .then(function (response) { + if (response.ok) { + opts.onSuccess && opts.onSuccess(); + } else { + var error = new Error( + "Sentry error code: " + response.status, + ); + // It's called request only to keep compatibility with XHR interface + // and not add more redundant checks in setBackoffState method + error.request = response; + opts.onError && opts.onError(error); + } + }) + ["catch"](function () { + opts.onError && + opts.onError( + new Error("Sentry error code: network unavailable"), + ); + }); + } + var request = _window.XMLHttpRequest && new _window.XMLHttpRequest(); if (!request) return; @@ -2208,8 +2458,6 @@ if (!hasCORS) return; - var url = opts.url; - if ("withCredentials" in request) { request.onreadystatechange = function () { if (request.readyState !== 4) { @@ -2243,14 +2491,37 @@ } } - // NOTE: auth is intentionally sent as part of query string (NOT as custom - // HTTP header) so as to avoid preflight CORS requests - request.open("POST", url + "?" + urlencode(opts.auth)); + request.open("POST", url); + + if (evaluatedHeaders) { + each(evaluatedHeaders, function (key, value) { + request.setRequestHeader(key, value); + }); + } + request.send(stringify(opts.data)); }, + _evaluateHash: function (hash) { + var evaluated = {}; + + for (var key in hash) { + if (hash.hasOwnProperty(key)) { + var value = hash[key]; + evaluated[key] = + typeof value === "function" ? value() : value; + } + } + + return evaluated; + }, + _logDebug: function (level) { - if (this._originalConsoleMethods[level] && this.debug) { + // We allow `Raven.debug` and `Raven.config(DSN, { debug: true })` to not make backward incompatible API change + if ( + this._originalConsoleMethods[level] && + (this.debug || this._globalOptions.debug) + ) { // In IE<10 console methods do not have their own 'apply' method Function.prototype.apply.call( this._originalConsoleMethods[level], @@ -2288,7 +2559,7 @@ : {}, ); }, - { 1: 1, 2: 2, 5: 5, 6: 6, 7: 7 }, + { 1: 1, 2: 2, 5: 5, 6: 6, 7: 7, 8: 8 }, ], 4: [ function (_dereq_, module, exports) { @@ -2328,6 +2599,42 @@ Raven.afterLoad(); module.exports = Raven; + + /** + * DISCLAIMER: + * + * Expose `Client` constructor for cases where user want to track multiple "sub-applications" in one larger app. + * It's not meant to be used by a wide audience, so pleaaase make sure that you know what you're doing before using it. + * Accidentally calling `install` multiple times, may result in an unexpected behavior that's very hard to debug. + * + * It's called `Client' to be in-line with Raven Node implementation. + * + * HOWTO: + * + * import Raven from 'raven-js'; + * + * const someAppReporter = new Raven.Client(); + * const someOtherAppReporter = new Raven.Client(); + * + * someAppReporter.config('__DSN__', { + * ...config goes here + * }); + * + * someOtherAppReporter.config('__OTHER_DSN__', { + * ...config goes here + * }); + * + * someAppReporter.captureMessage(...); + * someAppReporter.captureException(...); + * someAppReporter.captureBreadcrumb(...); + * + * someOtherAppReporter.captureMessage(...); + * someOtherAppReporter.captureException(...); + * someOtherAppReporter.captureBreadcrumb(...); + * + * It should "just work". + */ + module.exports.Client = RavenConstructor; }).call( this, typeof global !== "undefined" @@ -2344,6 +2651,8 @@ 5: [ function (_dereq_, module, exports) { (function (global) { + var stringify = _dereq_(7); + var _window = typeof window !== "undefined" ? window @@ -2360,7 +2669,7 @@ // Yanked from https://git.io/vS8DV re-used under CC0 // with some tiny modifications function isError(value) { - switch ({}.toString.call(value)) { + switch (Object.prototype.toString.call(value)) { case "[object Error]": return true; case "[object Exception]": @@ -2374,8 +2683,20 @@ function isErrorEvent(value) { return ( - supportsErrorEvent() && - {}.toString.call(value) === "[object ErrorEvent]" + Object.prototype.toString.call(value) === "[object ErrorEvent]" + ); + } + + function isDOMError(value) { + return ( + Object.prototype.toString.call(value) === "[object DOMError]" + ); + } + + function isDOMException(value) { + return ( + Object.prototype.toString.call(value) === + "[object DOMException]" ); } @@ -2387,6 +2708,10 @@ return typeof what === "function"; } + function isPlainObject(what) { + return Object.prototype.toString.call(what) === "[object Object]"; + } + function isString(what) { return Object.prototype.toString.call(what) === "[object String]"; } @@ -2396,6 +2721,8 @@ } function isEmptyObject(what) { + if (!isPlainObject(what)) return false; + for (var _ in what) { if (what.hasOwnProperty(_)) { return false; @@ -2413,6 +2740,59 @@ } } + function supportsDOMError() { + try { + new DOMError(""); // eslint-disable-line no-new + return true; + } catch (e) { + return false; + } + } + + function supportsDOMException() { + try { + new DOMException(""); // eslint-disable-line no-new + return true; + } catch (e) { + return false; + } + } + + function supportsFetch() { + if (!("fetch" in _window)) return false; + + try { + new Headers(); // eslint-disable-line no-new + new Request(""); // eslint-disable-line no-new + new Response(); // eslint-disable-line no-new + return true; + } catch (e) { + return false; + } + } + + // Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default + // https://caniuse.com/#feat=referrer-policy + // It doesn't. And it throw exception instead of ignoring this parameter... + // REF: https://github.com/getsentry/raven-js/issues/1233 + function supportsReferrerPolicy() { + if (!supportsFetch()) return false; + + try { + // eslint-disable-next-line no-new + new Request("pickleRick", { + referrerPolicy: "origin", + }); + return true; + } catch (e) { + return false; + } + } + + function supportsPromiseRejectionEvent() { + return typeof PromiseRejectionEvent === "function"; + } + function wrappedCallback(callback) { function dataCallback(data, original) { var normalizedData = callback(data) || data; @@ -2470,9 +2850,15 @@ } function truncate(str, max) { - return !max || str.length <= max - ? str - : str.substr(0, max) + "\u2026"; + if (typeof max !== "number") { + throw new Error( + "2nd argument to `truncate` function should be a number", + ); + } + if (typeof str !== "string" || max === 0) { + return str; + } + return str.length <= max ? str : str.substr(0, max) + "\u2026"; } /** @@ -2521,14 +2907,14 @@ return pairs.join("&"); } - // borrowed from https://datatracker.ietf.org/doc/html/rfc3986#appendix-B + // borrowed from https://tools.ietf.org/html/rfc3986#appendix-B // intentionally using regex and not href parsing trick because React Native and other // environments where DOM might not be available function parseUrl(url) { + if (typeof url !== "string") return {}; var match = url.match( /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/, ); - if (!match) return {}; // coerce to undefined values to empty string so we don't get 'undefined' var query = match[6] || ""; @@ -2675,6 +3061,13 @@ return !!(!!a ^ !!b); } + /** + * Returns true if both parameters are undefined + */ + function isBothUndefined(a, b) { + return isUndefined(a) && isUndefined(b); + } + /** * Returns true if the two input exception interfaces have the same content */ @@ -2687,6 +3080,9 @@ if (ex1.type !== ex2.type || ex1.value !== ex2.value) return false; + // in case both stacktraces are undefined, we can't decide so default to false + if (isBothUndefined(ex1.stacktrace, ex2.stacktrace)) return false; + return isSameStacktrace(ex1.stacktrace, ex2.stacktrace); } @@ -2726,6 +3122,7 @@ * @param track {optional} record instrumentation to an array */ function fill(obj, name, replacement, track) { + if (obj == null) return; var orig = obj[name]; obj[name] = replacement(orig); obj[name].__raven__ = true; @@ -2735,16 +3132,190 @@ } } + /** + * Join values in array + * @param input array of values to be joined together + * @param delimiter string to be placed in-between values + * @returns {string} + */ + function safeJoin(input, delimiter) { + if (!isArray(input)) return ""; + + var output = []; + + for (var i = 0; i < input.length; i++) { + try { + output.push(String(input[i])); + } catch (e) { + output.push("[value cannot be serialized]"); + } + } + + return output.join(delimiter); + } + + // Default Node.js REPL depth + var MAX_SERIALIZE_EXCEPTION_DEPTH = 3; + // 50kB, as 100kB is max payload size, so half sounds reasonable + var MAX_SERIALIZE_EXCEPTION_SIZE = 50 * 1024; + var MAX_SERIALIZE_KEYS_LENGTH = 40; + + function utf8Length(value) { + return ~-encodeURI(value).split(/%..|./).length; + } + + function jsonSize(value) { + return utf8Length(JSON.stringify(value)); + } + + function serializeValue(value) { + if (typeof value === "string") { + var maxLength = 40; + return truncate(value, maxLength); + } else if ( + typeof value === "number" || + typeof value === "boolean" || + typeof value === "undefined" + ) { + return value; + } + + var type = Object.prototype.toString.call(value); + + // Node.js REPL notation + if (type === "[object Object]") return "[Object]"; + if (type === "[object Array]") return "[Array]"; + if (type === "[object Function]") + return value.name + ? "[Function: " + value.name + "]" + : "[Function]"; + + return value; + } + + function serializeObject(value, depth) { + if (depth === 0) return serializeValue(value); + + if (isPlainObject(value)) { + return Object.keys(value).reduce(function (acc, key) { + acc[key] = serializeObject(value[key], depth - 1); + return acc; + }, {}); + } else if (Array.isArray(value)) { + return value.map(function (val) { + return serializeObject(val, depth - 1); + }); + } + + return serializeValue(value); + } + + function serializeException(ex, depth, maxSize) { + if (!isPlainObject(ex)) return ex; + + depth = + typeof depth !== "number" + ? MAX_SERIALIZE_EXCEPTION_DEPTH + : depth; + maxSize = + typeof depth !== "number" + ? MAX_SERIALIZE_EXCEPTION_SIZE + : maxSize; + + var serialized = serializeObject(ex, depth); + + if (jsonSize(stringify(serialized)) > maxSize) { + return serializeException(ex, depth - 1); + } + + return serialized; + } + + function serializeKeysForMessage(keys, maxLength) { + if (typeof keys === "number" || typeof keys === "string") + return keys.toString(); + if (!Array.isArray(keys)) return ""; + + keys = keys.filter(function (key) { + return typeof key === "string"; + }); + if (keys.length === 0) return "[object has no keys]"; + + maxLength = + typeof maxLength !== "number" + ? MAX_SERIALIZE_KEYS_LENGTH + : maxLength; + if (keys[0].length >= maxLength) return keys[0]; + + for (var usedKeys = keys.length; usedKeys > 0; usedKeys--) { + var serialized = keys.slice(0, usedKeys).join(", "); + if (serialized.length > maxLength) continue; + if (usedKeys === keys.length) return serialized; + return serialized + "\u2026"; + } + + return ""; + } + + function sanitize(input, sanitizeKeys) { + if ( + !isArray(sanitizeKeys) || + (isArray(sanitizeKeys) && sanitizeKeys.length === 0) + ) + return input; + + var sanitizeRegExp = joinRegExp(sanitizeKeys); + var sanitizeMask = "********"; + var safeInput; + + try { + safeInput = JSON.parse(stringify(input)); + } catch (o_O) { + return input; + } + + function sanitizeWorker(workerInput) { + if (isArray(workerInput)) { + return workerInput.map(function (val) { + return sanitizeWorker(val); + }); + } + + if (isPlainObject(workerInput)) { + return Object.keys(workerInput).reduce(function (acc, k) { + if (sanitizeRegExp.test(k)) { + acc[k] = sanitizeMask; + } else { + acc[k] = sanitizeWorker(workerInput[k]); + } + return acc; + }, {}); + } + + return workerInput; + } + + return sanitizeWorker(safeInput); + } + module.exports = { isObject: isObject, isError: isError, isErrorEvent: isErrorEvent, + isDOMError: isDOMError, + isDOMException: isDOMException, isUndefined: isUndefined, isFunction: isFunction, + isPlainObject: isPlainObject, isString: isString, isArray: isArray, isEmptyObject: isEmptyObject, supportsErrorEvent: supportsErrorEvent, + supportsDOMError: supportsDOMError, + supportsDOMException: supportsDOMException, + supportsFetch: supportsFetch, + supportsReferrerPolicy: supportsReferrerPolicy, + supportsPromiseRejectionEvent: supportsPromiseRejectionEvent, wrappedCallback: wrappedCallback, each: each, objectMerge: objectMerge, @@ -2760,6 +3331,10 @@ isSameStacktrace: isSameStacktrace, parseUrl: parseUrl, fill: fill, + safeJoin: safeJoin, + serializeException: serializeException, + serializeKeysForMessage: serializeKeysForMessage, + sanitize: sanitize, }; }).call( this, @@ -2772,7 +3347,7 @@ : {}, ); }, - {}, + { 7: 7 }, ], 6: [ function (_dereq_, module, exports) { @@ -2780,14 +3355,14 @@ var utils = _dereq_(5); /* - TraceKit - Cross brower stack traces - - This was originally forked from github.com/occ/TraceKit, but has since been - largely re-written and is now maintained as part of raven-js. Tests for - this are in test/vendor. - - MIT license -*/ + TraceKit - Cross brower stack traces + + This was originally forked from github.com/occ/TraceKit, but has since been + largely re-written and is now maintained as part of raven-js. Tests for + this are in test/vendor. + + MIT license + */ var TraceKit = { collectWindowErrors: true, @@ -2815,10 +3390,25 @@ function getLocationHref() { if (typeof document === "undefined" || document.location == null) return ""; - return document.location.href; } + function getLocationOrigin() { + if (typeof document === "undefined" || document.location == null) + return ""; + + // Oh dear IE10... + if (!document.location.origin) { + document.location.origin = + document.location.protocol + + "//" + + document.location.hostname + + (document.location.port ? ":" + document.location.port : ""); + } + + return document.location.origin; + } + /** * TraceKit.report: cross-browser processing of unhandled exceptions * @@ -2925,7 +3515,7 @@ /** * Ensures all global unhandled exceptions are recorded. * Supported by Gecko and IE. - * @param {string} message Error message. + * @param {string} msg Error message. * @param {string} url URL of script that generated the exception. * @param {(number|string)} lineNo The line number at which the error * occurred. @@ -2933,8 +3523,12 @@ * occurred. * @param {?Error} ex The actual Error object. */ - function traceKitWindowOnError(message, url, lineNo, colNo, ex) { + function traceKitWindowOnError(msg, url, lineNo, colNo, ex) { var stack = null; + // If 'ex' is ErrorEvent, get real Error from inside + var exception = utils.isErrorEvent(ex) ? ex.error : ex; + // If 'msg' is ErrorEvent, get real message from inside + var message = utils.isErrorEvent(msg) ? msg.message : msg; if (lastExceptionStack) { TraceKit.computeStackTrace.augmentStackTraceWithInitialElement( @@ -2944,13 +3538,13 @@ message, ); processLastException(); - } else if (ex && utils.isError(ex)) { - // non-string `ex` arg; attempt to extract stack trace + } else if (exception && utils.isError(exception)) { + // non-string `exception` arg; attempt to extract stack trace // New chrome and blink send along a real error object // Let's just report that like a normal error. // See: https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror - stack = TraceKit.computeStackTrace(ex); + stack = TraceKit.computeStackTrace(exception); notifyHandlers(stack, true); } else { var location = { @@ -2960,13 +3554,13 @@ }; var name = undefined; - var msg = message; // must be new var or will modify original `arguments` var groups; + if ({}.toString.call(message) === "[object String]") { var groups = message.match(ERROR_TYPES_RE); if (groups) { name = groups[1]; - msg = groups[2]; + message = groups[2]; } } @@ -2974,7 +3568,7 @@ stack = { name: name, - message: msg, + message: message, url: getLocationHref(), stack: [location], }; @@ -3163,20 +3757,22 @@ if (typeof ex.stack === "undefined" || !ex.stack) return; var chrome = - /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||[a-z]:|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i, - gecko = - /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i, - winjs = - /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i, - // Used to additionally parse URL/line/column from eval frames - geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i, - chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/, - lines = ex.stack.split("\n"), - stack = [], - submatch, - parts, - element, - reference = /^(.*) is undefined$/.exec(ex.message); + /^\s*at (?:(.*?) ?\()?((?:file|https?|blob|chrome-extension|native|eval|webpack||[a-z]:|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i; + var winjs = + /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx(?:-web)|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i; + // NOTE: blob urls are now supposed to always have an origin, therefore it's format + // which is `blob:http://url/path/with-some-uuid`, is matched by `blob.*?:\/` as well + var gecko = + /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|moz-extension).*?:\/.*?|\[native code\]|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i; + // Used to additionally parse URL/line/column from eval frames + var geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i; + var chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/; + var lines = ex.stack.split("\n"); + var stack = []; + var submatch; + var parts; + var element; + var reference = /^(.*) is undefined$/.exec(ex.message); for (var i = 0, j = lines.length; i < j; ++i) { if ((parts = chrome.exec(lines[i]))) { @@ -3236,6 +3832,47 @@ element.func = UNKNOWN_FUNCTION; } + if (element.url && element.url.substr(0, 5) === "blob:") { + // Special case for handling JavaScript loaded into a blob. + // We use a synchronous AJAX request here as a blob is already in + // memory - it's not making a network request. This will generate a warning + // in the browser console, but there has already been an error so that's not + // that much of an issue. + var xhr = new XMLHttpRequest(); + xhr.open("GET", element.url, false); + xhr.send(null); + + // If we failed to download the source, skip this patch + if (xhr.status === 200) { + var source = xhr.responseText || ""; + + // We trim the source down to the last 300 characters as sourceMappingURL is always at the end of the file. + // Why 300? To be in line with: https://github.com/getsentry/sentry/blob/4af29e8f2350e20c28a6933354e4f42437b4ba42/src/sentry/lang/javascript/processor.py#L164-L175 + source = source.slice(-300); + + // Now we dig out the source map URL + var sourceMaps = source.match( + /\/\/# sourceMappingURL=(.*)$/, + ); + + // If we don't find a source map comment or we find more than one, continue on to the next element. + if (sourceMaps) { + var sourceMapAddress = sourceMaps[1]; + + // Now we check to see if it's a relative URL. + // If it is, convert it to an absolute one. + if (sourceMapAddress.charAt(0) === "~") { + sourceMapAddress = + getLocationOrigin() + sourceMapAddress.slice(1); + } + + // Now we strip the '.map' off of the end of the URL and update the + // element so that Sentry can match the map to the blob. + element.url = sourceMapAddress.slice(0, -4); + } + } + } + stack.push(element); } @@ -3449,15 +4086,15 @@ 7: [ function (_dereq_, module, exports) { /* - json-stringify-safe - Like JSON.stringify, but doesn't throw on circular references. - - Originally forked from https://github.com/isaacs/json-stringify-safe - version 5.0.1 on 3/8/2017 and modified to handle Errors serialization - and IE8 compatibility. Tests for this are in test/vendor. - - ISC license: https://github.com/isaacs/json-stringify-safe/blob/master/LICENSE -*/ + json-stringify-safe + Like JSON.stringify, but doesn't throw on circular references. + + Originally forked from https://github.com/isaacs/json-stringify-safe + version 5.0.1 on 3/8/2017 and modified to handle Errors serialization + and IE8 compatibility. Tests for this are in test/vendor. + + ISC license: https://github.com/isaacs/json-stringify-safe/blob/master/LICENSE + */ exports = module.exports = stringify; exports.getSerialize = serializer; @@ -3535,6 +4172,281 @@ }, {}, ], + 8: [ + function (_dereq_, module, exports) { + /* + * JavaScript MD5 + * https://github.com/blueimp/JavaScript-MD5 + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + * + * Based on + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + + /* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ + function safeAdd(x, y) { + var lsw = (x & 0xffff) + (y & 0xffff); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xffff); + } + + /* + * Bitwise rotate a 32-bit number to the left. + */ + function bitRotateLeft(num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)); + } + + /* + * These functions implement the four basic operations the algorithm uses. + */ + function md5cmn(q, a, b, x, s, t) { + return safeAdd( + bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), + b, + ); + } + function md5ff(a, b, c, d, x, s, t) { + return md5cmn((b & c) | (~b & d), a, b, x, s, t); + } + function md5gg(a, b, c, d, x, s, t) { + return md5cmn((b & d) | (c & ~d), a, b, x, s, t); + } + function md5hh(a, b, c, d, x, s, t) { + return md5cmn(b ^ c ^ d, a, b, x, s, t); + } + function md5ii(a, b, c, d, x, s, t) { + return md5cmn(c ^ (b | ~d), a, b, x, s, t); + } + + /* + * Calculate the MD5 of an array of little-endian words, and a bit length. + */ + function binlMD5(x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << len % 32; + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var i; + var olda; + var oldb; + var oldc; + var oldd; + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + for (i = 0; i < x.length; i += 16) { + olda = a; + oldb = b; + oldc = c; + oldd = d; + + a = md5ff(a, b, c, d, x[i], 7, -680876936); + d = md5ff(d, a, b, c, x[i + 1], 12, -389564586); + c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); + b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330); + a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); + d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426); + c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); + b = md5ff(b, c, d, a, x[i + 7], 22, -45705983); + a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); + d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417); + c = md5ff(c, d, a, b, x[i + 10], 17, -42063); + b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162); + a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); + d = md5ff(d, a, b, c, x[i + 13], 12, -40341101); + c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); + b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329); + + a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); + d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632); + c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); + b = md5gg(b, c, d, a, x[i], 20, -373897302); + a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); + d = md5gg(d, a, b, c, x[i + 10], 9, 38016083); + c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); + b = md5gg(b, c, d, a, x[i + 4], 20, -405537848); + a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); + d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690); + c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); + b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501); + a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); + d = md5gg(d, a, b, c, x[i + 2], 9, -51403784); + c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); + b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734); + + a = md5hh(a, b, c, d, x[i + 5], 4, -378558); + d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463); + c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); + b = md5hh(b, c, d, a, x[i + 14], 23, -35309556); + a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); + d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353); + c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); + b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640); + a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); + d = md5hh(d, a, b, c, x[i], 11, -358537222); + c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); + b = md5hh(b, c, d, a, x[i + 6], 23, 76029189); + a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); + d = md5hh(d, a, b, c, x[i + 12], 11, -421815835); + c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); + b = md5hh(b, c, d, a, x[i + 2], 23, -995338651); + + a = md5ii(a, b, c, d, x[i], 6, -198630844); + d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415); + c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); + b = md5ii(b, c, d, a, x[i + 5], 21, -57434055); + a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); + d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606); + c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); + b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799); + a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); + d = md5ii(d, a, b, c, x[i + 15], 10, -30611744); + c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); + b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649); + a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); + d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379); + c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); + b = md5ii(b, c, d, a, x[i + 9], 21, -343485551); + + a = safeAdd(a, olda); + b = safeAdd(b, oldb); + c = safeAdd(c, oldc); + d = safeAdd(d, oldd); + } + return [a, b, c, d]; + } + + /* + * Convert an array of little-endian words to a string + */ + function binl2rstr(input) { + var i; + var output = ""; + var length32 = input.length * 32; + for (i = 0; i < length32; i += 8) { + output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff); + } + return output; + } + + /* + * Convert a raw string to an array of little-endian words + * Characters >255 have their high-byte silently ignored. + */ + function rstr2binl(input) { + var i; + var output = []; + output[(input.length >> 2) - 1] = undefined; + for (i = 0; i < output.length; i += 1) { + output[i] = 0; + } + var length8 = input.length * 8; + for (i = 0; i < length8; i += 8) { + output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32; + } + return output; + } + + /* + * Calculate the MD5 of a raw string + */ + function rstrMD5(s) { + return binl2rstr(binlMD5(rstr2binl(s), s.length * 8)); + } + + /* + * Calculate the HMAC-MD5, of a key and some data (raw strings) + */ + function rstrHMACMD5(key, data) { + var i; + var bkey = rstr2binl(key); + var ipad = []; + var opad = []; + var hash; + ipad[15] = opad[15] = undefined; + if (bkey.length > 16) { + bkey = binlMD5(bkey, key.length * 8); + } + for (i = 0; i < 16; i += 1) { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5c5c5c5c; + } + hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); + return binl2rstr(binlMD5(opad.concat(hash), 512 + 128)); + } + + /* + * Convert a raw string to a hex string + */ + function rstr2hex(input) { + var hexTab = "0123456789abcdef"; + var output = ""; + var x; + var i; + for (i = 0; i < input.length; i += 1) { + x = input.charCodeAt(i); + output += + hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f); + } + return output; + } + + /* + * Encode a string as utf-8 + */ + function str2rstrUTF8(input) { + return unescape(encodeURIComponent(input)); + } + + /* + * Take string arguments and return either raw or hex encoded strings + */ + function rawMD5(s) { + return rstrMD5(str2rstrUTF8(s)); + } + function hexMD5(s) { + return rstr2hex(rawMD5(s)); + } + function rawHMACMD5(k, d) { + return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d)); + } + function hexHMACMD5(k, d) { + return rstr2hex(rawHMACMD5(k, d)); + } + + function md5(string, key, raw) { + if (!key) { + if (!raw) { + return hexMD5(string); + } + return rawMD5(string); + } + if (!raw) { + return hexHMACMD5(key, string); + } + return rawHMACMD5(key, string); + } + + module.exports = md5; + }, + {}, + ], }, {}, [4],