summaryrefslogtreecommitdiff
path: root/shared/logger/node_modules/@sentry/utils/esm/instrument.js
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /shared/logger/node_modules/@sentry/utils/esm/instrument.js
init commit
Diffstat (limited to 'shared/logger/node_modules/@sentry/utils/esm/instrument.js')
-rw-r--r--shared/logger/node_modules/@sentry/utils/esm/instrument.js631
1 files changed, 631 insertions, 0 deletions
diff --git a/shared/logger/node_modules/@sentry/utils/esm/instrument.js b/shared/logger/node_modules/@sentry/utils/esm/instrument.js
new file mode 100644
index 0000000..618cf3c
--- /dev/null
+++ b/shared/logger/node_modules/@sentry/utils/esm/instrument.js
@@ -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