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;