forked from off-topic/apps.apple.com
init commit
This commit is contained in:
631
shared/logger/node_modules/@sentry/utils/esm/instrument.js
generated
vendored
Normal file
631
shared/logger/node_modules/@sentry/utils/esm/instrument.js
generated
vendored
Normal file
@@ -0,0 +1,631 @@
|
||||
import { isString } from './is.js';
|
||||
import { logger, CONSOLE_LEVELS } from './logger.js';
|
||||
import { fill } from './object.js';
|
||||
import { getFunctionName } from './stacktrace.js';
|
||||
import { supportsNativeFetch } from './supports.js';
|
||||
import { getGlobalObject } from './worldwide.js';
|
||||
import { supportsHistory } from './vendor/supportsHistory.js';
|
||||
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
const WINDOW = getGlobalObject();
|
||||
|
||||
const SENTRY_XHR_DATA_KEY = '__sentry_xhr_v2__';
|
||||
|
||||
/**
|
||||
* 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:
|
||||
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && 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) {
|
||||
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
|
||||
logger.error(
|
||||
`Error while triggering instrumentation handler.\nType: ${type}\nName: ${getFunctionName(handler)}\nError:`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** JSDoc */
|
||||
function instrumentConsole() {
|
||||
if (!('console' in WINDOW)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CONSOLE_LEVELS.forEach(function (level) {
|
||||
if (!(level in WINDOW.console)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fill(WINDOW.console, level, function (originalConsoleMethod) {
|
||||
return function (...args) {
|
||||
triggerHandlers('console', { args, level });
|
||||
|
||||
// this fails for some browsers. :(
|
||||
if (originalConsoleMethod) {
|
||||
originalConsoleMethod.apply(WINDOW.console, args);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** JSDoc */
|
||||
function instrumentFetch() {
|
||||
if (!supportsNativeFetch()) {
|
||||
return;
|
||||
}
|
||||
|
||||
fill(WINDOW, 'fetch', function (originalFetch) {
|
||||
return function (...args) {
|
||||
const { method, url } = parseFetchArgs(args);
|
||||
|
||||
const handlerData = {
|
||||
args,
|
||||
fetchData: {
|
||||
method,
|
||||
url,
|
||||
},
|
||||
startTimestamp: Date.now(),
|
||||
};
|
||||
|
||||
triggerHandlers('fetch', {
|
||||
...handlerData,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
return originalFetch.apply(WINDOW, args).then(
|
||||
(response) => {
|
||||
triggerHandlers('fetch', {
|
||||
...handlerData,
|
||||
endTimestamp: Date.now(),
|
||||
response,
|
||||
});
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
triggerHandlers('fetch', {
|
||||
...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;
|
||||
},
|
||||
);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function hasProp(obj, prop) {
|
||||
return !!obj && typeof obj === 'object' && !!(obj )[prop];
|
||||
}
|
||||
|
||||
function getUrlFromResource(resource) {
|
||||
if (typeof resource === 'string') {
|
||||
return resource;
|
||||
}
|
||||
|
||||
if (!resource) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (hasProp(resource, 'url')) {
|
||||
return resource.url;
|
||||
}
|
||||
|
||||
if (resource.toString) {
|
||||
return resource.toString();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the fetch arguments to find the used Http method and the url of the request
|
||||
*/
|
||||
function parseFetchArgs(fetchArgs) {
|
||||
if (fetchArgs.length === 0) {
|
||||
return { method: 'GET', url: '' };
|
||||
}
|
||||
|
||||
if (fetchArgs.length === 2) {
|
||||
const [url, options] = fetchArgs ;
|
||||
|
||||
return {
|
||||
url: getUrlFromResource(url),
|
||||
method: hasProp(options, 'method') ? String(options.method).toUpperCase() : 'GET',
|
||||
};
|
||||
}
|
||||
|
||||
const arg = fetchArgs[0];
|
||||
return {
|
||||
url: getUrlFromResource(arg ),
|
||||
method: hasProp(arg, 'method') ? String(arg.method).toUpperCase() : 'GET',
|
||||
};
|
||||
}
|
||||
|
||||
/** JSDoc */
|
||||
function instrumentXHR() {
|
||||
if (!('XMLHttpRequest' in WINDOW)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const xhrproto = XMLHttpRequest.prototype;
|
||||
|
||||
fill(xhrproto, 'open', function (originalOpen) {
|
||||
return function ( ...args) {
|
||||
const url = args[1];
|
||||
const xhrInfo = (this[SENTRY_XHR_DATA_KEY] = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
method: isString(args[0]) ? args[0].toUpperCase() : args[0],
|
||||
url: args[1],
|
||||
request_headers: {},
|
||||
});
|
||||
|
||||
// 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/)) {
|
||||
this.__sentry_own_request__ = true;
|
||||
}
|
||||
|
||||
const onreadystatechangeHandler = () => {
|
||||
// For whatever reason, this is not the same instance here as from the outer method
|
||||
const xhrInfo = this[SENTRY_XHR_DATA_KEY];
|
||||
|
||||
if (!xhrInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.readyState === 4) {
|
||||
try {
|
||||
// touching statusCode in some platforms throws
|
||||
// an exception
|
||||
xhrInfo.status_code = this.status;
|
||||
} catch (e) {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
triggerHandlers('xhr', {
|
||||
args: args ,
|
||||
endTimestamp: Date.now(),
|
||||
startTimestamp: Date.now(),
|
||||
xhr: this,
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
if ('onreadystatechange' in this && typeof this.onreadystatechange === 'function') {
|
||||
fill(this, 'onreadystatechange', function (original) {
|
||||
return function ( ...readyStateArgs) {
|
||||
onreadystatechangeHandler();
|
||||
return original.apply(this, readyStateArgs);
|
||||
};
|
||||
});
|
||||
} else {
|
||||
this.addEventListener('readystatechange', onreadystatechangeHandler);
|
||||
}
|
||||
|
||||
// Intercepting `setRequestHeader` to access the request headers of XHR instance.
|
||||
// This will only work for user/library defined headers, not for the default/browser-assigned headers.
|
||||
// Request cookies are also unavailable for XHR, as `Cookie` header can't be defined by `setRequestHeader`.
|
||||
fill(this, 'setRequestHeader', function (original) {
|
||||
return function ( ...setRequestHeaderArgs) {
|
||||
const [header, value] = setRequestHeaderArgs ;
|
||||
|
||||
const xhrInfo = this[SENTRY_XHR_DATA_KEY];
|
||||
|
||||
if (xhrInfo) {
|
||||
xhrInfo.request_headers[header.toLowerCase()] = value;
|
||||
}
|
||||
|
||||
return original.apply(this, setRequestHeaderArgs);
|
||||
};
|
||||
});
|
||||
|
||||
return originalOpen.apply(this, args);
|
||||
};
|
||||
});
|
||||
|
||||
fill(xhrproto, 'send', function (originalSend) {
|
||||
return function ( ...args) {
|
||||
const sentryXhrData = this[SENTRY_XHR_DATA_KEY];
|
||||
if (sentryXhrData && args[0] !== undefined) {
|
||||
sentryXhrData.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 = WINDOW.onpopstate;
|
||||
WINDOW.onpopstate = function ( ...args) {
|
||||
const to = WINDOW.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(WINDOW.history, 'pushState', historyReplacementFunction);
|
||||
fill(WINDOW.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 = WINDOW.setTimeout(() => {
|
||||
debounceTimerID = undefined;
|
||||
}, debounceDuration);
|
||||
};
|
||||
}
|
||||
|
||||
/** JSDoc */
|
||||
function instrumentDOM() {
|
||||
if (!('document' in WINDOW)) {
|
||||
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);
|
||||
WINDOW.document.addEventListener('click', globalDOMEventHandler, false);
|
||||
WINDOW.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 = (WINDOW )[target] && (WINDOW )[target].prototype;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-prototype-builtins
|
||||
if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fill(proto, 'addEventListener', function (originalAddEventListener) {
|
||||
return function (
|
||||
|
||||
type,
|
||||
listener,
|
||||
options,
|
||||
) {
|
||||
if (type === 'click' || type == 'keypress') {
|
||||
try {
|
||||
const el = this ;
|
||||
const handlers = (el.__sentry_instrumentation_handlers__ = el.__sentry_instrumentation_handlers__ || {});
|
||||
const handlerForType = (handlers[type] = handlers[type] || { refCount: 0 });
|
||||
|
||||
if (!handlerForType.handler) {
|
||||
const handler = makeDOMEventHandler(triggerDOMHandler);
|
||||
handlerForType.handler = handler;
|
||||
originalAddEventListener.call(this, type, handler, options);
|
||||
}
|
||||
|
||||
handlerForType.refCount++;
|
||||
} catch (e) {
|
||||
// Accessing dom properties is always fragile.
|
||||
// Also allows us to skip `addEventListenrs` calls with no proper `this` context.
|
||||
}
|
||||
}
|
||||
|
||||
return originalAddEventListener.call(this, type, listener, options);
|
||||
};
|
||||
});
|
||||
|
||||
fill(
|
||||
proto,
|
||||
'removeEventListener',
|
||||
function (originalRemoveEventListener) {
|
||||
return function (
|
||||
|
||||
type,
|
||||
listener,
|
||||
options,
|
||||
) {
|
||||
if (type === 'click' || type == 'keypress') {
|
||||
try {
|
||||
const el = this ;
|
||||
const handlers = el.__sentry_instrumentation_handlers__ || {};
|
||||
const handlerForType = handlers[type];
|
||||
|
||||
if (handlerForType) {
|
||||
handlerForType.refCount--;
|
||||
// If there are no longer any custom handlers of the current type on this element, we can remove ours, too.
|
||||
if (handlerForType.refCount <= 0) {
|
||||
originalRemoveEventListener.call(this, type, handlerForType.handler, options);
|
||||
handlerForType.handler = undefined;
|
||||
delete handlers[type]; // eslint-disable-line @typescript-eslint/no-dynamic-delete
|
||||
}
|
||||
|
||||
// If there are no longer any custom handlers of any type on this element, cleanup everything.
|
||||
if (Object.keys(handlers).length === 0) {
|
||||
delete el.__sentry_instrumentation_handlers__;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Accessing dom properties is always fragile.
|
||||
// Also allows us to skip `addEventListenrs` calls with no proper `this` context.
|
||||
}
|
||||
}
|
||||
|
||||
return originalRemoveEventListener.call(this, type, listener, options);
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let _oldOnErrorHandler = null;
|
||||
/** JSDoc */
|
||||
function instrumentError() {
|
||||
_oldOnErrorHandler = WINDOW.onerror;
|
||||
|
||||
WINDOW.onerror = function (msg, url, line, column, error) {
|
||||
triggerHandlers('error', {
|
||||
column,
|
||||
error,
|
||||
line,
|
||||
msg,
|
||||
url,
|
||||
});
|
||||
|
||||
if (_oldOnErrorHandler && !_oldOnErrorHandler.__SENTRY_LOADER__) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
return _oldOnErrorHandler.apply(this, arguments);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
WINDOW.onerror.__SENTRY_INSTRUMENTED__ = true;
|
||||
}
|
||||
|
||||
let _oldOnUnhandledRejectionHandler = null;
|
||||
/** JSDoc */
|
||||
function instrumentUnhandledRejection() {
|
||||
_oldOnUnhandledRejectionHandler = WINDOW.onunhandledrejection;
|
||||
|
||||
WINDOW.onunhandledrejection = function (e) {
|
||||
triggerHandlers('unhandledrejection', e);
|
||||
|
||||
if (_oldOnUnhandledRejectionHandler && !_oldOnUnhandledRejectionHandler.__SENTRY_LOADER__) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
return _oldOnUnhandledRejectionHandler.apply(this, arguments);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
WINDOW.onunhandledrejection.__SENTRY_INSTRUMENTED__ = true;
|
||||
}
|
||||
|
||||
export { SENTRY_XHR_DATA_KEY, addInstrumentationHandler, parseFetchArgs };
|
||||
//# sourceMappingURL=instrument.js.map
|
||||
Reference in New Issue
Block a user