feat(sdk-trace-web): web worker support (#2719)

Co-authored-by: Valentin Marchaud <contact@vmarchaud.fr>
This commit is contained in:
legendecas 2022-01-21 05:33:54 +08:00 committed by GitHub
parent e32879abbc
commit 04f9edd12f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 379 additions and 159 deletions

View File

@ -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

21
doc/web-api.md Normal file
View File

@ -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

View File

@ -1,9 +1,7 @@
module.exports = {
"env": {
"mocha": true,
"commonjs": true,
"browser": true,
"jquery": true
},
...require('../../../eslint.config.js')
}

View File

@ -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 }
};

27
karma.worker.js Normal file
View File

@ -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,
}],
};

View File

@ -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",

View File

@ -1,9 +1,7 @@
module.exports = {
"env": {
"mocha": true,
"commonjs": true,
"browser": true,
"jquery": true
},
...require('../../eslint.config.js')
}

View File

@ -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
}))
};

View File

@ -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",

View File

@ -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 =>

View File

@ -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', () => {

View File

@ -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);
}

View File

@ -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);

View File

@ -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 = `
<div>
<div></div>
<div></div>
<div></div>
<div>
<div></div>
<div>
</div>
<div id="text">lorep ipsum</div>
<div></div>
<div class="btn2">
foo
<button></button>
<button></button>
<button id="btn22"></button>
<button></button>
bar
</div>
<div>
aaaaaaaaa
<![CDATA[ /*Some code with < & and what not */ ]]>
<button id="btn23"></button>
bbb
</div>
<div></div>
<div id="comment"></div>
<div></div>
<div id="cdata">
<![CDATA[ /*Some code with < & and what not */ ]]>
<![CDATA[ /*Some code with < & and what not */ ]]>
<![CDATA[ /*Some code with < & and what not */ ]]>
bar
</div>
<div></div>
</div>
<div></div>
</div>
`;
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;
}

View File

@ -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 = `
<div>
<div></div>
<div></div>
<div></div>
<div>
<div></div>
<div>
</div>
<div id="text">lorep ipsum</div>
<div></div>
<div class="btn2">
foo
<button></button>
<button></button>
<button id="btn22"></button>
<button></button>
bar
</div>
<div>
aaaaaaaaa
<![CDATA[ /*Some code with < & and what not */ ]]>
<button id="btn23"></button>
bbb
</div>
<div></div>
<div id="comment"></div>
<div></div>
<div id="cdata">
<![CDATA[ /*Some code with < & and what not */ ]]>
<![CDATA[ /*Some code with < & and what not */ ]]>
<![CDATA[ /*Some code with < & and what not */ ]]>
bar
</div>
<div></div>
</div>
<div></div>
</div>
`;
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<keyof URLLike> = [
'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;
}