This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-client-js.git


The following commit(s) were added to refs/heads/master by this push:
     new d385d56  refactor: optimize the XHR interceptor to preserve the 
prototype chain by setting the prototype of custom constructor (#164)
d385d56 is described below

commit d385d5644438c705ecdbe8bbeff4edf437c85881
Author: Fine0830 <fanxue0...@gmail.com>
AuthorDate: Thu Jul 31 19:00:17 2025 +0800

    refactor: optimize the XHR interceptor to preserve the prototype chain by 
setting the prototype of custom constructor (#164)
---
 src/trace/interceptors/xhr.ts | 122 ++++++++++++++++++++++++++++--------------
 1 file changed, 83 insertions(+), 39 deletions(-)

diff --git a/src/trace/interceptors/xhr.ts b/src/trace/interceptors/xhr.ts
index 791c2ec..144e14a 100644
--- a/src/trace/interceptors/xhr.ts
+++ b/src/trace/interceptors/xhr.ts
@@ -20,28 +20,35 @@ import { encode } from 'js-base64';
 import { CustomOptionsType } from '../../types';
 import { SegmentFields, SpanFields } from '../type';
 
-let customConfig: CustomOptionsType | any = {};
+interface EnhancedXMLHttpRequest extends XMLHttpRequest {
+  getRequestConfig?: any;
+}
+
+let customConfig: CustomOptionsType = {} as CustomOptionsType;
 export default function xhrInterceptor(options: CustomOptionsType, segments: 
SegmentFields[]) {
   setOptions(options);
   const originalXHR = window.XMLHttpRequest as any;
-  const xhrSend = XMLHttpRequest.prototype.send;
-  const xhrOpen = XMLHttpRequest.prototype.open;
 
-  if (!(xhrSend && xhrOpen)) {
-    console.error('Tracing is not supported');
+  if (!originalXHR || !originalXHR.prototype || !originalXHR.prototype.open || 
!originalXHR.prototype.send) {
+    console.error('Tracing is not supported - XMLHttpRequest not available');
     return;
   }
-  originalXHR.getRequestConfig = [];
 
-  function ajaxEventTrigger(event: string) {
+  function ajaxEventTrigger(this: EnhancedXMLHttpRequest, event: string): void 
{
     const ajaxEvent = new CustomEvent(event, { detail: this });
 
     window.dispatchEvent(ajaxEvent);
   }
 
-  function customizedXHR() {
+  function customizedXHR(): EnhancedXMLHttpRequest {
+    // Create a new XMLHttpRequest instance using the original constructor
     const liveXHR = new originalXHR();
 
+    // Store the original methods before overriding
+    const originalOpen = liveXHR.open;
+    const originalSend = liveXHR.send;
+
+    // Add the readystatechange event listener for tracing
     liveXHR.addEventListener(
       'readystatechange',
       function () {
@@ -50,6 +57,7 @@ export default function xhrInterceptor(options: 
CustomOptionsType, segments: Seg
       false,
     );
 
+    // Override the open method to capture request configuration
     liveXHR.open = function (
       method: string,
       url: string,
@@ -57,22 +65,46 @@ export default function xhrInterceptor(options: 
CustomOptionsType, segments: Seg
       username?: string | null,
       password?: string | null,
     ) {
-      this.getRequestConfig = arguments;
-
-      return xhrOpen.apply(this, arguments);
+      // Store the request configuration for later use in tracing
+      (this as EnhancedXMLHttpRequest).getRequestConfig = arguments;
+      
+      // Call the original open method with the correct context
+      return originalOpen.apply(this, arguments);
     };
+
+    // Override the send method and keeping original functionality
     liveXHR.send = function (body?: Document | BodyInit | null) {
-      return xhrSend.apply(this, arguments);
+      return originalSend.apply(this, arguments);
     };
 
     return liveXHR;
   }
 
+  // Preserve the prototype chain by setting the prototype of our custom 
constructor
+  Object.defineProperty(customizedXHR, 'prototype', {
+    value: originalXHR.prototype,
+    writable: false,
+    configurable: false,
+    enumerable: false,
+  });
+  
+  // Ensure the constructor property points to our custom constructor and make 
it non-writable
+  Object.defineProperty(customizedXHR.prototype, 'constructor', {
+    value: customizedXHR,
+    writable: true,
+    configurable: true,
+    enumerable: false,
+  });
+
+  // Set the prototype of our custom constructor to inherit static properties 
from original XMLHttpRequest
+  // This automatically provides access to all static properties like UNSENT, 
OPENED, etc.
+  Object.setPrototypeOf(customizedXHR, originalXHR);
+
   (window as any).XMLHttpRequest = customizedXHR;
 
-  const segCollector: { event: XMLHttpRequest; startTime: number; traceId: 
string; traceSegmentId: string }[] = [];
+  const segCollector: { event: EnhancedXMLHttpRequest; startTime: number; 
traceId: string; traceSegmentId: string }[] = [];
 
-  window.addEventListener('xhrReadyStateChange', (event: 
CustomEvent<XMLHttpRequest & { getRequestConfig: any[] }>) => {
+  window.addEventListener('xhrReadyStateChange', (event: 
CustomEvent<EnhancedXMLHttpRequest>) => {
     let segment = {
       traceId: '',
       service: customConfig.service,
@@ -82,27 +114,38 @@ export default function xhrInterceptor(options: 
CustomOptionsType, segments: Seg
     } as SegmentFields;
     const xhrState = event.detail.readyState;
     const config = event.detail.getRequestConfig;
-    let url = {} as URL;
-    if (config[1].startsWith('http://') || config[1].startsWith('https://')) {
-      url = new URL(config[1]);
-    } else if (config[1].startsWith('//')) {
-      url = new URL(`${window.location.protocol}${config[1]}`);
-    } else {
-      url = new URL(window.location.href);
-      url.pathname = config[1];
+    if (!config || config.length < 2) {
+      return;
+    }
+    let url: URL;
+    
+    const urlString = config[1] as string;
+    if (!urlString) {
+      return;
+    }
+    
+    try {
+      if (urlString.startsWith('http://') || urlString.startsWith('https://')) 
{
+        url = new URL(urlString);
+      } else if (urlString.startsWith('//')) {
+        url = new URL(`${window.location.protocol}${urlString}`);
+      } else {
+        url = new URL(window.location.href);
+        url.pathname = urlString;
+      }
+    } catch (error) {
+      console.warn('Invalid URL in XHR request:', urlString, error);
+      return;
     }
 
-    const noTraceOrigins = customConfig.noTraceOrigins.some((rule: string | 
RegExp) => {
+    const noTraceOrigins = customConfig.noTraceOrigins?.some((rule: string | 
RegExp): boolean => {
       if (typeof rule === 'string') {
-        if (rule === url.origin) {
-          return true;
-        }
+        return rule === url.origin;
       } else if (rule instanceof RegExp) {
-        if (rule.test(url.origin)) {
-          return true;
-        }
+        return rule.test(url.origin);
       }
-    });
+      return false;
+    }) || false;
     if (noTraceOrigins) {
       return;
     }
@@ -144,9 +187,13 @@ export default function xhrInterceptor(options: 
CustomOptionsType, segments: Seg
       const endTime = new Date().getTime();
       for (let i = 0; i < segCollector.length; i++) {
         if (segCollector[i].event.readyState === ReadyStatus.DONE) {
-          let responseURL = {} as URL;
-          if (segCollector[i].event.status) {
-            responseURL = new URL(segCollector[i].event.responseURL);
+          let responseURL: URL | null = null;
+          if (segCollector[i].event.status && 
segCollector[i].event.responseURL) {
+            try {
+              responseURL = new URL(segCollector[i].event.responseURL);
+            } catch (error) {
+              console.warn('Invalid response URL:', 
segCollector[i].event.responseURL, error);
+            }
           }
           const tags = [
             {
@@ -158,6 +205,7 @@ export default function xhrInterceptor(options: 
CustomOptionsType, segments: Seg
               value: segCollector[i].event.responseURL || 
`${url.protocol}//${url.host}${url.pathname}`,
             },
           ];
+          const combinedTags = customConfig.detailMode ? [...tags, 
...(customConfig.customTags || [])] : undefined;
           const exitSpan: SpanFields = {
             operationName: customConfig.pagePath,
             startTime: segCollector[i].startTime,
@@ -168,12 +216,8 @@ export default function xhrInterceptor(options: 
CustomOptionsType, segments: Seg
             isError: event.detail.status === 0 || event.detail.status >= 400, 
// when requests failed, the status is 0
             parentSpanId: segment.spans.length - 1,
             componentId: ComponentId,
-            peer: responseURL.host,
-            tags: customConfig.detailMode
-              ? customConfig.customTags
-                ? [...tags, ...customConfig.customTags]
-                : tags
-              : undefined,
+            peer: responseURL?.host || url.host,
+            tags: combinedTags,
           };
           segment = {
             ...segment,

Reply via email to