feat: get response.body as a stream with the fetch API (#48)

diff --git a/dist/licenses/LICENSES-fetch.txt b/dist/licenses/LICENSES-fetch.txt
deleted file mode 100644
index 0e319d5..0000000
--- a/dist/licenses/LICENSES-fetch.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-Copyright (c) 2014-2016 GitHub, Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/package-lock.json b/package-lock.json
index d02c275..ff58732 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7636,11 +7636,6 @@
       "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
       "dev": true
     },
-    "whatwg-fetch": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npm.taobao.org/whatwg-fetch/download/whatwg-fetch-3.5.0.tgz",
-      "integrity": "sha1-YFos0KcUbl2xQeKdHGKrhMDEyGg="
-    },
     "which": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
diff --git a/package.json b/package.json
index 7aa409a..d1de945 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,6 @@
     "web-performance"
   ],
   "dependencies": {
-    "js-base64": "^3.6.0",
-    "whatwg-fetch": "^3.5.0"
+    "js-base64": "^3.6.0"
   }
 }
diff --git a/src/interceptors/fetch.js b/src/interceptors/fetch.js
deleted file mode 100644
index 62b9ea6..0000000
--- a/src/interceptors/fetch.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { fetch } from 'whatwg-fetch';
-export default function windowFetch() {
-  window.fetch = fetch;
-}
diff --git a/src/interceptors/xhr.ts b/src/interceptors/xhr.ts
deleted file mode 100644
index 5c55030..0000000
--- a/src/interceptors/xhr.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export default function xhrInterceptor() {
-  const originalXHR = window.XMLHttpRequest as any;
-  const xhrSend = XMLHttpRequest.prototype.send;
-  const xhrOpen = XMLHttpRequest.prototype.open;
-
-  originalXHR.getRequestConfig = [];
-
-  function ajaxEventTrigger(event: string) {
-    const ajaxEvent = new CustomEvent(event, { detail: this });
-
-    window.dispatchEvent(ajaxEvent);
-  }
-
-  function customizedXHR() {
-    const liveXHR = new originalXHR();
-
-    liveXHR.addEventListener(
-      'readystatechange',
-      function () {
-        ajaxEventTrigger.call(this, 'xhrReadyStateChange');
-      },
-      false,
-    );
-
-    liveXHR.open = function (
-      method: string,
-      url: string,
-      async: boolean,
-      username?: string | null,
-      password?: string | null,
-    ) {
-      this.getRequestConfig = arguments;
-
-      return xhrOpen.apply(this, arguments);
-    };
-    liveXHR.send = function (body?: Document | BodyInit | null) {
-      return xhrSend.apply(this, arguments);
-    };
-
-    return liveXHR;
-  }
-  (window as any).XMLHttpRequest = customizedXHR;
-}
diff --git a/src/trace/interceptors/fetch.ts b/src/trace/interceptors/fetch.ts
new file mode 100644
index 0000000..5d946ed
--- /dev/null
+++ b/src/trace/interceptors/fetch.ts
@@ -0,0 +1,118 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { encode } from 'js-base64';
+import uuid from '../../services/uuid';
+import { SegmentFeilds, SpanFeilds } from '../type';
+import { CustomOptionsType } from '../../types';
+import { SpanLayer, SpanType, ComponentId, ServiceTag, ReportTypes } from '../../services/constant';
+
+export default function windowFetch(options: CustomOptionsType, segments: SegmentFeilds[]) {
+  const fetch: any = window.fetch;
+  let segment = {
+    traceId: '',
+    service: options.service + ServiceTag,
+    spans: [],
+    serviceInstance: options.serviceVersion,
+    traceSegmentId: '',
+  } as SegmentFeilds;
+  let url = {} as URL;
+
+  window.fetch = (...args) =>
+    (async (args: any) => {
+      const startTime = new Date().getTime();
+      const traceId = uuid();
+      const traceSegmentId = uuid();
+
+      if (args[0].startsWith('http://') || args[0].startsWith('https://')) {
+        url = new URL(args[0]);
+      } else if (args[0].startsWith('//')) {
+        url = new URL(`${window.location.protocol}${args[0]}`);
+      } else {
+        url = new URL(window.location.href);
+        url.pathname = args[0];
+      }
+
+      const noTrace = options.noTraceOrigins.some((rule: string | RegExp) => {
+        if (typeof rule === 'string') {
+          if (rule === url.origin) {
+            return true;
+          }
+        } else if (rule instanceof RegExp) {
+          if (rule.test(url.origin)) {
+            return true;
+          }
+        }
+      });
+      const hasTrace = !(
+        noTrace ||
+        (([ReportTypes.ERROR, ReportTypes.PERF, ReportTypes.SEGMENTS] as string[]).includes(url.pathname) &&
+          !options.traceSDKInternal)
+      );
+
+      if (hasTrace) {
+        const traceIdStr = String(encode(traceId));
+        const segmentId = String(encode(traceSegmentId));
+        const service = String(encode(segment.service));
+        const instance = String(encode(segment.serviceInstance));
+        const endpoint = String(encode(options.pagePath));
+        const peer = String(encode(url.host));
+        const index = segment.spans.length;
+        const values = `${1}-${traceIdStr}-${segmentId}-${index}-${service}-${instance}-${endpoint}-${peer}`;
+
+        args[1].headers['sw8'] = values;
+      }
+
+      const result = await fetch(...args);
+
+      if (hasTrace) {
+        const endTime = new Date().getTime();
+        const exitSpan: SpanFeilds = {
+          operationName: options.pagePath,
+          startTime: startTime,
+          endTime,
+          spanId: segment.spans.length,
+          spanLayer: SpanLayer,
+          spanType: SpanType,
+          isError: result.status === 0 || result.status >= 400 ? true : false, // when requests failed, the status is 0
+          parentSpanId: segment.spans.length - 1,
+          componentId: ComponentId,
+          peer: result.url.host,
+          tags: options.detailMode
+            ? [
+                {
+                  key: 'http.method',
+                  value: args[1].method,
+                },
+                {
+                  key: 'url',
+                  value: result.url,
+                },
+              ]
+            : undefined,
+        };
+        segment = {
+          ...segment,
+          traceId: traceId,
+          traceSegmentId: traceSegmentId,
+        };
+        segment.spans.push(exitSpan);
+        segments.push(segment);
+      }
+
+      return result;
+    })(args);
+}
diff --git a/src/trace/interceptors/xhr.ts b/src/trace/interceptors/xhr.ts
new file mode 100644
index 0000000..1274f51
--- /dev/null
+++ b/src/trace/interceptors/xhr.ts
@@ -0,0 +1,178 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { SpanLayer, SpanType, ReadyStatus, ComponentId, ServiceTag, ReportTypes } from '../../services/constant';
+import uuid from '../../services/uuid';
+import { encode } from 'js-base64';
+import { CustomOptionsType } from '../../types';
+import { SegmentFeilds, SpanFeilds } from '../type';
+
+export default function xhrInterceptor(options: CustomOptionsType, segments: SegmentFeilds[]) {
+  const originalXHR = window.XMLHttpRequest as any;
+  const xhrSend = XMLHttpRequest.prototype.send;
+  const xhrOpen = XMLHttpRequest.prototype.open;
+
+  originalXHR.getRequestConfig = [];
+
+  function ajaxEventTrigger(event: string) {
+    const ajaxEvent = new CustomEvent(event, { detail: this });
+
+    window.dispatchEvent(ajaxEvent);
+  }
+
+  function customizedXHR() {
+    const liveXHR = new originalXHR();
+
+    liveXHR.addEventListener(
+      'readystatechange',
+      function () {
+        ajaxEventTrigger.call(this, 'xhrReadyStateChange');
+      },
+      false,
+    );
+
+    liveXHR.open = function (
+      method: string,
+      url: string,
+      async: boolean,
+      username?: string | null,
+      password?: string | null,
+    ) {
+      this.getRequestConfig = arguments;
+
+      return xhrOpen.apply(this, arguments);
+    };
+    liveXHR.send = function (body?: Document | BodyInit | null) {
+      return xhrSend.apply(this, arguments);
+    };
+
+    return liveXHR;
+  }
+  (window as any).XMLHttpRequest = customizedXHR;
+
+  const segCollector: { event: XMLHttpRequest; startTime: number; traceId: string; traceSegmentId: string }[] = [];
+
+  window.addEventListener('xhrReadyStateChange', (event: CustomEvent<XMLHttpRequest & { getRequestConfig: any[] }>) => {
+    let segment = {
+      traceId: '',
+      service: options.service + ServiceTag,
+      spans: [],
+      serviceInstance: options.serviceVersion,
+      traceSegmentId: '',
+    } as SegmentFeilds;
+    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];
+    }
+
+    const noTrace = options.noTraceOrigins.some((rule: string | RegExp) => {
+      if (typeof rule === 'string') {
+        if (rule === url.origin) {
+          return true;
+        }
+      } else if (rule instanceof RegExp) {
+        if (rule.test(url.origin)) {
+          return true;
+        }
+      }
+    });
+    if (noTrace) {
+      return;
+    }
+
+    if (
+      ([ReportTypes.ERROR, ReportTypes.PERF, ReportTypes.SEGMENTS] as string[]).includes(url.pathname) &&
+      !options.traceSDKInternal
+    ) {
+      return;
+    }
+
+    // The values of xhrState are from https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState
+    if (xhrState === ReadyStatus.OPENED) {
+      const traceId = uuid();
+      const traceSegmentId = uuid();
+
+      segCollector.push({
+        event: event.detail,
+        startTime: new Date().getTime(),
+        traceId,
+        traceSegmentId,
+      });
+
+      const traceIdStr = String(encode(traceId));
+      const segmentId = String(encode(traceSegmentId));
+      const service = String(encode(segment.service));
+      const instance = String(encode(segment.serviceInstance));
+      const endpoint = String(encode(options.pagePath));
+      const peer = String(encode(url.host));
+      const index = segment.spans.length;
+      const values = `${1}-${traceIdStr}-${segmentId}-${index}-${service}-${instance}-${endpoint}-${peer}`;
+
+      event.detail.setRequestHeader('sw8', values);
+    }
+
+    if (xhrState === ReadyStatus.DONE) {
+      const endTime = new Date().getTime();
+      for (let i = 0; i < segCollector.length; i++) {
+        if (segCollector[i].event.readyState === ReadyStatus.DONE) {
+          let url = {} as URL;
+          if (segCollector[i].event.status) {
+            url = new URL(segCollector[i].event.responseURL);
+          }
+          const exitSpan: SpanFeilds = {
+            operationName: options.pagePath,
+            startTime: segCollector[i].startTime,
+            endTime,
+            spanId: segment.spans.length,
+            spanLayer: SpanLayer,
+            spanType: SpanType,
+            isError: event.detail.status === 0 || event.detail.status >= 400 ? true : false, // when requests failed, the status is 0
+            parentSpanId: segment.spans.length - 1,
+            componentId: ComponentId,
+            peer: url.host,
+            tags: options.detailMode
+              ? [
+                  {
+                    key: 'http.method',
+                    value: config[0],
+                  },
+                  {
+                    key: 'url',
+                    value: segCollector[i].event.responseURL,
+                  },
+                ]
+              : undefined,
+          };
+          segment = {
+            ...segment,
+            traceId: segCollector[i].traceId,
+            traceSegmentId: segCollector[i].traceSegmentId,
+          };
+          segment.spans.push(exitSpan);
+          segCollector.splice(i, 1);
+        }
+      }
+      segments.push(segment);
+    }
+  });
+}
diff --git a/src/trace/segment.ts b/src/trace/segment.ts
index 083fbb1..e050daf 100644
--- a/src/trace/segment.ts
+++ b/src/trace/segment.ts
@@ -14,131 +14,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { encode } from 'js-base64';
-import xhrInterceptor from '../interceptors/xhr';
-import uuid from '../services/uuid';
+
+import xhrInterceptor from './interceptors/xhr';
+import windowFetch from './interceptors/fetch';
 import Report from '../services/report';
-import { SegmentFeilds, SpanFeilds } from './type';
-import { SpanLayer, SpanType, ReadyStatus, ComponentId, ServiceTag, ReportTypes } from '../services/constant';
+import { SegmentFeilds } from './type';
 import { CustomOptionsType } from '../types';
-import windowFetch from '../interceptors/fetch';
 
 export default function traceSegment(options: CustomOptionsType) {
   let segments = [] as SegmentFeilds[];
-  const segCollector: { event: XMLHttpRequest; startTime: number; traceId: string; traceSegmentId: string }[] = [];
   // inject interceptor
-  xhrInterceptor();
-  windowFetch();
-  window.addEventListener('xhrReadyStateChange', (event: CustomEvent<XMLHttpRequest & { getRequestConfig: any[] }>) => {
-    let segment = {
-      traceId: '',
-      service: options.service + ServiceTag,
-      spans: [],
-      serviceInstance: options.serviceVersion,
-      traceSegmentId: '',
-    } as SegmentFeilds;
-    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];
-    }
+  xhrInterceptor(options, segments);
+  windowFetch(options, segments);
 
-    const noTrace = options.noTraceOrigins.some((rule) => {
-      if (typeof rule === 'string') {
-        if (rule === url.origin) {
-          return true;
-        }
-      } else if (rule instanceof RegExp) {
-        if (rule.test(url.origin)) {
-          return true;
-        }
-      }
-    });
-    if (noTrace) {
-      return;
-    }
-
-    if (
-      ([ReportTypes.ERROR, ReportTypes.PERF, ReportTypes.SEGMENTS] as string[]).includes(url.pathname) &&
-      !options.traceSDKInternal
-    ) {
-      return;
-    }
-
-    // The values of xhrState are from https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState
-    if (xhrState === ReadyStatus.OPENED) {
-      const traceId = uuid();
-      const traceSegmentId = uuid();
-
-      segCollector.push({
-        event: event.detail,
-        startTime: new Date().getTime(),
-        traceId,
-        traceSegmentId,
-      });
-
-      const traceIdStr = String(encode(traceId));
-      const segmentId = String(encode(traceSegmentId));
-      const service = String(encode(segment.service));
-      const instance = String(encode(segment.serviceInstance));
-      const endpoint = String(encode(options.pagePath));
-      const peer = String(encode(url.host));
-      const index = segment.spans.length;
-      const values = `${1}-${traceIdStr}-${segmentId}-${index}-${service}-${instance}-${endpoint}-${peer}`;
-
-      event.detail.setRequestHeader('sw8', values);
-    }
-
-    if (xhrState === ReadyStatus.DONE) {
-      const endTime = new Date().getTime();
-      for (let i = 0; i < segCollector.length; i++) {
-        if (segCollector[i].event.readyState === ReadyStatus.DONE) {
-          let url = {} as URL;
-          if (segCollector[i].event.status) {
-            url = new URL(segCollector[i].event.responseURL);
-          }
-          const exitSpan: SpanFeilds = {
-            operationName: options.pagePath,
-            startTime: segCollector[i].startTime,
-            endTime,
-            spanId: segment.spans.length,
-            spanLayer: SpanLayer,
-            spanType: SpanType,
-            isError: event.detail.status === 0 || event.detail.status >= 400 ? true : false, // when requests failed, the status is 0
-            parentSpanId: segment.spans.length - 1,
-            componentId: ComponentId,
-            peer: url.host,
-            tags: options.detailMode
-              ? [
-                  {
-                    key: 'http.method',
-                    value: config[0],
-                  },
-                  {
-                    key: 'url',
-                    value: segCollector[i].event.responseURL,
-                  },
-                ]
-              : undefined,
-          };
-          segment = {
-            ...segment,
-            traceId: segCollector[i].traceId,
-            traceSegmentId: segCollector[i].traceSegmentId,
-          };
-          segment.spans.push(exitSpan);
-          segCollector.splice(i, 1);
-        }
-      }
-      segments.push(segment);
-    }
-  });
   window.onbeforeunload = function (e: Event) {
     if (!segments.length) {
       return;