diff --git a/README.md b/README.md index 8e18c085535..95d5b443796 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ OpenTelemetry is vendor-agnostic and can upload data to any backend with various OpenTelemetry can collect tracing data automatically using plugins. Vendors/Users can also create and use their own. Currently, OpenTelemetry supports automatic tracing for: +#### Node Plugins - [@opentelemetry/plugin-http](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-http) - [@opentelemetry/plugin-grpc](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-grpc) - [@opentelemetry/plugin-https](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-https) @@ -112,6 +113,9 @@ OpenTelemetry can collect tracing data automatically using plugins. Vendors/User - [@opentelemetry/plugin-mongodb](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-mongodb) - WIP - [@opentelemetry/plugin-postgres](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-postgres) - WIP +#### Web Plugins +- [@opentelemetry/plugin-document-load](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-document-load) + To request automatic tracing support for a module not on this list, please [file an issue](https://github.com/open-telemetry/opentelemetry-js/issues). Alternatively, you can [write a plugin yourself](https://github.com/open-telemetry/opentelemetry-js/blob/master/doc/plugin-guide.md). ### Shims diff --git a/examples/tracer-web/index.html b/examples/tracer-web/index.html index 4e9afa7bd64..947630827c0 100644 --- a/examples/tracer-web/index.html +++ b/examples/tracer-web/index.html @@ -23,6 +23,9 @@ Example of using Web Tracer with document load plugin and console exporter +
+ + diff --git a/examples/tracer-web/index.js b/examples/tracer-web/index.js index a8e3e34b413..0e5e9956e33 100644 --- a/examples/tracer-web/index.js +++ b/examples/tracer-web/index.js @@ -1,11 +1,83 @@ import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; import { WebTracer } from '@opentelemetry/web'; import { DocumentLoad } from '@opentelemetry/plugin-document-load'; +import { ZoneScopeManager } from '@opentelemetry/scope-zone'; const webTracer = new WebTracer({ plugins: [ new DocumentLoad() ] }); - webTracer.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); + +const webTracerWithZone = new WebTracer({ + scopeManager: new ZoneScopeManager(), + plugins: [ + new DocumentLoad() + ] +}); +webTracerWithZone.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); + +console.log('Current span is window', webTracerWithZone.getCurrentSpan() === window); + +// example of keeping track of scope between async operations +const prepareClickEvent = () => { + const url1 = 'https://raw.githubusercontent.com/open-telemetry/opentelemetry-js/master/package.json'; + const url2 = 'https://raw.githubusercontent.com/open-telemetry/opentelemetry-js/master/packages/opentelemetry-web/package.json'; + + const element = document.getElementById('button1'); + let mainSpan = webTracerWithZone.startSpan('main-span'); + webTracerWithZone.bind(element, mainSpan); + + const onClick = () => { + const span1 = webTracerWithZone.startSpan(`files-series-info-1`, { + parent: webTracerWithZone.getCurrentSpan() + }); + + const span2 = webTracerWithZone.startSpan(`files-series-info-2`, { + parent: webTracerWithZone.getCurrentSpan() + }); + + webTracerWithZone.withSpan(span1, () => { + getData(url1).then((data) => { + console.log('current span is span1', webTracerWithZone.getCurrentSpan() === span1); + console.log('info from package.json', data.description, data.version); + webTracerWithZone.getCurrentSpan().addEvent('fetching-span1-completed'); + span1.end(); + }); + }); + + webTracerWithZone.withSpan(span2, () => { + getData(url2).then((data) => { + setTimeout(() => { + console.log('current span is span2', webTracerWithZone.getCurrentSpan() === span2); + console.log('info from package.json', data.description, data.version); + webTracerWithZone.getCurrentSpan().addEvent('fetching-span2-completed'); + span2.end(); + }, 100); + }); + }); + }; + element.addEventListener('click', onClick); +}; + +const getData = (url) => { + return new Promise(async (resolve, reject) => { + const req = new XMLHttpRequest(); + req.open('GET', url, true); + req.send(); + req.onload = function () { + let json; + try { + json = JSON.parse(req.responseText); + } catch (e) { + reject(e); + } + resolve(json); + }; + }); +}; + +window.addEventListener('load', () => { + prepareClickEvent(); +}); diff --git a/examples/tracer-web/package.json b/examples/tracer-web/package.json index bc38aa6a1de..35c98c82408 100644 --- a/examples/tracer-web/package.json +++ b/examples/tracer-web/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@opentelemetry/plugin-document-load": "^0.2.0", + "@opentelemetry/scope-zone": "^0.2.0", "@opentelemetry/tracing": "^0.2.0", "@opentelemetry/web": "^0.2.0" }, diff --git a/examples/tracer-web/webpack.config.js b/examples/tracer-web/webpack.config.js index 69ad1b74f29..0c4efe02b24 100644 --- a/examples/tracer-web/webpack.config.js +++ b/examples/tracer-web/webpack.config.js @@ -26,14 +26,6 @@ const common = { } ] }, - plugins: [ - new webpack.ProvidePlugin({ - jQuery: 'jquery', - $: 'jquery', - jquery: 'jquery', - 'window.jQuery': 'jquery' - }) - ], resolve: { modules: [ path.resolve(mainPath, 'src'), @@ -52,8 +44,6 @@ module.exports = webpackMerge(common, { }, devServer: { contentBase: path.resolve(__dirname), - // contentBase: path.resolve('.'), - // historyApiFallback: true }, plugins: [ new webpack.DefinePlugin({ diff --git a/packages/opentelemetry-scope-zone-peer-dep/LICENSE b/packages/opentelemetry-scope-zone-peer-dep/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/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-scope-zone-peer-dep/README.md b/packages/opentelemetry-scope-zone-peer-dep/README.md new file mode 100644 index 00000000000..72d83ed591d --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/README.md @@ -0,0 +1,71 @@ +# OpenTelemetry Scope Zone Peer Dependency +[![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 *Zone Scope Manager with a peer dependency for [zone-js]* for Web applications. +If you use Angular you already have the [zone-js] and you should use this package. +If you don't have your own [zone-js] please use [@opentelemetry/scope-zone] + + +## Installation + +```bash +npm install --save @opentelemetry/scope-zone-peer-dep +``` + +## Usage +```js +import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; +import { WebTracer } from '@opentelemetry/web'; +import { ZoneScopeManager } from '@opentelemetry/scope-zone-peer-dep'; + +const webTracerWithZone = new WebTracer({ + scopeManager: new ZoneScopeManager() +}); +webTracerWithZone.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); + +// Example how the ZoneScopeManager keeps the reference to the correct scope during async operations +const span1 = webTracerWithZone.startSpan('foo1'); +webTracerWithZone.withSpan(span1, () => { + console.log('Current span is span1', webTracerWithZone.getCurrentSpan() === span1); + setTimeout(() => { + const span2 = webTracerWithZone.startSpan('foo2'); + console.log('Current span is span1', webTracerWithZone.getCurrentSpan() === span1); + webTracerWithZone.withSpan(span2, () => { + console.log('Current span is span2', webTracerWithZone.getCurrentSpan() === span2); + setTimeout(() => { + console.log('Current span is span2', webTracerWithZone.getCurrentSpan() === span2); + }, 500); + }); + // there is a timeout which still keeps span2 active + console.log('Current span is span2', webTracerWithZone.getCurrentSpan() === span2); + }, 500); + console.log('Current span is span1', webTracerWithZone.getCurrentSpan() === span1); +}); + +``` + +## 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-scope-zone-peer-dep +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-scope-zone-peer-dep +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-scope-zone-peer-dep +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-web&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/scope-zone-peer-dep +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fscope-zone-peer-dep.svg +[zone-js]: https://www.npmjs.com/package/zone.js +[@opentelemetry/scope-zone]: https://www.npmjs.com/package/@opentelemetry/scope-zone diff --git a/packages/opentelemetry-scope-zone-peer-dep/karma.conf.js b/packages/opentelemetry-scope-zone-peer-dep/karma.conf.js new file mode 100644 index 00000000000..7183aab0336 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/karma.conf.js @@ -0,0 +1,24 @@ +/*! + * Copyright 2019, 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-scope-zone-peer-dep/package.json b/packages/opentelemetry-scope-zone-peer-dep/package.json new file mode 100644 index 00000000000..0497800f462 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/package.json @@ -0,0 +1,80 @@ +{ + "name": "@opentelemetry/scope-zone-peer-dep", + "version": "0.2.0", + "description": "OpenTelemetry Scope Zone with peer dependency for zone.js", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "check": "gts check", + "clean": "rimraf build/*", + "codecov:browser": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "compile": "tsc -p .", + "fix": "gts fix", + "prepare": "npm run compile", + "tdd": "karma start", + "test:browser": "nyc karma start --single-run", + "watch": "tsc -w" + }, + "keywords": [ + "opentelemetry", + "web", + "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", + "@types/mocha": "^5.2.5", + "@types/node": "^12.6.8", + "@types/sinon": "^7.0.13", + "@types/webpack-env": "1.13.9", + "@types/zone.js": "^0.5.12", + "babel-loader": "^8.0.6", + "codecov": "^3.1.0", + "gts": "^1.0.0", + "istanbul-instrumenter-loader": "^3.0.1", + "karma": "^4.4.1", + "karma-chrome-launcher": "^3.1.0", + "karma-coverage-istanbul-reporter": "^2.1.0", + "karma-mocha": "^1.3.0", + "karma-spec-reporter": "^0.0.32", + "karma-webpack": "^4.0.2", + "mocha": "^6.1.0", + "nyc": "^14.1.1", + "rimraf": "^3.0.0", + "sinon": "^7.5.0", + "tslint-consistent-codestyle": "^1.16.0", + "tslint-microsoft-contrib": "^6.2.0", + "ts-loader": "^6.0.4", + "ts-mocha": "^6.0.0", + "ts-node": "^8.0.0", + "typescript": "^3.6.3", + "webpack": "^4.35.2", + "webpack-cli": "^3.3.9", + "zone.js": "^0.10.2" + }, + "dependencies": { + "@opentelemetry/scope-base": "^0.2.0" + }, + "peerDependencies": { + "zone.js": "^0.10.2" + }, + "sideEffects": false +} diff --git a/packages/opentelemetry-scope-zone-peer-dep/src/ZoneScopeManager.ts b/packages/opentelemetry-scope-zone-peer-dep/src/ZoneScopeManager.ts new file mode 100644 index 00000000000..d735e73c7d5 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/src/ZoneScopeManager.ts @@ -0,0 +1,258 @@ +/*! + * Copyright 2019, 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 { ScopeManager } from '@opentelemetry/scope-base'; +import { Func, TargetWithEvents } from './types'; +import { isListenerObject } from './util'; + +/* Key name to be used to save a scope reference in Zone */ +const ZONE_SCOPE_KEY = 'OT_ZONE_SCOPE'; + +/** + * ZoneScopeManager + * This module provides an easy functionality for tracing action between asynchronous operations in web. + * It was not possible with standard [StackScopeManager]{@link https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-web/src/StackScopeManager.ts}. + * It heavily depends on [zone.js]{@link https://www.npmjs.com/package/zone.js}. + * It stores the information about scope in zone. Each Scope will have always new Zone; + * It also supports binding a certain Span to a target that has "addEventListener" and "removeEventListener". + * When this happens a new zone is being created and the provided Span is being assigned to this zone. + */ +export class ZoneScopeManager implements ScopeManager { + /** + * whether the scope manager is enabled or not + */ + private _enabled = false; + + /** + * Helps to create a unique name for the zones - part of zone name + */ + private _zoneCounter = 0; + + /** + * Returns the active scope from certain zone name + * @param activeZone + */ + private _activeScopeFromZone( + activeZone: Zone | undefined + ): unknown | undefined { + return activeZone && activeZone.get(ZONE_SCOPE_KEY); + } + + /** + * @param target Function to be executed within the scope + * @param scope A scope (span) to be executed within target function + */ + private _bindFunction(target: T, scope?: unknown): T { + const manager = this; + const contextWrapper = function(this: any, ...args: unknown[]) { + return manager.with(scope, () => target.apply(this || scope, args)); + }; + Object.defineProperty(contextWrapper, 'length', { + enumerable: false, + configurable: true, + writable: false, + value: target.length, + }); + return (contextWrapper as unknown) as T; + } + + /** + * @param obj target object on which the listeners will be patched + * @param scope A scope (span) to be bind to target + */ + private _bindListener(obj: T, scope?: unknown): T { + const target = (obj as unknown) as TargetWithEvents; + if (target.__ot_listeners !== undefined) { + return obj; + } + target.__ot_listeners = {}; + + if (typeof target.addEventListener === 'function') { + target.addEventListener = this._patchAddEventListener( + target, + target.addEventListener, + scope + ); + } + + if (typeof target.removeEventListener === 'function') { + target.removeEventListener = this._patchRemoveEventListener( + target, + target.removeEventListener + ); + } + + return obj; + } + + /** + * Creates a new unique zone name + */ + private _createZoneName() { + this._zoneCounter++; + const random = Math.random(); + return `${this._zoneCounter}-${random}`; + } + + /** + * Creates a new zone + * @param zoneName zone name + * @param scope A scope (span) to be bind with Zone + */ + private _createZone(zoneName: string, scope: unknown): Zone { + return Zone.root.fork({ + name: zoneName, + properties: { + [ZONE_SCOPE_KEY]: scope, + }, + }); + } + + /** + * Returns the active zone + */ + private _getActiveZone(): Zone | undefined { + return Zone.current; + } + + /** + * Patches addEventListener method + * @param target any target that has "addEventListener" method + * @param original reference to the patched method + * @param [scope] scope to be bind to the listener + */ + private _patchAddEventListener( + target: TargetWithEvents, + original: Function, + scope?: unknown + ) { + const scopeManager = this; + + return function(this: {}, event: string, listener: Func, opts?: any) { + if (target.__ot_listeners === undefined) { + target.__ot_listeners = {}; + } + let listeners = target.__ot_listeners[event]; + if (listeners === undefined) { + listeners = new WeakMap(); + target.__ot_listeners[event] = listeners; + } + const patchedListener = scopeManager.bind(listener, scope); + // store a weak reference of the user listener to ours + listeners.set(listener, patchedListener); + return original.call(this, event, patchedListener, opts); + }; + } + + /** + * Patches removeEventListener method + * @param target any target that has "removeEventListener" method + * @param original reference to the patched method + */ + private _patchRemoveEventListener( + target: TargetWithEvents, + original: Function + ) { + return function(this: {}, event: string, listener: Func) { + if ( + target.__ot_listeners === undefined || + target.__ot_listeners[event] === undefined + ) { + return original.call(this, event, listener); + } + const events = target.__ot_listeners[event]; + const patchedListener = events.get(listener); + events.delete(listener); + return original.call(this, event, patchedListener || listener); + }; + } + + /** + * Returns the active scope + */ + active(): unknown | undefined { + const activeZone = this._getActiveZone(); + + const active = this._activeScopeFromZone(activeZone); + if (active) { + return active; + } + if (this._enabled) { + return window; + } + return undefined; + } + + /** + * Binds a the certain scope or the active one to the target function and then returns the target + * @param target + * @param scope A scope (span) to be bind to target + */ + bind(target: T | TargetWithEvents, scope?: unknown): T { + // if no specific scope to propagate is given, we use the current one + if (scope === undefined) { + scope = this.active(); + } + if (typeof target === 'function') { + return this._bindFunction(target, scope); + } else if (isListenerObject(target)) { + this._bindListener(target, scope); + } + return (target as unknown) as T; + } + + /** + * Disable the scope manager (clears all the scopes) + */ + disable(): this { + this._enabled = false; + return this; + } + + /** + * Enables the scope manager and creates a default(root) scope + */ + enable(): this { + if (this._enabled) { + return this; + } + this._enabled = true; + return this; + } + + /** + * Calls the callback function [fn] with the provided [scope]. + * If [scope] is undefined then it will use the active scope. + * The scope will be set as active + * @param scope A scope (span) to be called with provided callback + * @param fn Callback function + */ + with ReturnType>( + scope: unknown, + fn: () => ReturnType + ): ReturnType { + // if no scope use active from active zone + if (typeof scope === 'undefined' || scope === null) { + scope = this.active(); + } + + const zoneName = this._createZoneName(); + + const newZone = this._createZone(zoneName, scope); + + return newZone.run(fn, scope); + } +} diff --git a/packages/opentelemetry-scope-zone-peer-dep/src/index.ts b/packages/opentelemetry-scope-zone-peer-dep/src/index.ts new file mode 100644 index 00000000000..3360b42ee33 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/src/index.ts @@ -0,0 +1,18 @@ +/*! + * Copyright 2019, 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 './ZoneScopeManager'; +export * from './types'; diff --git a/packages/opentelemetry-scope-zone-peer-dep/src/types.ts b/packages/opentelemetry-scope-zone-peer-dep/src/types.ts new file mode 100644 index 00000000000..e67614be708 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/src/types.ts @@ -0,0 +1,35 @@ +/*! + * Copyright 2019, 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 type Func = (...args: unknown[]) => T; + +/** + * Minimum requirements that the object needs to have so that it can bind to the events instead of function + * this is "addEventListener" and "removeEventListener" - see {@link isListenerObject} + */ +export interface TargetWithEvents { + addEventListener?( + event: string, + listener: (...args: any[]) => void, + opts?: { once: boolean } + ): any; + removeEventListener?( + event: string, + listener: (...args: any[]) => void, + opts?: { once: boolean } + ): any; + __ot_listeners?: { [name: string]: WeakMap, Func> }; +} diff --git a/packages/opentelemetry-scope-zone-peer-dep/src/util.ts b/packages/opentelemetry-scope-zone-peer-dep/src/util.ts new file mode 100644 index 00000000000..98022f92195 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/src/util.ts @@ -0,0 +1,28 @@ +/*! + * Copyright 2019, 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 { TargetWithEvents } from './types'; + +/** + * check if an object has addEventListener and removeEventListener functions then it will return true + * @param obj + */ +export function isListenerObject(obj: TargetWithEvents = {}): boolean { + return ( + typeof obj.addEventListener === 'function' && + typeof obj.removeEventListener === 'function' + ); +} diff --git a/packages/opentelemetry-scope-zone-peer-dep/test/ZoneScopeManager.test.ts b/packages/opentelemetry-scope-zone-peer-dep/test/ZoneScopeManager.test.ts new file mode 100644 index 00000000000..490d5bfe455 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/test/ZoneScopeManager.test.ts @@ -0,0 +1,290 @@ +/*! + * Copyright 2019, 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 'zone.js'; +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import { ZoneScopeManager } from '../src'; + +let clock: any; + +describe('ZoneScopeManager', () => { + let scopeManager: ZoneScopeManager; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + scopeManager = new ZoneScopeManager(); + scopeManager.enable(); + }); + + afterEach(() => { + clock.restore(); + scopeManager.disable(); + }); + + describe('.enable()', () => { + it('should work', () => { + assert.doesNotThrow(() => { + assert(scopeManager.enable() === scopeManager, 'should return this'); + assert(scopeManager.active() === window, 'should has root scope'); + }); + }); + }); + + describe('.disable()', () => { + it('should work', () => { + assert.doesNotThrow(() => { + assert(scopeManager.disable() === scopeManager, 'should return this'); + assert(scopeManager.active() === undefined, 'should has no scope'); + }); + }); + }); + + describe('.with()', () => { + it('should run the callback (null as target)', done => { + scopeManager.with(null, done); + }); + + it('should run the callback (object as target)', done => { + const test = { a: 1 }; + scopeManager.with(test, () => { + assert.strictEqual(scopeManager.active(), test, 'should have scope'); + return done(); + }); + }); + + it('should run the callback (when disabled)', done => { + scopeManager.disable(); + scopeManager.with(null, () => { + scopeManager.enable(); + return done(); + }); + }); + + it('should rethrow errors', done => { + assert.throws(() => { + scopeManager.with(null, () => { + throw new Error('This should be rethrown'); + }); + }); + return done(); + }); + + it('should finally restore an old scope, including the async task', done => { + const scope1 = 'scope1'; + const scope2 = 'scope2'; + const scope3 = 'scope3'; + + scopeManager.with(scope1, () => { + assert.strictEqual(scopeManager.active(), 'scope1'); + scopeManager.with(scope2, () => { + assert.strictEqual(scopeManager.active(), 'scope2'); + scopeManager.with(scope3, () => { + assert.strictEqual(scopeManager.active(), 'scope3'); + }); + assert.strictEqual(scopeManager.active(), 'scope2'); + }); + assert.strictEqual(scopeManager.active(), 'scope1'); + setTimeout(() => { + assert.strictEqual(scopeManager.active(), 'scope1'); + done(); + }, 500); + clock.tick(500); + }); + assert.strictEqual(scopeManager.active(), window); + }); + + it('should finally restore an old scope when scope is an object, including the async task', done => { + const scope1 = { a: 1 }; + const scope2 = { a: 2 }; + const scope3 = { a: 3 }; + scopeManager.with(scope1, () => { + assert.strictEqual(scopeManager.active(), scope1); + scopeManager.with(scope2, () => { + assert.strictEqual(scopeManager.active(), scope2); + scopeManager.with(scope3, () => { + assert.strictEqual(scopeManager.active(), scope3); + }); + assert.strictEqual(scopeManager.active(), scope2); + }); + assert.strictEqual(scopeManager.active(), scope1); + setTimeout(() => { + assert.strictEqual(scopeManager.active(), scope1); + done(); + }, 500); + clock.tick(500); + }); + assert.strictEqual(scopeManager.active(), window); + }); + it('should correctly return the scopes for 3 parallel actions', () => { + const rootSpan = { name: 'rootSpan' }; + scopeManager.with(rootSpan, () => { + assert.ok( + scopeManager.active() === rootSpan, + 'Current span is rootSpan' + ); + const concurrentSpan1 = { name: 'concurrentSpan1' }; + const concurrentSpan2 = { name: 'concurrentSpan2' }; + const concurrentSpan3 = { name: 'concurrentSpan3' }; + + scopeManager.with(concurrentSpan1, () => { + setTimeout(() => { + assert.ok( + scopeManager.active() === concurrentSpan1, + 'Current span is concurrentSpan1' + ); + }, 10); + }); + + scopeManager.with(concurrentSpan2, () => { + setTimeout(() => { + assert.ok( + scopeManager.active() === concurrentSpan2, + 'Current span is concurrentSpan2' + ); + }, 20); + }); + + scopeManager.with(concurrentSpan3, () => { + setTimeout(() => { + assert.ok( + scopeManager.active() === concurrentSpan3, + 'Current span is concurrentSpan3' + ); + }, 30); + }); + }); + }); + }); + + describe('.bind(function)', () => { + it('should call the function with previously assigned scope', () => { + class Obj { + title: string; + + constructor(title: string) { + this.title = title; + } + + getTitle() { + return this.title; + } + } + + const obj1 = new Obj('a1'); + obj1.title = 'a2'; + const obj2 = new Obj('b1'); + const wrapper: any = scopeManager.bind(obj2.getTitle, obj1); + assert.ok(wrapper(), 'a2'); + }); + + it('should return the same target (when enabled)', () => { + const test = { a: 1 }; + assert.deepStrictEqual(scopeManager.bind(test), test); + }); + + it('should return the same target (when disabled)', () => { + scopeManager.disable(); + const test = { a: 1 }; + assert.deepStrictEqual(scopeManager.bind(test), test); + scopeManager.enable(); + }); + + it('should return current scope (when enabled)', done => { + const scope = { a: 1 }; + const fn: any = scopeManager.bind(() => { + assert.strictEqual(scopeManager.active(), scope, 'should have scope'); + return done(); + }, scope); + fn(); + }); + + it('should return current scope (when disabled)', done => { + scopeManager.disable(); + const scope = { a: 1 }; + const fn: any = scopeManager.bind(() => { + assert.strictEqual(scopeManager.active(), scope, 'should have scope'); + return done(); + }, scope); + fn(); + }); + + it('should bind the the certain scope to the target "addEventListener" function', done => { + const scope1 = { a: 1 }; + const element = document.createElement('div'); + + scopeManager.bind(element, scope1); + + element.addEventListener('click', () => { + assert.strictEqual(scopeManager.active(), scope1); + setTimeout(() => { + assert.strictEqual(scopeManager.active(), scope1); + done(); + }, 500); + clock.tick(500); + }); + + element.dispatchEvent( + new CustomEvent('click', { + bubbles: true, + cancelable: false, + composed: true, + }) + ); + }); + + it('should preserve zone when creating new click event inside zone', done => { + const scope1 = { a: 1 }; + const element = document.createElement('div'); + + scopeManager.bind(element, scope1); + + element.addEventListener('click', () => { + assert.strictEqual(scopeManager.active(), scope1); + setTimeout(() => { + assert.strictEqual(scopeManager.active(), scope1); + const element2 = document.createElement('div'); + + element2.addEventListener('click', () => { + assert.strictEqual(scopeManager.active(), scope1); + setTimeout(() => { + assert.strictEqual(scopeManager.active(), scope1); + done(); + }, 500); + clock.tick(500); + }); + + element2.dispatchEvent( + new CustomEvent('click', { + bubbles: true, + cancelable: false, + composed: true, + }) + ); + }, 500); + clock.tick(500); + }); + + element.dispatchEvent( + new CustomEvent('click', { + bubbles: true, + cancelable: false, + composed: true, + }) + ); + }); + }); +}); diff --git a/packages/opentelemetry-scope-zone-peer-dep/test/index-webpack.ts b/packages/opentelemetry-scope-zone-peer-dep/test/index-webpack.ts new file mode 100644 index 00000000000..7731f090914 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/test/index-webpack.ts @@ -0,0 +1,23 @@ +/*! + * Copyright 2019, 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 file is the webpack entry point for the browser Karma tests. It requires +// all modules ending in "test" from the current folder and all its subfolders. +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-scope-zone-peer-dep/test/utils.test.ts b/packages/opentelemetry-scope-zone-peer-dep/test/utils.test.ts new file mode 100644 index 00000000000..790bbb28fbe --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/test/utils.test.ts @@ -0,0 +1,54 @@ +/*! + * Copyright 2019, 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 utils from '../src/util'; + +describe('ZoneScopeManager utils', () => { + describe('isListenerObject', () => { + describe('when object contains "addEventListener" and "removeEventListener"', () => { + it('should return true', () => { + const obj = { + addEventListener: function() {}, + removeEventListener: function() {}, + }; + assert.strictEqual(utils.isListenerObject(obj), true); + }); + }); + describe('when object doesn\'t contain "addEventListener" and "removeEventListener"', () => { + it('should return true', () => { + const obj = {}; + assert.strictEqual(utils.isListenerObject(obj), false); + }); + }); + describe('when object contains "addEventListener" only', () => { + it('should return false', () => { + const obj = { + addEventListener: function() {}, + }; + assert.strictEqual(utils.isListenerObject(obj), false); + }); + }); + describe('when object contains "removeEventListener" only', () => { + it('should return false', () => { + const obj = { + removeEventListener: function() {}, + }; + assert.strictEqual(utils.isListenerObject(obj), false); + }); + }); + }); +}); diff --git a/packages/opentelemetry-scope-zone-peer-dep/tsconfig.json b/packages/opentelemetry-scope-zone-peer-dep/tsconfig.json new file mode 100644 index 00000000000..ab49dd3fbd6 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "files": [ "node_modules/zone.js/dist/zone.js.d.ts"], + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/packages/opentelemetry-scope-zone-peer-dep/tslint.json b/packages/opentelemetry-scope-zone-peer-dep/tslint.json new file mode 100644 index 00000000000..0710b135d07 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/tslint.json @@ -0,0 +1,4 @@ +{ + "rulesDirectory": ["node_modules/tslint-microsoft-contrib"], + "extends": ["../../tslint.base.js", "./node_modules/tslint-consistent-codestyle"] +} diff --git a/packages/opentelemetry-scope-zone/LICENSE b/packages/opentelemetry-scope-zone/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/packages/opentelemetry-scope-zone/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-scope-zone/README.md b/packages/opentelemetry-scope-zone/README.md new file mode 100644 index 00000000000..3ab2fd1f1c4 --- /dev/null +++ b/packages/opentelemetry-scope-zone/README.md @@ -0,0 +1,71 @@ +# OpenTelemetry Scope Zone +[![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 *Zone Scope Manager with bundled [zone-js]* for Web applications. +If you have your own [zone-js] please use [@opentelemetry/scope-zone-peer-dep] +If you use Angular it means you already have the [zone-js] and you should use [@opentelemetry/scope-zone-peer-dep] + + +## Installation + +```bash +npm install --save @opentelemetry/scope-zone +``` + +## Usage +```js +import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; +import { WebTracer } from '@opentelemetry/web'; +import { ZoneScopeManager } from '@opentelemetry/scope-zone'; + +const webTracerWithZone = new WebTracer({ + scopeManager: new ZoneScopeManager() +}); +webTracerWithZone.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); + +// Example how the ZoneScopeManager keeps the reference to the correct scope during async operations +const span1 = webTracerWithZone.startSpan('foo1'); +webTracerWithZone.withSpan(span1, () => { + console.log('Current span is span1', webTracerWithZone.getCurrentSpan() === span1); + setTimeout(() => { + const span2 = webTracerWithZone.startSpan('foo2'); + console.log('Current span is span1', webTracerWithZone.getCurrentSpan() === span1); + webTracerWithZone.withSpan(span2, () => { + console.log('Current span is span2', webTracerWithZone.getCurrentSpan() === span2); + setTimeout(() => { + console.log('Current span is span2', webTracerWithZone.getCurrentSpan() === span2); + }, 500); + }); + // there is a timeout which still keeps span2 active + console.log('Current span is span2', webTracerWithZone.getCurrentSpan() === span2); + }, 500); + console.log('Current span is span1', webTracerWithZone.getCurrentSpan() === span1); +}); + +``` + +## 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-scope-zone +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-scope-zone +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-scope-zone +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-web&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/scope-zone +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fscope-zone.svg +[zone-js]: https://www.npmjs.com/package/zone.js +[@opentelemetry/scope-zone-peer-dep]: https://www.npmjs.com/package/@opentelemetry/scope-zone-peer-dep diff --git a/packages/opentelemetry-scope-zone/package.json b/packages/opentelemetry-scope-zone/package.json new file mode 100644 index 00000000000..537571c6a45 --- /dev/null +++ b/packages/opentelemetry-scope-zone/package.json @@ -0,0 +1,73 @@ +{ + "name": "@opentelemetry/scope-zone", + "version": "0.2.0", + "description": "OpenTelemetry Scope Zone", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "check": "gts check", + "clean": "rimraf build/*", + "compile": "tsc -p .", + "fix": "gts fix", + "prepare": "npm run compile", + "watch": "tsc -w" + }, + "keywords": [ + "opentelemetry", + "web", + "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": { + "@types/mocha": "^5.2.5", + "@types/node": "^12.6.8", + "@types/webpack-env": "1.13.9", + "@types/sinon": "^7.0.13", + "@babel/core": "^7.6.0", + "babel-loader": "^8.0.6", + "codecov": "^3.1.0", + "gts": "^1.0.0", + "karma": "^4.1.0", + "karma-chrome-launcher": "^2.2.0", + "karma-mocha": "^1.3.0", + "karma-spec-reporter": "^0.0.32", + "karma-webpack": "^4.0.2", + "mocha": "^6.1.0", + "nyc": "^14.1.1", + "rimraf": "^3.0.0", + "sinon": "^7.5.0", + "tslint-consistent-codestyle": "^1.16.0", + "tslint-microsoft-contrib": "^6.2.0", + "ts-loader": "^6.0.4", + "ts-mocha": "^6.0.0", + "ts-node": "^8.0.0", + "typescript": "^3.6.3", + "webpack": "^4.35.2", + "webpack-cli": "^3.3.9", + "webpack-merge": "^4.2.2" + }, + "dependencies": { + "@opentelemetry/scope-zone-peer-dep": "^0.2.0", + "zone.js": "^0.10.2" + }, + + "sideEffects": true +} diff --git a/packages/opentelemetry-scope-zone/src/index.ts b/packages/opentelemetry-scope-zone/src/index.ts new file mode 100644 index 00000000000..425ddebfda3 --- /dev/null +++ b/packages/opentelemetry-scope-zone/src/index.ts @@ -0,0 +1,18 @@ +/*! + * Copyright 2019, 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 '@opentelemetry/scope-zone-peer-dep'; +import 'zone.js'; diff --git a/packages/opentelemetry-scope-zone/tsconfig.json b/packages/opentelemetry-scope-zone/tsconfig.json new file mode 100644 index 00000000000..ab49dd3fbd6 --- /dev/null +++ b/packages/opentelemetry-scope-zone/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "files": [ "node_modules/zone.js/dist/zone.js.d.ts"], + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/packages/opentelemetry-scope-zone/tslint.json b/packages/opentelemetry-scope-zone/tslint.json new file mode 100644 index 00000000000..0710b135d07 --- /dev/null +++ b/packages/opentelemetry-scope-zone/tslint.json @@ -0,0 +1,4 @@ +{ + "rulesDirectory": ["node_modules/tslint-microsoft-contrib"], + "extends": ["../../tslint.base.js", "./node_modules/tslint-consistent-codestyle"] +} diff --git a/packages/opentelemetry-web/README.md b/packages/opentelemetry-web/README.md index 20117c68fce..54e6a8d94cc 100644 --- a/packages/opentelemetry-web/README.md +++ b/packages/opentelemetry-web/README.md @@ -8,14 +8,36 @@ This module provides *automated instrumentation and tracing* for Web applications. For manual instrumentation see the -[@opentelemetry/web](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-web) package. +[@opentelemetry/tracing](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-tracing) package. ## How does automatic tracing work? +This package exposes a class `WebTracer` that will be able to automatically trace things in Browser only. + +See the example how to use it. + +OpenTelemetry comes with a growing number of instrumentation plugins for well know modules (see [supported modules](https://github.com/open-telemetry/opentelemetry-js#plugins)) and an API to create custom plugins (see [the plugin developer guide](https://github.com/open-telemetry/opentelemetry-js/blob/master/doc/plugin-guide.md)). + +Web Tracer currently supports one plugin for document load. +Unlike Node Tracer, the plugins needs to be initialised and passed in configuration. +The reason is to give user full control over which plugin will be bundled into web page. + +You can choose to use the ZoneScopeManager if you want to trace asynchronous operations. + +## Installation + +```bash +npm install --save @opentelemetry/web +``` + +## Usage + ```js import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; import { WebTracer } from '@opentelemetry/web'; import { DocumentLoad } from '@opentelemetry/plugin-document-load'; +import { ZoneScopeManager } from '@opentelemetry/scope-zone'; +// Minimum required setup - supports only synchronous operations const webTracer = new WebTracer({ plugins: [ new DocumentLoad() @@ -23,26 +45,15 @@ const webTracer = new WebTracer({ }); webTracer.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); -``` - -## Installation - -```bash -npm install --save @opentelemetry/web -``` -## Usage - -```js -// Manual -const { WebTracer } = require('@opentelemetry/web'); -const webTracer = new WebTracer(); -const span = webTracer.startSpan('span1'); -webTracer.withSpan(span, function () { - this.addEvent('start'); +// Changing default scopeManager to use ZoneScopeManager - supports asynchronous operations +const webTracerWithZone = new WebTracer({ + scopeManager: new ZoneScopeManager(), + plugins: [ + new DocumentLoad() + ] }); -span.addEvent('middle'); -span.end(); +webTracerWithZone.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); ``` diff --git a/packages/opentelemetry-web/package.json b/packages/opentelemetry-web/package.json index e8ae37fa6c0..188f0df5b37 100644 --- a/packages/opentelemetry-web/package.json +++ b/packages/opentelemetry-web/package.json @@ -68,7 +68,8 @@ "typescript": "3.7.2", "webpack": "^4.35.2", "webpack-cli": "^3.3.9", - "webpack-merge": "^4.2.2" + "webpack-merge": "^4.2.2", + "@opentelemetry/scope-zone": "^0.2.0" }, "dependencies": { "@opentelemetry/core": "^0.2.0", diff --git a/packages/opentelemetry-web/test/WebTracer.test.ts b/packages/opentelemetry-web/test/WebTracer.test.ts index e1474508ea7..5aa3a5a8590 100644 --- a/packages/opentelemetry-web/test/WebTracer.test.ts +++ b/packages/opentelemetry-web/test/WebTracer.test.ts @@ -21,6 +21,7 @@ import { BasicTracerConfig } from '@opentelemetry/tracing'; import { WebTracerConfig } from '../src'; import { StackScopeManager } from '../src/StackScopeManager'; import { WebTracer } from '../src/WebTracer'; +import { ZoneScopeManager } from '@opentelemetry/scope-zone'; class DummyPlugin extends BasePlugin { patch() {} @@ -78,5 +79,46 @@ describe('WebTracer', () => { tracer = new WebTracer({}); }); }); + + describe('when scopeManager is "ZoneScopeManager"', () => { + it('should correctly return the scopes for 2 parallel actions', () => { + const webTracerWithZone = new WebTracer({ + scopeManager: new ZoneScopeManager(), + }); + + const rootSpan = webTracerWithZone.startSpan('rootSpan'); + + webTracerWithZone.withSpan(rootSpan, () => { + assert.ok( + webTracerWithZone.getCurrentSpan() === rootSpan, + 'Current span is rootSpan' + ); + const concurrentSpan1 = webTracerWithZone.startSpan( + 'concurrentSpan1' + ); + const concurrentSpan2 = webTracerWithZone.startSpan( + 'concurrentSpan2' + ); + + webTracerWithZone.withSpan(concurrentSpan1, () => { + setTimeout(() => { + assert.ok( + webTracerWithZone.getCurrentSpan() === concurrentSpan1, + 'Current span is concurrentSpan1' + ); + }, 10); + }); + + webTracerWithZone.withSpan(concurrentSpan2, () => { + setTimeout(() => { + assert.ok( + webTracerWithZone.getCurrentSpan() === concurrentSpan2, + 'Current span is concurrentSpan2' + ); + }, 20); + }); + }); + }); + }); }); });