diff --git a/README.md b/README.md
index 272461f9886..6347bef6794 100644
--- a/README.md
+++ b/README.md
@@ -206,6 +206,7 @@ These plugins are hosted at and make sure you have the browser console open. The application is using the `ConsoleSpanExporter` and will post the created spans to the browser console.
+
## Useful links
- For more information on OpenTelemetry, visit:
diff --git a/examples/tracer-web/examples/fetch/index.html b/examples/tracer-web/examples/fetch/index.html
new file mode 100644
index 00000000000..c8311c8e05a
--- /dev/null
+++ b/examples/tracer-web/examples/fetch/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+ Fetch Plugin Example
+
+
+
+
+
+
+ Example of using Web Tracer with Fetch plugin with console exporter and collector exporter
+
+
+
+
+
+
+
diff --git a/examples/tracer-web/examples/fetch/index.js b/examples/tracer-web/examples/fetch/index.js
new file mode 100644
index 00000000000..5be984bbbe8
--- /dev/null
+++ b/examples/tracer-web/examples/fetch/index.js
@@ -0,0 +1,71 @@
+'use strict';
+
+import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing';
+import { CollectorExporter } from '@opentelemetry/exporter-collector';
+import { WebTracerProvider } from '@opentelemetry/web';
+import { FetchPlugin } from '@opentelemetry/plugin-fetch';
+import { ZoneContextManager } from '@opentelemetry/context-zone';
+import { B3Propagator } from '@opentelemetry/core';
+
+const provider = new WebTracerProvider({
+ plugins: [
+ new FetchPlugin({
+ ignoreUrls: [/localhost:8090\/sockjs-node/],
+ propagateTraceHeaderCorsUrls: [
+ 'https://cors-test.appspot.com/test',
+ 'https://httpbin.org/get',
+ ],
+ clearTimingResources: true
+ }),
+ ],
+});
+
+provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
+provider.addSpanProcessor(new SimpleSpanProcessor(new CollectorExporter()));
+provider.register({
+ contextManager: new ZoneContextManager(),
+ propagator: new B3Propagator(),
+});
+
+const webTracerWithZone = provider.getTracer('example-tracer-web');
+
+const getData = (url) => fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+});
+
+// example of keeping track of context between async operations
+const prepareClickEvent = () => {
+ const url1 = 'https://httpbin.org/get';
+
+ const element = document.getElementById('button1');
+
+ const onClick = () => {
+ const span1 = webTracerWithZone.startSpan(`files-series-info`, {
+ parent: webTracerWithZone.getCurrentSpan(),
+ });
+ webTracerWithZone.withSpan(span1, () => {
+ getData(url1).then((_data) => {
+ webTracerWithZone.getCurrentSpan().addEvent('fetching-span1-completed');
+ span1.end();
+ });
+ });
+ for (let i = 0, j = 5; i < j; i += 1) {
+ const span1 = webTracerWithZone.startSpan(`files-series-info-${i}`, {
+ parent: webTracerWithZone.getCurrentSpan(),
+ });
+ webTracerWithZone.withSpan(span1, () => {
+ getData(url1).then((_data) => {
+ webTracerWithZone.getCurrentSpan().addEvent('fetching-span1-completed');
+ span1.end();
+ });
+ });
+ }
+ };
+ element.addEventListener('click', onClick);
+};
+
+window.addEventListener('load', prepareClickEvent);
diff --git a/examples/tracer-web/package.json b/examples/tracer-web/package.json
index ecf4b1a222c..bb8a3e4fe54 100644
--- a/examples/tracer-web/package.json
+++ b/examples/tracer-web/package.json
@@ -38,6 +38,7 @@
"@opentelemetry/core": "^0.8.3",
"@opentelemetry/exporter-collector": "^0.8.3",
"@opentelemetry/plugin-document-load": "^0.6.1",
+ "@opentelemetry/plugin-fetch": "^0.8.3",
"@opentelemetry/plugin-user-interaction": "^0.6.1",
"@opentelemetry/plugin-xml-http-request": "^0.8.3",
"@opentelemetry/tracing": "^0.8.3",
diff --git a/examples/tracer-web/webpack.config.js b/examples/tracer-web/webpack.config.js
index b23949d731c..3e1c14ef2f2 100644
--- a/examples/tracer-web/webpack.config.js
+++ b/examples/tracer-web/webpack.config.js
@@ -8,6 +8,7 @@ const common = {
mode: 'development',
entry: {
'document-load': 'examples/document-load/index.js',
+ fetch: 'examples/fetch/index.js',
'xml-http-request': 'examples/xml-http-request/index.js',
'user-interaction': 'examples/user-interaction/index.js',
},
diff --git a/karma.base.js b/karma.base.js
index 70435dda119..cbae813f7ba 100644
--- a/karma.base.js
+++ b/karma.base.js
@@ -20,7 +20,7 @@ module.exports = {
browsers: ['ChromeHeadless'],
frameworks: ['mocha'],
coverageIstanbulReporter: {
- reports: ['json'],
+ reports: ['html', 'json'],
dir: '.nyc_output',
fixWebpackSourcePaths: true
},
diff --git a/packages/opentelemetry-plugin-fetch/.eslintignore b/packages/opentelemetry-plugin-fetch/.eslintignore
new file mode 100644
index 00000000000..378eac25d31
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/.eslintignore
@@ -0,0 +1 @@
+build
diff --git a/packages/opentelemetry-plugin-fetch/.eslintrc.js b/packages/opentelemetry-plugin-fetch/.eslintrc.js
new file mode 100644
index 00000000000..9dfe62f9b8c
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/.eslintrc.js
@@ -0,0 +1,9 @@
+module.exports = {
+ "env": {
+ "mocha": true,
+ "commonjs": true,
+ "node": true,
+ "browser": true
+ },
+ ...require('../../eslint.config.js')
+}
diff --git a/packages/opentelemetry-plugin-fetch/.npmignore b/packages/opentelemetry-plugin-fetch/.npmignore
new file mode 100644
index 00000000000..9505ba9450f
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/.npmignore
@@ -0,0 +1,4 @@
+/bin
+/coverage
+/doc
+/test
diff --git a/packages/opentelemetry-plugin-fetch/LICENSE b/packages/opentelemetry-plugin-fetch/LICENSE
new file mode 100644
index 00000000000..261eeb9e9f8
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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.
diff --git a/packages/opentelemetry-plugin-fetch/README.md b/packages/opentelemetry-plugin-fetch/README.md
new file mode 100644
index 00000000000..cdbbce00e88
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/README.md
@@ -0,0 +1,68 @@
+# OpenTelemetry Fetch Instrumentation for web
+[![Gitter chat][gitter-image]][gitter-url]
+[![NPM Published Version][npm-img]][npm-url]
+[![dependencies][dependencies-image]][dependencies-url]
+[![devDependencies][devDependencies-image]][devDependencies-url]
+[![Apache License][license-image]][license-image]
+
+This module provides auto instrumentation for web using fetch.
+
+## Installation
+
+```bash
+npm install --save @opentelemetry/plugin-fetch
+```
+
+## Usage
+
+```js
+'use strict';
+import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing';
+import { WebTracerProvider } from '@opentelemetry/web';
+import { FetchPlugin } from '@opentelemetry/plugin-fetch';
+import { ZoneContextManager } from '@opentelemetry/context-zone';
+
+const provider = new WebTracerProvider({
+ plugins: [
+ new FetchPlugin(),
+ ],
+});
+
+provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
+
+provider.register({
+ contextManager: new ZoneContextManager(),
+});
+
+// and some test
+
+fetch('http://localhost:8090/fetch.js');
+
+```
+
+## Example Screenshots
+![Screenshot of the running example](images/trace1.png)
+![Screenshot of the running example](images/trace2.png)
+![Screenshot of the running example](images/trace3.png)
+
+See [examples/tracer-web/fetch](https://github.com/open-telemetry/opentelemetry-js/tree/master/examples/tracer-web) for a short example.
+
+## Useful links
+- For more information on OpenTelemetry, visit:
+- For more about OpenTelemetry JavaScript:
+- For help or feedback on this project, join us on [gitter][gitter-url]
+
+## License
+
+Apache 2.0 - See [LICENSE][license-url] for more information.
+
+[gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-js.svg
+[gitter-url]: https://gitter.im/open-telemetry/opentelemetry-node?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
+[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/master/LICENSE
+[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat
+[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=packages/opentelemetry-plugin-fetch
+[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-fetch
+[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-plugin-fetch
+[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-fetch&type=dev
+[npm-url]: https://www.npmjs.com/package/@opentelemetry/plugin-fetch
+[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fplugin-fetch.svg
diff --git a/packages/opentelemetry-plugin-fetch/images/trace1.png b/packages/opentelemetry-plugin-fetch/images/trace1.png
new file mode 100644
index 00000000000..f26085537de
Binary files /dev/null and b/packages/opentelemetry-plugin-fetch/images/trace1.png differ
diff --git a/packages/opentelemetry-plugin-fetch/images/trace2.png b/packages/opentelemetry-plugin-fetch/images/trace2.png
new file mode 100644
index 00000000000..0f9130ce8a5
Binary files /dev/null and b/packages/opentelemetry-plugin-fetch/images/trace2.png differ
diff --git a/packages/opentelemetry-plugin-fetch/images/trace3.png b/packages/opentelemetry-plugin-fetch/images/trace3.png
new file mode 100644
index 00000000000..e2a602f2954
Binary files /dev/null and b/packages/opentelemetry-plugin-fetch/images/trace3.png differ
diff --git a/packages/opentelemetry-plugin-fetch/karma.conf.js b/packages/opentelemetry-plugin-fetch/karma.conf.js
new file mode 100644
index 00000000000..edcd9f055fd
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/karma.conf.js
@@ -0,0 +1,24 @@
+/*!
+ * Copyright 2020, OpenTelemetry Authors
+ *
+ * Licensed 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.
+ */
+
+const karmaWebpackConfig = require('../../karma.webpack');
+const karmaBaseConfig = require('../../karma.base');
+
+module.exports = (config) => {
+ config.set(Object.assign({}, karmaBaseConfig, {
+ webpack: karmaWebpackConfig
+ }))
+};
diff --git a/packages/opentelemetry-plugin-fetch/package.json b/packages/opentelemetry-plugin-fetch/package.json
new file mode 100644
index 00000000000..2f1fbae5da3
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/package.json
@@ -0,0 +1,82 @@
+{
+ "name": "@opentelemetry/plugin-fetch",
+ "version": "0.8.3",
+ "description": "OpenTelemetry fetch automatic instrumentation package.",
+ "main": "build/src/index.js",
+ "types": "build/src/index.d.ts",
+ "repository": "open-telemetry/opentelemetry-js",
+ "scripts": {
+ "lint": "eslint . --ext .ts",
+ "lint:fix": "eslint . --ext .ts --fix",
+ "clean": "rimraf build/*",
+ "codecov:browser": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../",
+ "precompile": "tsc --version",
+ "version:update": "node ../../scripts/version-update.js",
+ "compile": "npm run version:update && tsc -p .",
+ "prepare": "npm run compile",
+ "tdd": "karma start",
+ "test:browser": "nyc karma start --single-run",
+ "watch": "tsc -w"
+ },
+ "keywords": [
+ "fetch",
+ "opentelemetry",
+ "browser",
+ "tracing",
+ "profiling",
+ "metrics",
+ "stats"
+ ],
+ "author": "OpenTelemetry Authors",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "files": [
+ "build/src/**/*.js",
+ "build/src/**/*.d.ts",
+ "doc",
+ "LICENSE",
+ "README.md"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.6.0",
+ "@opentelemetry/context-zone": "^0.8.2",
+ "@opentelemetry/tracing": "^0.8.2",
+ "@types/mocha": "^7.0.0",
+ "@types/node": "^14.0.5",
+ "@types/shimmer": "^1.0.1",
+ "@types/sinon": "^7.0.13",
+ "@types/webpack-env": "1.15.2",
+ "babel-loader": "^8.0.6",
+ "codecov": "^3.1.0",
+ "gts": "^2.0.0",
+ "istanbul-instrumenter-loader": "^3.0.1",
+ "karma": "^5.0.5",
+ "karma-chrome-launcher": "^3.1.0",
+ "karma-coverage-istanbul-reporter": "^3.0.2",
+ "karma-mocha": "^2.0.1",
+ "karma-spec-reporter": "^0.0.32",
+ "karma-webpack": "^4.0.2",
+ "mocha": "^7.1.2",
+ "nyc": "^15.0.0",
+ "rimraf": "^3.0.0",
+ "sinon": "^7.5.0",
+ "ts-loader": "^6.0.4",
+ "ts-mocha": "^7.0.0",
+ "ts-node": "^8.6.2",
+ "typescript": "3.6.4",
+ "webpack": "^4.35.2",
+ "webpack-cli": "^3.3.9",
+ "webpack-merge": "^4.2.2"
+ },
+ "dependencies": {
+ "@opentelemetry/api": "^0.8.3",
+ "@opentelemetry/core": "^0.8.3",
+ "@opentelemetry/web": "^0.8.3",
+ "shimmer": "^1.2.1"
+ }
+}
diff --git a/packages/opentelemetry-plugin-fetch/src/enums/AttributeNames.ts b/packages/opentelemetry-plugin-fetch/src/enums/AttributeNames.ts
new file mode 100644
index 00000000000..452a1111dd7
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/src/enums/AttributeNames.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed 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
+ *
+ * https://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.
+ */
+
+/**
+ * https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md
+ */
+export enum AttributeNames {
+ COMPONENT = 'component',
+ HTTP_HOST = 'http.host',
+ HTTP_FLAVOR = 'http.flavor',
+ HTTP_METHOD = 'http.method',
+ HTTP_SCHEME = 'http.scheme',
+ HTTP_STATUS_CODE = 'http.status_code',
+ HTTP_STATUS_TEXT = 'http.status_text',
+ HTTP_URL = 'http.url',
+ HTTP_TARGET = 'http.target',
+ HTTP_USER_AGENT = 'http.user_agent',
+}
diff --git a/packages/opentelemetry-plugin-fetch/src/fetch.ts b/packages/opentelemetry-plugin-fetch/src/fetch.ts
new file mode 100644
index 00000000000..cccc7d43f93
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/src/fetch.ts
@@ -0,0 +1,351 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed 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
+ *
+ * https://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 * as shimmer from 'shimmer';
+import * as api from '@opentelemetry/api';
+import * as core from '@opentelemetry/core';
+import * as web from '@opentelemetry/web';
+import { AttributeNames } from './enums/AttributeNames';
+import { FetchError, FetchResponse, SpanData } from './types';
+import { VERSION } from './version';
+
+// how long to wait for observer to collect information about resources
+// this is needed as event "load" is called before observer
+// hard to say how long it should really wait, seems like 300ms is
+// safe enough
+const OBSERVER_WAIT_TIME_MS = 300;
+
+/**
+ * FetchPlugin Config
+ */
+export interface FetchPluginConfig extends api.PluginConfig {
+ // the number of timing resources is limited, after the limit
+ // (chrome 250, safari 150) the information is not collected anymore
+ // the only way to prevent that is to regularly clean the resources
+ // whenever it is possible, this is needed only when PerformanceObserver
+ // is not available
+ clearTimingResources?: boolean;
+ // urls which should include trace headers when origin doesn't match
+ propagateTraceHeaderCorsUrls?: web.PropagateTraceHeaderCorsUrls;
+}
+
+/**
+ * This class represents a fetch plugin for auto instrumentation
+ */
+export class FetchPlugin extends core.BasePlugin> {
+ moduleName = 'fetch';
+ private _usedResources = new WeakSet();
+ private _tasksCount = 0;
+
+ constructor(protected _config: FetchPluginConfig = {}) {
+ super('@opentelemetry/plugin-fetch', VERSION);
+ }
+
+ /**
+ * Add cors pre flight child span
+ * @param span
+ * @param corsPreFlightRequest
+ */
+ private _addChildSpan(
+ span: api.Span,
+ corsPreFlightRequest: PerformanceResourceTiming
+ ): void {
+ const childSpan = this._tracer.startSpan('CORS Preflight', {
+ parent: span,
+ startTime: corsPreFlightRequest[web.PerformanceTimingNames.FETCH_START],
+ });
+ web.addSpanNetworkEvents(childSpan, corsPreFlightRequest);
+ childSpan.end(
+ corsPreFlightRequest[web.PerformanceTimingNames.RESPONSE_END]
+ );
+ }
+
+ /**
+ * Adds more attributes to span just before ending it
+ * @param span
+ * @param response
+ */
+ private _addFinalSpanAttributes(
+ span: api.Span,
+ response: FetchResponse
+ ): void {
+ const parsedUrl = web.parseUrl(response.url);
+ span.setAttribute(AttributeNames.HTTP_STATUS_CODE, response.status);
+ span.setAttribute(AttributeNames.HTTP_STATUS_TEXT, response.statusText);
+ span.setAttribute(AttributeNames.HTTP_HOST, parsedUrl.host);
+ span.setAttribute(
+ AttributeNames.HTTP_SCHEME,
+ parsedUrl.protocol.replace(':', '')
+ );
+ span.setAttribute(AttributeNames.HTTP_USER_AGENT, navigator.userAgent);
+ }
+
+ /**
+ * Add headers
+ * @param options
+ * @param spanUrl
+ */
+ private _addHeaders(options: RequestInit, spanUrl: string): void {
+ if (
+ !web.shouldPropagateTraceHeaders(
+ spanUrl,
+ this._config.propagateTraceHeaderCorsUrls
+ )
+ ) {
+ return;
+ }
+ const headers: { [key: string]: unknown } = {};
+ api.propagation.inject(headers);
+ options.headers = Object.assign({}, headers, options.headers || {});
+ }
+
+ /**
+ * Clears the resource timings and all resources assigned with spans
+ * when {@link FetchPluginConfig.clearTimingResources} is
+ * set to true (default false)
+ * @private
+ */
+ private _clearResources() {
+ if (this._tasksCount === 0 && this._config.clearTimingResources) {
+ performance.clearResourceTimings();
+ this._usedResources = new WeakSet();
+ }
+ }
+
+ /**
+ * Creates a new span
+ * @param url
+ * @param options
+ */
+ private _createSpan(
+ url: string,
+ options: Partial = {}
+ ): api.Span | undefined {
+ if (core.isUrlIgnored(url, this._config.ignoreUrls)) {
+ this._logger.debug('ignoring span as url matches ignored url');
+ return;
+ }
+ const method = (options.method || 'GET').toUpperCase();
+ const spanName = `HTTP ${method}`;
+ return this._tracer.startSpan(spanName, {
+ kind: api.SpanKind.CLIENT,
+ attributes: {
+ [AttributeNames.COMPONENT]: this.moduleName,
+ [AttributeNames.HTTP_METHOD]: method,
+ [AttributeNames.HTTP_URL]: url,
+ },
+ });
+ }
+
+ /**
+ * Finds appropriate resource and add network events to the span
+ * @param span
+ * @param resourcesObserver
+ * @param endTime
+ */
+ private _findResourceAndAddNetworkEvents(
+ span: api.Span,
+ resourcesObserver: SpanData,
+ endTime: api.HrTime
+ ): void {
+ let resources: PerformanceResourceTiming[] = resourcesObserver.entries;
+ if (!resources.length) {
+ // fallback - either Observer is not available or it took longer
+ // then OBSERVER_WAIT_TIME_MS and observer didn't collect enough
+ // information
+ resources = performance.getEntriesByType(
+ 'resource'
+ ) as PerformanceResourceTiming[];
+ }
+ const resource = web.getResource(
+ resourcesObserver.spanUrl,
+ resourcesObserver.startTime,
+ endTime,
+ resources,
+ this._usedResources,
+ 'fetch'
+ );
+
+ if (resource.mainRequest) {
+ const mainRequest = resource.mainRequest;
+ this._markResourceAsUsed(mainRequest);
+
+ const corsPreFlightRequest = resource.corsPreFlightRequest;
+ if (corsPreFlightRequest) {
+ this._addChildSpan(span, corsPreFlightRequest);
+ this._markResourceAsUsed(corsPreFlightRequest);
+ }
+ web.addSpanNetworkEvents(span, mainRequest);
+ }
+ }
+
+ /**
+ * Marks certain [resource]{@link PerformanceResourceTiming} when information
+ * from this is used to add events to span.
+ * This is done to avoid reusing the same resource again for next span
+ * @param resource
+ */
+ private _markResourceAsUsed(resource: PerformanceResourceTiming): void {
+ this._usedResources.add(resource);
+ }
+
+ /**
+ * Finish span, add attributes, network events etc.
+ * @param span
+ * @param spanData
+ * @param response
+ */
+ private _endSpan(
+ span: api.Span,
+ spanData: SpanData,
+ response: FetchResponse
+ ) {
+ const endTime = core.hrTime();
+ spanData.observer.disconnect();
+ this._addFinalSpanAttributes(span, response);
+
+ setTimeout(() => {
+ this._findResourceAndAddNetworkEvents(span, spanData, endTime);
+ this._tasksCount--;
+ this._clearResources();
+ span.end(endTime);
+ }, OBSERVER_WAIT_TIME_MS);
+ }
+
+ /**
+ * Patches the constructor of fetch
+ */
+ private _patchConstructor(): (
+ original: (input: RequestInfo, init?: RequestInit) => Promise
+ ) => (input: RequestInfo, init?: RequestInit) => Promise {
+ return (
+ original: (input: RequestInfo, init?: RequestInit) => Promise
+ ): ((input: RequestInfo, init?: RequestInit) => Promise) => {
+ const plugin = this;
+
+ return function patchConstructor(
+ this: (input: RequestInfo, init?: RequestInit) => Promise,
+ input: RequestInfo,
+ init?: RequestInit
+ ): Promise {
+ const url = input instanceof Request ? input.url : input;
+ const options: RequestInit =
+ input instanceof Request ? input : init || {};
+
+ const span = plugin._createSpan(url, options);
+ if (!span) {
+ return original.apply(this, [url, options]);
+ }
+ const spanData = plugin._prepareSpanData(url);
+
+ function onSuccess(
+ span: api.Span,
+ resolve: (
+ value?: Response | PromiseLike | undefined
+ ) => void,
+ response: Response
+ ) {
+ try {
+ if (response.status >= 200 && response.status < 400) {
+ plugin._endSpan(span, spanData, response);
+ } else {
+ plugin._endSpan(span, spanData, {
+ status: response.status,
+ statusText: response.statusText,
+ url,
+ });
+ }
+ } finally {
+ resolve(response);
+ }
+ }
+
+ function onError(
+ span: api.Span,
+ reject: (reason?: unknown) => void,
+ error: FetchError
+ ) {
+ try {
+ plugin._endSpan(span, spanData, {
+ status: error.status || 0,
+ statusText: error.message,
+ url,
+ });
+ } finally {
+ reject(error);
+ }
+ }
+
+ return new Promise((resolve, reject) => {
+ return plugin._tracer.withSpan(span, () => {
+ plugin._addHeaders(options, url);
+ plugin._tasksCount++;
+ return original
+ .apply(this, [url, options])
+ .then(
+ onSuccess.bind(this, span, resolve),
+ onError.bind(this, span, reject)
+ );
+ });
+ });
+ };
+ };
+ }
+
+ /**
+ * Prepares a span data - needed later for matching appropriate network
+ * resources
+ * @param spanUrl
+ */
+ private _prepareSpanData(spanUrl: string): SpanData {
+ const startTime = core.hrTime();
+ const entries: PerformanceResourceTiming[] = [];
+ const observer: PerformanceObserver = new PerformanceObserver(list => {
+ const entries = list.getEntries() as PerformanceResourceTiming[];
+ entries.forEach(entry => {
+ if (entry.initiatorType === 'fetch' && entry.name === spanUrl) {
+ entries.push(entry);
+ }
+ });
+ });
+ observer.observe({
+ entryTypes: ['resource'],
+ });
+ return { entries, observer, startTime, spanUrl };
+ }
+
+ /**
+ * implements patch function
+ */
+ patch() {
+ if (core.isWrapped(window.fetch)) {
+ shimmer.unwrap(window, 'fetch');
+ this._logger.debug('removing previous patch for constructor');
+ }
+
+ shimmer.wrap(window, 'fetch', this._patchConstructor());
+
+ return this._moduleExports;
+ }
+
+ /**
+ * implements unpatch function
+ */
+ unpatch() {
+ shimmer.unwrap(window, 'fetch');
+ this._usedResources = new WeakSet();
+ }
+}
diff --git a/packages/opentelemetry-plugin-fetch/src/index.ts b/packages/opentelemetry-plugin-fetch/src/index.ts
new file mode 100644
index 00000000000..1d397925608
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/src/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed 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
+ *
+ * https://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 * from './fetch';
diff --git a/packages/opentelemetry-plugin-fetch/src/types.ts b/packages/opentelemetry-plugin-fetch/src/types.ts
new file mode 100644
index 00000000000..4144aceaa1e
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/src/types.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed 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
+ *
+ * https://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 * as api from '@opentelemetry/api';
+
+/**
+ * Interface used to provide information to finish span on fetch response
+ */
+export interface FetchResponse {
+ status: number;
+ statusText?: string;
+ url: string;
+}
+
+/**
+ * Interface used to provide information to finish span on fetch error
+ */
+export interface FetchError {
+ status?: number;
+ message: string;
+}
+
+/**
+ * Interface used to keep information about span between creating and
+ * ending span
+ */
+export interface SpanData {
+ entries: PerformanceResourceTiming[];
+ observer: PerformanceObserver;
+ spanUrl: string;
+ startTime: api.HrTime;
+}
diff --git a/packages/opentelemetry-plugin-fetch/src/version.ts b/packages/opentelemetry-plugin-fetch/src/version.ts
new file mode 100644
index 00000000000..9e616149a48
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/src/version.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed 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
+ *
+ * https://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.
+ */
+
+// this is autogenerated file, see scripts/version-update.js
+export const VERSION = '0.8.3';
diff --git a/packages/opentelemetry-plugin-fetch/test/fetch.test.ts b/packages/opentelemetry-plugin-fetch/test/fetch.test.ts
new file mode 100644
index 00000000000..cf27f84f40d
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/test/fetch.test.ts
@@ -0,0 +1,563 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed 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
+ *
+ * https://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 * as api from '@opentelemetry/api';
+import * as core from '@opentelemetry/core';
+import { ZoneContextManager } from '@opentelemetry/context-zone';
+import * as tracing from '@opentelemetry/tracing';
+import {
+ PerformanceTimingNames as PTN,
+ WebTracerProvider,
+} from '@opentelemetry/web';
+import * as assert from 'assert';
+import * as sinon from 'sinon';
+import { FetchPlugin, FetchPluginConfig } from '../src';
+import { AttributeNames } from '../src/enums/AttributeNames';
+
+class DummySpanExporter implements tracing.SpanExporter {
+ export(spans: any) {}
+
+ shutdown() {}
+}
+
+const getData = (url: string, method?: string) =>
+ fetch(url, {
+ method: method || 'GET',
+ headers: {
+ foo: 'bar',
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ });
+
+const defaultResource = {
+ connectEnd: 15,
+ connectStart: 13,
+ decodedBodySize: 0,
+ domainLookupEnd: 12,
+ domainLookupStart: 11,
+ encodedBodySize: 0,
+ fetchStart: 10.1,
+ initiatorType: 'fetch',
+ nextHopProtocol: '',
+ redirectEnd: 0,
+ redirectStart: 0,
+ requestStart: 16,
+ responseEnd: 20.5,
+ responseStart: 17,
+ secureConnectionStart: 14,
+ transferSize: 0,
+ workerStart: 0,
+ duration: 0,
+ entryType: '',
+ name: '',
+ startTime: 0,
+};
+
+function createResource(resource = {}): PerformanceResourceTiming {
+ return Object.assign(
+ {},
+ defaultResource,
+ resource
+ ) as PerformanceResourceTiming;
+}
+
+function createMasterResource(resource = {}): PerformanceResourceTiming {
+ const masterResource: any = createResource(resource);
+ Object.keys(masterResource).forEach((key: string) => {
+ if (typeof masterResource[key] === 'number') {
+ masterResource[key] = masterResource[key] + 30;
+ }
+ });
+ return masterResource;
+}
+
+describe('fetch', () => {
+ let sandbox: sinon.SinonSandbox;
+ let contextManager: ZoneContextManager;
+ let lastResponse: any | undefined;
+ let webTracerWithZone: api.Tracer;
+ let webTracerProviderWithZone: WebTracerProvider;
+ let dummySpanExporter: DummySpanExporter;
+ let exportSpy: any;
+ let clearResourceTimingsSpy: any;
+ let rootSpan: api.Span;
+ let fakeNow = 0;
+ let fetchPlugin: FetchPlugin;
+
+ const url = 'http://localhost:8090/get';
+ const badUrl = 'http://foo.bar.com/get';
+
+ const clearData = () => {
+ sandbox.restore();
+ lastResponse = undefined;
+ };
+
+ const prepareData = (
+ done: any,
+ fileUrl: string,
+ config: FetchPluginConfig,
+ method?: string
+ ) => {
+ sandbox = sinon.createSandbox();
+ sandbox.useFakeTimers();
+
+ sandbox.stub(core.otperformance, 'timeOrigin').value(0);
+ sandbox.stub(core.otperformance, 'now').callsFake(() => fakeNow);
+
+ function fakeFetch(input: RequestInfo, init: RequestInit = {}) {
+ return new Promise((resolve, reject) => {
+ const response: any = {
+ args: {},
+ url: fileUrl,
+ };
+ response.headers = Object.assign({}, init.headers);
+
+ if (init.method === 'DELETE') {
+ response.status = 405;
+ response.statusText = 'OK';
+ resolve(new window.Response('foo', response));
+ } else if (input === url) {
+ response.status = 200;
+ response.statusText = 'OK';
+ resolve(new window.Response(JSON.stringify(response), response));
+ } else {
+ response.status = 404;
+ response.statusText = 'Bad request';
+ reject(new window.Response(JSON.stringify(response), response));
+ }
+ });
+ }
+
+ sandbox.stub(window, 'fetch').callsFake(fakeFetch as any);
+
+ const resources: PerformanceResourceTiming[] = [];
+ resources.push(
+ createResource({
+ name: fileUrl,
+ }),
+ createMasterResource({
+ name: fileUrl,
+ })
+ );
+
+ const spyEntries = sandbox.stub(performance, 'getEntriesByType');
+ spyEntries.withArgs('resource').returns(resources);
+ fetchPlugin = new FetchPlugin(config);
+ webTracerProviderWithZone = new WebTracerProvider({
+ logLevel: core.LogLevel.ERROR,
+ plugins: [fetchPlugin],
+ });
+ webTracerWithZone = webTracerProviderWithZone.getTracer('fetch-test');
+ dummySpanExporter = new DummySpanExporter();
+ exportSpy = sandbox.stub(dummySpanExporter, 'export');
+ clearResourceTimingsSpy = sandbox.stub(performance, 'clearResourceTimings');
+ webTracerProviderWithZone.addSpanProcessor(
+ new tracing.SimpleSpanProcessor(dummySpanExporter)
+ );
+
+ rootSpan = webTracerWithZone.startSpan('root');
+ webTracerWithZone.withSpan(rootSpan, () => {
+ fakeNow = 0;
+ getData(fileUrl, method).then(
+ response => {
+ // this is a bit tricky as the only way to get all request headers from
+ // fetch is to use json()
+ response.json().then(
+ json => {
+ lastResponse = json;
+ const headers: { [key: string]: string } = {};
+ Object.keys(lastResponse.headers).forEach(key => {
+ headers[key.toLowerCase()] = lastResponse.headers[key];
+ });
+ lastResponse.headers = headers;
+ // OBSERVER_WAIT_TIME_MS
+ sandbox.clock.tick(300);
+ done();
+ },
+ () => {
+ lastResponse = undefined;
+ // OBSERVER_WAIT_TIME_MS
+ sandbox.clock.tick(300);
+ done();
+ }
+ );
+ },
+ () => {
+ lastResponse = undefined;
+ // OBSERVER_WAIT_TIME_MS
+ sandbox.clock.tick(300);
+ done();
+ }
+ );
+ fakeNow = 300;
+ });
+ };
+
+ beforeEach(() => {
+ contextManager = new ZoneContextManager().enable();
+ api.context.setGlobalContextManager(contextManager);
+ });
+
+ afterEach(() => {
+ api.context.disable();
+ });
+
+ before(() => {
+ api.propagation.setGlobalPropagator(new core.B3Propagator());
+ });
+
+ describe('when request is successful', () => {
+ beforeEach(done => {
+ const propagateTraceHeaderCorsUrls = [url];
+ prepareData(done, url, { propagateTraceHeaderCorsUrls });
+ });
+
+ afterEach(() => {
+ clearData();
+ });
+
+ it('should wrap methods', () => {
+ assert.ok(core.isWrapped(window.fetch));
+ fetchPlugin.patch();
+ assert.ok(core.isWrapped(window.fetch));
+ });
+
+ it('should unwrap methods', () => {
+ assert.ok(core.isWrapped(window.fetch));
+ fetchPlugin.unpatch();
+ assert.ok(!core.isWrapped(window.fetch));
+ });
+
+ it('should create a span with correct root span', () => {
+ const span: tracing.ReadableSpan = exportSpy.args[1][0][0];
+ assert.strictEqual(
+ span.parentSpanId,
+ rootSpan.context().spanId,
+ 'parent span is not root span'
+ );
+ });
+
+ it('span should have correct name', () => {
+ const span: tracing.ReadableSpan = exportSpy.args[1][0][0];
+ assert.strictEqual(span.name, 'HTTP GET', 'span has wrong name');
+ });
+
+ it('span should have correct kind', () => {
+ const span: tracing.ReadableSpan = exportSpy.args[1][0][0];
+ assert.strictEqual(span.kind, api.SpanKind.CLIENT, 'span has wrong kind');
+ });
+
+ it('span should have correct attributes', () => {
+ const span: tracing.ReadableSpan = exportSpy.args[1][0][0];
+ const attributes = span.attributes;
+ const keys = Object.keys(attributes);
+
+ assert.ok(
+ attributes[keys[0]] !== '',
+ `attributes ${AttributeNames.COMPONENT} is not defined`
+ );
+ assert.strictEqual(
+ attributes[keys[1]],
+ 'GET',
+ `attributes ${AttributeNames.HTTP_METHOD} is wrong`
+ );
+ assert.strictEqual(
+ attributes[keys[2]],
+ url,
+ `attributes ${AttributeNames.HTTP_URL} is wrong`
+ );
+ assert.strictEqual(
+ attributes[keys[3]],
+ 200,
+ `attributes ${AttributeNames.HTTP_STATUS_CODE} is wrong`
+ );
+ assert.ok(
+ attributes[keys[4]] === 'OK' || attributes[keys[4]] === '',
+ `attributes ${AttributeNames.HTTP_STATUS_TEXT} is wrong`
+ );
+ assert.ok(
+ (attributes[keys[5]] as string).indexOf('localhost') === 0,
+ `attributes ${AttributeNames.HTTP_HOST} is wrong`
+ );
+ assert.ok(
+ attributes[keys[6]] === 'http' || attributes[keys[6]] === 'https',
+ `attributes ${AttributeNames.HTTP_SCHEME} is wrong`
+ );
+ assert.ok(
+ attributes[keys[7]] !== '',
+ `attributes ${AttributeNames.HTTP_USER_AGENT} is not defined`
+ );
+
+ assert.strictEqual(keys.length, 8, 'number of attributes is wrong');
+ });
+
+ it('span should have correct events', () => {
+ const span: tracing.ReadableSpan = exportSpy.args[1][0][0];
+ const events = span.events;
+ assert.strictEqual(events.length, 9, 'number of events is wrong');
+
+ assert.strictEqual(
+ events[0].name,
+ PTN.FETCH_START,
+ `event ${PTN.FETCH_START} is not defined`
+ );
+ assert.strictEqual(
+ events[1].name,
+ PTN.DOMAIN_LOOKUP_START,
+ `event ${PTN.DOMAIN_LOOKUP_START} is not defined`
+ );
+ assert.strictEqual(
+ events[2].name,
+ PTN.DOMAIN_LOOKUP_END,
+ `event ${PTN.DOMAIN_LOOKUP_END} is not defined`
+ );
+ assert.strictEqual(
+ events[3].name,
+ PTN.CONNECT_START,
+ `event ${PTN.CONNECT_START} is not defined`
+ );
+ assert.strictEqual(
+ events[4].name,
+ PTN.SECURE_CONNECTION_START,
+ `event ${PTN.SECURE_CONNECTION_START} is not defined`
+ );
+ assert.strictEqual(
+ events[5].name,
+ PTN.CONNECT_END,
+ `event ${PTN.CONNECT_END} is not defined`
+ );
+ assert.strictEqual(
+ events[6].name,
+ PTN.REQUEST_START,
+ `event ${PTN.REQUEST_START} is not defined`
+ );
+ assert.strictEqual(
+ events[7].name,
+ PTN.RESPONSE_START,
+ `event ${PTN.RESPONSE_START} is not defined`
+ );
+ assert.strictEqual(
+ events[8].name,
+ PTN.RESPONSE_END,
+ `event ${PTN.RESPONSE_END} is not defined`
+ );
+ });
+
+ it('should create a span for preflight request', () => {
+ const span: tracing.ReadableSpan = exportSpy.args[0][0][0];
+ const parentSpan: tracing.ReadableSpan = exportSpy.args[1][0][0];
+ assert.strictEqual(
+ span.parentSpanId,
+ parentSpan.spanContext.spanId,
+ 'parent span is not root span'
+ );
+ });
+
+ it('preflight request span should have correct name', () => {
+ const span: tracing.ReadableSpan = exportSpy.args[0][0][0];
+ assert.strictEqual(
+ span.name,
+ 'CORS Preflight',
+ 'preflight request span has wrong name'
+ );
+ });
+
+ it('preflight request span should have correct kind', () => {
+ const span: tracing.ReadableSpan = exportSpy.args[0][0][0];
+ assert.strictEqual(
+ span.kind,
+ api.SpanKind.INTERNAL,
+ 'span has wrong kind'
+ );
+ });
+
+ it('preflight request span should have correct events', () => {
+ const span: tracing.ReadableSpan = exportSpy.args[0][0][0];
+ const events = span.events;
+ assert.strictEqual(events.length, 9, 'number of events is wrong');
+
+ assert.strictEqual(
+ events[0].name,
+ PTN.FETCH_START,
+ `event ${PTN.FETCH_START} is not defined`
+ );
+ assert.strictEqual(
+ events[1].name,
+ PTN.DOMAIN_LOOKUP_START,
+ `event ${PTN.DOMAIN_LOOKUP_START} is not defined`
+ );
+ assert.strictEqual(
+ events[2].name,
+ PTN.DOMAIN_LOOKUP_END,
+ `event ${PTN.DOMAIN_LOOKUP_END} is not defined`
+ );
+ assert.strictEqual(
+ events[3].name,
+ PTN.CONNECT_START,
+ `event ${PTN.CONNECT_START} is not defined`
+ );
+ assert.strictEqual(
+ events[4].name,
+ PTN.SECURE_CONNECTION_START,
+ `event ${PTN.SECURE_CONNECTION_START} is not defined`
+ );
+ assert.strictEqual(
+ events[5].name,
+ PTN.CONNECT_END,
+ `event ${PTN.CONNECT_END} is not defined`
+ );
+ assert.strictEqual(
+ events[6].name,
+ PTN.REQUEST_START,
+ `event ${PTN.REQUEST_START} is not defined`
+ );
+ assert.strictEqual(
+ events[7].name,
+ PTN.RESPONSE_START,
+ `event ${PTN.RESPONSE_START} is not defined`
+ );
+ assert.strictEqual(
+ events[8].name,
+ PTN.RESPONSE_END,
+ `event ${PTN.RESPONSE_END} is not defined`
+ );
+ });
+
+ it('should set trace headers', () => {
+ const span: api.Span = exportSpy.args[1][0][0];
+ assert.strictEqual(
+ lastResponse.headers[core.X_B3_TRACE_ID],
+ span.context().traceId,
+ `trace header '${core.X_B3_TRACE_ID}' not set`
+ );
+ assert.strictEqual(
+ lastResponse.headers[core.X_B3_SPAN_ID],
+ span.context().spanId,
+ `trace header '${core.X_B3_SPAN_ID}' not set`
+ );
+ assert.strictEqual(
+ lastResponse.headers[core.X_B3_SAMPLED],
+ String(span.context().traceFlags),
+ `trace header '${core.X_B3_SAMPLED}' not set`
+ );
+ });
+
+ it('should NOT clear the resources', () => {
+ assert.strictEqual(
+ clearResourceTimingsSpy.args.length,
+ 0,
+ 'resources have been cleared'
+ );
+ });
+
+ describe('when propagateTraceHeaderCorsUrls does NOT MATCH', () => {
+ beforeEach(done => {
+ clearData();
+ prepareData(done, url, {});
+ });
+ it('should NOT set trace headers', () => {
+ assert.strictEqual(
+ lastResponse.headers[core.X_B3_TRACE_ID],
+ undefined,
+ `trace header '${core.X_B3_TRACE_ID}' should not be set`
+ );
+ assert.strictEqual(
+ lastResponse.headers[core.X_B3_SPAN_ID],
+ undefined,
+ `trace header '${core.X_B3_SPAN_ID}' should not be set`
+ );
+ assert.strictEqual(
+ lastResponse.headers[core.X_B3_SAMPLED],
+ undefined,
+ `trace header '${core.X_B3_SAMPLED}' should not be set`
+ );
+ });
+ });
+ });
+
+ describe('when url is ignored', () => {
+ beforeEach(done => {
+ const propagateTraceHeaderCorsUrls = url;
+ prepareData(done, url, {
+ propagateTraceHeaderCorsUrls,
+ ignoreUrls: [propagateTraceHeaderCorsUrls],
+ });
+ });
+ afterEach(() => {
+ clearData();
+ });
+ it('should NOT create any span', () => {
+ assert.strictEqual(exportSpy.args.length, 0, "span shouldn't b exported");
+ });
+ });
+
+ describe('when clearTimingResources is TRUE', () => {
+ beforeEach(done => {
+ const propagateTraceHeaderCorsUrls = url;
+ prepareData(done, url, {
+ propagateTraceHeaderCorsUrls,
+ clearTimingResources: true,
+ });
+ });
+ afterEach(() => {
+ clearData();
+ });
+ it('should clear the resources', () => {
+ assert.strictEqual(
+ clearResourceTimingsSpy.args.length,
+ 1,
+ "resources haven't been cleared"
+ );
+ });
+ });
+
+ describe('when request is NOT successful (wrong url)', () => {
+ beforeEach(done => {
+ const propagateTraceHeaderCorsUrls = badUrl;
+ prepareData(done, badUrl, { propagateTraceHeaderCorsUrls });
+ });
+ afterEach(() => {
+ clearData();
+ });
+ it('should create a span with correct root span', () => {
+ const span: tracing.ReadableSpan = exportSpy.args[1][0][0];
+ assert.strictEqual(
+ span.parentSpanId,
+ rootSpan.context().spanId,
+ 'parent span is not root span'
+ );
+ });
+ });
+
+ describe('when request is NOT successful (405)', () => {
+ beforeEach(done => {
+ const propagateTraceHeaderCorsUrls = url;
+ prepareData(done, url, { propagateTraceHeaderCorsUrls }, 'DELETE');
+ });
+ afterEach(() => {
+ clearData();
+ });
+
+ it('should create a span with correct root span', () => {
+ const span: tracing.ReadableSpan = exportSpy.args[1][0][0];
+ assert.strictEqual(
+ span.parentSpanId,
+ rootSpan.context().spanId,
+ 'parent span is not root span'
+ );
+ });
+ });
+});
diff --git a/packages/opentelemetry-plugin-fetch/test/index-webpack.ts b/packages/opentelemetry-plugin-fetch/test/index-webpack.ts
new file mode 100644
index 00000000000..061a48ccfa7
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/test/index-webpack.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed 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
+ *
+ * https://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.
+ */
+const testsContext = require.context('.', true, /test$/);
+testsContext.keys().forEach(testsContext);
+
+const srcContext = require.context('.', true, /src$/);
+srcContext.keys().forEach(srcContext);
diff --git a/packages/opentelemetry-plugin-fetch/tsconfig.json b/packages/opentelemetry-plugin-fetch/tsconfig.json
new file mode 100644
index 00000000000..71661a842ee
--- /dev/null
+++ b/packages/opentelemetry-plugin-fetch/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../tsconfig.base",
+ "compilerOptions": {
+ "rootDir": ".",
+ "outDir": "build",
+ "skipLibCheck": true
+ },
+ "include": [
+ "src/**/*.ts",
+ "test/**/*.ts"
+ ]
+}
diff --git a/packages/opentelemetry-plugin-xml-http-request/src/xhr.ts b/packages/opentelemetry-plugin-xml-http-request/src/xhr.ts
index ff18cb02f12..59b59cc03da 100644
--- a/packages/opentelemetry-plugin-xml-http-request/src/xhr.ts
+++ b/packages/opentelemetry-plugin-xml-http-request/src/xhr.ts
@@ -21,17 +21,17 @@ import {
isUrlIgnored,
isWrapped,
otperformance,
- urlMatches,
} from '@opentelemetry/core';
import {
HttpAttribute,
GeneralAttribute,
} from '@opentelemetry/semantic-conventions';
import {
- addSpanNetworkEvent,
+ addSpanNetworkEvents,
getResource,
parseUrl,
PerformanceTimingNames as PTN,
+ shouldPropagateTraceHeaders,
} from '@opentelemetry/web';
import * as shimmer from 'shimmer';
import { EventNames } from './enums/EventNames';
@@ -86,7 +86,12 @@ export class XMLHttpRequestPlugin extends BasePlugin {
* @private
*/
private _addHeaders(xhr: XMLHttpRequest, spanUrl: string) {
- if (!this._shouldPropagateTraceHeaders(spanUrl)) {
+ if (
+ !shouldPropagateTraceHeaders(
+ spanUrl,
+ this._config.propagateTraceHeaderCorsUrls
+ )
+ ) {
return;
}
const headers: { [key: string]: unknown } = {};
@@ -96,34 +101,6 @@ export class XMLHttpRequestPlugin extends BasePlugin {
});
}
- /**
- * checks if trace headers should be propagated
- * @param spanUrl
- * @private
- */
- _shouldPropagateTraceHeaders(spanUrl: string) {
- let propagateTraceHeaderUrls =
- this._config.propagateTraceHeaderCorsUrls || [];
- if (
- typeof propagateTraceHeaderUrls === 'string' ||
- propagateTraceHeaderUrls instanceof RegExp
- ) {
- propagateTraceHeaderUrls = [propagateTraceHeaderUrls];
- }
- const parsedSpanUrl = parseUrl(spanUrl);
-
- if (parsedSpanUrl.origin === window.location.origin) {
- return true;
- } else {
- for (const propagateTraceHeaderUrl of propagateTraceHeaderUrls) {
- if (urlMatches(spanUrl, propagateTraceHeaderUrl)) {
- return true;
- }
- }
- return false;
- }
- }
-
/**
* Add cors pre flight child span
* @param span
@@ -138,7 +115,7 @@ export class XMLHttpRequestPlugin extends BasePlugin {
const childSpan = this._tracer.startSpan('CORS Preflight', {
startTime: corsPreFlightRequest[PTN.FETCH_START],
});
- this._addSpanNetworkEvents(childSpan, corsPreFlightRequest);
+ addSpanNetworkEvents(childSpan, corsPreFlightRequest);
childSpan.end(corsPreFlightRequest[PTN.RESPONSE_END]);
});
}
@@ -168,27 +145,6 @@ export class XMLHttpRequestPlugin extends BasePlugin {
}
}
- /**
- * Adds Network events to the span
- * @param span
- * @param resource
- * @private
- */
- private _addSpanNetworkEvents(
- span: api.Span,
- resource: PerformanceResourceTiming
- ) {
- addSpanNetworkEvent(span, PTN.FETCH_START, resource);
- addSpanNetworkEvent(span, PTN.DOMAIN_LOOKUP_START, resource);
- addSpanNetworkEvent(span, PTN.DOMAIN_LOOKUP_END, resource);
- addSpanNetworkEvent(span, PTN.CONNECT_START, resource);
- addSpanNetworkEvent(span, PTN.SECURE_CONNECTION_START, resource);
- addSpanNetworkEvent(span, PTN.CONNECT_END, resource);
- addSpanNetworkEvent(span, PTN.REQUEST_START, resource);
- addSpanNetworkEvent(span, PTN.RESPONSE_START, resource);
- addSpanNetworkEvent(span, PTN.RESPONSE_END, resource);
- }
-
/**
* will collect information about all resources created
* between "send" and "end" with additional waiting for main resource
@@ -260,6 +216,7 @@ export class XMLHttpRequestPlugin extends BasePlugin {
// information
resources = otperformance.getEntriesByType(
// ts thinks this is the perf_hooks module, but it is the browser performance api
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
'resource' as any
) as PerformanceResourceTiming[];
}
@@ -281,7 +238,7 @@ export class XMLHttpRequestPlugin extends BasePlugin {
this._addChildSpan(span, corsPreFlightRequest);
this._markResourceAsUsed(corsPreFlightRequest);
}
- this._addSpanNetworkEvents(span, mainRequest);
+ addSpanNetworkEvents(span, mainRequest);
}
}
diff --git a/packages/opentelemetry-web/src/StackContextManager.ts b/packages/opentelemetry-web/src/StackContextManager.ts
index 78d8d34dc31..718ec88035b 100644
--- a/packages/opentelemetry-web/src/StackContextManager.ts
+++ b/packages/opentelemetry-web/src/StackContextManager.ts
@@ -42,7 +42,7 @@ export class StackContextManager implements ContextManager {
context = Context.ROOT_CONTEXT
): T {
const manager = this;
- const contextWrapper = function (this: any, ...args: any[]) {
+ const contextWrapper = function (this: unknown, ...args: unknown[]) {
return manager.with(context, () => target.apply(this, args));
};
Object.defineProperty(contextWrapper, 'length', {
diff --git a/packages/opentelemetry-web/src/types.ts b/packages/opentelemetry-web/src/types.ts
index 45ed1a9e035..10ee04f14ec 100644
--- a/packages/opentelemetry-web/src/types.ts
+++ b/packages/opentelemetry-web/src/types.ts
@@ -53,3 +53,12 @@ export interface PerformanceResourceTimingInfo {
corsPreFlightRequest?: PerformanceResourceTiming;
mainRequest?: PerformanceResourceTiming;
}
+
+type PropagateTraceHeaderCorsUrl = string | RegExp;
+
+/**
+ * urls which should include trace headers when origin doesn't match
+ */
+export type PropagateTraceHeaderCorsUrls =
+ | PropagateTraceHeaderCorsUrl
+ | PropagateTraceHeaderCorsUrl[];
diff --git a/packages/opentelemetry-web/src/utils.ts b/packages/opentelemetry-web/src/utils.ts
index 1edeb64f0c2..aa3dd320b69 100644
--- a/packages/opentelemetry-web/src/utils.ts
+++ b/packages/opentelemetry-web/src/utils.ts
@@ -14,16 +14,25 @@
* limitations under the License.
*/
-import { PerformanceEntries, PerformanceResourceTimingInfo } from './types';
+import {
+ PerformanceEntries,
+ PerformanceResourceTimingInfo,
+ PropagateTraceHeaderCorsUrls,
+} from './types';
import { PerformanceTimingNames as PTN } from './enums/PerformanceTimingNames';
import * as api from '@opentelemetry/api';
-import { hrTimeToNanoseconds, timeInputToHrTime } from '@opentelemetry/core';
+import {
+ hrTimeToNanoseconds,
+ timeInputToHrTime,
+ urlMatches,
+} from '@opentelemetry/core';
/**
* Helper function to be able to use enum as typed key in type and in interface when using forEach
* @param obj
* @param key
*/
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function hasKey(obj: O, key: keyof any): key is keyof O {
return key in obj;
}
@@ -54,6 +63,26 @@ export function addSpanNetworkEvent(
return undefined;
}
+/**
+ * Helper function for adding network events
+ * @param span
+ * @param resource
+ */
+export function addSpanNetworkEvents(
+ span: api.Span,
+ resource: PerformanceEntries
+): void {
+ addSpanNetworkEvent(span, PTN.FETCH_START, resource);
+ addSpanNetworkEvent(span, PTN.DOMAIN_LOOKUP_START, resource);
+ addSpanNetworkEvent(span, PTN.DOMAIN_LOOKUP_END, resource);
+ addSpanNetworkEvent(span, PTN.CONNECT_START, resource);
+ addSpanNetworkEvent(span, PTN.SECURE_CONNECTION_START, resource);
+ addSpanNetworkEvent(span, PTN.CONNECT_END, resource);
+ addSpanNetworkEvent(span, PTN.REQUEST_START, resource);
+ addSpanNetworkEvent(span, PTN.RESPONSE_START, resource);
+ addSpanNetworkEvent(span, PTN.RESPONSE_END, resource);
+}
+
/**
* sort resources by startTime
* @param filteredResources
@@ -79,6 +108,7 @@ export function sortResources(filteredResources: PerformanceResourceTiming[]) {
* @param endTimeHR
* @param resources
* @param ignoredResources
+ * @param initiatorType
*/
export function getResource(
spanUrl: string,
@@ -87,14 +117,16 @@ export function getResource(
resources: PerformanceResourceTiming[],
ignoredResources: WeakSet = new WeakSet<
PerformanceResourceTiming
- >()
+ >(),
+ initiatorType?: string
): PerformanceResourceTimingInfo {
const filteredResources = filterResourcesForSpan(
spanUrl,
startTimeHR,
endTimeHR,
resources,
- ignoredResources
+ ignoredResources,
+ initiatorType
);
if (filteredResources.length === 0) {
@@ -192,7 +224,8 @@ function filterResourcesForSpan(
startTimeHR: api.HrTime,
endTimeHR: api.HrTime,
resources: PerformanceResourceTiming[],
- ignoredResources: WeakSet
+ ignoredResources: WeakSet,
+ initiatorType?: string
) {
const startTime = hrTimeToNanoseconds(startTimeHR);
const endTime = hrTimeToNanoseconds(endTimeHR);
@@ -205,7 +238,8 @@ function filterResourcesForSpan(
);
return (
- resource.initiatorType.toLowerCase() === 'xmlhttprequest' &&
+ resource.initiatorType.toLowerCase() ===
+ (initiatorType || 'xmlhttprequest') &&
resource.name === spanUrl &&
resourceStartTime >= startTime &&
resourceEndTime <= endTime
@@ -237,6 +271,7 @@ export function parseUrl(url: string): HTMLAnchorElement {
* @param optimised - when id attribute of element is present the xpath can be
* simplified to contain id
*/
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getElementXPath(target: any, optimised?: boolean) {
if (target.nodeType === Node.DOCUMENT_NODE) {
return '/';
@@ -312,3 +347,30 @@ function getNodeValue(target: HTMLElement, optimised?: boolean): string {
}
return `/${nodeValue}`;
}
+
+/**
+ * Checks if trace headers should be propagated
+ * @param spanUrl
+ * @private
+ */
+export function shouldPropagateTraceHeaders(
+ spanUrl: string,
+ propagateTraceHeaderCorsUrls?: PropagateTraceHeaderCorsUrls
+) {
+ let propagateTraceHeaderUrls = propagateTraceHeaderCorsUrls || [];
+ if (
+ typeof propagateTraceHeaderUrls === 'string' ||
+ propagateTraceHeaderUrls instanceof RegExp
+ ) {
+ propagateTraceHeaderUrls = [propagateTraceHeaderUrls];
+ }
+ const parsedSpanUrl = parseUrl(spanUrl);
+
+ if (parsedSpanUrl.origin === window.location.origin) {
+ return true;
+ } else {
+ return propagateTraceHeaderUrls.some(propagateTraceHeaderUrl =>
+ urlMatches(spanUrl, propagateTraceHeaderUrl)
+ );
+ }
+}
diff --git a/packages/opentelemetry-web/test/utils.test.ts b/packages/opentelemetry-web/test/utils.test.ts
index 52a417ba42a..805986de71e 100644
--- a/packages/opentelemetry-web/test/utils.test.ts
+++ b/packages/opentelemetry-web/test/utils.test.ts
@@ -26,9 +26,11 @@ import * as assert from 'assert';
import * as sinon from 'sinon';
import {
addSpanNetworkEvent,
+ addSpanNetworkEvents,
getElementXPath,
getResource,
PerformanceEntries,
+ shouldPropagateTraceHeaders,
} from '../src';
import { PerformanceTimingNames as PTN } from '../src/enums/PerformanceTimingNames';
@@ -132,6 +134,31 @@ describe('utils', () => {
sandbox.restore();
});
+ describe('addSpanNetworkEvents', () => {
+ it('should add all network events to span', () => {
+ const addEventSpy = sinon.spy();
+ const span = ({
+ addEvent: addEventSpy,
+ } as unknown) as tracing.Span;
+ const entries = {
+ [PTN.FETCH_START]: 123,
+ [PTN.DOMAIN_LOOKUP_START]: 123,
+ [PTN.DOMAIN_LOOKUP_END]: 123,
+ [PTN.CONNECT_START]: 123,
+ [PTN.SECURE_CONNECTION_START]: 123,
+ [PTN.CONNECT_END]: 123,
+ [PTN.REQUEST_START]: 123,
+ [PTN.RESPONSE_START]: 123,
+ [PTN.RESPONSE_END]: 123,
+ } as PerformanceEntries;
+
+ assert.strictEqual(addEventSpy.callCount, 0);
+
+ addSpanNetworkEvents(span, entries);
+
+ assert.strictEqual(addEventSpy.callCount, 9);
+ });
+ });
describe('addSpanNetworkEvent', () => {
describe('when entries contain the performance', () => {
it('should add event to span', () => {
@@ -508,6 +535,40 @@ describe('utils', () => {
assert.strictEqual(node, getElementByXpath(element));
});
});
+
+ describe('shouldPropagateTraceHeaders', () => {
+ it('should propagate trace when url is the same as origin', () => {
+ const result = shouldPropagateTraceHeaders(
+ `${window.location.origin}/foo/bar`
+ );
+ assert.strictEqual(result, true);
+ });
+ it('should propagate trace when url match', () => {
+ const result = shouldPropagateTraceHeaders(
+ 'http://foo.com',
+ 'http://foo.com'
+ );
+ assert.strictEqual(result, true);
+ });
+ it('should propagate trace when url match regexp', () => {
+ const result = shouldPropagateTraceHeaders('http://foo.com', /foo.+/);
+ assert.strictEqual(result, true);
+ });
+ it('should propagate trace when url match array of string', () => {
+ const result = shouldPropagateTraceHeaders('http://foo.com', [
+ 'http://foo.com',
+ ]);
+ assert.strictEqual(result, true);
+ });
+ it('should propagate trace when url match array of regexp', () => {
+ const result = shouldPropagateTraceHeaders('http://foo.com', [/foo.+/]);
+ assert.strictEqual(result, true);
+ });
+ it("should NOT propagate trace when url doesn't match", () => {
+ const result = shouldPropagateTraceHeaders('http://foo.com');
+ assert.strictEqual(result, false);
+ });
+ });
});
function getElementByXpath(path: string) {