@sentry/browser
Version:
Official Sentry SDK for browsers
1,326 lines (1,310 loc) • 270 kB
JavaScript
/*! @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