UNPKG

@sentry/browser

Version:
1,326 lines (1,310 loc) 270 kB
/*! @sentry/browser 6.19.7 (5b3a175) | https://github.com/getsentry/sentry-javascript */ var Sentry = (function (exports) { /** * TODO(v7): Remove this enum and replace with SeverityLevel */ exports.Severity = void 0; (function (Severity) { /** JSDoc */ Severity["Fatal"] = "fatal"; /** JSDoc */ Severity["Error"] = "error"; /** JSDoc */ Severity["Warning"] = "warning"; /** JSDoc */ Severity["Log"] = "log"; /** JSDoc */ Severity["Info"] = "info"; /** JSDoc */ Severity["Debug"] = "debug"; /** JSDoc */ Severity["Critical"] = "critical"; })(exports.Severity || (exports.Severity = {})); /** * Consumes the promise and logs the error when it rejects. * @param promise A promise to forget. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function forget(promise) { void promise.then(null, e => { // TODO: Use a better logging mechanism // eslint-disable-next-line no-console console.error(e); }); } /** * NOTE: In order to avoid circular dependencies, if you add a function to this module and it needs to print something, * you must either a) use `console.log` rather than the logger, or b) put your function elsewhere. */ const fallbackGlobalObject = {}; /** * Safely get global scope object * * @returns Global scope object */ function getGlobalObject() { return (typeof window !== 'undefined' // eslint-disable-line no-restricted-globals ? window // eslint-disable-line no-restricted-globals : typeof self !== 'undefined' ? self : fallbackGlobalObject); } /** * Returns a global singleton contained in the global `__SENTRY__` object. * * If the singleton doesn't already exist in `__SENTRY__`, it will be created using the given factory * function and added to the `__SENTRY__` object. * * @param name name of the global singleton on __SENTRY__ * @param creator creator Factory function to create the singleton if it doesn't already exist on `__SENTRY__` * @param obj (Optional) The global object on which to look for `__SENTRY__`, if not `getGlobalObject`'s return value * @returns the singleton */ function getGlobalSingleton(name, creator, obj) { const global = (obj || getGlobalObject()); const __SENTRY__ = (global.__SENTRY__ = global.__SENTRY__ || {}); const singleton = __SENTRY__[name] || (__SENTRY__[name] = creator()); return singleton; } /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ // eslint-disable-next-line @typescript-eslint/unbound-method const objectToString = Object.prototype.toString; /** * Checks whether given value's type is one of a few Error or Error-like * {@link isError}. * * @param wat A value to be checked. * @returns A boolean representing the result. */ function isError(wat) { switch (objectToString.call(wat)) { case '[object Error]': case '[object Exception]': case '[object DOMException]': return true; default: return isInstanceOf(wat, Error); } } function isBuiltin(wat, ty) { return objectToString.call(wat) === `[object ${ty}]`; } /** * Checks whether given value's type is ErrorEvent * {@link isErrorEvent}. * * @param wat A value to be checked. * @returns A boolean representing the result. */ function isErrorEvent(wat) { return isBuiltin(wat, 'ErrorEvent'); } /** * Checks whether given value's type is DOMError * {@link isDOMError}. * * @param wat A value to be checked. * @returns A boolean representing the result. */ function isDOMError(wat) { return isBuiltin(wat, 'DOMError'); } /** * Checks whether given value's type is DOMException * {@link isDOMException}. * * @param wat A value to be checked. * @returns A boolean representing the result. */ function isDOMException(wat) { return isBuiltin(wat, 'DOMException'); } /** * Checks whether given value's type is a string * {@link isString}. * * @param wat A value to be checked. * @returns A boolean representing the result. */ function isString(wat) { return isBuiltin(wat, 'String'); } /** * Checks whether given value is a primitive (undefined, null, number, boolean, string, bigint, symbol) * {@link isPrimitive}. * * @param wat A value to be checked. * @returns A boolean representing the result. */ function isPrimitive(wat) { return wat === null || (typeof wat !== 'object' && typeof wat !== 'function'); } /** * Checks whether given value's type is an object literal * {@link isPlainObject}. * * @param wat A value to be checked. * @returns A boolean representing the result. */ function isPlainObject(wat) { return isBuiltin(wat, 'Object'); } /** * Checks whether given value's type is an Event instance * {@link isEvent}. * * @param wat A value to be checked. * @returns A boolean representing the result. */ function isEvent(wat) { return typeof Event !== 'undefined' && isInstanceOf(wat, Event); } /** * Checks whether given value's type is an Element instance * {@link isElement}. * * @param wat A value to be checked. * @returns A boolean representing the result. */ function isElement(wat) { return typeof Element !== 'undefined' && isInstanceOf(wat, Element); } /** * Checks whether given value's type is an regexp * {@link isRegExp}. * * @param wat A value to be checked. * @returns A boolean representing the result. */ function isRegExp(wat) { return isBuiltin(wat, 'RegExp'); } /** * Checks whether given value has a then function. * @param wat A value to be checked. */ function isThenable(wat) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return Boolean(wat && wat.then && typeof wat.then === 'function'); } /** * Checks whether given value's type is a SyntheticEvent * {@link isSyntheticEvent}. * * @param wat A value to be checked. * @returns A boolean representing the result. */ function isSyntheticEvent(wat) { return isPlainObject(wat) && 'nativeEvent' in wat && 'preventDefault' in wat && 'stopPropagation' in wat; } /** * Checks whether given value is NaN * {@link isNaN}. * * @param wat A value to be checked. * @returns A boolean representing the result. */ function isNaN$1(wat) { return typeof wat === 'number' && wat !== wat; } /** * Checks whether given value's type is an instance of provided constructor. * {@link isInstanceOf}. * * @param wat A value to be checked. * @param base A constructor to be used in a check. * @returns A boolean representing the result. */ function isInstanceOf(wat, base) { try { return wat instanceof base; } catch (_e) { return false; } } /** * Given a child DOM element, returns a query-selector statement describing that * and its ancestors * e.g. [HTMLElement] => body > div > input#foo.btn[name=baz] * @returns generated DOM path */ function htmlTreeAsString(elem, keyAttrs) { // try/catch both: // - accessing event.target (see getsentry/raven-js#838, #768) // - `htmlTreeAsString` because it's complex, and just accessing the DOM incorrectly // - can throw an exception in some circumstances. try { let currentElem = elem; const MAX_TRAVERSE_HEIGHT = 5; const MAX_OUTPUT_LEN = 80; const out = []; let height = 0; let len = 0; const separator = ' > '; const sepLength = separator.length; let nextStr; // eslint-disable-next-line no-plusplus while (currentElem && height++ < MAX_TRAVERSE_HEIGHT) { nextStr = _htmlElementAsString(currentElem, keyAttrs); // bail out if // - nextStr is the 'html' element // - the length of the string that would be created exceeds MAX_OUTPUT_LEN // (ignore this limit if we are on the first iteration) if (nextStr === 'html' || (height > 1 && len + out.length * sepLength + nextStr.length >= MAX_OUTPUT_LEN)) { break; } out.push(nextStr); len += nextStr.length; currentElem = currentElem.parentNode; } return out.reverse().join(separator); } catch (_oO) { return '<unknown>'; } } /** * Returns a simple, query-selector representation of a DOM element * e.g. [HTMLElement] => input#foo.btn[name=baz] * @returns generated DOM path */ function _htmlElementAsString(el, keyAttrs) { const elem = el; const out = []; let className; let classes; let key; let attr; let i; if (!elem || !elem.tagName) { return ''; } out.push(elem.tagName.toLowerCase()); // Pairs of attribute keys defined in `serializeAttribute` and their values on element. const keyAttrPairs = keyAttrs && keyAttrs.length ? keyAttrs.filter(keyAttr => elem.getAttribute(keyAttr)).map(keyAttr => [keyAttr, elem.getAttribute(keyAttr)]) : null; if (keyAttrPairs && keyAttrPairs.length) { keyAttrPairs.forEach(keyAttrPair => { out.push(`[${keyAttrPair[0]}="${keyAttrPair[1]}"]`); }); } else { if (elem.id) { out.push(`#${elem.id}`); } // eslint-disable-next-line prefer-const className = elem.className; if (className && isString(className)) { classes = className.split(/\s+/); for (i = 0; i < classes.length; i++) { out.push(`.${classes[i]}`); } } } const allowedAttrs = ['type', 'name', 'title', 'alt']; for (i = 0; i < allowedAttrs.length; i++) { key = allowedAttrs[i]; attr = elem.getAttribute(key); if (attr) { out.push(`[${key}="${attr}"]`); } } return out.join(''); } /** * A safe form of location.href */ function getLocationHref() { const global = getGlobalObject(); try { return global.document.location.href; } catch (oO) { return ''; } } const setPrototypeOf = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array ? setProtoOf : mixinProperties); /** * setPrototypeOf polyfill using __proto__ */ // eslint-disable-next-line @typescript-eslint/ban-types function setProtoOf(obj, proto) { // @ts-ignore __proto__ does not exist on obj obj.__proto__ = proto; return obj; } /** * setPrototypeOf polyfill using mixin */ // eslint-disable-next-line @typescript-eslint/ban-types function mixinProperties(obj, proto) { for (const prop in proto) { if (!Object.prototype.hasOwnProperty.call(obj, prop)) { // @ts-ignore typescript complains about indexing so we remove obj[prop] = proto[prop]; } } return obj; } /** An error emitted by Sentry SDKs and related utilities. */ class SentryError extends Error { constructor(message) { super(message); this.message = message; this.name = new.target.prototype.constructor.name; setPrototypeOf(this, new.target.prototype); } } /* * This file defines flags and constants that can be modified during compile time in order to facilitate tree shaking * for users. * * Debug flags need to be declared in each package individually and must not be imported across package boundaries, * because some build tools have trouble tree-shaking imported guards. * * As a convention, we define debug flags in a `flags.ts` file in the root of a package's `src` folder. * * Debug flag files will contain "magic strings" like `true` that may get replaced with actual values during * our, or the user's build process. Take care when introducing new flags - they must not throw if they are not * replaced. */ /** Flag that is true for debug builds, false otherwise. */ const IS_DEBUG_BUILD$3 = true; /** Regular expression used to parse a Dsn. */ const DSN_REGEX = /^(?:(\w+):)\/\/(?:(\w+)(?::(\w+))?@)([\w.-]+)(?::(\d+))?\/(.+)/; function isValidProtocol(protocol) { return protocol === 'http' || protocol === 'https'; } /** * Renders the string representation of this Dsn. * * By default, this will render the public representation without the password * component. To get the deprecated private representation, set `withPassword` * to true. * * @param withPassword When set to true, the password will be included. */ function dsnToString(dsn, withPassword = false) { const { host, path, pass, port, projectId, protocol, publicKey } = dsn; return (`${protocol}://${publicKey}${withPassword && pass ? `:${pass}` : ''}` + `@${host}${port ? `:${port}` : ''}/${path ? `${path}/` : path}${projectId}`); } function dsnFromString(str) { const match = DSN_REGEX.exec(str); if (!match) { throw new SentryError(`Invalid Sentry Dsn: ${str}`); } const [protocol, publicKey, pass = '', host, port = '', lastPath] = match.slice(1); let path = ''; let projectId = lastPath; const split = projectId.split('/'); if (split.length > 1) { path = split.slice(0, -1).join('/'); projectId = split.pop(); } if (projectId) { const projectMatch = projectId.match(/^\d+/); if (projectMatch) { projectId = projectMatch[0]; } } return dsnFromComponents({ host, pass, path, projectId, port, protocol: protocol, publicKey }); } function dsnFromComponents(components) { // TODO this is for backwards compatibility, and can be removed in a future version if ('user' in components && !('publicKey' in components)) { components.publicKey = components.user; } return { user: components.publicKey || '', protocol: components.protocol, publicKey: components.publicKey || '', pass: components.pass || '', host: components.host, port: components.port || '', path: components.path || '', projectId: components.projectId, }; } function validateDsn(dsn) { if (!IS_DEBUG_BUILD$3) { return; } const { port, projectId, protocol } = dsn; const requiredComponents = ['protocol', 'publicKey', 'host', 'projectId']; requiredComponents.forEach(component => { if (!dsn[component]) { throw new SentryError(`Invalid Sentry Dsn: ${component} missing`); } }); if (!projectId.match(/^\d+$/)) { throw new SentryError(`Invalid Sentry Dsn: Invalid projectId ${projectId}`); } if (!isValidProtocol(protocol)) { throw new SentryError(`Invalid Sentry Dsn: Invalid protocol ${protocol}`); } if (port && isNaN(parseInt(port, 10))) { throw new SentryError(`Invalid Sentry Dsn: Invalid port ${port}`); } return true; } /** The Sentry Dsn, identifying a Sentry instance and project. */ function makeDsn(from) { const components = typeof from === 'string' ? dsnFromString(from) : dsnFromComponents(from); validateDsn(components); return components; } const SeverityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug', 'critical']; // TODO: Implement different loggers for different environments const global$6 = getGlobalObject(); /** Prefix for logging strings */ const PREFIX = 'Sentry Logger '; const CONSOLE_LEVELS = ['debug', 'info', 'warn', 'error', 'log', 'assert']; /** * Temporarily disable sentry console instrumentations. * * @param callback The function to run against the original `console` messages * @returns The results of the callback */ function consoleSandbox(callback) { const global = getGlobalObject(); if (!('console' in global)) { return callback(); } const originalConsole = global.console; const wrappedLevels = {}; // Restore all wrapped console methods CONSOLE_LEVELS.forEach(level => { // TODO(v7): Remove this check as it's only needed for Node 6 const originalWrappedFunc = originalConsole[level] && originalConsole[level].__sentry_original__; if (level in global.console && originalWrappedFunc) { wrappedLevels[level] = originalConsole[level]; originalConsole[level] = originalWrappedFunc; } }); try { return callback(); } finally { // Revert restoration to wrapped state Object.keys(wrappedLevels).forEach(level => { originalConsole[level] = wrappedLevels[level]; }); } } function makeLogger() { let enabled = false; const logger = { enable: () => { enabled = true; }, disable: () => { enabled = false; }, }; if (IS_DEBUG_BUILD$3) { CONSOLE_LEVELS.forEach(name => { // eslint-disable-next-line @typescript-eslint/no-explicit-any logger[name] = (...args) => { if (enabled) { consoleSandbox(() => { global$6.console[name](`${PREFIX}[${name}]:`, ...args); }); } }; }); } else { CONSOLE_LEVELS.forEach(name => { logger[name] = () => undefined; }); } return logger; } // Ensure we only have a single logger instance, even if multiple versions of @sentry/utils are being used let logger; if (IS_DEBUG_BUILD$3) { logger = getGlobalSingleton('logger', makeLogger); } else { logger = makeLogger(); } /** * Truncates given string to the maximum characters count * * @param str An object that contains serializable values * @param max Maximum number of characters in truncated string (0 = unlimited) * @returns string Encoded */ function truncate(str, max = 0) { if (typeof str !== 'string' || max === 0) { return str; } return str.length <= max ? str : `${str.substr(0, max)}...`; } /** * Join values in array * @param input array of values to be joined together * @param delimiter string to be placed in-between values * @returns Joined values */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function safeJoin(input, delimiter) { if (!Array.isArray(input)) { return ''; } const output = []; // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < input.length; i++) { const value = input[i]; try { output.push(String(value)); } catch (e) { output.push('[value cannot be serialized]'); } } return output.join(delimiter); } /** * Checks if the value matches a regex or includes the string * @param value The string value to be checked against * @param pattern Either a regex or a string that must be contained in value */ function isMatchingPattern(value, pattern) { if (!isString(value)) { return false; } if (isRegExp(pattern)) { return pattern.test(value); } if (typeof pattern === 'string') { return value.indexOf(pattern) !== -1; } return false; } /** * Replace a method in an object with a wrapped version of itself. * * @param source An object that contains a method to be wrapped. * @param name The name of the method to be wrapped. * @param replacementFactory A higher-order function that takes the original version of the given method and returns a * wrapped version. Note: The function returned by `replacementFactory` needs to be a non-arrow function, in order to * preserve the correct value of `this`, and the original method must be called using `origMethod.call(this, <other * args>)` or `origMethod.apply(this, [<other args>])` (rather than being called directly), again to preserve `this`. * @returns void */ function fill(source, name, replacementFactory) { if (!(name in source)) { return; } const original = source[name]; const wrapped = replacementFactory(original); // Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work // otherwise it'll throw "TypeError: Object.defineProperties called on non-object" if (typeof wrapped === 'function') { try { markFunctionWrapped(wrapped, original); } catch (_Oo) { // This can throw if multiple fill happens on a global object like XMLHttpRequest // Fixes https://github.com/getsentry/sentry-javascript/issues/2043 } } source[name] = wrapped; } /** * Defines a non-enumerable property on the given object. * * @param obj The object on which to set the property * @param name The name of the property to be set * @param value The value to which to set the property */ function addNonEnumerableProperty(obj, name, value) { Object.defineProperty(obj, name, { // enumerable: false, // the default, so we can save on bundle size by not explicitly setting it value: value, writable: true, configurable: true, }); } /** * Remembers the original function on the wrapped function and * patches up the prototype. * * @param wrapped the wrapper function * @param original the original function that gets wrapped */ function markFunctionWrapped(wrapped, original) { const proto = original.prototype || {}; wrapped.prototype = original.prototype = proto; addNonEnumerableProperty(wrapped, '__sentry_original__', original); } /** * This extracts the original function if available. See * `markFunctionWrapped` for more information. * * @param func the function to unwrap * @returns the unwrapped version of the function if available. */ function getOriginalFunction(func) { return func.__sentry_original__; } /** * Encodes given object into url-friendly format * * @param object An object that contains serializable values * @returns string Encoded */ function urlEncode(object) { return Object.keys(object) .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(object[key])}`) .join('&'); } /** * Transforms any object into an object literal with all its attributes * attached to it. * * @param value Initial source that we have to transform in order for it to be usable by the serializer */ function convertToPlainObject(value) { let newObj = value; if (isError(value)) { newObj = Object.assign({ message: value.message, name: value.name, stack: value.stack }, getOwnProperties(value)); } else if (isEvent(value)) { const event = value; newObj = Object.assign({ type: event.type, target: serializeEventTarget(event.target), currentTarget: serializeEventTarget(event.currentTarget) }, getOwnProperties(event)); if (typeof CustomEvent !== 'undefined' && isInstanceOf(value, CustomEvent)) { newObj.detail = event.detail; } } return newObj; } /** Creates a string representation of the target of an `Event` object */ function serializeEventTarget(target) { try { return isElement(target) ? htmlTreeAsString(target) : Object.prototype.toString.call(target); } catch (_oO) { return '<unknown>'; } } /** Filters out all but an object's own properties */ function getOwnProperties(obj) { const extractedProps = {}; for (const property in obj) { if (Object.prototype.hasOwnProperty.call(obj, property)) { extractedProps[property] = obj[property]; } } return extractedProps; } /** * Given any captured exception, extract its keys and create a sorted * and truncated list that will be used inside the event message. * eg. `Non-error exception captured with keys: foo, bar, baz` */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types function extractExceptionKeysForMessage(exception, maxLength = 40) { const keys = Object.keys(convertToPlainObject(exception)); keys.sort(); if (!keys.length) { return '[object has no keys]'; } if (keys[0].length >= maxLength) { return truncate(keys[0], maxLength); } for (let includedKeys = keys.length; includedKeys > 0; includedKeys--) { const serialized = keys.slice(0, includedKeys).join(', '); if (serialized.length > maxLength) { continue; } if (includedKeys === keys.length) { return serialized; } return truncate(serialized, maxLength); } return ''; } /** * Given any object, return the new object with removed keys that value was `undefined`. * Works recursively on objects and arrays. */ function dropUndefinedKeys(val) { if (isPlainObject(val)) { const rv = {}; for (const key of Object.keys(val)) { if (typeof val[key] !== 'undefined') { rv[key] = dropUndefinedKeys(val[key]); } } return rv; } if (Array.isArray(val)) { return val.map(dropUndefinedKeys); } return val; } const STACKTRACE_LIMIT = 50; /** * Creates a stack parser with the supplied line parsers * * StackFrames are returned in the correct order for Sentry Exception * frames and with Sentry SDK internal frames removed from the top and bottom * */ function createStackParser(...parsers) { const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map(p => p[1]); return (stack, skipFirst = 0) => { const frames = []; for (const line of stack.split('\n').slice(skipFirst)) { for (const parser of sortedParsers) { const frame = parser(line); if (frame) { frames.push(frame); break; } } } return stripSentryFramesAndReverse(frames); }; } /** * @hidden */ function stripSentryFramesAndReverse(stack) { if (!stack.length) { return []; } let localStack = stack; const firstFrameFunction = localStack[0].function || ''; const lastFrameFunction = localStack[localStack.length - 1].function || ''; // If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call) if (firstFrameFunction.indexOf('captureMessage') !== -1 || firstFrameFunction.indexOf('captureException') !== -1) { localStack = localStack.slice(1); } // If stack ends with one of our internal API calls, remove it (ends, meaning it's the bottom of the stack - aka top-most call) if (lastFrameFunction.indexOf('sentryWrapped') !== -1) { localStack = localStack.slice(0, -1); } // The frame where the crash happened, should be the last entry in the array return localStack .slice(0, STACKTRACE_LIMIT) .map(frame => (Object.assign(Object.assign({}, frame), { filename: frame.filename || localStack[0].filename, function: frame.function || '?' }))) .reverse(); } const defaultFunctionName = '<anonymous>'; /** * Safely extract function name from itself */ function getFunctionName(fn) { try { if (!fn || typeof fn !== 'function') { return defaultFunctionName; } return fn.name || defaultFunctionName; } catch (e) { // Just accessing custom props in some Selenium environments // can cause a "Permission denied" exception (see raven-js#495). return defaultFunctionName; } } /** * Tells whether current environment supports Fetch API * {@link supportsFetch}. * * @returns Answer to the given question. */ function supportsFetch() { if (!('fetch' in getGlobalObject())) { return false; } try { new Headers(); new Request(''); new Response(); return true; } catch (e) { return false; } } /** * isNativeFetch checks if the given function is a native implementation of fetch() */ // eslint-disable-next-line @typescript-eslint/ban-types function isNativeFetch(func) { return func && /^function fetch\(\)\s+\{\s+\[native code\]\s+\}$/.test(func.toString()); } /** * Tells whether current environment supports Fetch API natively * {@link supportsNativeFetch}. * * @returns true if `window.fetch` is natively implemented, false otherwise */ function supportsNativeFetch() { if (!supportsFetch()) { return false; } const global = getGlobalObject(); // Fast path to avoid DOM I/O // eslint-disable-next-line @typescript-eslint/unbound-method if (isNativeFetch(global.fetch)) { return true; } // window.fetch is implemented, but is polyfilled or already wrapped (e.g: by a chrome extension) // so create a "pure" iframe to see if that has native fetch let result = false; const doc = global.document; // eslint-disable-next-line deprecation/deprecation if (doc && typeof doc.createElement === 'function') { try { const sandbox = doc.createElement('iframe'); sandbox.hidden = true; doc.head.appendChild(sandbox); if (sandbox.contentWindow && sandbox.contentWindow.fetch) { // eslint-disable-next-line @typescript-eslint/unbound-method result = isNativeFetch(sandbox.contentWindow.fetch); } doc.head.removeChild(sandbox); } catch (err) { logger.warn('Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ', err); } } return result; } /** * Tells whether current environment supports Referrer Policy API * {@link supportsReferrerPolicy}. * * @returns Answer to the given question. */ function supportsReferrerPolicy() { // Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default' // (see https://caniuse.com/#feat=referrer-policy), // it doesn't. And it throws an exception instead of ignoring this parameter... // REF: https://github.com/getsentry/raven-js/issues/1233 if (!supportsFetch()) { return false; } try { new Request('_', { referrerPolicy: 'origin', }); return true; } catch (e) { return false; } } /** * Tells whether current environment supports History API * {@link supportsHistory}. * * @returns Answer to the given question. */ function supportsHistory() { // NOTE: in Chrome App environment, touching history.pushState, *even inside // a try/catch block*, will cause Chrome to output an error to console.error // borrowed from: https://github.com/angular/angular.js/pull/13945/files const global = getGlobalObject(); /* eslint-disable @typescript-eslint/no-unsafe-member-access */ // eslint-disable-next-line @typescript-eslint/no-explicit-any const chrome = global.chrome; const isChromePackagedApp = chrome && chrome.app && chrome.app.runtime; /* eslint-enable @typescript-eslint/no-unsafe-member-access */ const hasHistoryApi = 'history' in global && !!global.history.pushState && !!global.history.replaceState; return !isChromePackagedApp && hasHistoryApi; } const global$5 = getGlobalObject(); /** * Instrument native APIs to call handlers that can be used to create breadcrumbs, APM spans etc. * - Console API * - Fetch API * - XHR API * - History API * - DOM API (click/typing) * - Error API * - UnhandledRejection API */ const handlers = {}; const instrumented = {}; /** Instruments given API */ function instrument(type) { if (instrumented[type]) { return; } instrumented[type] = true; switch (type) { case 'console': instrumentConsole(); break; case 'dom': instrumentDOM(); break; case 'xhr': instrumentXHR(); break; case 'fetch': instrumentFetch(); break; case 'history': instrumentHistory(); break; case 'error': instrumentError(); break; case 'unhandledrejection': instrumentUnhandledRejection(); break; default: logger.warn('unknown instrumentation type:', type); return; } } /** * Add handler that will be called when given type of instrumentation triggers. * Use at your own risk, this might break without changelog notice, only used internally. * @hidden */ function addInstrumentationHandler(type, callback) { handlers[type] = handlers[type] || []; handlers[type].push(callback); instrument(type); } /** JSDoc */ function triggerHandlers(type, data) { if (!type || !handlers[type]) { return; } for (const handler of handlers[type] || []) { try { handler(data); } catch (e) { logger.error(`Error while triggering instrumentation handler.\nType: ${type}\nName: ${getFunctionName(handler)}\nError:`, e); } } } /** JSDoc */ function instrumentConsole() { if (!('console' in global$5)) { return; } CONSOLE_LEVELS.forEach(function (level) { if (!(level in global$5.console)) { return; } fill(global$5.console, level, function (originalConsoleMethod) { return function (...args) { triggerHandlers('console', { args, level }); // this fails for some browsers. :( if (originalConsoleMethod) { originalConsoleMethod.apply(global$5.console, args); } }; }); }); } /** JSDoc */ function instrumentFetch() { if (!supportsNativeFetch()) { return; } fill(global$5, 'fetch', function (originalFetch) { return function (...args) { const handlerData = { args, fetchData: { method: getFetchMethod(args), url: getFetchUrl(args), }, startTimestamp: Date.now(), }; triggerHandlers('fetch', Object.assign({}, handlerData)); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return originalFetch.apply(global$5, args).then((response) => { triggerHandlers('fetch', Object.assign(Object.assign({}, handlerData), { endTimestamp: Date.now(), response })); return response; }, (error) => { triggerHandlers('fetch', Object.assign(Object.assign({}, handlerData), { endTimestamp: Date.now(), error })); // NOTE: If you are a Sentry user, and you are seeing this stack frame, // it means the sentry.javascript SDK caught an error invoking your application code. // This is expected behavior and NOT indicative of a bug with sentry.javascript. throw error; }); }; }); } /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /** Extract `method` from fetch call arguments */ function getFetchMethod(fetchArgs = []) { if ('Request' in global$5 && isInstanceOf(fetchArgs[0], Request) && fetchArgs[0].method) { return String(fetchArgs[0].method).toUpperCase(); } if (fetchArgs[1] && fetchArgs[1].method) { return String(fetchArgs[1].method).toUpperCase(); } return 'GET'; } /** Extract `url` from fetch call arguments */ function getFetchUrl(fetchArgs = []) { if (typeof fetchArgs[0] === 'string') { return fetchArgs[0]; } if ('Request' in global$5 && isInstanceOf(fetchArgs[0], Request)) { return fetchArgs[0].url; } return String(fetchArgs[0]); } /* eslint-enable @typescript-eslint/no-unsafe-member-access */ /** JSDoc */ function instrumentXHR() { if (!('XMLHttpRequest' in global$5)) { return; } const xhrproto = XMLHttpRequest.prototype; fill(xhrproto, 'open', function (originalOpen) { return function (...args) { // eslint-disable-next-line @typescript-eslint/no-this-alias const xhr = this; const url = args[1]; const xhrInfo = (xhr.__sentry_xhr__ = { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access method: isString(args[0]) ? args[0].toUpperCase() : args[0], url: args[1], }); // if Sentry key appears in URL, don't capture it as a request // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (isString(url) && xhrInfo.method === 'POST' && url.match(/sentry_key/)) { xhr.__sentry_own_request__ = true; } const onreadystatechangeHandler = function () { if (xhr.readyState === 4) { try { // touching statusCode in some platforms throws // an exception xhrInfo.status_code = xhr.status; } catch (e) { /* do nothing */ } triggerHandlers('xhr', { args, endTimestamp: Date.now(), startTimestamp: Date.now(), xhr, }); } }; if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') { fill(xhr, 'onreadystatechange', function (original) { return function (...readyStateArgs) { onreadystatechangeHandler(); return original.apply(xhr, readyStateArgs); }; }); } else { xhr.addEventListener('readystatechange', onreadystatechangeHandler); } return originalOpen.apply(xhr, args); }; }); fill(xhrproto, 'send', function (originalSend) { return function (...args) { if (this.__sentry_xhr__ && args[0] !== undefined) { this.__sentry_xhr__.body = args[0]; } triggerHandlers('xhr', { args, startTimestamp: Date.now(), xhr: this, }); return originalSend.apply(this, args); }; }); } let lastHref; /** JSDoc */ function instrumentHistory() { if (!supportsHistory()) { return; } const oldOnPopState = global$5.onpopstate; global$5.onpopstate = function (...args) { const to = global$5.location.href; // keep track of the current URL state, as we always receive only the updated state const from = lastHref; lastHref = to; triggerHandlers('history', { from, to, }); if (oldOnPopState) { // Apparently this can throw in Firefox when incorrectly implemented plugin is installed. // https://github.com/getsentry/sentry-javascript/issues/3344 // https://github.com/bugsnag/bugsnag-js/issues/469 try { return oldOnPopState.apply(this, args); } catch (_oO) { // no-empty } } }; /** @hidden */ function historyReplacementFunction(originalHistoryFunction) { return function (...args) { const url = args.length > 2 ? args[2] : undefined; if (url) { // coerce to string (this is what pushState does) const from = lastHref; const to = String(url); // keep track of the current URL state, as we always receive only the updated state lastHref = to; triggerHandlers('history', { from, to, }); } return originalHistoryFunction.apply(this, args); }; } fill(global$5.history, 'pushState', historyReplacementFunction); fill(global$5.history, 'replaceState', historyReplacementFunction); } const debounceDuration = 1000; let debounceTimerID; let lastCapturedEvent; /** * Decide whether the current event should finish the debounce of previously captured one. * @param previous previously captured event * @param current event to be captured */ function shouldShortcircuitPreviousDebounce(previous, current) { // If there was no previous event, it should always be swapped for the new one. if (!previous) { return true; } // If both events have different type, then user definitely performed two separate actions. e.g. click + keypress. if (previous.type !== current.type) { return true; } try { // If both events have the same type, it's still possible that actions were performed on different targets. // e.g. 2 clicks on different buttons. if (previous.target !== current.target) { return true; } } catch (e) { // just accessing `target` property can throw an exception in some rare circumstances // see: https://github.com/getsentry/sentry-javascript/issues/838 } // If both events have the same type _and_ same `target` (an element which triggered an event, _not necessarily_ // to which an event listener was attached), we treat them as the same action, as we want to capture // only one breadcrumb. e.g. multiple clicks on the same button, or typing inside a user input box. return false; } /** * Decide whether an event should be captured. * @param event event to be captured */ function shouldSkipDOMEvent(event) { // We are only interested in filtering `keypress` events for now. if (event.type !== 'keypress') { return false; } try { const target = event.target; if (!target || !target.tagName) { return true; } // Only consider keypress events on actual input elements. This will disregard keypresses targeting body // e.g.tabbing through elements, hotkeys, etc. if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { return false; } } catch (e) { // just accessing `target` property can throw an exception in some rare circumstances // see: https://github.com/getsentry/sentry-javascript/issues/838 } return true; } /** * Wraps addEventListener to capture UI breadcrumbs * @param handler function that will be triggered * @param globalListener indicates whether event was captured by the global event listener * @returns wrapped breadcrumb events handler * @hidden */ function makeDOMEventHandler(handler, globalListener = false) { return (event) => { // It's possible this handler might trigger multiple times for the same // event (e.g. event propagation through node ancestors). // Ignore if we've already captured that event. if (!event || lastCapturedEvent === event) { return; } // We always want to skip _some_ events. if (shouldSkipDOMEvent(event)) { return; } const name = event.type === 'keypress' ? 'input' : event.type; // If there is no debounce timer, it means that we can safely capture the new event and store it for future comparisons. if (debounceTimerID === undefined) { handler({ event: event, name, global: globalListener, }); lastCapturedEvent = event; } // If there is a debounce awaiting, see if the new event is different enough to treat it as a unique one. // If that's the case, emit the previous event and store locally the newly-captured DOM event. else if (shouldShortcircuitPreviousDebounce(lastCapturedEvent, event)) { handler({ event: event, name, global: globalListener, }); lastCapturedEvent = event; } // Start a new debounce timer that will prevent us from capturing multiple events that should be grouped together. clearTimeout(debounceTimerID); debounceTimerID = global$5.setTimeout(() => { debounceTimerID = undefined; }, debounceDuration); }; } /** JSDoc */ function instrumentDOM() { if (!('document' in global$5)) { return; } // Make it so that any click or keypress that is unhandled / bubbled up all the way to the document triggers our dom // handlers. (Normally we have only one, which captures a breadcrumb for each click or keypress.) Do this before // we instrument `addEventListener` so that we don't end up attaching this handler twice. const triggerDOMHandler = triggerHandlers.bind(null, 'dom'); const globalDOMEventHandler = makeDOMEventHandler(triggerDOMHandler, true); global$5.document.addEventListener('click', globalDOMEventHandler, false); global$5.document.addEventListener('keypress', globalDOMEventHandler, false); // After hooking into click and keypress events bubbled up to `document`, we also hook into user-handled // clicks & keypresses, by adding an event listener of our own to any element to which they add a listener. That // way, whenever one of their handlers is triggered, ours will be, too. (This is needed because their handler // could potentially prevent the event from bubbling up to our global listeners. This way, our handler are still // guaranteed to fire at least once.) ['EventTarget', 'Node'].forEach((target) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const proto = global$5[target] && global$5[target].prototype; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-prototype-builtins if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) { retu