/** * XMLHttpRequest Instrumentation and Wrapper * * Provides comprehensive XMLHttpRequest monitoring and instrumentation: * - Request/response interception * - Method and URL tracking * - Header manipulation monitoring * - Request body capture * - Response status tracking */ const XHR_DATA_KEY = "__sentry_xhr_v3__"; /** * Instrument XMLHttpRequest for monitoring * Creates a wrapper around XHR methods to capture request/response data */ function instrumentXHR() { const globalObj = getGlobalObject(); if (!globalObj.XMLHttpRequest) { return; } const xhrPrototype = XMLHttpRequest.prototype; // Instrument the open method fillPrototype(xhrPrototype, "open", function (originalOpen) { return function instrumentedOpen(...args) { const startTime = Date.now(); const method = isString(args[0]) ? args[0].toUpperCase() : undefined; const url = parseUrl(args[1]); if (!method || !url) { return originalOpen.apply(this, args); } // Store request metadata this[XHR_DATA_KEY] = { method, url, request_headers: {}, }; // Skip instrumentation for Sentry's own requests if (method === "POST" && url.match(/sentry_key/)) { this.__sentry_own_request__ = true; } const onReadyStateChange = () => { const xhrData = this[XHR_DATA_KEY]; if (!xhrData) return; if (this.readyState === 4) { try { xhrData.status_code = this.status; } catch (error) { // Status might not be available } const eventData = { args: [method, url], endTimestamp: Date.now(), startTimestamp: startTime, xhr: this, }; triggerXhrHandlers("xhr", eventData); } }; // Handle readystatechange event if ( "onreadystatechange" in this && typeof this.onreadystatechange === "function" ) { fillPrototype(this, "onreadystatechange", function (originalHandler) { return function wrappedHandler(...handlerArgs) { onReadyStateChange(); return originalHandler.apply(this, handlerArgs); }; }); } else { this.addEventListener("readystatechange", onReadyStateChange); } // Instrument setRequestHeader fillPrototype(this, "setRequestHeader", function (originalSetHeader) { return function instrumentedSetHeader(...headerArgs) { const [headerName, headerValue] = headerArgs; const xhrData = this[XHR_DATA_KEY]; if (xhrData && isString(headerName) && isString(headerValue)) { xhrData.request_headers[headerName.toLowerCase()] = headerValue; } return originalSetHeader.apply(this, headerArgs); }; }); return originalOpen.apply(this, args); }; }); // Instrument the send method fillPrototype(xhrPrototype, "send", function (originalSend) { return function instrumentedSend(...args) { const xhrData = this[XHR_DATA_KEY]; if (!xhrData) { return originalSend.apply(this, args); } // Capture request body if (args[0] !== undefined) { xhrData.body = args[0]; } const eventData = { args: [xhrData.method, xhrData.url], startTimestamp: Date.now(), xhr: this, }; triggerXhrHandlers("xhr", eventData); return originalSend.apply(this, args); }; }); } /** * Parse URL from various input types * @param {any} url - URL to parse * @returns {string|undefined} Parsed URL string */ function parseUrl(url) { if (isString(url)) { return url; } try { return url.toString(); } catch (error) { return undefined; } } /** * Add XHR instrumentation handler * @param {Function} handler - Handler function to call on XHR events */ function addXhrInstrumentationHandler(handler) { addHandler("xhr", handler); maybeInstrument("xhr", instrumentXHR); } /** * Create comprehensive XHR monitoring wrapper * @param {Object} options - Configuration options * @returns {Object} XHR monitoring interface */ function createXhrMonitor(options = {}) { const { onRequest = null, onResponse = null, onError = null, captureBody = true, captureHeaders = true, } = options; const handlers = []; const monitor = { /** * Start monitoring XHR requests */ start() { const handler = (data) => { try { if (data.startTimestamp && !data.endTimestamp) { // Request started if (onRequest) { onRequest({ method: data.args[0], url: data.args[1], timestamp: data.startTimestamp, xhr: data.xhr, }); } } else if (data.endTimestamp) { // Request completed const xhr = data.xhr; const xhrData = xhr[XHR_DATA_KEY]; const responseData = { method: data.args[0], url: data.args[1], startTime: data.startTimestamp, endTime: data.endTimestamp, duration: data.endTimestamp - data.startTimestamp, status: xhrData ? xhrData.status_code : xhr.status, headers: captureHeaders && xhrData ? xhrData.request_headers : {}, body: captureBody && xhrData ? xhrData.body : undefined, response: { status: xhr.status, statusText: xhr.statusText, headers: getResponseHeaders(xhr), }, }; if (xhr.status >= 200 && xhr.status < 300) { if (onResponse) onResponse(responseData); } else { if (onError) onError({ ...responseData, error: new Error(`HTTP ${xhr.status}`), }); } } } catch (error) { if (onError) { onError({ error, data }); } } }; handlers.push(handler); addXhrInstrumentationHandler(handler); }, /** * Stop monitoring XHR requests */ stop() { // Note: Cannot easily remove handlers in current implementation // Would need to modify the handler system to support removal handlers.length = 0; }, /** * Get current configuration */ getConfig() { return { captureBody, captureHeaders, handlersCount: handlers.length, }; }, }; return monitor; } /** * Extract response headers from XHR object * @param {XMLHttpRequest} xhr - XHR object * @returns {Object} Response headers object */ function getResponseHeaders(xhr) { try { const headerString = xhr.getAllResponseHeaders(); if (!headerString) return {}; const headers = {}; const headerLines = headerString.split("\r\n"); for (const line of headerLines) { const colonIndex = line.indexOf(":"); if (colonIndex > 0) { const name = line.slice(0, colonIndex).trim().toLowerCase(); const value = line.slice(colonIndex + 1).trim(); headers[name] = value; } } return headers; } catch (error) { return {}; } } // Helper functions (would normally be imported) function getGlobalObject() { return ( (typeof globalThis === "object" && globalThis) || (typeof window === "object" && window) || (typeof self === "object" && self) || (typeof global === "object" && global) || {} ); } function isString(value) { return typeof value === "string"; } function fillPrototype(source, property, replacer) { if (!(property in source)) return; const original = source[property]; const replacement = replacer(original); if (typeof replacement === "function") { try { replacement.prototype = original.prototype; } catch (error) { // Ignore prototype setting errors } source[property] = replacement; } } // Mock handler system (would normally be imported) let handlers = {}; let instrumented = {}; function addHandler(type, handler) { if (!handlers[type]) { handlers[type] = []; } handlers[type].push(handler); } function maybeInstrument(type, instrumentFunction) { if (!instrumented[type]) { instrumentFunction(); instrumented[type] = true; } } function triggerXhrHandlers(type, data) { const typeHandlers = handlers[type]; if (!typeHandlers) return; for (const handler of typeHandlers) { try { handler(data); } catch (error) { console.error("Error in XHR handler:", error); } } } module.exports = { instrumentXHR, addXhrInstrumentationHandler, createXhrMonitor, parseUrl, getResponseHeaders, XHR_DATA_KEY, };