import { __assign, __values } from "tslib"; import { isInstanceOf, isString } from './is'; import { logger } from './logger'; import { getGlobalObject } from './misc'; import { fill } from './object'; import { getFunctionName } from './stacktrace'; import { supportsHistory, supportsNativeFetch } from './supports'; var global = 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 */ var handlers = {}; var 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); } } /** * 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 */ export function addInstrumentationHandler(handler) { if (!handler || typeof handler.type !== 'string' || typeof handler.callback !== 'function') { return; } handlers[handler.type] = handlers[handler.type] || []; handlers[handler.type].push(handler.callback); instrument(handler.type); } /** JSDoc */ function triggerHandlers(type, data) { var e_1, _a; if (!type || !handlers[type]) { return; } try { for (var _b = __values(handlers[type] || []), _c = _b.next(); !_c.done; _c = _b.next()) { var handler = _c.value; try { handler(data); } catch (e) { logger.error("Error while triggering instrumentation handler.\nType: " + type + "\nName: " + getFunctionName(handler) + "\nError: " + e); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } } /** JSDoc */ function instrumentConsole() { if (!('console' in global)) { return; } ['debug', 'info', 'warn', 'error', 'log', 'assert'].forEach(function (level) { if (!(level in global.console)) { return; } fill(global.console, level, function (originalConsoleLevel) { return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } triggerHandlers('console', { args: args, level: level }); // this fails for some browsers. :( if (originalConsoleLevel) { Function.prototype.apply.call(originalConsoleLevel, global.console, args); } }; }); }); } /** JSDoc */ function instrumentFetch() { if (!supportsNativeFetch()) { return; } fill(global, 'fetch', function (originalFetch) { return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } var handlerData = { args: args, fetchData: { method: getFetchMethod(args), url: getFetchUrl(args), }, startTimestamp: Date.now(), }; triggerHandlers('fetch', __assign({}, handlerData)); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return originalFetch.apply(global, args).then(function (response) { triggerHandlers('fetch', __assign(__assign({}, handlerData), { endTimestamp: Date.now(), response: response })); return response; }, function (error) { triggerHandlers('fetch', __assign(__assign({}, handlerData), { endTimestamp: Date.now(), error: 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 (fetchArgs === void 0) { fetchArgs = []; } if ('Request' in global && 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 (fetchArgs === void 0) { fetchArgs = []; } if (typeof fetchArgs[0] === 'string') { return fetchArgs[0]; } if ('Request' in global && 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)) { return; } // Poor man's implementation of ES6 `Map`, tracking and keeping in sync key and value separately. var requestKeys = []; var requestValues = []; var xhrproto = XMLHttpRequest.prototype; fill(xhrproto, 'open', function (originalOpen) { return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } // eslint-disable-next-line @typescript-eslint/no-this-alias var xhr = this; var url = args[1]; 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) && xhr.__sentry_xhr__.method === 'POST' && url.match(/sentry_key/)) { xhr.__sentry_own_request__ = true; } var onreadystatechangeHandler = function () { if (xhr.readyState === 4) { try { // touching statusCode in some platforms throws // an exception if (xhr.__sentry_xhr__) { xhr.__sentry_xhr__.status_code = xhr.status; } } catch (e) { /* do nothing */ } try { var requestPos = requestKeys.indexOf(xhr); if (requestPos !== -1) { // Make sure to pop both key and value to keep it in sync. requestKeys.splice(requestPos); var args_1 = requestValues.splice(requestPos)[0]; if (xhr.__sentry_xhr__ && args_1[0] !== undefined) { xhr.__sentry_xhr__.body = args_1[0]; } } } catch (e) { /* do nothing */ } triggerHandlers('xhr', { args: args, endTimestamp: Date.now(), startTimestamp: Date.now(), xhr: xhr, }); } }; if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') { fill(xhr, 'onreadystatechange', function (original) { return function () { var readyStateArgs = []; for (var _i = 0; _i < arguments.length; _i++) { readyStateArgs[_i] = arguments[_i]; } onreadystatechangeHandler(); return original.apply(xhr, readyStateArgs); }; }); } else { xhr.addEventListener('readystatechange', onreadystatechangeHandler); } return originalOpen.apply(xhr, args); }; }); fill(xhrproto, 'send', function (originalSend) { return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } requestKeys.push(this); requestValues.push(args); triggerHandlers('xhr', { args: args, startTimestamp: Date.now(), xhr: this, }); return originalSend.apply(this, args); }; }); } var lastHref; /** JSDoc */ function instrumentHistory() { if (!supportsHistory()) { return; } var oldOnPopState = global.onpopstate; global.onpopstate = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } var to = global.location.href; // keep track of the current URL state, as we always receive only the updated state var from = lastHref; lastHref = to; triggerHandlers('history', { from: from, to: to, }); if (oldOnPopState) { return oldOnPopState.apply(this, args); } }; /** @hidden */ function historyReplacementFunction(originalHistoryFunction) { return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } var url = args.length > 2 ? args[2] : undefined; if (url) { // coerce to string (this is what pushState does) var from = lastHref; var to = String(url); // keep track of the current URL state, as we always receive only the updated state lastHref = to; triggerHandlers('history', { from: from, to: to, }); } return originalHistoryFunction.apply(this, args); }; } fill(global.history, 'pushState', historyReplacementFunction); fill(global.history, 'replaceState', historyReplacementFunction); } /** JSDoc */ function instrumentDOM() { if (!('document' in global)) { return; } // Capture breadcrumbs from any click that is unhandled / bubbled up all the way // to the document. Do this before we instrument addEventListener. global.document.addEventListener('click', domEventHandler('click', triggerHandlers.bind(null, 'dom')), false); global.document.addEventListener('keypress', keypressEventHandler(triggerHandlers.bind(null, 'dom')), false); // After hooking into document bubbled up click and keypresses events, we also hook into user handled click & keypresses. ['EventTarget', 'Node'].forEach(function (target) { /* eslint-disable @typescript-eslint/no-unsafe-member-access */ var proto = global[target] && global[target].prototype; // eslint-disable-next-line no-prototype-builtins if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) { return; } /* eslint-enable @typescript-eslint/no-unsafe-member-access */ fill(proto, 'addEventListener', function (original) { return function (eventName, fn, options) { if (fn && fn.handleEvent) { if (eventName === 'click') { fill(fn, 'handleEvent', function (innerOriginal) { return function (event) { domEventHandler('click', triggerHandlers.bind(null, 'dom'))(event); return innerOriginal.call(this, event); }; }); } if (eventName === 'keypress') { fill(fn, 'handleEvent', function (innerOriginal) { return function (event) { keypressEventHandler(triggerHandlers.bind(null, 'dom'))(event); return innerOriginal.call(this, event); }; }); } } else { if (eventName === 'click') { domEventHandler('click', triggerHandlers.bind(null, 'dom'), true)(this); } if (eventName === 'keypress') { keypressEventHandler(triggerHandlers.bind(null, 'dom'))(this); } } return original.call(this, eventName, fn, options); }; }); fill(proto, 'removeEventListener', function (original) { return function (eventName, fn, options) { try { original.call(this, eventName, fn.__sentry_wrapped__, options); } catch (e) { // ignore, accessing __sentry_wrapped__ will throw in some Selenium environments } return original.call(this, eventName, fn, options); }; }); }); } var debounceDuration = 1000; var debounceTimer = 0; var keypressTimeout; var lastCapturedEvent; /** * Wraps addEventListener to capture UI breadcrumbs * @param name the event name (e.g. "click") * @param handler function that will be triggered * @param debounce decides whether it should wait till another event loop * @returns wrapped breadcrumb events handler * @hidden */ function domEventHandler(name, handler, debounce) { if (debounce === void 0) { debounce = false; } return function (event) { // reset keypress timeout; e.g. triggering a 'click' after // a 'keypress' will reset the keypress debounce so that a new // set of keypresses can be recorded keypressTimeout = undefined; // 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 the event. if (!event || lastCapturedEvent === event) { return; } lastCapturedEvent = event; if (debounceTimer) { clearTimeout(debounceTimer); } if (debounce) { debounceTimer = setTimeout(function () { handler({ event: event, name: name }); }); } else { handler({ event: event, name: name }); } }; } /** * Wraps addEventListener to capture keypress UI events * @param handler function that will be triggered * @returns wrapped keypress events handler * @hidden */ function keypressEventHandler(handler) { // TODO: if somehow user switches keypress target before // debounce timeout is triggered, we will only capture // a single breadcrumb from the FIRST target (acceptable?) return function (event) { var target; try { target = event.target; } catch (e) { // just accessing event properties can throw an exception in some rare circumstances // see: https://github.com/getsentry/raven-js/issues/838 return; } var tagName = target && target.tagName; // only consider keypress events on actual input elements // this will disregard keypresses targeting body (e.g. tabbing // through elements, hotkeys, etc) if (!tagName || (tagName !== 'INPUT' && tagName !== 'TEXTAREA' && !target.isContentEditable)) { return; } // record first keypress in a series, but ignore subsequent // keypresses until debounce clears if (!keypressTimeout) { domEventHandler('input', handler)(event); } clearTimeout(keypressTimeout); keypressTimeout = setTimeout(function () { keypressTimeout = undefined; }, debounceDuration); }; } var _oldOnErrorHandler = null; /** JSDoc */ function instrumentError() { _oldOnErrorHandler = global.onerror; global.onerror = function (msg, url, line, column, error) { triggerHandlers('error', { column: column, error: error, line: line, msg: msg, url: url, }); if (_oldOnErrorHandler) { // eslint-disable-next-line prefer-rest-params return _oldOnErrorHandler.apply(this, arguments); } return false; }; } var _oldOnUnhandledRejectionHandler = null; /** JSDoc */ function instrumentUnhandledRejection() { _oldOnUnhandledRejectionHandler = global.onunhandledrejection; global.onunhandledrejection = function (e) { triggerHandlers('unhandledrejection', e); if (_oldOnUnhandledRejectionHandler) { // eslint-disable-next-line prefer-rest-params return _oldOnUnhandledRejectionHandler.apply(this, arguments); } return true; }; } //# sourceMappingURL=instrument.js.map