diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 4cc9daca63..d054a29f93 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -81,6 +81,41 @@ jobs: run: npm run test:browser - name: Report Coverage run: npm run codecov:browser + webworker-tests-stable: + runs-on: ubuntu-latest + container: + image: circleci/node:14-browsers + env: + NPM_CONFIG_UNSAFE_PERM: true + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Permission Setup + run: sudo chmod -R 777 /github /__w + + - name: restore lerna + uses: actions/cache@v2 + id: cache + with: + path: | + node_modules + */*/node_modules + key: browser-tests-stable-${{ runner.os }}-${{ matrix.node_version }}-${{ hashFiles('**/package.json') }} + + - name: Bootstrap + if: steps.cache.outputs.cache-hit != 'true' + run: | + npm install --ignore-scripts + npx lerna bootstrap --no-ci --hoist --nohoist='zone.js' + + - name: Build 🔧 + run: | + npm run compile + + - name: Unit tests + run: npm run test:webworker + - name: Report Coverage + run: npm run codecov:webworker node-tests-experimental: strategy: fail-fast: false @@ -164,3 +199,42 @@ jobs: - name: Report Coverage working-directory: experimental run: npm run codecov:browser + webworker-tests-experimental: + runs-on: ubuntu-latest + container: + image: circleci/node:14-browsers + env: + NPM_CONFIG_UNSAFE_PERM: true + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Permission Setup + run: sudo chmod -R 777 /github /__w + + - name: restore lerna + uses: actions/cache@v2 + id: cache + with: + path: | + experimental/node_modules + experimental/*/*/node_modules + key: browser-tests-experimental-${{ runner.os }}-${{ matrix.node_version }}-${{ hashFiles('**/package.json') }} + + - name: Bootstrap + if: steps.cache.outputs.cache-hit != 'true' + working-directory: experimental + run: | + npm install --ignore-scripts + npx lerna bootstrap --no-ci --hoist --nohoist='zone.js' + + - name: Build 🔧 + working-directory: experimental + run: | + npm run compile + + - name: Unit tests + working-directory: experimental + run: npm run test:webworker + - name: Report Coverage + working-directory: experimental + run: npm run codecov:webworker diff --git a/doc/web-api.md b/doc/web-api.md new file mode 100644 index 0000000000..8d916e5847 --- /dev/null +++ b/doc/web-api.md @@ -0,0 +1,21 @@ +# Web API Usages Guidance + +The packages of OpenTelemetry that targeting web platforms should be compatible +with the following web environments: + +- [Browsing Context](https://developer.mozilla.org/en-US/docs/Glossary/Browsing_context), +- [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers). + +As such, the usage of Web API that depends on APIs like [`window`], +[`document`] and [`navigator`] is discouraged. + +If the use of the browsing context API is necessary, like adding `onload` +listeners, an alternative code path for Web Worker environment should also be +supported. + +It is an exception to above guidance if the package is instrumenting the +browsing context only. + +[`window`]: https://developer.mozilla.org/en-US/docs/Web/API/window +[`document`]: https://developer.mozilla.org/en-US/docs/Web/API/Document +[`navigator]: https://developer.mozilla.org/en-US/docs/Web/API/Navigator diff --git a/experimental/packages/opentelemetry-instrumentation-xml-http-request/.eslintrc.js b/experimental/packages/opentelemetry-instrumentation-xml-http-request/.eslintrc.js index 6ad6c9f216..b50dfd815b 100644 --- a/experimental/packages/opentelemetry-instrumentation-xml-http-request/.eslintrc.js +++ b/experimental/packages/opentelemetry-instrumentation-xml-http-request/.eslintrc.js @@ -1,9 +1,7 @@ module.exports = { "env": { "mocha": true, - "commonjs": true, "browser": true, - "jquery": true }, ...require('../../../eslint.config.js') } diff --git a/karma.base.js b/karma.base.js index cbae813f7b..7df76d7b13 100644 --- a/karma.base.js +++ b/karma.base.js @@ -26,6 +26,8 @@ module.exports = { }, reporters: ['spec', 'coverage-istanbul'], files: ['test/index-webpack.ts'], - preprocessors: { 'test/index-webpack.ts': ['webpack'] }, + preprocessors: { + 'test/index-webpack*.ts': ['webpack'] + }, webpackMiddleware: { noInfo: true } }; diff --git a/karma.worker.js b/karma.worker.js new file mode 100644 index 0000000000..9a6f9d5eb3 --- /dev/null +++ b/karma.worker.js @@ -0,0 +1,27 @@ +/*! + * 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 + * + * 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 baseConfig = require('./karma.base'); + +module.exports = { + ...baseConfig, + frameworks: ['mocha-webworker'], + + files: [{ + pattern: 'test/index-webpack.worker.ts', + included: false, + }], +}; diff --git a/package.json b/package.json index 693ccde5d6..6798a9a996 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,13 @@ "version:update": "lerna run version:update", "test": "lerna run test", "test:browser": "lerna run test:browser", + "test:webworker": "lerna run test:webworker", "test:backcompat": "lerna run test:backcompat", "bootstrap": "lerna bootstrap --hoist --nohoist='zone.js'", "changelog": "lerna-changelog", "codecov": "lerna run codecov", "codecov:browser": "lerna run codecov:browser", + "codecov:webworker": "lerna run codecov:webworker", "predocs-test": "npm run docs", "docs": "typedoc && touch docs/.nojekyll", "docs-deploy": "gh-pages --dotfiles --dist docs", diff --git a/packages/opentelemetry-sdk-trace-web/.eslintrc.js b/packages/opentelemetry-sdk-trace-web/.eslintrc.js index 01b8d09740..86d794e2f5 100644 --- a/packages/opentelemetry-sdk-trace-web/.eslintrc.js +++ b/packages/opentelemetry-sdk-trace-web/.eslintrc.js @@ -1,9 +1,7 @@ module.exports = { "env": { "mocha": true, - "commonjs": true, "browser": true, - "jquery": true }, ...require('../../eslint.config.js') } diff --git a/packages/opentelemetry-sdk-trace-web/karma.worker.js b/packages/opentelemetry-sdk-trace-web/karma.worker.js new file mode 100644 index 0000000000..d392e58ba6 --- /dev/null +++ b/packages/opentelemetry-sdk-trace-web/karma.worker.js @@ -0,0 +1,24 @@ +/*! + * 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 + * + * 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.worker'); + +module.exports = (config) => { + config.set(Object.assign({}, karmaBaseConfig, { + webpack: karmaWebpackConfig + })) +}; diff --git a/packages/opentelemetry-sdk-trace-web/package.json b/packages/opentelemetry-sdk-trace-web/package.json index 2e88f6520e..7e6ce6b66f 100644 --- a/packages/opentelemetry-sdk-trace-web/package.json +++ b/packages/opentelemetry-sdk-trace-web/package.json @@ -13,9 +13,11 @@ "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", "codecov:browser": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "codecov:webworker": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", "version": "node ../../scripts/version-update.js", "tdd": "karma start", "test:browser": "nyc karma start --single-run", + "test:webworker": "nyc karma start karma.worker.js --single-run", "watch": "tsc --build --watch tsconfig.all.json", "precompile": "lerna run version --scope $(npm pkg get name) --include-dependencies", "prewatch": "npm run precompile" @@ -69,6 +71,7 @@ "karma-coverage-istanbul-reporter": "3.0.3", "karma-jquery": "0.2.4", "karma-mocha": "2.0.1", + "karma-mocha-webworker": "1.3.0", "karma-spec-reporter": "0.0.32", "karma-webpack": "4.0.2", "mocha": "7.2.0", diff --git a/packages/opentelemetry-sdk-trace-web/src/utils.ts b/packages/opentelemetry-sdk-trace-web/src/utils.ts index 58831f4f21..fc3978abad 100644 --- a/packages/opentelemetry-sdk-trace-web/src/utils.ts +++ b/packages/opentelemetry-sdk-trace-web/src/utils.ts @@ -164,7 +164,7 @@ export function getResource( } const sorted = sortResources(filteredResources); - if (parsedSpanUrl.origin !== window.location.origin && sorted.length > 1) { + if (parsedSpanUrl.origin !== location.origin && sorted.length > 1) { let corsPreFlightRequest: PerformanceResourceTiming | undefined = sorted[0]; let mainRequest: PerformanceResourceTiming = findMainRequest( sorted, @@ -421,7 +421,7 @@ export function shouldPropagateTraceHeaders( } const parsedSpanUrl = parseUrl(spanUrl); - if (parsedSpanUrl.origin === window.location.origin) { + if (parsedSpanUrl.origin === location.origin) { return true; } else { return propagateTraceHeaderUrls.some(propagateTraceHeaderUrl => diff --git a/packages/opentelemetry-sdk-trace-web/test/StackContextManager.test.ts b/packages/opentelemetry-sdk-trace-web/test/StackContextManager.test.ts index 1509780e7b..8fb1e34d13 100644 --- a/packages/opentelemetry-sdk-trace-web/test/StackContextManager.test.ts +++ b/packages/opentelemetry-sdk-trace-web/test/StackContextManager.test.ts @@ -111,7 +111,7 @@ describe('StackContextManager', () => { assert.strictEqual(contextManager.active(), ctx1); return done(); }); - assert.strictEqual(contextManager.active(), window); + assert.strictEqual(contextManager.active(), globalThis); }); it('should finally restore an old context when context is an object', done => { @@ -130,7 +130,7 @@ describe('StackContextManager', () => { assert.strictEqual(contextManager.active(), ctx1); return done(); }); - assert.strictEqual(contextManager.active(), window); + assert.strictEqual(contextManager.active(), globalThis); }); it('should forward this, arguments and return value', () => { diff --git a/packages/opentelemetry-sdk-trace-web/test/index-webpack.ts b/packages/opentelemetry-sdk-trace-web/test/index-webpack.ts index 061a48ccfa..e455815ca9 100644 --- a/packages/opentelemetry-sdk-trace-web/test/index-webpack.ts +++ b/packages/opentelemetry-sdk-trace-web/test/index-webpack.ts @@ -13,8 +13,13 @@ * 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); +{ + const testsContext = require.context('./', false, /test$/); + testsContext.keys().forEach(testsContext); +} + +{ + const testsContext = require.context('./window', false, /test$/); + testsContext.keys().forEach(testsContext); +} diff --git a/packages/opentelemetry-sdk-trace-web/test/index-webpack.worker.ts b/packages/opentelemetry-sdk-trace-web/test/index-webpack.worker.ts new file mode 100644 index 0000000000..4ee65b7b12 --- /dev/null +++ b/packages/opentelemetry-sdk-trace-web/test/index-webpack.worker.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. + */ + +const testsContext = require.context('./', false, /test$/); +testsContext.keys().forEach(testsContext); diff --git a/packages/opentelemetry-sdk-trace-web/test/utils.test.ts b/packages/opentelemetry-sdk-trace-web/test/utils.test.ts index b8217e4f13..f870450013 100644 --- a/packages/opentelemetry-sdk-trace-web/test/utils.test.ts +++ b/packages/opentelemetry-sdk-trace-web/test/utils.test.ts @@ -27,7 +27,6 @@ import * as sinon from 'sinon'; import { addSpanNetworkEvent, addSpanNetworkEvents, - getElementXPath, getResource, normalizeUrl, parseUrl, @@ -48,45 +47,6 @@ function createHrTime(startTime: HrTime, addToStart: number): HrTime { } return [seconds, nanos]; } -const fixture = ` -
-
-
-
-
-
-
-
-
lorep ipsum
-
-
- foo - - - - - bar -
-
- aaaaaaaaa - - - bbb -
-
-
-
-
- - - - bar -
-
-
-
-
-`; function createResource( resource = {}, @@ -475,92 +435,11 @@ describe('utils', () => { }); }); }); - describe('getElementXPath', () => { - let $fixture: any; - let child: any; - before(() => { - $fixture = $(fixture); - const body = document.querySelector('body'); - if (body) { - body.appendChild($fixture[0]); - child = body.lastChild; - } - }); - after(() => { - child.parentNode.removeChild(child); - }); - - it('should return correct path for element with id and optimise = true', () => { - const element = getElementXPath($fixture.find('#btn22')[0], true); - assert.strictEqual(element, '//*[@id="btn22"]'); - assert.strictEqual( - $fixture.find('#btn22')[0], - getElementByXpath(element) - ); - }); - - it( - 'should return correct path for element with id and surrounded by the' + - ' same type', - () => { - const element = getElementXPath($fixture.find('#btn22')[0]); - assert.strictEqual(element, '//html/body/div/div[4]/div[5]/button[3]'); - assert.strictEqual( - $fixture.find('#btn22')[0], - getElementByXpath(element) - ); - } - ); - - it( - 'should return correct path for element with id and and surrounded by' + - ' text nodes mixed with cnode', - () => { - const element = getElementXPath($fixture.find('#btn23')[0]); - assert.strictEqual(element, '//html/body/div/div[4]/div[6]/button'); - assert.strictEqual( - $fixture.find('#btn23')[0], - getElementByXpath(element) - ); - } - ); - - it( - 'should return correct path for text node element surrounded by cdata' + - ' nodes', - () => { - const text = $fixture.find('#cdata')[0]; - const textNode = document.createTextNode('foobar'); - text.appendChild(textNode); - const element = getElementXPath(textNode); - assert.strictEqual(element, '//html/body/div/div[4]/div[10]/text()[5]'); - assert.strictEqual(textNode, getElementByXpath(element)); - } - ); - - it('should return correct path when element is text node', () => { - const text = $fixture.find('#text')[0]; - const textNode = document.createTextNode('foobar'); - text.appendChild(textNode); - const element = getElementXPath(textNode); - assert.strictEqual(element, '//html/body/div/div[4]/div[3]/text()[2]'); - assert.strictEqual(textNode, getElementByXpath(element)); - }); - - it('should return correct path when element is comment node', () => { - const comment = $fixture.find('#comment')[0]; - const node = document.createComment('foobar'); - comment.appendChild(node); - const element = getElementXPath(node); - assert.strictEqual(element, '//html/body/div/div[4]/div[8]/comment()'); - 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` + `${globalThis.location.origin}/foo/bar` ); assert.strictEqual(result, true); }); @@ -611,14 +490,6 @@ describe('utils', () => { assert.strictEqual(typeof url[field], 'string'); }); }); - - it('should parse url with fallback', () => { - sinon.stub(window, 'URL').value(undefined); - const url = parseUrl('https://opentelemetry.io/foo'); - urlFields.forEach(field => { - assert.strictEqual(typeof url[field], 'string'); - }); - }); }); describe('normalizeUrl', () => { @@ -626,21 +497,5 @@ describe('utils', () => { const url = normalizeUrl('https://opentelemetry.io/你好'); assert.strictEqual(url, 'https://opentelemetry.io/%E4%BD%A0%E5%A5%BD'); }); - - it('should parse url with fallback', () => { - sinon.stub(window, 'URL').value(undefined); - const url = normalizeUrl('https://opentelemetry.io/你好'); - assert.strictEqual(url, 'https://opentelemetry.io/%E4%BD%A0%E5%A5%BD'); - }); }); }); - -function getElementByXpath(path: string) { - return document.evaluate( - path, - document, - null, - XPathResult.FIRST_ORDERED_NODE_TYPE, - null - ).singleNodeValue; -} diff --git a/packages/opentelemetry-sdk-trace-web/test/window/utils.test.ts b/packages/opentelemetry-sdk-trace-web/test/window/utils.test.ts new file mode 100644 index 0000000000..fb184c2d1b --- /dev/null +++ b/packages/opentelemetry-sdk-trace-web/test/window/utils.test.ts @@ -0,0 +1,193 @@ +/* + * 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 assert from 'assert'; +import * as sinon from 'sinon'; +import { + getElementXPath, + normalizeUrl, + parseUrl, + URLLike, +} from '../../src/utils'; + +const fixture = ` +
+
+
+
+
+
+
+
+
lorep ipsum
+
+
+ foo + + + + + bar +
+
+ aaaaaaaaa + + + bbb +
+
+
+
+
+ + + + bar +
+
+
+
+
+`; + +describe('utils', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('getElementXPath', () => { + let $fixture: any; + let child: any; + before(() => { + $fixture = $(fixture); + const body = document.querySelector('body'); + if (body) { + body.appendChild($fixture[0]); + child = body.lastChild; + } + }); + after(() => { + child.parentNode.removeChild(child); + }); + + it('should return correct path for element with id and optimise = true', () => { + const element = getElementXPath($fixture.find('#btn22')[0], true); + assert.strictEqual(element, '//*[@id="btn22"]'); + assert.strictEqual( + $fixture.find('#btn22')[0], + getElementByXpath(element) + ); + }); + + it( + 'should return correct path for element with id and surrounded by the' + + ' same type', + () => { + const element = getElementXPath($fixture.find('#btn22')[0]); + assert.strictEqual(element, '//html/body/div/div[4]/div[5]/button[3]'); + assert.strictEqual( + $fixture.find('#btn22')[0], + getElementByXpath(element) + ); + } + ); + + it( + 'should return correct path for element with id and and surrounded by' + + ' text nodes mixed with cnode', + () => { + const element = getElementXPath($fixture.find('#btn23')[0]); + assert.strictEqual(element, '//html/body/div/div[4]/div[6]/button'); + assert.strictEqual( + $fixture.find('#btn23')[0], + getElementByXpath(element) + ); + } + ); + + it( + 'should return correct path for text node element surrounded by cdata' + + ' nodes', + () => { + const text = $fixture.find('#cdata')[0]; + const textNode = document.createTextNode('foobar'); + text.appendChild(textNode); + const element = getElementXPath(textNode); + assert.strictEqual(element, '//html/body/div/div[4]/div[10]/text()[5]'); + assert.strictEqual(textNode, getElementByXpath(element)); + } + ); + + it('should return correct path when element is text node', () => { + const text = $fixture.find('#text')[0]; + const textNode = document.createTextNode('foobar'); + text.appendChild(textNode); + const element = getElementXPath(textNode); + assert.strictEqual(element, '//html/body/div/div[4]/div[3]/text()[2]'); + assert.strictEqual(textNode, getElementByXpath(element)); + }); + + it('should return correct path when element is comment node', () => { + const comment = $fixture.find('#comment')[0]; + const node = document.createComment('foobar'); + comment.appendChild(node); + const element = getElementXPath(node); + assert.strictEqual(element, '//html/body/div/div[4]/div[8]/comment()'); + assert.strictEqual(node, getElementByXpath(element)); + }); + }); + + describe('parseUrl', () => { + const urlFields: Array = [ + 'hash', + 'host', + 'hostname', + 'href', + 'origin', + 'password', + 'pathname', + 'port', + 'protocol', + 'search', + 'username', + ]; + it('should parse url with fallback', () => { + sinon.stub(globalThis, 'URL').value(undefined); + const url = parseUrl('https://opentelemetry.io/foo'); + urlFields.forEach(field => { + assert.strictEqual(typeof url[field], 'string'); + }); + }); + }); + + describe('normalizeUrl', () => { + it('should parse url with fallback', () => { + sinon.stub(globalThis, 'URL').value(undefined); + const url = normalizeUrl('https://opentelemetry.io/你好'); + assert.strictEqual(url, 'https://opentelemetry.io/%E4%BD%A0%E5%A5%BD'); + }); + }); +}); + +function getElementByXpath(path: string) { + return document.evaluate( + path, + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ).singleNodeValue; +}