diff --git a/packages/opentelemetry-browser-extension-autoinjection/.eslintrc.js b/packages/opentelemetry-browser-extension-autoinjection/.eslintrc.js new file mode 100644 index 0000000000..0d90aec2bb --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/.eslintrc.js @@ -0,0 +1,19 @@ +module.exports = { + "env": { + "mocha": true, + "commonjs": true, + "browser": true, + "jquery": true + }, + "ignorePatterns": [ + ".eslintrc.js", + "build/*", + "ts-build/*" + ], + plugins: [ + "@typescript-eslint", + "json5", + "header" + ], + ...require('../../eslint.config.js') +}; diff --git a/packages/opentelemetry-browser-extension-autoinjection/.gitignore b/packages/opentelemetry-browser-extension-autoinjection/.gitignore new file mode 100644 index 0000000000..41f3dc1005 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/.gitignore @@ -0,0 +1 @@ +ts-build diff --git a/packages/opentelemetry-browser-extension-autoinjection/LICENSE b/packages/opentelemetry-browser-extension-autoinjection/LICENSE new file mode 100644 index 0000000000..f49a4e16e6 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/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. \ No newline at end of file diff --git a/packages/opentelemetry-browser-extension-autoinjection/README.md b/packages/opentelemetry-browser-extension-autoinjection/README.md new file mode 100644 index 0000000000..6386d9853f --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/README.md @@ -0,0 +1,61 @@ +# OpenTelemetry Browser Extension + +This browser extension allows you to inject [OpenTelemetry](https://opentelemetry.io/) instrumentation in any web page. It uses the [Web SDK](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-web) and can export data to Zipkin or an OpenTelemetry Collector. + +![This animated image shows the process of activating the extension and seeing console output in the developer toolbar](./images/inject-opentelemetry.gif) + +## Supported Environments + +* Google Chrome (with [Manifest Version 3](https://developer.chrome.com/docs/extensions/mv3/intro/) support) +* Chromium (with Manifest Version 2) +* Firefox (*unstable*, with Manifest Version 2) + +## Installation + +### from Download + +* Go to [Releases](https://github.com/svrnm/opentelemetry-browser-extension/releases) and download the latest opentelemetry-browser-extension--.zip from Assets. +* Unzip that file locally +* Open a new browser window and go to chrome://extensions +* Turn on "Developer Mode" +* Click on "Load unpacked" and the select the folder, where the unzipped extension lives. +### from Source + +Run the following in your shell to download and build the extension from source: + +```shell +git clone https://github.com/svrnm/opentelemetry-browser-extension +cd opentelemetry-browser-extension +npm install +npm run compile +``` + +This will create a so called unpacked extension into the `build/` folder you now can load into your browser: + +* Open a new browser window and go to chrome://extensions +* Turn on "Developer Mode" +* Click on "Load unpacked" and select the `build/mv3` (or `build/mv2`) folder, which contains the extension + +If all goes well you should see the extension listed: + +![This image shows the extension being installed in chrome://extensions](./images/extensionCard.png) + +## Usage + +When visiting a website, click on the extension icon, add an url filter that partially matches the current domain, e.g for [https://opentelemetry.io/](https://opentelemetry.io/) you can set "opentel" as value: + +![This image shows an open extension popup with url filter set to "opentel"](./images/popup.png) + +Click on `Save & Reload`, check the developer toolbar to see how spans being are printed to the console and being sent to your collector: + +![This image shows spans being printed into the console of the developer toolbar for opentelemetry.io](./images/console.png) + +## Known Limitations + +1. The extension works with [active tab](https://developer.chrome.com/docs/extensions/mv3/manifest/activeTab/) permission, this means that every time you want to use it, you have to click the extension icon at least once for your tab. + +2. The use of the zone context manager and the used instrumentation libraries are fixed. + +3. Firefox support is unstable, sometimes it works, sometimes not. If you have experience building extensions for firefox, please reach out. + +4. The website you are targeting with this extension might have a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) (CSP) in place and block the extension from injecting javascript or block the exporters from sending spans to a collector. To work around this limitation, you need another browser extension, that allows you to disable CSP for a website. \ No newline at end of file diff --git a/packages/opentelemetry-browser-extension-autoinjection/images/console.png b/packages/opentelemetry-browser-extension-autoinjection/images/console.png new file mode 100644 index 0000000000..e19c8cbf4e Binary files /dev/null and b/packages/opentelemetry-browser-extension-autoinjection/images/console.png differ diff --git a/packages/opentelemetry-browser-extension-autoinjection/images/extensionCard.png b/packages/opentelemetry-browser-extension-autoinjection/images/extensionCard.png new file mode 100644 index 0000000000..c732afd8e2 Binary files /dev/null and b/packages/opentelemetry-browser-extension-autoinjection/images/extensionCard.png differ diff --git a/packages/opentelemetry-browser-extension-autoinjection/images/inject-opentelemetry.gif b/packages/opentelemetry-browser-extension-autoinjection/images/inject-opentelemetry.gif new file mode 100644 index 0000000000..7ecc11152b Binary files /dev/null and b/packages/opentelemetry-browser-extension-autoinjection/images/inject-opentelemetry.gif differ diff --git a/packages/opentelemetry-browser-extension-autoinjection/images/options.png b/packages/opentelemetry-browser-extension-autoinjection/images/options.png new file mode 100644 index 0000000000..e9e364c4d4 Binary files /dev/null and b/packages/opentelemetry-browser-extension-autoinjection/images/options.png differ diff --git a/packages/opentelemetry-browser-extension-autoinjection/images/popup.png b/packages/opentelemetry-browser-extension-autoinjection/images/popup.png new file mode 100644 index 0000000000..52b7323fcf Binary files /dev/null and b/packages/opentelemetry-browser-extension-autoinjection/images/popup.png differ diff --git a/packages/opentelemetry-browser-extension-autoinjection/package.json b/packages/opentelemetry-browser-extension-autoinjection/package.json new file mode 100644 index 0000000000..f2d2202b96 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/package.json @@ -0,0 +1,85 @@ +{ + "name": "@opentelemetry/browser-extension-autoinjection", + "version": "0.22.0", + "description": "A browser extension that injects opentelemetry tracers into any website", + "scripts": { + "clean": "rimraf build/*", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "compile": "tsc --build tsconfig.json", + "build": "npx webpack --mode=production", + "build:mv2": "npx webpack --mode=production --env MV=2", + "build:mv3": "npx webpack --mode=production --env MV=3", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "precompile": "tsc --version", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "tdd": "npm run test -- --watch-extensions ts --watch", + "watch": "npx webpack --mode=development --watch", + "watch:mv2": "npx webpack --mode=development --watch --env MV=2", + "watch:mv3": "npx webpack --mode=development --watch --env MV=3" + }, + "private": true, + "keywords": [], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "devDependencies": { + "@opentelemetry/api": "1.0.0", + "@types/chrome": "0.0.145", + "@types/jsdom": "^16.2.11", + "@types/mocha": "^8.2.2", + "@types/react": "^17.0.11", + "@types/react-dom": "^17.0.8", + "@types/sinon": "^10.0.2", + "@types/sinon-chrome": "^2.2.10", + "@typescript-eslint/eslint-plugin": "^4.27.0", + "@typescript-eslint/parser": "^4.27.0", + "codecov": "^3.8.2", + "eslint": "^7.28.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-plugin-header": "^3.1.1", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-json5": "^0.1.2", + "gts": "^3.1.0", + "html-webpack-plugin": "^5.3.1", + "jimp": "^0.16.1", + "jsdom": "^15.2.1", + "mocha": "^7.2.0", + "null-loader": "^4.0.1", + "nyc": "^15.1.0", + "responsive-loader": "^2.3.0", + "rimraf": "^3.0.2", + "sinon": "^10.0.0", + "sinon-chrome": "^3.0.1", + "ts-loader": "^9.2.3", + "ts-mocha": "^8.0.0", + "ts-node": "^10.0.0", + "typescript": "^4.2.4", + "webpack": "^5.39.0", + "webpack-cli": "^4.7.2", + "webpack-merge": "^5.8.0" + }, + "dependencies": { + "@material-ui/core": "^4.11.4", + "@material-ui/icons": "^4.11.2", + "@material-ui/lab": "^4.0.0-alpha.58", + "@opentelemetry/context-zone": "^0.22.0", + "@opentelemetry/core": "^0.22.0", + "@opentelemetry/exporter-collector": "^0.22.0", + "@opentelemetry/exporter-zipkin": "^0.22.0", + "@opentelemetry/instrumentation": "^0.22.0", + "@opentelemetry/instrumentation-document-load": "^0.22.0", + "@opentelemetry/instrumentation-fetch": "^0.22.0", + "@opentelemetry/instrumentation-xml-http-request": "^0.22.0", + "@opentelemetry/resources": "^0.22.0", + "@opentelemetry/semantic-conventions": "^0.22.0", + "@opentelemetry/tracing": "^0.22.0", + "@opentelemetry/web": "^0.22.0", + "change-case": "^4.1.2", + "json5": "^2.2.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" + } +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/background/ProgrammaticContentScriptInjector.ts b/packages/opentelemetry-browser-extension-autoinjection/src/background/ProgrammaticContentScriptInjector.ts new file mode 100644 index 0000000000..b85790bedf --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/background/ProgrammaticContentScriptInjector.ts @@ -0,0 +1,56 @@ +/* + * 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 { TabStatus, CONTENT_SCRIPT_NAME } from '../types'; + +export class ProgrammaticContentScriptInjector { + scope: typeof chrome; + + constructor(scope: typeof chrome) { + this.scope = scope; + } + + injectContentScript(tabId: number) { + this.scope.tabs.get(tabId, (tab: chrome.tabs.Tab) => { + if (tab.url) { + if (this.scope.scripting) { + this.scope.scripting.executeScript({ + target: { + allFrames: true, + tabId, + }, + files: [CONTENT_SCRIPT_NAME], + }); + } else { + this.scope.tabs.executeScript(tabId, { + file: CONTENT_SCRIPT_NAME, + allFrames: true, + }); + } + } + }); + } + + register() { + this.scope.tabs.onUpdated.addListener( + (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => { + if (changeInfo.status === TabStatus.LOADING) { + this.injectContentScript(tabId); + } + } + ); + } +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/background/index.ts b/packages/opentelemetry-browser-extension-autoinjection/src/background/index.ts new file mode 100644 index 0000000000..903e14e134 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/background/index.ts @@ -0,0 +1,28 @@ +/* + * 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. + */ + +// the following two 'require' are here for webpack. +require('../manifest.json5'); +require('../icons/otel-logo.png'); + +import { ProgrammaticContentScriptInjector } from './ProgrammaticContentScriptInjector'; + +// An error thrown in the background service worker will not be reported in the logs, it's caught here and printed. (MV3 only) +try { + new ProgrammaticContentScriptInjector(chrome).register(); +} catch (e) { + console.error(e); +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/contentScript/InstrumentationInjector.ts b/packages/opentelemetry-browser-extension-autoinjection/src/contentScript/InstrumentationInjector.ts new file mode 100644 index 0000000000..5c4369f21a --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/contentScript/InstrumentationInjector.ts @@ -0,0 +1,74 @@ +/* + * 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 { + DomAttributes, + DomElements, + INSTRUMENTATION_SCRIPT_NAME, + Settings, +} from '../types'; + +export class InstrumentationInjector { + scope: typeof chrome; + doc: Document; + logger: Console; + + constructor(scope: typeof chrome, doc: Document, logger: Console) { + this.scope = scope; + this.doc = doc; + this.logger = logger; + } + + inject(settings: Settings) { + const script = this.scope.runtime.getURL(INSTRUMENTATION_SCRIPT_NAME); + this.logger.log( + `[otel-extension] injecting ${INSTRUMENTATION_SCRIPT_NAME}` + ); + const tag = this.doc.createElement('script'); + tag.setAttribute('src', script); + tag.setAttribute('id', DomElements.CONFIG_TAG); + // Config is based via this data attribute, since CSP might not allow inline script tags, so this is more robust. + tag.setAttribute(`data-${DomAttributes.CONFIG}`, JSON.stringify(settings)); + this.doc.head.appendChild(tag); + this.logger.log(`[otel-extension] ${INSTRUMENTATION_SCRIPT_NAME} injected`); + } + + static checkUrlFilter(urlFilter: string, href: string) { + return urlFilter !== '' && (urlFilter === '*' || href.includes(urlFilter)); + } + + execute() { + this.scope.storage.local.get('settings', ({ settings }) => { + // Define label of badge. + const urlFilter = settings.urlFilter; + if ( + InstrumentationInjector.checkUrlFilter( + urlFilter, + this.doc.location.href + ) + ) { + this.logger.log( + `[otel-extension] ${this.doc.location.href} includes ${urlFilter}` + ); + this.inject(settings); + } else { + this.logger.log( + `[otel-extension] ${this.doc.location.href} does not include ${urlFilter}` + ); + } + }); + } +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/contentScript/index.ts b/packages/opentelemetry-browser-extension-autoinjection/src/contentScript/index.ts new file mode 100644 index 0000000000..90da8a7760 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/contentScript/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { InstrumentationInjector } from './InstrumentationInjector'; + +new InstrumentationInjector(chrome, document, console).execute(); diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/icons/otel-logo.png b/packages/opentelemetry-browser-extension-autoinjection/src/icons/otel-logo.png new file mode 100644 index 0000000000..1e9deec244 Binary files /dev/null and b/packages/opentelemetry-browser-extension-autoinjection/src/icons/otel-logo.png differ diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/instrumentation/WebInstrumentation.ts b/packages/opentelemetry-browser-extension-autoinjection/src/instrumentation/WebInstrumentation.ts new file mode 100644 index 0000000000..2d1d981757 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/instrumentation/WebInstrumentation.ts @@ -0,0 +1,115 @@ +/* + * 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 { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'; +import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'; +import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'; +import { WebTracerProvider } from '@opentelemetry/web'; +import { ZoneContextManager } from '@opentelemetry/context-zone'; +import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; +import { CollectorTraceExporter } from '@opentelemetry/exporter-collector'; +import { + BatchSpanProcessor, + ConsoleSpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import { + Exporters, + ExporterType, + InstrumentationConfiguration, + InstrumentationType, +} from '../types'; + +export class WebInstrumentation { + withZoneContextManager: boolean; + provider: WebTracerProvider; + exporters: Exporters; + instrumentations: { + [InstrumentationType.DOCUMENT_LOAD]: { enabled: boolean }; + [InstrumentationType.FETCH]: { enabled: boolean }; + [InstrumentationType.XML_HTTP_REQUEST]: { enabled: boolean }; + }; + constructor( + config: InstrumentationConfiguration, + provider: WebTracerProvider + ) { + this.exporters = config.exporters; + this.instrumentations = config.instrumentations; + this.provider = provider; + this.withZoneContextManager = config.withZoneContextManager; + } + + addExporters() { + if (this.exporters[ExporterType.CONSOLE].enabled) { + this.provider.addSpanProcessor( + new SimpleSpanProcessor(new ConsoleSpanExporter()) + ); + } + + if (this.exporters[ExporterType.ZIPKIN].enabled) { + this.provider.addSpanProcessor( + new BatchSpanProcessor( + new ZipkinExporter({ + url: this.exporters[ExporterType.ZIPKIN].url, + }) + ) + ); + } + + if (this.exporters[ExporterType.COLLECTOR_TRACE].enabled) { + this.provider.addSpanProcessor( + new BatchSpanProcessor( + new CollectorTraceExporter({ + url: this.exporters[ExporterType.COLLECTOR_TRACE].url, + }) + ) + ); + } + } + + registerInstrumentations() { + const instrumentations = []; + + if (this.instrumentations[InstrumentationType.DOCUMENT_LOAD].enabled) { + instrumentations.push(new DocumentLoadInstrumentation()); + } + + if (this.instrumentations[InstrumentationType.FETCH].enabled) { + instrumentations.push(new FetchInstrumentation()); + } + + if (this.instrumentations[InstrumentationType.XML_HTTP_REQUEST].enabled) { + instrumentations.push(new XMLHttpRequestInstrumentation()); + } + + registerInstrumentations({ + instrumentations, + tracerProvider: this.provider, + }); + } + + register() { + this.addExporters(); + + if (this.withZoneContextManager) { + this.provider.register({ + contextManager: new ZoneContextManager(), + }); + } + + this.registerInstrumentations(); + } +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/instrumentation/index.ts b/packages/opentelemetry-browser-extension-autoinjection/src/instrumentation/index.ts new file mode 100644 index 0000000000..ef0cebd8f6 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/instrumentation/index.ts @@ -0,0 +1,54 @@ +/* + * 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 { WebTracerProvider } from '@opentelemetry/web'; +import { + DomAttributes, + DomElements, + InstrumentationType, + Settings, +} from '../types'; +import { WebInstrumentation } from './WebInstrumentation'; +import { Resource } from '@opentelemetry/resources'; +import { ResourceAttributes } from '@opentelemetry/semantic-conventions'; + +const configTag = document.getElementById(DomElements['CONFIG_TAG']); +const { exporters }: Settings = configTag + ? JSON.parse(String(configTag.dataset[DomAttributes['CONFIG']])) + : {}; + +new WebInstrumentation( + { + exporters, + instrumentations: { + [InstrumentationType.DOCUMENT_LOAD]: { + enabled: true, + }, + [InstrumentationType.FETCH]: { + enabled: true, + }, + [InstrumentationType.XML_HTTP_REQUEST]: { + enabled: true, + }, + }, + withZoneContextManager: true, + }, + new WebTracerProvider({ + resource: new Resource({ + [ResourceAttributes.SERVICE_NAME]: window.location.hostname, + }), + }) +).register(); diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/manifest.json5 b/packages/opentelemetry-browser-extension-autoinjection/src/manifest.json5 new file mode 100644 index 0000000000..48b89d5249 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/manifest.json5 @@ -0,0 +1,23 @@ +{ + "permissions": [ + "activeTab", + "scripting", + "webNavigation", + "storage" + ], + "action": { + "default_popup": "popup.html" + }, + "icons": "icons/otel-logo_{size}.png", + "background": "background.js", + "options_ui": { + "page": "options.html", + "open_in_tab": true, + }, + "web_accessible_resources": [ + "instrumentation.js" + ], + "host_permissions": [ + '' + ] +} \ No newline at end of file diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/template.html b/packages/opentelemetry-browser-extension-autoinjection/src/template.html new file mode 100644 index 0000000000..37e14402dd --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/template.html @@ -0,0 +1,12 @@ + + + + + OpenTelemetry Browser Extension + + +
+
+ + + diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/types.ts b/packages/opentelemetry-browser-extension-autoinjection/src/types.ts new file mode 100644 index 0000000000..f4e37f0b3a --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/types.ts @@ -0,0 +1,154 @@ +/* + * 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 { styles } from './ui/styles'; +import { WithStyles } from '@material-ui/core'; + +export interface Exporters { + [ExporterType.CONSOLE]: { + enabled: boolean; + }; + [ExporterType.ZIPKIN]: { + enabled: boolean; + url: string; + }; + [ExporterType.COLLECTOR_TRACE]: { + enabled: boolean; + url: string; + }; +} + +export interface InstrumentationConfiguration { + exporters: Exporters; + instrumentations: { + [InstrumentationType.DOCUMENT_LOAD]: { + enabled: boolean; + }; + [InstrumentationType.FETCH]: { + enabled: boolean; + }; + [InstrumentationType.XML_HTTP_REQUEST]: { + enabled: boolean; + }; + }; + withZoneContextManager: boolean; +} + +export interface Settings { + urlFilter: string; + exporters: Exporters; +} + +export class Storage { + settings: Settings; + isPermissionAlertDismissed: boolean; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(storage: { [key: string]: any }) { + this.settings = storage.settings; + this.isPermissionAlertDismissed = storage.isPermissionAlertDismissed; + } +} + +export interface PermissionManagerProps { + permissions: chrome.permissions.Permissions; + onTogglePermissions: (currentValue: boolean) => void; + removingPermissionsFailed: boolean; +} + +export interface PermissionAlertProps { + permissions: chrome.permissions.Permissions; + dismissed: boolean; + onDismiss: () => void; + onGrantPermission: () => void; +} + +export interface ExporterOptionProps { + for: ExporterType; + isEnabled: boolean; + onToggle: (exporter: ExporterType) => void; + onValueChange?: ( + name: ExporterType.ZIPKIN | ExporterType.COLLECTOR_TRACE, + newValue: string + ) => void; + exporterPackageUrl: string; + placeholderValue?: PlaceholderValues; + value?: string; +} + +export interface SaveButtonProps { + label: Labels; + onClick: () => void; +} + +export interface AppProps extends WithStyles { + permissions: chrome.permissions.Permissions; + settings: Settings; + isPermissionAlertDismissed: boolean; + app: AppType; + activeTab: chrome.tabs.Tab | undefined; +} + +export interface AppState { + settings: Settings; + permissions: chrome.permissions.Permissions; + isPermissionAlertDismissed: boolean; + removingPermissionsFailed: boolean; +} + +export enum AppType { + OPTIONS = 'options', + POPUP = 'popup', +} + +export enum ExporterType { + CONSOLE = 'Console', + ZIPKIN = 'Zipkin', + COLLECTOR_TRACE = 'CollectorTrace', +} + +export enum InstrumentationType { + DOCUMENT_LOAD = 'DocumentLoad', + FETCH = 'Fetch', + XML_HTTP_REQUEST = 'XMLHttpRequest', +} + +export enum DomElements { + CONFIG_TAG = 'open-telemetry-instrumentation', +} + +export enum DomAttributes { + CONFIG = 'config', +} + +export enum PlaceholderValues { + ZIPKIN_URL = 'http://localhost:9411/api/v2/spans', + COLLECTOR_TRACE_URL = 'http://localhost:55681/v1/trace', +} + +export enum Labels { + SAVE = 'Save', + SAVE_AND_RELOAD = 'Save & Reload', +} + +export enum TabStatus { + UNLOADED = 'unloaded', + LOADING = 'loading', + COMPLETE = 'complete', +} + +export const CONTENT_SCRIPT_NAME = 'contentScript.js'; +export const INSTRUMENTATION_SCRIPT_NAME = 'instrumentation.js'; diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/ui/App.tsx b/packages/opentelemetry-browser-extension-autoinjection/src/ui/App.tsx new file mode 100644 index 0000000000..56d95b4922 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/ui/App.tsx @@ -0,0 +1,286 @@ +/* + * 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 React from 'react'; +import { + AppType, + ExporterType, + Labels, + AppProps as AppProps, + AppState as AppState, + PlaceholderValues, +} from '../types'; +import { + AppBar, + CssBaseline, + Paper, + Toolbar, + Typography, + Grid, + TextField, +} from '@material-ui/core'; +import { ExporterOption } from './ExporterOption'; +import { capitalCase } from 'change-case'; +import { SaveButton } from './SaveButton'; +import { OpenOptionsPage } from './OpenOptionsPage'; +import { PermissionManager } from './PermissionManager'; +import { PermissionAlert } from './PermissionAlert'; +const packageJson = require('../../package.json'); + +export class App extends React.Component { + permissionsUpdated: () => void; + constructor(props: AppProps) { + super(props); + + this.state = { + settings: props.settings, + permissions: props.permissions, + isPermissionAlertDismissed: props.isPermissionAlertDismissed, + removingPermissionsFailed: false, + }; + + this.handleFilterChange = this.handleFilterChange.bind(this); + this.handleSaveSettings = this.handleSaveSettings.bind(this); + this.handleUrlChange = this.handleUrlChange.bind(this); + this.toggleExporter = this.toggleExporter.bind(this); + this.onTogglePermissions = this.onTogglePermissions.bind(this); + this.dismissPermissionAlert = this.dismissPermissionAlert.bind(this); + + this.permissionsUpdated = () => { + chrome.permissions.getAll(permissions => { + this.setState({ permissions }); + }); + }; + } + + componentDidMount() { + if (chrome.permissions.onAdded) { + chrome.permissions.onAdded.addListener(this.permissionsUpdated); + chrome.permissions.onRemoved.addListener(this.permissionsUpdated); + } + } + + handleFilterChange(event: React.ChangeEvent) { + this.setState(state => { + state.settings.urlFilter = event.target.value; + return state; + }); + } + + handleUrlChange( + name: ExporterType.ZIPKIN | ExporterType.COLLECTOR_TRACE, + value: string + ) { + this.setState(state => { + state.settings.exporters[name].url = value; + return state; + }); + } + + toggleExporter(name: ExporterType) { + this.setState(state => { + state.settings.exporters[name].enabled = + !state.settings.exporters[name].enabled; + return state; + }); + } + + async handleSaveSettings() { + chrome.storage.local.set( + { + settings: this.state.settings, + }, + async () => { + if (this.props.activeTab) { + const tabId = Number(this.props.activeTab.id); + if (chrome.scripting) { + chrome.scripting.executeScript({ + target: { + tabId, + }, + function: () => { + window.location.reload(); + }, + }); + } else { + chrome.tabs.executeScript(tabId, { + code: 'window.location.reload();', + }); + } + } + } + ); + } + + onTogglePermissions(currentValue: boolean) { + if (currentValue) { + chrome.permissions.remove( + { + origins: ['http://*/*', 'https://*/*'], + }, + () => { + if (chrome.runtime.lastError) { + this.setState({ + removingPermissionsFailed: true, + }); + } + } + ); + } else { + chrome.permissions.request({ + origins: ['http://*/*', 'https://*/*'], + }); + } + } + + dismissPermissionAlert() { + this.setState( + { + isPermissionAlertDismissed: true, + }, + () => { + chrome.storage.local.set({ + isPermissionAlertDismissed: this.state.isPermissionAlertDismissed, + }); + } + ); + } + + render() { + const { urlFilter, exporters } = this.state.settings; + + const classes = this.props.classes; + + const saveLabel = + this.props.app === AppType.POPUP ? Labels.SAVE_AND_RELOAD : Labels.SAVE; + + return ( + + + + + {this.props.app === AppType.OPTIONS ? ( + + {capitalCase(packageJson.name)} ({packageJson.version}) + + ) : ( + + + + + + )} + + +
+ this.onTogglePermissions(false)} + /> + + + Injection Settings + + + + + + + + + + Exporter Settings + + + + + + + + + + Manage Permissions + + + + + + + + + + +
+
+ ); + } +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/ui/ExporterOption.tsx b/packages/opentelemetry-browser-extension-autoinjection/src/ui/ExporterOption.tsx new file mode 100644 index 0000000000..df94ff2efd --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/ui/ExporterOption.tsx @@ -0,0 +1,83 @@ +/* + * 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 { + FormControlLabel, + FormGroup, + FormHelperText, + Grid, + Link, + Switch, + TextField, +} from '@material-ui/core'; +import * as React from 'react'; +import { ExporterOptionProps, ExporterType } from '../types'; + +export class ExporterOption extends React.Component { + render() { + return ( + + + + this.props.onToggle(this.props.for)} + > + } + label={this.props.for} + /> + + Toggle to enable{' '} + + {this.props.for}Exporter + + + + + {this.props.value !== undefined ? ( + + + this.props.onValueChange + ? this.props.onValueChange( + this.props.for as + | ExporterType.ZIPKIN + | ExporterType.COLLECTOR_TRACE, + event.target.value + ) + : () => {} + } + /> + + ) : ( + '' + )} + + ); + } +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/ui/OpenOptionsPage.tsx b/packages/opentelemetry-browser-extension-autoinjection/src/ui/OpenOptionsPage.tsx new file mode 100644 index 0000000000..d7ce1197dc --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/ui/OpenOptionsPage.tsx @@ -0,0 +1,43 @@ +/* + * 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 { Link } from '@material-ui/core'; +import { Launch } from '@material-ui/icons'; +import * as React from 'react'; + +export class OpenOptionsPage extends React.Component { + constructor(props: {}) { + super(props); + this.openOptionsPage = this.openOptionsPage.bind(this); + } + + openOptionsPage(event: React.MouseEvent) { + event.preventDefault(); + if (chrome.runtime.openOptionsPage) { + chrome.runtime.openOptionsPage(); + } else { + window.open(chrome.runtime.getURL('options.html')); + } + } + + render() { + return ( + + + + ); + } +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/ui/PermissionAlert.tsx b/packages/opentelemetry-browser-extension-autoinjection/src/ui/PermissionAlert.tsx new file mode 100644 index 0000000000..032bdbdf3d --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/ui/PermissionAlert.tsx @@ -0,0 +1,43 @@ +/* + * 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 React from 'react'; +import { PermissionAlertProps } from '../types'; +import { Alert } from '@material-ui/lab'; +import { Link } from '@material-ui/core'; + +export class PermissionAlert extends React.Component { + render() { + const origins = this.props.permissions.origins ?? []; + + const accessToAllUrlsGranted = + origins.includes('') || + (origins.includes('http://*/*') && origins.includes('https://*/*')); + + if (this.props.dismissed || accessToAllUrlsGranted) { + return ''; + } + return ( + + Without the permission to access all websites, you need to click on the + extension icon once every time you open a new tab.{' '} + Click here to grant + access or dismiss this + warning. + + ); + } +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/ui/PermissionManager.tsx b/packages/opentelemetry-browser-extension-autoinjection/src/ui/PermissionManager.tsx new file mode 100644 index 0000000000..33573fd739 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/ui/PermissionManager.tsx @@ -0,0 +1,78 @@ +/* + * 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 { + Grid, + FormGroup, + FormControlLabel, + Switch, + FormHelperText, + Link, +} from '@material-ui/core'; +import * as React from 'react'; +import { PermissionManagerProps } from '../types'; +import { Alert } from '@material-ui/lab'; + +export class PermissionManager extends React.Component { + render() { + const origins = this.props.permissions.origins ?? []; + + const accessToAllUrlsGranted = + origins.includes('') || + (origins.includes('http://*/*') && origins.includes('https://*/*')); + + return ( + + + {this.props.removingPermissionsFailed ? ( + + Permissions can not be revoked. Go to chrome://extensions, open + the details of this extension and revoke them manually. + + ) : ( + '' + )} + + + this.props.onTogglePermissions(accessToAllUrlsGranted) + } + > + } + label="Access all websites" + /> + + Toggle to have injection work immediately on opening a new tab. + Otherwise, you need to click the extension icon once to active it.{' '} + ( + + Learn More + + ) + + + + + ); + } +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/ui/SaveButton.tsx b/packages/opentelemetry-browser-extension-autoinjection/src/ui/SaveButton.tsx new file mode 100644 index 0000000000..345a205809 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/ui/SaveButton.tsx @@ -0,0 +1,34 @@ +/* + * 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 { Button } from '@material-ui/core'; +import * as React from 'react'; +import { SaveButtonProps } from '../types'; + +export class SaveButton extends React.Component { + render() { + return ( + + ); + } +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/ui/index.tsx b/packages/opentelemetry-browser-extension-autoinjection/src/ui/index.tsx new file mode 100644 index 0000000000..2d6ed46554 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/ui/index.tsx @@ -0,0 +1,69 @@ +/* + * 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 React from 'react'; +import * as ReactDOM from 'react-dom'; +import { AppType } from '../types'; +import { styles } from './styles'; +import { withStyles } from '@material-ui/core'; +import { loadFromStorage } from '../utils/storage'; +import { App } from './App'; + +loadFromStorage() + .then(async storage => { + const app = window.location.pathname.startsWith('/options.html') + ? AppType.OPTIONS + : AppType.POPUP; + + let activeTab: chrome.tabs.Tab | undefined; + + if (app === AppType.POPUP) { + const tabs = await new Promise(resolve => { + chrome.tabs.query( + { + active: true, + lastFocusedWindow: true, + }, + result => { + resolve(result); + } + ); + }); + activeTab = tabs[0]; + } + + const permissions = await new Promise( + resolve => { + chrome.permissions.getAll(permissions => resolve(permissions)); + } + ); + + const StyledApp = withStyles(styles)(App); + + ReactDOM.render( + , + document.getElementById('root') + ); + }) + .catch(error => { + // eslint-disable-next-line no-console + console.error(error); + }); diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/ui/styles.ts b/packages/opentelemetry-browser-extension-autoinjection/src/ui/styles.ts new file mode 100644 index 0000000000..9851a9a9e3 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/ui/styles.ts @@ -0,0 +1,36 @@ +/* + * 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 { Theme, createStyles } from '@material-ui/core'; + +export const styles = (theme: Theme) => + createStyles({ + appBar: { + position: 'relative', + }, + layout: { + width: 'auto', + marginLeft: theme.spacing(2), + marginRight: theme.spacing(2), + }, + paper: { + marginTop: theme.spacing(3), + marginBottom: theme.spacing(3), + padding: theme.spacing(2), + }, + title: { + flexGrow: 1, + }, + }); diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/utils/manifest-loader.ts b/packages/opentelemetry-browser-extension-autoinjection/src/utils/manifest-loader.ts new file mode 100644 index 0000000000..eee7bfb055 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/utils/manifest-loader.ts @@ -0,0 +1,93 @@ +/* + * 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. + */ + +/* eslint-disable node/no-extraneous-import */ + +import { capitalCase } from 'change-case'; +import * as json5 from 'json5'; + +// From https://github.com/TypeStrong/ts-loader/blob/main/src/interfaces.ts +interface WebpackLoaderContext { + emitFile(name: string, content: string): void; + getOptions(): { + manifestVersion: number; + }; +} + +interface IconSet { + [key: string]: string; +} + +export default function (this: WebpackLoaderContext, source: string): string { + const p = require('../../package.json'); + const options = this.getOptions(); + + const manifest5 = json5.parse(source); + + const sizes = ['16', '32', '48', '128']; + manifest5.icons = sizes.reduce((result: IconSet, size: string) => { + result[size] = manifest5.icons.replace('{size}', size); + return result; + }, {}); + + manifest5.action['default_icon'] = manifest5.icons; + + const background = + Number(options.manifestVersion) === 3 + ? { + service_worker: manifest5.background, + } + : { + scripts: [manifest5.background], + }; + + const web_accessible_resources = + Number(options.manifestVersion) === 3 + ? [ + { + resources: manifest5.web_accessible_resources, + matches: [''], + }, + ] + : manifest5.web_accessible_resources; + + if (Number(options.manifestVersion) === 2) { + manifest5.permissions = manifest5.permissions.filter( + (permission: string) => permission !== 'scripting' + ); + manifest5.browser_action = Object.assign({}, manifest5.action); + delete manifest5.action; + + manifest5.optional_permissions = Object.values(manifest5.host_permissions); + delete manifest5.host_permissions; + } + + const result = JSON.stringify( + Object.assign(manifest5, { + manifest_version: options.manifestVersion, + version: p.version, + background, + web_accessible_resources, + description: p.description, + name: capitalCase(p.name), + }), + null, + 2 + ); + + this.emitFile('manifest.json', result); + return source; +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/src/utils/storage.ts b/packages/opentelemetry-browser-extension-autoinjection/src/utils/storage.ts new file mode 100644 index 0000000000..20be513eb5 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/src/utils/storage.ts @@ -0,0 +1,44 @@ +/* + * 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 { ExporterType, Storage } from '../types'; + +export function loadFromStorage(): Promise { + return new Promise(resolve => { + chrome.storage.local.get( + { + isPermissionAlertDismissed: false, + settings: { + urlFilter: '', + exporters: { + [ExporterType.CONSOLE]: { + enabled: true, + }, + [ExporterType.ZIPKIN]: { + enabled: false, + url: '', + }, + [ExporterType.COLLECTOR_TRACE]: { + enabled: false, + url: '', + }, + }, + }, + }, + storage => resolve(new Storage(storage)) + ); + }); +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/test/background.test.ts b/packages/opentelemetry-browser-extension-autoinjection/test/background.test.ts new file mode 100644 index 0000000000..4e5c711ea0 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/test/background.test.ts @@ -0,0 +1,116 @@ +/* + * 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. + */ + +/* eslint-disable node/no-unpublished-import */ + +import { ProgrammaticContentScriptInjector } from '../src/background/ProgrammaticContentScriptInjector'; +import * as chromeMock from 'sinon-chrome'; +import * as assert from 'assert'; +import sinon = require('sinon'); + +import { TAB_ID } from './utils'; +import { CONTENT_SCRIPT_NAME, TabStatus } from '../src/types'; + +describe('ProgrammaticContentScriptInjector', () => { + let listener: ProgrammaticContentScriptInjector; + let sandbox: sinon.SinonSandbox; + + before(() => { + sandbox = sinon.createSandbox(); + listener = new ProgrammaticContentScriptInjector( + chromeMock as unknown as typeof chrome + ); + listener.register(); + }); + + afterEach(async () => { + sandbox.restore(); + }); + + it('should subscribe on chrome.tabs.onUpdated', () => { + assert.ok(chromeMock.tabs.onUpdated.addListener.calledOnce); + }); + + it('should only be triggered on tab status "loading"', () => { + const spy = sandbox.spy(listener, 'injectContentScript'); + chromeMock.tabs.onUpdated.dispatch(TAB_ID, { + status: TabStatus.COMPLETE, + }); + chromeMock.tabs.onUpdated.dispatch(TAB_ID, { + status: TabStatus.UNLOADED, + }); + assert.ok(spy.notCalled); + assert.ok(chromeMock.tabs.get.notCalled); + + chromeMock.tabs.onUpdated.dispatch(TAB_ID, { + status: TabStatus.LOADING, + }); + assert.ok(spy.calledOnce, 'injectContentScript not triggered on "loading"'); + assert.ok(chromeMock.tabs.get.calledOnce); + }); + + it('should inject the content script if the url property of a tab is accessible', () => { + chromeMock.tabs.get.reset(); + chromeMock.tabs.get + .onFirstCall() + .callsArgWith(1, { url: undefined } as chrome.tabs.Tab); + chromeMock.tabs.get + .onSecondCall() + .callsArgWith(1, { url: 'http://www.example.com' } as chrome.tabs.Tab); + chromeMock.tabs.get + .onThirdCall() + .callsArgWith(1, { url: 'http://www.example.com' } as chrome.tabs.Tab); + + chromeMock.tabs.onUpdated.dispatch(TAB_ID, { + status: TabStatus.LOADING, + }); + + assert.ok(chromeMock.tabs.executeScript.notCalled); + + chromeMock.tabs.onUpdated.dispatch(TAB_ID, { + status: TabStatus.LOADING, + }); + + assert.ok( + chromeMock.tabs.executeScript.calledOnceWith(TAB_ID, { + file: CONTENT_SCRIPT_NAME, + allFrames: true, + }) + ); + + const chromeMockV3 = chromeMock as typeof chromeMock & { + scripting: { + executeScript: (args: any) => void; + }; + }; + + chromeMockV3.scripting = { + executeScript: args => { + assert.deepStrictEqual(args, { + target: { + allFrames: true, + tabId: TAB_ID, + }, + files: [CONTENT_SCRIPT_NAME], + }); + }, + }; + + chromeMock.tabs.onUpdated.dispatch(TAB_ID, { + status: TabStatus.LOADING, + }); + }); +}); diff --git a/packages/opentelemetry-browser-extension-autoinjection/test/contentScript.test.ts b/packages/opentelemetry-browser-extension-autoinjection/test/contentScript.test.ts new file mode 100644 index 0000000000..fd8e842a71 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/test/contentScript.test.ts @@ -0,0 +1,140 @@ +/* + * 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. + */ + +/* eslint-disable node/no-unpublished-import */ + +import * as chromeMock from 'sinon-chrome'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; + +import { InstrumentationInjector } from '../src/contentScript/InstrumentationInjector'; +import { JSDOM } from 'jsdom'; +import { + DomAttributes, + DomElements, + INSTRUMENTATION_SCRIPT_NAME, + Settings, +} from '../src/types'; +import { TEST_URL } from './utils'; + +describe('InstrumentationInjector', () => { + let sandbox: sinon.SinonSandbox; + let injector: InstrumentationInjector; + let jsdom: JSDOM; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + jsdom = new JSDOM('', { + url: TEST_URL, + }); + injector = new InstrumentationInjector( + chromeMock as unknown as typeof chrome, + jsdom.window.document, + { + log: () => {}, + } as Console + ); + }); + + afterEach(async () => { + sandbox.restore(); + chromeMock.reset(); + }); + + describe('checkUrlFilter', () => { + it('matches on parts of the URL', () => { + assert.ok( + InstrumentationInjector.checkUrlFilter( + 'example', + 'http://www.example.com' + ) + ); + + assert.ok( + InstrumentationInjector.checkUrlFilter( + 'www.exa', + 'http://www.example.com' + ) + ); + + assert.ok( + !InstrumentationInjector.checkUrlFilter('123', 'http://www.example.com') + ); + }); + + it('accepts "*" as a catch all', () => { + assert.ok( + InstrumentationInjector.checkUrlFilter('*', 'http://www.example.com') + ); + + assert.ok( + InstrumentationInjector.checkUrlFilter( + '*', + 'http://www.opentelemetry.io' + ) + ); + }); + }); + + describe('execute', () => { + it('should load settings from storage', () => { + injector.execute(); + assert.ok(chromeMock.storage.local.get.calledOnceWith('settings')); + }); + + it('should only inject instrumentation if urlFilter matches', () => { + const spy = sandbox.spy(injector, 'inject'); + chromeMock.storage.local.get.onFirstCall().callsArgWith(1, { + settings: { + urlFilter: '123', + }, + }); + chromeMock.storage.local.get.onSecondCall().callsArgWith(1, { + settings: { + urlFilter: 'example', + }, + }); + + injector.execute(); + assert.ok(spy.notCalled); + + injector.execute(); + assert.ok(spy.calledOnce); + }); + }); + + describe('inject', () => { + it('adds a script element to the DOM that loads the instrumentation code', () => { + const scriptName = `chrome-extension://id/${INSTRUMENTATION_SCRIPT_NAME}`; + + chromeMock.runtime.getURL.onFirstCall().returns(scriptName); + + const settings = { exporters: {} }; + injector.inject(settings as Settings); + const configTag = jsdom.window.document.getElementById( + DomElements.CONFIG_TAG + ); + assert.ok(configTag instanceof jsdom.window.HTMLScriptElement); + assert.deepStrictEqual( + settings, + JSON.parse( + String(configTag.getAttribute(`data-${DomAttributes.CONFIG}`)) + ) + ); + assert.ok(configTag.getAttribute('src'), scriptName); + }); + }); +}); diff --git a/packages/opentelemetry-browser-extension-autoinjection/test/instrumentation.test.ts b/packages/opentelemetry-browser-extension-autoinjection/test/instrumentation.test.ts new file mode 100644 index 0000000000..e8a14a9a82 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/test/instrumentation.test.ts @@ -0,0 +1,88 @@ +/* + * 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. + */ + +/* eslint-disable node/no-unpublished-import */ + +import * as chromeMock from 'sinon-chrome'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { WebInstrumentation } from '../src/instrumentation/WebInstrumentation'; +import { + ExporterType, + InstrumentationType, + PlaceholderValues, +} from '../src/types'; +import { WebTracerProvider } from '@opentelemetry/web'; +import { JSDOM } from 'jsdom'; +import { TEST_URL } from './utils'; + +describe('WebInstrumentation', () => { + let sandbox: sinon.SinonSandbox; + let provider: WebTracerProvider; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + provider = new WebTracerProvider(); + const { window } = new JSDOM('', { + url: TEST_URL, + }); + + global.window = window as any; + global.XMLHttpRequest = window.XMLHttpRequest; + global.document = window.document; + }); + + afterEach(async () => { + sandbox.restore(); + chromeMock.reset(); + }); + + it('adds exporters to the trace provider', () => { + const addSpanProcessorSpy = sinon.spy(provider, 'addSpanProcessor'); + const instrumentation = new WebInstrumentation( + { + exporters: { + [ExporterType.CONSOLE]: { + enabled: true, + }, + [ExporterType.ZIPKIN]: { + enabled: true, + url: PlaceholderValues.ZIPKIN_URL, + }, + [ExporterType.COLLECTOR_TRACE]: { + enabled: true, + url: PlaceholderValues.COLLECTOR_TRACE_URL, + }, + }, + instrumentations: { + [InstrumentationType.DOCUMENT_LOAD]: { + enabled: true, + }, + [InstrumentationType.FETCH]: { + enabled: false, + }, + [InstrumentationType.XML_HTTP_REQUEST]: { + enabled: true, + }, + }, + withZoneContextManager: true, + }, + provider + ); + instrumentation.register(); + assert.ok(addSpanProcessorSpy.callCount === 3); + }); +}); diff --git a/packages/opentelemetry-browser-extension-autoinjection/test/utils.ts b/packages/opentelemetry-browser-extension-autoinjection/test/utils.ts new file mode 100644 index 0000000000..f273cc1b7f --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/test/utils.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. + */ + +export const TAB_ID = 13; +export const TEST_URL = 'http://www.example.com'; diff --git a/packages/opentelemetry-browser-extension-autoinjection/tsconfig.json b/packages/opentelemetry-browser-extension-autoinjection/tsconfig.json new file mode 100644 index 0000000000..c1e3fae6fe --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build", + "jsx": "react" + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "test/**/*.ts", + "webpack.config.ts" + ] +} diff --git a/packages/opentelemetry-browser-extension-autoinjection/webpack.config.ts b/packages/opentelemetry-browser-extension-autoinjection/webpack.config.ts new file mode 100644 index 0000000000..53fdc8fa85 --- /dev/null +++ b/packages/opentelemetry-browser-extension-autoinjection/webpack.config.ts @@ -0,0 +1,157 @@ +/* + * 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. + */ + +/* eslint-disable node/no-unpublished-import */ + +import * as path from 'path'; +import { mergeWithRules } from 'webpack-merge'; +import * as HtmlWebpackPlugin from 'html-webpack-plugin'; + +// Read the environment variables, and check for the existence of the "MV" variable +// This can be used to only build the one or the other target. +module.exports = (env: { MV?: string; WEBPACK_BUILD: boolean }) => { + // Build the extension for "Manifest Version 2" (Chromium, Firefox & others.) + const baseConfig = { + entry: { + background: './src/background/index.ts', + contentScript: './src/contentScript/index.ts', + instrumentation: './src/instrumentation/index.ts', + ui: './src/ui/index.tsx', + }, + module: { + rules: [ + { + include: [path.resolve(__dirname, 'src/manifest.json5')], + test: /manifest.json5$/, + use: [ + { + loader: 'null-loader', + options: {}, + }, + { + loader: path.resolve('src/utils/manifest-loader.ts'), + options: { + manifestVersion: 2, + }, + }, + ], + }, + { + include: [path.resolve(__dirname, 'src')], + test: /\.tsx?$/, + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + experimentalWatchApi: true, + }, + }, + ], + }, + { + include: [path.resolve(__dirname, 'src/icons')], + test: /\.(jpe?g|png|webp)$/i, + use: [ + // We are not going to use any of the images for real, throw away all output + { + loader: 'null-loader', + options: {}, + }, + { + loader: 'responsive-loader', + options: { + name: '[name]_[width].[ext]', + outputPath: 'icons/', + sizes: [16, 32, 48, 128], + }, + }, + ], + }, + ], + }, + plugins: [ + new HtmlWebpackPlugin({ + chunks: ['ui'], + inject: 'head', + filename: 'options.html', + template: 'src/template.html', + }), + new HtmlWebpackPlugin({ + chunks: ['ui'], + filename: 'popup.html', + inject: 'head', + template: 'src/template.html', + }), + ], + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, + }; + + const merge = mergeWithRules({ + module: { + rules: { + test: 'match', + include: 'match', + use: { + loader: 'match', + options: 'replace', + }, + }, + }, + }); + + const targetMV2 = merge(baseConfig, { + output: { + filename: '[name].js', + path: path.resolve(__dirname, 'build/mv2'), + }, + }); + const targetMV3 = merge(baseConfig, { + module: { + rules: [ + { + include: [path.resolve(__dirname, 'src/manifest.json5')], + test: /manifest.json5$/, + use: [ + { + loader: path.resolve('src/utils/manifest-loader.ts'), + options: { + manifestVersion: 3, + }, + }, + ], + }, + ], + }, + output: { + filename: '[name].js', + path: path.resolve(__dirname, 'build/mv3'), + }, + }); + + const exports = []; + + if (env.MV) { + exports.push(Number(env.MV) === 3 ? targetMV3 : targetMV2); + } else { + exports.push(targetMV3); + exports.push(targetMV2); + } + + return exports; +};