diff --git a/README.md b/README.md
index 05c41b024b..4193af729c 100644
--- a/README.md
+++ b/README.md
@@ -69,6 +69,7 @@ OpenTelemetry can collect tracing data automatically using plugins. Vendors/User
- [@opentelemetry/plugin-document-load][otel-plugin-document-load]
- [@opentelemetry/plugin-xml-http-request][otel-plugin-xml-http-request]
- [@opentelemetry/plugin-user-interaction][otel-plugin-user-interaction]
+- [@opentelemetry/plugin-react-load][otel-plugin-react-load]
## Contributing
@@ -92,6 +93,7 @@ Apache 2.0 - See [LICENSE][license-url] for more information.
[otel-plugin-https]: https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-https
[otel-plugin-dns]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-dns
[otel-plugin-document-load]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/web/opentelemetry-plugin-document-load
+[otel-plugin-react-load]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/web/opentelemetry-plugin-react-load
[otel-plugin-ioredis]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-ioredis
[otel-plugin-mongodb]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-mongodb
[otel-plugin-mysql]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-mysql
diff --git a/examples/react-load/preact/.babelrc b/examples/react-load/preact/.babelrc
new file mode 100644
index 0000000000..156eaae3d9
--- /dev/null
+++ b/examples/react-load/preact/.babelrc
@@ -0,0 +1,9 @@
+{
+ "env": {
+ "test": {
+ "presets": [
+ ["preact-cli/babel", { "modules": "commonjs" }]
+ ]
+ }
+ }
+}
diff --git a/examples/react-load/preact/.gitignore b/examples/react-load/preact/.gitignore
new file mode 100644
index 0000000000..77c1ee0571
--- /dev/null
+++ b/examples/react-load/preact/.gitignore
@@ -0,0 +1 @@
+size-plugin.json
\ No newline at end of file
diff --git a/examples/react-load/preact/README.md b/examples/react-load/preact/README.md
new file mode 100644
index 0000000000..ef77db47de
--- /dev/null
+++ b/examples/react-load/preact/README.md
@@ -0,0 +1,65 @@
+# Overview
+
+The React Load plugin provides auto-instrumentation for react lifecycle methods.
+This example uses the plugin and exports them to the console.
+
+The example will show traces belong to the mounting, updating, and umounting flows as defined by React 16.4+. In this example we can show how this plugin can also be used in a Preact app.
+
+# Installation
+```
+# from this directory
+npm install
+```
+
+# Run the example
+Run docker
+```
+# from this directory
+npm run docker:start
+```
+
+Run app
+```
+# from this directory
+npm run build
+npm start
+```
+
+By default, the application runs on port 8080.
+
+Open Zipkin page at http://localhost:9411/zipkin/ - you should be able to see the spans in zipkin
+
+## Screenshots of traces
+Take note of the parent-child relationships.
+### First load
+Upon loading, http://localhost:8080 mounting spans will be exported
+
+
+
+### Pressing 'Enter'
+Here we can see the previous component unmounting and the new component mounting.
+
+
+
+
+### Pressing 'Make Request'
+While in loading state:
+
+
+
+After a few seconds (when the request is fulfilled):
+
+
+
+
+# Useful links
+- For more information on OpenTelemetry, visit: [opentelemetry.io][otel]
+- For more information on OpenTelemetry for Node.js, visit: [@opentelemetry/node][otel-node]
+
+# LICENSE
+
+Apache 2.0 - See [LICENSE][license-url] for more information.
+
+[license-url]: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/master/LICENSE
+[otel]: https://opentelemetry.io/
+[otel-node]: https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-node
\ No newline at end of file
diff --git a/examples/react-load/preact/docker/collector-config.yaml b/examples/react-load/preact/docker/collector-config.yaml
new file mode 100644
index 0000000000..2f64d8b4fc
--- /dev/null
+++ b/examples/react-load/preact/docker/collector-config.yaml
@@ -0,0 +1,18 @@
+receivers:
+ otlp:
+ endpoint: 0.0.0.0:55678
+
+exporters:
+ zipkin:
+ url: "http://zipkin-all-in-one:9411/api/v2/spans"
+
+processors:
+ batch:
+ queued_retry:
+
+service:
+ pipelines:
+ traces:
+ receivers: [otlp]
+ exporters: [zipkin]
+ processors: [batch, queued_retry]
diff --git a/examples/react-load/preact/docker/docker-compose.yaml b/examples/react-load/preact/docker/docker-compose.yaml
new file mode 100644
index 0000000000..fee8f1aca0
--- /dev/null
+++ b/examples/react-load/preact/docker/docker-compose.yaml
@@ -0,0 +1,19 @@
+version: "2"
+services:
+
+ # Collector
+ collector:
+ image: omnition/opentelemetry-collector-contrib:0.2.8
+ command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"]
+ volumes:
+ - ./collector-config.yaml:/conf/collector-config.yaml
+ ports:
+ - "55678:55678"
+ depends_on:
+ - zipkin-all-in-one
+
+ # Zipkin
+ zipkin-all-in-one:
+ image: openzipkin/zipkin:latest
+ ports:
+ - "9411:9411"
diff --git a/examples/react-load/preact/images/mounting.png b/examples/react-load/preact/images/mounting.png
new file mode 100644
index 0000000000..e581fbf5c0
Binary files /dev/null and b/examples/react-load/preact/images/mounting.png differ
diff --git a/examples/react-load/preact/images/redirect.png b/examples/react-load/preact/images/redirect.png
new file mode 100644
index 0000000000..c0d8650c97
Binary files /dev/null and b/examples/react-load/preact/images/redirect.png differ
diff --git a/examples/react-load/preact/images/updating.png b/examples/react-load/preact/images/updating.png
new file mode 100644
index 0000000000..7d5e7175c3
Binary files /dev/null and b/examples/react-load/preact/images/updating.png differ
diff --git a/examples/react-load/preact/images/updating2.png b/examples/react-load/preact/images/updating2.png
new file mode 100644
index 0000000000..5b54157610
Binary files /dev/null and b/examples/react-load/preact/images/updating2.png differ
diff --git a/examples/react-load/preact/images/zipkin-mounting.png b/examples/react-load/preact/images/zipkin-mounting.png
new file mode 100644
index 0000000000..697d81ef42
Binary files /dev/null and b/examples/react-load/preact/images/zipkin-mounting.png differ
diff --git a/examples/react-load/preact/images/zipkin-redirect.png b/examples/react-load/preact/images/zipkin-redirect.png
new file mode 100644
index 0000000000..1b74ebb54d
Binary files /dev/null and b/examples/react-load/preact/images/zipkin-redirect.png differ
diff --git a/examples/react-load/preact/images/zipkin-redirect2.png b/examples/react-load/preact/images/zipkin-redirect2.png
new file mode 100644
index 0000000000..f980348736
Binary files /dev/null and b/examples/react-load/preact/images/zipkin-redirect2.png differ
diff --git a/examples/react-load/preact/images/zipkin-updating.png b/examples/react-load/preact/images/zipkin-updating.png
new file mode 100644
index 0000000000..3459de278e
Binary files /dev/null and b/examples/react-load/preact/images/zipkin-updating.png differ
diff --git a/examples/react-load/preact/images/zipkin-updating2.png b/examples/react-load/preact/images/zipkin-updating2.png
new file mode 100644
index 0000000000..71c9e9fc8f
Binary files /dev/null and b/examples/react-load/preact/images/zipkin-updating2.png differ
diff --git a/examples/react-load/preact/package.json b/examples/react-load/preact/package.json
new file mode 100644
index 0000000000..bb4cf716b2
--- /dev/null
+++ b/examples/react-load/preact/package.json
@@ -0,0 +1,62 @@
+{
+ "private": true,
+ "name": "react-load-preact-example",
+ "version": "0.9.0",
+ "description": "Example of using @opentelemetry/plugin-react-load in browser with Preact",
+ "main": "index.js",
+ "scripts": {
+ "build": "preact build --no-prerender",
+ "docker:start": "cd ./docker && docker-compose down && docker-compose up",
+ "docker:startd": "cd ./docker && docker-compose down && docker-compose up -d",
+ "docker:stop": "cd ./docker && docker-compose down",
+ "start": "sirv build --port 8080 --cors --single"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com:open-telemetry/opentelemetry-js-contrib.git"
+ },
+ "keywords": [
+ "opentelemetry",
+ "react",
+ "preact",
+ "web",
+ "tracing"
+ ],
+ "engines": {
+ "node": ">=8"
+ },
+ "author": "OpenTelemetry Authors",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/open-telemetry/opentelemetry-js-contrib/issues"
+ },
+ "devDependencies": {
+ "identity-obj-proxy": "^3.0.0",
+ "preact-cli": "^3.0.0",
+ "preact-render-spy": "^1.2.1",
+ "sirv-cli": "1.0.3"
+ },
+ "dependencies": {
+ "@opentelemetry/context-zone": "^0.10.1",
+ "@opentelemetry/exporter-collector": "^0.10.1",
+ "@opentelemetry/plugin-react-load": "^0.8.0",
+ "@opentelemetry/tracing": "^0.10.1",
+ "@opentelemetry/web": "^0.10.1",
+ "preact": "^10.3.2",
+ "preact-render-to-string": "^5.1.4",
+ "preact-router": "^3.2.1"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "homepage": "https://github.com/opentelemetry-js-contrib/examples/react-load/preact#readme"
+}
diff --git a/examples/react-load/preact/public/index.html b/examples/react-load/preact/public/index.html
new file mode 100644
index 0000000000..4ad84066d0
--- /dev/null
+++ b/examples/react-load/preact/public/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ React Load Example: Preact
+
+
+
+
+
diff --git a/examples/react-load/preact/src/components/Content.js b/examples/react-load/preact/src/components/Content.js
new file mode 100644
index 0000000000..afd4d72c5f
--- /dev/null
+++ b/examples/react-load/preact/src/components/Content.js
@@ -0,0 +1,59 @@
+import { BaseOpenTelemetryComponent } from '@opentelemetry/plugin-react-load'
+
+class Content extends BaseOpenTelemetryComponent {
+ constructor(props){
+ super(props)
+ this.state = {
+ results: null,
+ isLoading: false
+ }
+ }
+
+ componentDidMount(){
+ // Example, do something here
+ }
+
+ buttonHandler() {
+ this.setState({isLoading: true})
+ const randomDelay = Math.random() * 10000;
+ setTimeout(() => {
+ this.setState({
+ isLoading: false,
+ results: randomDelay
+ })
+ },
+ randomDelay);
+ }
+
+ renderResults(){
+ if(this.state.isLoading){
+ return
Loading results...
;
+ }
+ if (!this.state.results){
+ return (
+
No Results
+ )
+ }
+ return (
+
+ Request was delayed {this.state.results} ms
+
+ )
+ }
+
+ render() {
+ return (
+
+
React Plugin Demo App
+
+
+ {this.renderResults()}
+
+
+ )
+ }
+}
+
+export default Content;
diff --git a/examples/react-load/preact/src/components/Home.js b/examples/react-load/preact/src/components/Home.js
new file mode 100644
index 0000000000..fbf78e6677
--- /dev/null
+++ b/examples/react-load/preact/src/components/Home.js
@@ -0,0 +1,17 @@
+import { Link } from 'preact-router/match';
+import { BaseOpenTelemetryComponent } from '@opentelemetry/plugin-react-load';
+
+class Home extends BaseOpenTelemetryComponent {
+ render() {
+ return (
+
+
+ React Plugin Demo App: Preact
+
+
+
+ )
+ }
+}
+
+export default Home;
diff --git a/examples/react-load/preact/src/components/app.js b/examples/react-load/preact/src/components/app.js
new file mode 100644
index 0000000000..a240496437
--- /dev/null
+++ b/examples/react-load/preact/src/components/app.js
@@ -0,0 +1,29 @@
+import { h, Component} from 'preact';
+import { Router } from 'preact-router';
+import Home from './Home';
+import Content from './Content';
+import Tracer from '../web-tracer';
+
+Tracer('react-load-preact-examples');
+
+export default class App extends Component {
+
+ /** Gets fired when the route changes.
+ * @param {Object} event "change" event from [preact-router](http://git.io/preact-router)
+ * @param {string} event.url The newly routed URL
+ */
+ handleRoute = e => {
+ this.currentUrl = e.url;
+ };
+
+ render() {
+ return (
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/examples/react-load/preact/src/index.js b/examples/react-load/preact/src/index.js
new file mode 100644
index 0000000000..a2f7b30f88
--- /dev/null
+++ b/examples/react-load/preact/src/index.js
@@ -0,0 +1,3 @@
+import App from './components/app';
+
+export default App;
diff --git a/examples/react-load/preact/src/sw.js b/examples/react-load/preact/src/sw.js
new file mode 100644
index 0000000000..146aa01dd9
--- /dev/null
+++ b/examples/react-load/preact/src/sw.js
@@ -0,0 +1,4 @@
+import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/';
+
+setupRouting();
+setupPrecaching(getFiles());
diff --git a/examples/react-load/preact/src/web-tracer.js b/examples/react-load/preact/src/web-tracer.js
new file mode 100644
index 0000000000..db3821964f
--- /dev/null
+++ b/examples/react-load/preact/src/web-tracer.js
@@ -0,0 +1,27 @@
+import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing';
+import { WebTracerProvider } from '@opentelemetry/web';
+import { BaseOpenTelemetryComponent } from '@opentelemetry/plugin-react-load';
+import { ZoneContextManager } from '@opentelemetry/context-zone';
+import { CollectorTraceExporter } from '@opentelemetry/exporter-collector';
+
+export default (serviceName) => {
+ const provider = new WebTracerProvider();
+
+ const exporter = new CollectorTraceExporter({
+ url: 'http://localhost:55678/v1/trace',
+ });
+
+ provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
+ provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
+
+ provider.register({
+ contextManager: new ZoneContextManager(),
+ });
+
+ const tracer = provider.getTracer(serviceName);
+
+ BaseOpenTelemetryComponent.setTracer(serviceName)
+ BaseOpenTelemetryComponent.setLogger(provider.logger)
+
+ return tracer;
+}
diff --git a/examples/react-load/react/README.md b/examples/react-load/react/README.md
new file mode 100644
index 0000000000..013c5c5656
--- /dev/null
+++ b/examples/react-load/react/README.md
@@ -0,0 +1,71 @@
+# Overview
+
+The React Load plugin provides auto-instrumentation for react lifecycle methods.
+This example uses the plugin and exports them to the console.
+
+The example will show traces belong to the mounting, updating, and umounting flows as defined by React 16.4+.
+
+# Installation
+```
+# from this directory
+npm install
+```
+
+# Run the example
+Run docker
+```
+# from this directory
+npm run docker:start
+```
+
+Run app
+
+```
+# from this directory
+npm run build
+npm start
+```
+
+By default, the application runs on port 5000.
+
+Open Zipkin page at http://localhost:9411/zipkin/ - you should be able to see the spans in zipkin
+
+
+## Screenshots of traces
+Take note of the parent-child relationships.
+### First load
+Upon loading, http://localhost:5000 mounting spans will be exported
+
+
+
+### Pressing 'Enter'
+Here we can see the previous component unmounting and the new component mounting.
+
+
+
+
+### Pressing 'Make Request'
+While in loading state:
+
+
+
+After a few seconds (when the request is fulfilled):
+
+
+
+Since the example adds in a delay to the request, we can see that reflected in the duration of some spans:
+
+
+
+
+# Useful links
+- For more information on OpenTelemetry, visit: [opentelemetry.io][otel]
+- For more information on OpenTelemetry for Node.js, visit: [@opentelemetry/node][otel-node]
+
+# LICENSE
+
+Apache 2.0 - See [LICENSE][license-url] for more information.
+
+[license-url]: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/master/LICENSE
+[otel]: https://opentelemetry.io/
+[otel-node]: https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-node
diff --git a/examples/react-load/react/docker/collector-config.yaml b/examples/react-load/react/docker/collector-config.yaml
new file mode 100644
index 0000000000..2f64d8b4fc
--- /dev/null
+++ b/examples/react-load/react/docker/collector-config.yaml
@@ -0,0 +1,18 @@
+receivers:
+ otlp:
+ endpoint: 0.0.0.0:55678
+
+exporters:
+ zipkin:
+ url: "http://zipkin-all-in-one:9411/api/v2/spans"
+
+processors:
+ batch:
+ queued_retry:
+
+service:
+ pipelines:
+ traces:
+ receivers: [otlp]
+ exporters: [zipkin]
+ processors: [batch, queued_retry]
diff --git a/examples/react-load/react/docker/docker-compose.yaml b/examples/react-load/react/docker/docker-compose.yaml
new file mode 100644
index 0000000000..fee8f1aca0
--- /dev/null
+++ b/examples/react-load/react/docker/docker-compose.yaml
@@ -0,0 +1,19 @@
+version: "2"
+services:
+
+ # Collector
+ collector:
+ image: omnition/opentelemetry-collector-contrib:0.2.8
+ command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"]
+ volumes:
+ - ./collector-config.yaml:/conf/collector-config.yaml
+ ports:
+ - "55678:55678"
+ depends_on:
+ - zipkin-all-in-one
+
+ # Zipkin
+ zipkin-all-in-one:
+ image: openzipkin/zipkin:latest
+ ports:
+ - "9411:9411"
diff --git a/examples/react-load/react/images/duration.png b/examples/react-load/react/images/duration.png
new file mode 100644
index 0000000000..7fa7d7eb5e
Binary files /dev/null and b/examples/react-load/react/images/duration.png differ
diff --git a/examples/react-load/react/images/mounting.png b/examples/react-load/react/images/mounting.png
new file mode 100644
index 0000000000..78e42587b7
Binary files /dev/null and b/examples/react-load/react/images/mounting.png differ
diff --git a/examples/react-load/react/images/redirect.png b/examples/react-load/react/images/redirect.png
new file mode 100644
index 0000000000..3e2c30d360
Binary files /dev/null and b/examples/react-load/react/images/redirect.png differ
diff --git a/examples/react-load/react/images/updating.png b/examples/react-load/react/images/updating.png
new file mode 100644
index 0000000000..10cce19878
Binary files /dev/null and b/examples/react-load/react/images/updating.png differ
diff --git a/examples/react-load/react/images/updating2.png b/examples/react-load/react/images/updating2.png
new file mode 100644
index 0000000000..58543175de
Binary files /dev/null and b/examples/react-load/react/images/updating2.png differ
diff --git a/examples/react-load/react/images/zipkin-mounting.png b/examples/react-load/react/images/zipkin-mounting.png
new file mode 100644
index 0000000000..4f257b717f
Binary files /dev/null and b/examples/react-load/react/images/zipkin-mounting.png differ
diff --git a/examples/react-load/react/images/zipkin-redirect.png b/examples/react-load/react/images/zipkin-redirect.png
new file mode 100644
index 0000000000..b128082fcf
Binary files /dev/null and b/examples/react-load/react/images/zipkin-redirect.png differ
diff --git a/examples/react-load/react/images/zipkin-redirect2.png b/examples/react-load/react/images/zipkin-redirect2.png
new file mode 100644
index 0000000000..6f60d690cb
Binary files /dev/null and b/examples/react-load/react/images/zipkin-redirect2.png differ
diff --git a/examples/react-load/react/images/zipkin-updating.png b/examples/react-load/react/images/zipkin-updating.png
new file mode 100644
index 0000000000..6b1c4638b9
Binary files /dev/null and b/examples/react-load/react/images/zipkin-updating.png differ
diff --git a/examples/react-load/react/images/zipkin-updating2.png b/examples/react-load/react/images/zipkin-updating2.png
new file mode 100644
index 0000000000..541cfa1c2a
Binary files /dev/null and b/examples/react-load/react/images/zipkin-updating2.png differ
diff --git a/examples/react-load/react/package.json b/examples/react-load/react/package.json
new file mode 100644
index 0000000000..232f864cba
--- /dev/null
+++ b/examples/react-load/react/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "react-load-example",
+ "private": true,
+ "version": "0.9.0",
+ "description": "Example of using @opentelemetry/plugin-react-load in browser with React",
+ "main": "index.jsx",
+ "scripts": {
+ "build": "react-scripts build",
+ "dev": "react-scripts start",
+ "docker:start": "cd ./docker && docker-compose down && docker-compose up",
+ "docker:startd": "cd ./docker && docker-compose down && docker-compose up -d",
+ "docker:stop": "cd ./docker && docker-compose down",
+ "start": "serve -s build"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com:open-telemetry/opentelemetry-js-contrib.git"
+ },
+ "keywords": [
+ "opentelemetry",
+ "tracing",
+ "web",
+ "react"
+ ],
+ "engines": {
+ "node": ">=8"
+ },
+ "author": "OpenTelemetry Authors",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/open-telemetry/opentelemetry-js-contrib/issues"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.6.0",
+ "babel-loader": "^8.0.6",
+ "ts-loader": "^6.0.4"
+ },
+ "dependencies": {
+ "@opentelemetry/context-zone": "^0.9.0",
+ "@opentelemetry/core": "^0.9.0",
+ "@opentelemetry/exporter-collector": "^0.10.1",
+ "@opentelemetry/plugin-react-load": "^0.8.0",
+ "@opentelemetry/tracing": "^0.9.0",
+ "@opentelemetry/web": "^0.9.0",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^3.4.1",
+ "reactstrap": "^8.5.1"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/examples/react-load/react/public/index.html b/examples/react-load/react/public/index.html
new file mode 100644
index 0000000000..ba71be6534
--- /dev/null
+++ b/examples/react-load/react/public/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ React Load Example
+
+
+
+
+
+
diff --git a/examples/react-load/react/src/Content.jsx b/examples/react-load/react/src/Content.jsx
new file mode 100644
index 0000000000..3ac0c945e0
--- /dev/null
+++ b/examples/react-load/react/src/Content.jsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { Button} from 'reactstrap';
+import { BaseOpenTelemetryComponent } from '@opentelemetry/plugin-react-load'
+
+class Content extends BaseOpenTelemetryComponent {
+ constructor(props){
+ super(props)
+ this.state = {
+ results: null,
+ isLoading: false
+ }
+ }
+
+ componentDidMount(){
+ // Example, do something here
+ }
+
+ buttonHandler() {
+ this.setState({isLoading: true})
+ const randomDelay = Math.random() * 10000;
+ setTimeout(() => {
+ this.setState({
+ isLoading: false,
+ results: randomDelay
+ })
+ },
+ randomDelay);
+ }
+
+ renderResults(){
+ if(this.state.isLoading){
+ return
Loading results...
;
+ }
+ if (!this.state.results){
+ return (
+
No Results
+ )
+ }
+ return (
+
+ Request was delayed {this.state.results} ms
+
+ )
+ }
+
+ render() {
+ return (
+
+
React Plugin Demo App
+
+
+ {this.renderResults()}
+
+
+ )
+ }
+}
+
+export default Content;
diff --git a/examples/react-load/react/src/Home.jsx b/examples/react-load/react/src/Home.jsx
new file mode 100644
index 0000000000..7ac0b6d4bb
--- /dev/null
+++ b/examples/react-load/react/src/Home.jsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { BaseOpenTelemetryComponent } from '@opentelemetry/plugin-react-load';
+
+class Home extends BaseOpenTelemetryComponent {
+ render() {
+ return (
+
+
+ React Plugin Demo App
+
+
+
+ )
+ }
+}
+
+export default Home;
diff --git a/examples/react-load/react/src/index.jsx b/examples/react-load/react/src/index.jsx
new file mode 100644
index 0000000000..08bd3690a5
--- /dev/null
+++ b/examples/react-load/react/src/index.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {BrowserRouter as Router, Route } from 'react-router-dom';
+import Home from './Home';
+import Content from './Content';
+import Tracer from './web-tracer.js';
+
+Tracer('example-react-load')
+
+ReactDOM.render(
+
+
+
+
+
+
+,document.getElementById('root'));
diff --git a/examples/react-load/react/src/web-tracer.js b/examples/react-load/react/src/web-tracer.js
new file mode 100644
index 0000000000..5be32f4915
--- /dev/null
+++ b/examples/react-load/react/src/web-tracer.js
@@ -0,0 +1,27 @@
+import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing';
+import { WebTracerProvider } from '@opentelemetry/web';
+import { BaseOpenTelemetryComponent } from '@opentelemetry/plugin-react-load';
+import { ZoneContextManager } from '@opentelemetry/context-zone';
+import { CollectorTraceExporter } from '@opentelemetry/exporter-collector';
+
+export default (serviceName) => {
+ const provider = new WebTracerProvider();
+
+ const exporter = new CollectorTraceExporter({
+ url: 'http://localhost:55678/v1/trace',
+ });
+
+ provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
+ provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
+
+ provider.register({
+ contextManager: new ZoneContextManager(),
+ });
+
+ const tracer = provider.getTracer(serviceName);
+
+ BaseOpenTelemetryComponent.setTracer(serviceName)
+ BaseOpenTelemetryComponent.setLogger(provider.logger)
+
+ return tracer;
+}
diff --git a/karma.webpack.js b/karma.webpack.js
index d62d6292c5..36c6884dd2 100644
--- a/karma.webpack.js
+++ b/karma.webpack.js
@@ -21,11 +21,11 @@ module.exports = {
mode: 'development',
target: 'web',
output: { filename: 'bundle.js' },
- resolve: { extensions: ['.ts', '.js'] },
+ resolve: { extensions: ['.ts', '.js', '.tsx'] },
devtool: 'inline-source-map',
module: {
rules: [
- { test: /\.ts$/, use: 'ts-loader' },
+ { test: /\.tsx?$/, use: 'ts-loader' },
{
enforce: 'post',
exclude: /(node_modules|\.test\.[tj]sx?$)/,
diff --git a/plugins/web/opentelemetry-plugin-react-load/.eslintignore b/plugins/web/opentelemetry-plugin-react-load/.eslintignore
new file mode 100644
index 0000000000..378eac25d3
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/.eslintignore
@@ -0,0 +1 @@
+build
diff --git a/plugins/web/opentelemetry-plugin-react-load/.eslintrc.js b/plugins/web/opentelemetry-plugin-react-load/.eslintrc.js
new file mode 100644
index 0000000000..6ad6c9f216
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/.eslintrc.js
@@ -0,0 +1,9 @@
+module.exports = {
+ "env": {
+ "mocha": true,
+ "commonjs": true,
+ "browser": true,
+ "jquery": true
+ },
+ ...require('../../../eslint.config.js')
+}
diff --git a/plugins/web/opentelemetry-plugin-react-load/LICENSE b/plugins/web/opentelemetry-plugin-react-load/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/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/plugins/web/opentelemetry-plugin-react-load/README.md b/plugins/web/opentelemetry-plugin-react-load/README.md
new file mode 100644
index 0000000000..7de8ee50bc
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/README.md
@@ -0,0 +1,47 @@
+# OpenTelemetry Plugin React Load
+[![Gitter chat][gitter-image]][gitter-url]
+[![NPM Published Version][npm-img]][npm-url]
+[![Apache License][license-image]][license-image]
+
+This module provides *automated instrumentation for React lifecycles* for Web applications.
+
+## Installation
+
+```bash
+npm install --save @opentelemetry/plugin-react-load
+```
+
+## Usage
+
+```js
+import { BaseOpenTelemetryComponent } from '@opentelemetry/plugin-react-load';
+
+// Set once for the entire plugin
+BaseOpenTelemetryComponent.setLogger(logger);
+BaseOpenTelemetryComponent.setTracer('name', 'version');
+```
+
+To instrument components, extend `BaseOpenTelemetryComponent`:
+```js
+import { BaseOpenTelemetryComponent } from '@opentelemetry/plugin-react-load';
+
+export class Component1 extends BaseOpenTelemetryComponent { ... }
+```
+
+See [/examples/react-load](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/examples/react-load) for a short example.
+
+## Useful links
+- For more information on OpenTelemetry, visit:
+- For more about OpenTelemetry JavaScript:
+- For help or feedback on this project, join us on [gitter][gitter-url]
+
+## License
+
+Apache 2.0 - See [LICENSE][license-url] for more information.
+
+[gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-js.svg
+[gitter-url]: https://gitter.im/open-telemetry/opentelemetry-node?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
+[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/master/LICENSE
+[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat
+[npm-url]: https://www.npmjs.com/package/@opentelemetry/plugin-react-load
+[npm-img]: TODO:set_img_url
diff --git a/plugins/web/opentelemetry-plugin-react-load/karma.conf.js b/plugins/web/opentelemetry-plugin-react-load/karma.conf.js
new file mode 100644
index 0000000000..d4b27cc2b3
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/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/plugins/web/opentelemetry-plugin-react-load/package.json b/plugins/web/opentelemetry-plugin-react-load/package.json
new file mode 100644
index 0000000000..4d9c53da10
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/package.json
@@ -0,0 +1,92 @@
+{
+ "name": "@opentelemetry/plugin-react-load",
+ "version": "0.8.0",
+ "description": "OpenTelemetry React loading automatic instrumentation package.",
+ "main": "build/src/index.js",
+ "types": "build/src/index.d.ts",
+ "repository": "open-telemetry/opentelemetry-js-contrib",
+ "scripts": {
+ "clean": "rimraf build/*",
+ "lint": "eslint . --ext .ts",
+ "lint:fix": "eslint . --ext .ts --fix",
+ "codecov:browser": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../../",
+ "precompile": "tsc --version",
+ "version:update": "node ../../../scripts/version-update.js",
+ "compile": "npm run version:update && tsc -p .",
+ "prepare": "npm run compile",
+ "tdd": "karma start",
+ "test:browser": "nyc karma start --single-run",
+ "watch": "tsc -w"
+ },
+ "keywords": [
+ "opentelemetry",
+ "react",
+ "web",
+ "tracing",
+ "profiling",
+ "plugin"
+ ],
+ "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.10.2",
+ "@types/mocha": "7.0.2",
+ "@types/node": "12.12.47",
+ "@types/react": "^16.9.41",
+ "@types/react-addons-test-utils": "^0.14.25",
+ "@types/react-dom": "^16.9.8",
+ "@types/shimmer": "1.0.1",
+ "@types/sinon": "9.0.4",
+ "@types/webpack-env": "1.15.2",
+ "babel-loader": "8.1.0",
+ "codecov": "3.7.0",
+ "gts": "2.0.2",
+ "istanbul-instrumenter-loader": "3.0.1",
+ "karma": "5.1.0",
+ "karma-chrome-launcher": "3.1.0",
+ "karma-coverage-istanbul-reporter": "3.0.3",
+ "karma-mocha": "2.0.1",
+ "karma-spec-reporter": "0.0.32",
+ "karma-webpack": "4.0.2",
+ "mocha": "7.2.0",
+ "nyc": "15.1.0",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
+ "rimraf": "3.0.2",
+ "sinon": "9.0.2",
+ "ts-loader": "7.0.5",
+ "ts-mocha": "7.0.0",
+ "ts-node": "8.10.2",
+ "tslint-consistent-codestyle": "1.16.0",
+ "tslint-microsoft-contrib": "6.2.0",
+ "typescript": "3.9.5",
+ "webpack": "4.43.0",
+ "webpack-cli": "3.3.12",
+ "webpack-merge": "4.2.2"
+ },
+ "peerDependencies": {
+ "react": "^16.13.1"
+ },
+ "dependencies": {
+ "@opentelemetry/api": "^0.9.0",
+ "@opentelemetry/context-zone": "^0.9.0",
+ "@opentelemetry/core": "^0.9.0",
+ "@opentelemetry/semantic-conventions": "^0.9.0",
+ "@opentelemetry/tracing": "^0.9.0",
+ "@opentelemetry/web": "^0.9.0",
+ "shimmer": "^1.2.1"
+ }
+}
diff --git a/plugins/web/opentelemetry-plugin-react-load/src/BaseOpenTelemetryComponent.ts b/plugins/web/opentelemetry-plugin-react-load/src/BaseOpenTelemetryComponent.ts
new file mode 100644
index 0000000000..11b04f509c
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/src/BaseOpenTelemetryComponent.ts
@@ -0,0 +1,548 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as api from '@opentelemetry/api';
+import { isWrapped } from '@opentelemetry/core';
+import * as shimmer from 'shimmer';
+import { GeneralAttribute } from '@opentelemetry/semantic-conventions';
+import { AttributeNames } from './enums/AttributeNames';
+import * as React from 'react';
+import { VERSION } from './version';
+import {
+ RenderFunction,
+ ComponentDidMountFunction,
+ ComponentDidUpdateFunction,
+ ShouldComponentUpdateFunction,
+ SetStateFunction,
+ ForceUpdateFunction,
+ GetSnapshotBeforeUpdateFunction,
+ ComponentWillUnmountFunction,
+} from './types';
+
+/**
+ * This class is the base component for a React component with lifecycle instrumentation
+ */
+export class BaseOpenTelemetryComponent extends React.Component {
+ readonly component: string = 'react-load';
+ moduleName = this.component;
+ private _parentSpanMap: WeakMap;
+ private static _tracer: api.Tracer;
+ private static _logger: api.Logger;
+
+ /**
+ * @param props Props of the React component
+ */
+ constructor(props: Readonly) {
+ super(props);
+ this._parentSpanMap = new WeakMap();
+ this.patch();
+ }
+
+ /**
+ * Sets the tracer for all components being instrumented
+ * @param name Name of tracer
+ * @param version Version of tracer, this is optional. When not provided it will use the latest.
+ */
+ static setTracer(name: string, version?: string): void {
+ BaseOpenTelemetryComponent._tracer = api.trace.getTracer(
+ name,
+ version ? version : VERSION
+ );
+ }
+
+ /**
+ * Sets the logger for all components being instrumented
+ * @param logger
+ */
+ static setLogger(logger: api.Logger): void {
+ BaseOpenTelemetryComponent._logger = logger;
+ }
+
+ /**
+ * Creates a new span as a child of the current parent span.
+ * If parent span is undefined, just the child is created.
+ * @param react React component currently being instrumented
+ * @param name Name of span
+ */
+ private _createSpanWithParent(
+ react: React.Component,
+ name: string,
+ parent: api.Span
+ ): api.Span {
+ return BaseOpenTelemetryComponent._tracer.startSpan(name, {
+ attributes: this._getAttributes(react),
+ parent,
+ });
+ }
+
+ /**
+ * Creates a new span
+ * @param react React component currently being instrumented
+ * @param name Name of span
+ */
+ private _createSpan(react: React.Component, name: string): api.Span {
+ return BaseOpenTelemetryComponent._tracer.startSpan(name, {
+ attributes: this._getAttributes(react),
+ });
+ }
+
+ /**
+ * Provides instrumentation for a function
+ * @param react React component currently instrumenting.
+ * @param spanName Name to set the span of the instrumented function to.
+ * @param original Original function currently being wrapped.
+ * @parentName Name to set parent span to on error.
+ */
+ private _instrumentFunction(
+ react: React.Component,
+ spanName: string,
+ parent: api.Span,
+ original: any
+ ) {
+ const span = this._createSpanWithParent(react, spanName, parent);
+ let wasError = false;
+ try {
+ return BaseOpenTelemetryComponent._tracer.withSpan(span, () => {
+ return original();
+ });
+ } catch (err) {
+ span.setAttribute(AttributeNames.REACT_ERROR, err.stack);
+ wasError = true;
+ throw err;
+ } finally {
+ span.end();
+ if (wasError) {
+ this._endParentSpan(react);
+ }
+ }
+ }
+
+ /**
+ * Ends the current parent span.
+ * @param react React component parent span belongs to.
+ */
+ private _endParentSpan(react: React.Component) {
+ const parentSpan = this._parentSpanMap.get(react);
+ if (parentSpan) {
+ parentSpan.end();
+ this._parentSpanMap.delete(react);
+ }
+ }
+
+ /**
+ * Returns attributes object for spans
+ * @param react React component currently being instrumented
+ **/
+ private _getAttributes(react: React.Component) {
+ let state: string;
+ try {
+ state = JSON.stringify(react.state);
+ } catch {
+ state = '{"message": "state could not be turned into string"}';
+ }
+ return {
+ [GeneralAttribute.COMPONENT]: this.moduleName,
+ [AttributeNames.LOCATION_URL]: window.location.href,
+ [AttributeNames.REACT_NAME]: react.constructor.name,
+ [AttributeNames.REACT_STATE]: state,
+ };
+ }
+
+ /**
+ * This function returns a parent span. If the parent doesn't
+ * exist, the function creates one
+ * @param react React component parent span belongs to.
+ */
+ private _getParentSpan(react: React.Component, parentName: string): api.Span {
+ const parentSpan: api.Span | undefined = this._parentSpanMap.get(react);
+ if (!parentSpan) {
+ const span = this._createSpan(react, parentName);
+ this._parentSpanMap.set(react, span);
+ }
+ return this._parentSpanMap.get(react)!;
+ }
+
+ /**
+ * Patches the render lifecycle method
+ */
+ private _patchRender() {
+ return (original: RenderFunction): RenderFunction => {
+ const plugin = this;
+ return function patchRender(
+ this: React.Component,
+ ...args
+ ): React.ReactNode {
+ // Render is the first method in the mounting flow, if a parent span wasn't created already then we're in the mounting flow
+ let parentSpan: api.Span;
+ if (!plugin._parentSpanMap.get(this)) {
+ parentSpan = plugin._getParentSpan(
+ this,
+ AttributeNames.MOUNTING_SPAN
+ );
+ } else {
+ parentSpan = plugin._getParentSpan(
+ this,
+ AttributeNames.UPDATING_SPAN
+ );
+ }
+
+ return plugin._instrumentFunction(this, 'render', parentSpan, () => {
+ return original!.apply(this, args);
+ });
+ };
+ };
+ }
+
+ /**
+ * Patches the componentDidMount lifecycle method
+ */
+ private _patchComponentDidMount() {
+ return (original: ComponentDidMountFunction): ComponentDidMountFunction => {
+ const plugin = this;
+
+ return function patchComponentDidMount(
+ this: React.Component,
+ ...args
+ ): void {
+ const parentSpan = plugin._getParentSpan(
+ this,
+ AttributeNames.MOUNTING_SPAN
+ );
+ const apply = plugin._instrumentFunction(
+ this,
+ 'componentDidMount',
+ parentSpan,
+ () => {
+ return original!.apply(this, args);
+ }
+ );
+ plugin._endParentSpan(this);
+ return apply;
+ };
+ };
+ }
+
+ /**
+ * Patches the setState function
+ */
+ private _patchSetState() {
+ return (original: SetStateFunction): SetStateFunction => {
+ const plugin = this;
+ return function patchSetState(this: React.Component, ...args): void {
+ const parentSpan = plugin._getParentSpan(
+ this,
+ AttributeNames.UPDATING_SPAN
+ );
+ return plugin._instrumentFunction(
+ this,
+ 'setState()',
+ parentSpan,
+ () => {
+ return original!.apply(this, args);
+ }
+ );
+ };
+ };
+ }
+
+ /**
+ * Patches the forceUpdate function
+ */
+ private _patchForceUpdate() {
+ return (original: ForceUpdateFunction): ForceUpdateFunction => {
+ const plugin = this;
+ return function patchForceUpdate(this: React.Component, ...args): void {
+ const parentSpan = plugin._getParentSpan(
+ this,
+ AttributeNames.UPDATING_SPAN
+ );
+ return plugin._instrumentFunction(
+ this,
+ 'forceUpdate()',
+ parentSpan,
+ () => {
+ return original!.apply(this, args);
+ }
+ );
+ };
+ };
+ }
+
+ /**
+ * Patches the shouldComponentUpdate lifecycle method
+ */
+ private _patchShouldComponentUpdate() {
+ return (
+ original: ShouldComponentUpdateFunction
+ ): ShouldComponentUpdateFunction => {
+ const plugin = this;
+
+ return function patchShouldComponentUpdate(
+ this: React.Component,
+ ...args
+ ): boolean {
+ const parentSpan = plugin._getParentSpan(
+ this,
+ AttributeNames.UPDATING_SPAN
+ );
+ const apply = plugin._instrumentFunction(
+ this,
+ 'shouldComponentUpdate',
+ parentSpan,
+ () => {
+ return original!.apply(this, args);
+ }
+ );
+ // if shouldComponentUpdate returns false, the component does not get
+ // updated and no other lifecycle methods get called
+ if (!apply) {
+ plugin._endParentSpan(this);
+ }
+
+ return apply;
+ };
+ };
+ }
+
+ /**
+ * Patches the shouldComponentUpdate lifecycle method
+ */
+ private _patchGetSnapshotBeforeUpdate() {
+ return (
+ original: GetSnapshotBeforeUpdateFunction
+ ): GetSnapshotBeforeUpdateFunction => {
+ const plugin = this;
+
+ return function patchGetSnapshotBeforeUpdate(
+ this: React.Component,
+ ...args
+ ): any {
+ const parentSpan = plugin._getParentSpan(
+ this,
+ AttributeNames.UPDATING_SPAN
+ );
+ return plugin._instrumentFunction(
+ this,
+ 'getSnapshotBeforeUpdate',
+ parentSpan,
+ () => {
+ return original!.apply(this, args);
+ }
+ );
+ };
+ };
+ }
+
+ /**
+ * Patches the componentDidUpdate lifecycle method
+ */
+ private _patchComponentDidUpdate() {
+ return (
+ original: ComponentDidUpdateFunction
+ ): ComponentDidUpdateFunction => {
+ const plugin = this;
+
+ return function patchComponentDidUpdate(
+ this: React.Component,
+ ...args
+ ): void {
+ const parentSpan = plugin._getParentSpan(
+ this,
+ AttributeNames.UPDATING_SPAN
+ );
+ const apply = plugin._instrumentFunction(
+ this,
+ 'componentDidUpdate',
+ parentSpan,
+ () => {
+ return original!.apply(this, args);
+ }
+ );
+ plugin._endParentSpan(this);
+ return apply;
+ };
+ };
+ }
+
+ /**
+ * Patches the componentWillUnmount lifecycle method
+ */
+ private _patchComponentWillUnmount() {
+ return (
+ original: ComponentWillUnmountFunction
+ ): ComponentWillUnmountFunction => {
+ const plugin = this;
+
+ return function patchComponentWillUnmount(
+ this: React.Component,
+ ...args
+ ): void {
+ const parentSpan = plugin._getParentSpan(
+ this,
+ AttributeNames.UNMOUNTING_SPAN
+ );
+ const apply = plugin._instrumentFunction(
+ this,
+ 'componentWillUnmount',
+ parentSpan,
+ () => {
+ return original!.apply(this, args);
+ }
+ );
+ plugin._endParentSpan(this);
+ return apply;
+ };
+ };
+ }
+
+ /**
+ * patch function which wraps all the lifecycle methods
+ */
+ public patch(): void {
+ BaseOpenTelemetryComponent._logger.debug(
+ 'applying patch to',
+ this.moduleName,
+ VERSION
+ );
+
+ if (isWrapped(this.render)) {
+ shimmer.unwrap(this, 'render');
+ BaseOpenTelemetryComponent._logger.warn(
+ 'removing previous patch from method render'
+ );
+ }
+ if (isWrapped(this.componentDidMount)) {
+ shimmer.unwrap(this, 'componentDidMount');
+ BaseOpenTelemetryComponent._logger.warn(
+ 'removing previous patch from method componentDidMount'
+ );
+ }
+ if (isWrapped(this.shouldComponentUpdate)) {
+ shimmer.unwrap(this, 'shouldComponentUpdate');
+ BaseOpenTelemetryComponent._logger.warn(
+ 'removing previous patch from method shouldComponentUpdate'
+ );
+ }
+ if (isWrapped(this.getSnapshotBeforeUpdate)) {
+ shimmer.unwrap(this, 'getSnapshotBeforeUpdate');
+ BaseOpenTelemetryComponent._logger.warn(
+ 'removing previous patch from method getSnapshotBeforeUpdate'
+ );
+ }
+ if (isWrapped(this.setState)) {
+ shimmer.unwrap(this, 'setState');
+ BaseOpenTelemetryComponent._logger.warn(
+ 'removing previous patch from method setState'
+ );
+ }
+ if (isWrapped(this.forceUpdate)) {
+ shimmer.unwrap(this, 'forceUpdate');
+ BaseOpenTelemetryComponent._logger.warn(
+ 'removing previous patch from method forceUpdate'
+ );
+ }
+ if (isWrapped(this.componentDidUpdate)) {
+ shimmer.unwrap(this, 'componentDidUpdate');
+ BaseOpenTelemetryComponent._logger.warn(
+ 'removing previous patch from method componentDidUpdate'
+ );
+ }
+ if (isWrapped(this.componentWillUnmount)) {
+ shimmer.unwrap(this, 'componentWillUnmount');
+ BaseOpenTelemetryComponent._logger.warn(
+ 'removing previous patch from method componentWillUnmount'
+ );
+ }
+
+ // Lifecycle methods must exist when patching, even if not defined in component
+ if (!this.render) {
+ this.render = () => {
+ return null;
+ };
+ }
+ if (!this.componentDidMount) {
+ this.componentDidMount = () => {
+ return;
+ };
+ }
+ if (!this.shouldComponentUpdate) {
+ this.shouldComponentUpdate = () => {
+ return true;
+ };
+ }
+ if (!this.getSnapshotBeforeUpdate) {
+ this.getSnapshotBeforeUpdate = () => {
+ return null;
+ };
+ }
+ if (!this.componentDidUpdate) {
+ this.componentDidUpdate = () => {
+ return;
+ };
+ }
+ if (!this.componentWillUnmount) {
+ this.componentWillUnmount = () => {
+ return;
+ };
+ }
+
+ shimmer.wrap(this, 'render', this._patchRender());
+ shimmer.wrap(this, 'componentDidMount', this._patchComponentDidMount());
+ shimmer.wrap(this, 'setState', this._patchSetState());
+ shimmer.wrap(this, 'forceUpdate', this._patchForceUpdate());
+ shimmer.wrap(
+ this,
+ 'shouldComponentUpdate',
+ this._patchShouldComponentUpdate()
+ );
+ shimmer.wrap(
+ this,
+ 'getSnapshotBeforeUpdate',
+ this._patchGetSnapshotBeforeUpdate()
+ );
+ shimmer.wrap(this, 'componentDidUpdate', this._patchComponentDidUpdate());
+ shimmer.wrap(
+ this,
+ 'componentWillUnmount',
+ this._patchComponentWillUnmount()
+ );
+ }
+
+ /**
+ * unpatch function to unwrap all the lifecycle methods
+ */
+ public unpatch(): void {
+ BaseOpenTelemetryComponent._logger.debug(
+ 'removing patch from',
+ this.moduleName,
+ VERSION
+ );
+
+ shimmer.unwrap(this, 'render');
+
+ shimmer.unwrap(this, 'componentDidMount');
+
+ shimmer.unwrap(this, 'setState');
+ shimmer.unwrap(this, 'forceUpdate');
+ shimmer.unwrap(this, 'shouldComponentUpdate');
+ shimmer.unwrap(this, 'getSnapshotBeforeUpdate');
+ shimmer.unwrap(this, 'componentDidUpdate');
+
+ shimmer.unwrap(this, 'componentWillUnmount');
+
+ this._parentSpanMap = new WeakMap();
+ }
+}
diff --git a/plugins/web/opentelemetry-plugin-react-load/src/enums/AttributeNames.ts b/plugins/web/opentelemetry-plugin-react-load/src/enums/AttributeNames.ts
new file mode 100644
index 0000000000..598872e96d
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/src/enums/AttributeNames.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 enum AttributeNames {
+ MOUNTING_SPAN = 'reactLoad: mounting',
+ UPDATING_SPAN = 'reactLoad: updating',
+ UNMOUNTING_SPAN = 'reactLoad: unmounting',
+ LOCATION_URL = 'location',
+ REACT_NAME = 'react_component_name',
+ REACT_STATE = 'react_component_state',
+ REACT_ERROR = 'react_error',
+}
diff --git a/plugins/web/opentelemetry-plugin-react-load/src/index.ts b/plugins/web/opentelemetry-plugin-react-load/src/index.ts
new file mode 100644
index 0000000000..e7784b30a9
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/src/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './BaseOpenTelemetryComponent';
diff --git a/plugins/web/opentelemetry-plugin-react-load/src/types.ts b/plugins/web/opentelemetry-plugin-react-load/src/types.ts
new file mode 100644
index 0000000000..920caa0767
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/src/types.ts
@@ -0,0 +1,81 @@
+/*
+ * 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';
+
+/*
+ * method "render" from React.Component
+ */
+export type RenderFunction = () => React.ReactNode;
+
+/*
+ * method "componentDidMount" from React.Component
+ */
+export type ComponentDidMountFunction = (() => void) | undefined;
+
+/*
+ * method "componentDidUpdate" from React.Component
+ */
+export type ComponentDidUpdateFunction =
+ | ((
+ prevProps: Readonly,
+ prevState: Readonly,
+ snapshot?: any
+ ) => void)
+ | undefined;
+
+/*
+ * method "shouldComponentUpdate" from React.Component
+ */
+export type ShouldComponentUpdateFunction =
+ | ((
+ nextProps: Readonly,
+ nextState: Readonly,
+ nextContext: any
+ ) => boolean)
+ | undefined;
+
+/*
+ * method "setState" from React.Component
+ */
+export type SetStateFunction = (
+ state:
+ | any
+ | ((
+ prevState: Readonly,
+ props: Readonly
+ ) => any | Pick | null)
+ | Pick
+ | null,
+ callback?: (() => void) | undefined
+) => void;
+
+/*
+ * method "setState" from React.Component
+ */
+export type ForceUpdateFunction = (callback?: (() => void) | undefined) => void;
+
+/*
+ * method "getSnapshotBeforeUpdate" from React.Component
+ */
+export type GetSnapshotBeforeUpdateFunction =
+ | ((prevProps: Readonly, prevState: Readonly) => any)
+ | undefined;
+
+/*
+ * method "componentWillUnmount" from React.Component
+ */
+export type ComponentWillUnmountFunction = (() => void) | undefined;
diff --git a/plugins/web/opentelemetry-plugin-react-load/src/version.ts b/plugins/web/opentelemetry-plugin-react-load/src/version.ts
new file mode 100644
index 0000000000..6b83319f62
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/src/version.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// this is autogenerated file, see scripts/version-update.js
+export const VERSION = '0.8.0';
diff --git a/plugins/web/opentelemetry-plugin-react-load/test/BaseOpenTelemetryComponent.test.ts b/plugins/web/opentelemetry-plugin-react-load/test/BaseOpenTelemetryComponent.test.ts
new file mode 100644
index 0000000000..78aa8f1f04
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/test/BaseOpenTelemetryComponent.test.ts
@@ -0,0 +1,838 @@
+/*
+ * 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 { context, Logger, propagation, trace } from '@opentelemetry/api';
+import { ConsoleLogger, B3Propagator, isWrapped } from '@opentelemetry/core';
+import {
+ BasicTracerProvider,
+ SimpleSpanProcessor,
+ SpanExporter,
+ ReadableSpan,
+} from '@opentelemetry/tracing';
+import { StackContextManager } from '@opentelemetry/web';
+import { GeneralAttribute } from '@opentelemetry/semantic-conventions';
+import * as assert from 'assert';
+import * as sinon from 'sinon';
+import AllLifecycles from './test-react-components/AllLifecycles';
+import MissingRender from './test-react-components/MissingRender';
+import MissingComponentDidMount from './test-react-components/MissingComponentDidMount';
+import MissingShouldComponentUpdate from './test-react-components/MissingShouldComponentUpdate';
+import MissingGetSnapshotBeforeUpdate from './test-react-components/MissingGetSnapshotBeforeUpdate';
+import MissingComponentDidUpdate from './test-react-components/MissingComponentDidUpdate';
+import ShouldComponentUpdateFalse from './test-react-components/ShouldComponentUpdateFalse';
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import { AttributeNames } from '../src/enums/AttributeNames';
+import { BaseOpenTelemetryComponent } from '../src';
+
+export class DummyExporter implements SpanExporter {
+ export(spans: ReadableSpan[]): any {}
+ shutdown(): any {}
+}
+
+interface TestCases {
+ component: any;
+ testName: string;
+}
+
+describe('ReactLoad Instrumentation', () => {
+ let provider: BasicTracerProvider;
+ let logger: Logger;
+ let spanProcessor: SimpleSpanProcessor;
+ let dummyExporter: DummyExporter;
+ let contextManager: StackContextManager;
+ let sandbox: sinon.SinonSandbox;
+ let exportSpy: any;
+ let rootContainer: any;
+
+ before(() => {
+ propagation.setGlobalPropagator(new B3Propagator());
+ contextManager = new StackContextManager().enable();
+ context.setGlobalContextManager(contextManager);
+
+ provider = new BasicTracerProvider();
+ logger = new ConsoleLogger();
+
+ dummyExporter = new DummyExporter();
+ spanProcessor = new SimpleSpanProcessor(dummyExporter);
+ provider.addSpanProcessor(spanProcessor);
+ sandbox = sinon.createSandbox();
+
+ trace.setGlobalTracerProvider(provider);
+
+ BaseOpenTelemetryComponent.setTracer('default');
+ BaseOpenTelemetryComponent.setLogger(logger);
+ });
+
+ after(() => {
+ context.disable();
+ });
+
+ beforeEach(() => {
+ exportSpy = sandbox.spy(dummyExporter, 'export');
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ const componentTestCases: TestCases[] = [
+ {
+ component: AllLifecycles,
+ testName: 'when every lifecycle method is defined in the source code',
+ },
+ {
+ component: MissingRender,
+ testName: 'when render is NOT defined in the source code',
+ },
+ {
+ component: MissingComponentDidMount,
+ testName: 'when componentDidMount is NOT defined in the source code',
+ },
+ {
+ component: MissingShouldComponentUpdate,
+ testName: 'when shouldComponentUpdate is NOT defined in the source code',
+ },
+ {
+ component: MissingGetSnapshotBeforeUpdate,
+ testName:
+ 'when getSnapshotBeforeUpdate is NOT defined in the source code',
+ },
+ {
+ component: MissingComponentDidUpdate,
+ testName: 'when componentDidUpdate is NOT defined in the source code',
+ },
+ ];
+
+ componentTestCases.forEach(testCase => {
+ describe(testCase.testName, () => {
+ let component: any;
+ let constructed: BaseOpenTelemetryComponent;
+
+ beforeEach(() => {
+ component = testCase.component;
+ constructed = new component();
+ });
+
+ it('should always have defined lifecycle methods', () => {
+ assert.ok(constructed.render, 'render is not defined');
+ assert.ok(
+ constructed.componentDidMount,
+ 'componentDidMount is not defined'
+ );
+
+ assert.ok(
+ constructed.shouldComponentUpdate,
+ 'shouldComponentUpdate is not defined'
+ );
+ assert.ok(
+ constructed.getSnapshotBeforeUpdate,
+ 'getSnapshotBeforeUpdate is not defined'
+ );
+ assert.ok(
+ constructed.componentDidUpdate,
+ 'componentDidUpdate is not defined'
+ );
+ });
+
+ it('should wrap functions', () => {
+ assert.ok(
+ isWrapped(constructed.render),
+ 'render function is not wrapped before'
+ );
+ assert.ok(
+ isWrapped(constructed.componentDidMount),
+ 'componentDidMount function is not wrapped before'
+ );
+ assert.ok(
+ isWrapped(constructed.shouldComponentUpdate),
+ 'shouldComponentUpdate function is not wrapped before'
+ );
+ assert.ok(
+ isWrapped(constructed.getSnapshotBeforeUpdate),
+ 'getSnapshotBeforeUpdate function is not wrapped before'
+ );
+ assert.ok(
+ isWrapped(constructed.componentDidUpdate),
+ 'componentDidUpdate function is not wrapped before'
+ );
+ assert.ok(
+ isWrapped(constructed.setState),
+ 'setState function is not wrapped before'
+ );
+ assert.ok(
+ isWrapped(constructed.forceUpdate),
+ 'forceUpdate function is not wrapped before'
+ );
+
+ constructed.patch();
+
+ assert.ok(
+ isWrapped(constructed.render),
+ 'render function is not wrapped after'
+ );
+ assert.ok(
+ isWrapped(constructed.componentDidMount),
+ 'componentDidMount function is not wrapped after'
+ );
+ assert.ok(
+ isWrapped(constructed.shouldComponentUpdate),
+ 'shouldComponentUpdate function is not wrapped after'
+ );
+ assert.ok(
+ isWrapped(constructed.getSnapshotBeforeUpdate),
+ 'getSnapshotBeforeUpdate function is not wrapped after'
+ );
+ assert.ok(
+ isWrapped(constructed.componentDidUpdate),
+ 'componentDidUpdate function is not wrapped after'
+ );
+ assert.ok(
+ isWrapped(constructed.setState),
+ 'setState function is not wrapped after'
+ );
+ assert.ok(
+ isWrapped(constructed.forceUpdate),
+ 'forceUpdate function is not wrapped after'
+ );
+ });
+
+ it('should unwrap functions', () => {
+ assert.ok(
+ isWrapped(constructed.render),
+ 'render function is not wrapped before'
+ );
+ assert.ok(
+ isWrapped(constructed.componentDidMount),
+ 'componentDidMount function is not wrapped before'
+ );
+ assert.ok(
+ isWrapped(constructed.shouldComponentUpdate),
+ 'shouldComponentUpdate function is not wrapped before'
+ );
+ assert.ok(
+ isWrapped(constructed.getSnapshotBeforeUpdate),
+ 'getSnapshotBeforeUpdate function is not wrapped before'
+ );
+ assert.ok(
+ isWrapped(constructed.componentDidUpdate),
+ 'componentDidUpdate function is not wrapped before'
+ );
+ assert.ok(
+ isWrapped(constructed.setState),
+ 'setState function is not wrapped before'
+ );
+ assert.ok(
+ isWrapped(constructed.forceUpdate),
+ 'forceUpdate function is not wrapped before'
+ );
+
+ constructed.unpatch();
+
+ assert.ok(
+ !isWrapped(constructed.render),
+ 'render function is not unwrapped after'
+ );
+ assert.ok(
+ !isWrapped(constructed.componentDidMount),
+ 'componentDidMount function is not unwrapped after'
+ );
+ assert.ok(
+ !isWrapped(constructed.shouldComponentUpdate),
+ 'shouldComponentUpdate function is not unwrapped after'
+ );
+ assert.ok(
+ !isWrapped(constructed.getSnapshotBeforeUpdate),
+ 'getSnapshotBeforeUpdate function is not unwrapped after'
+ );
+ assert.ok(
+ !isWrapped(constructed.componentDidUpdate),
+ 'componentDidUpdate function is not unwrapped after'
+ );
+ assert.ok(
+ !isWrapped(constructed.setState),
+ 'setState function is not unwrapped after'
+ );
+ assert.ok(
+ !isWrapped(constructed.forceUpdate),
+ 'forceUpdate function is not unwrapped after'
+ );
+ });
+
+ describe('AND component is mounting', () => {
+ beforeEach(() => {
+ rootContainer = document.createElement('div');
+ document.body.appendChild(rootContainer);
+ const reactElement = React.createElement(component, null, null);
+ act(() => {
+ ReactDOM.render(reactElement, rootContainer);
+ });
+ });
+
+ afterEach(() => {
+ document.body.removeChild(rootContainer);
+ rootContainer = null;
+ });
+
+ it('should export spans render and componentDidMount as children', () => {
+ const renderSpan: ReadableSpan = exportSpy.args[0][0][0];
+ const componentDidMountSpan: ReadableSpan = exportSpy.args[1][0][0];
+ const mountingSpan: ReadableSpan = exportSpy.args[2][0][0];
+
+ assert.equal(
+ mountingSpan.parentSpanId,
+ undefined,
+ 'mounting span is should not have a parent'
+ );
+ assert.equal(
+ renderSpan.parentSpanId,
+ mountingSpan.spanContext.spanId,
+ 'render span is not a child of the mounting span'
+ );
+ assert.equal(
+ componentDidMountSpan.parentSpanId,
+ mountingSpan.spanContext.spanId,
+ 'componentDidMount span is not a child of the mounting span'
+ );
+
+ assert.strictEqual(
+ exportSpy.args.length,
+ 3,
+ 'total number of spans is wrong'
+ );
+ });
+
+ it('spans should have correct name', () => {
+ const renderSpan: ReadableSpan = exportSpy.args[0][0][0];
+ const componentDidMountSpan: ReadableSpan = exportSpy.args[1][0][0];
+ const mountingSpan: ReadableSpan = exportSpy.args[2][0][0];
+
+ assert.equal(
+ mountingSpan.name,
+ 'reactLoad: mounting',
+ 'mounting span has wrong name'
+ );
+ assert.equal(renderSpan.name, 'render', 'render span has wrong name');
+ assert.equal(
+ componentDidMountSpan.name,
+ 'componentDidMount',
+ 'componentDidMount span has wrong name'
+ );
+ });
+
+ it('spans should have correct attributes', () => {
+ const spans: [] = exportSpy.args;
+ assert.strictEqual(spans.length, 3, 'number of spans is wrong');
+ spans.forEach(element => {
+ const span: ReadableSpan = element[0][0];
+ const attributes = span.attributes;
+ const keys = Object.keys(attributes);
+
+ assert.ok(
+ attributes[keys[0]] !== '',
+ `attributes ${GeneralAttribute.COMPONENT} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[1]] !== '',
+ `attributes ${AttributeNames.LOCATION_URL} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[2]] !== '',
+ `attributes ${AttributeNames.REACT_NAME} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[3]] !== '',
+ `attributes ${AttributeNames.REACT_STATE} is not defined for span "${span.name}"`
+ );
+
+ assert.strictEqual(
+ keys.length,
+ 4,
+ `number of attributes is wrong for span "${span.name}"`
+ );
+ });
+ });
+ });
+
+ describe('AND component is updated by calling setState()', () => {
+ beforeEach(() => {
+ rootContainer = document.createElement('div');
+ document.body.appendChild(rootContainer);
+ const reactElement = React.createElement(component, null, null);
+ act(() => {
+ const reactComponent = ReactDOM.render(reactElement, rootContainer);
+ reactComponent.setState({ test: 'newState' });
+ });
+ });
+
+ afterEach(() => {
+ document.body.removeChild(rootContainer);
+ rootContainer = null;
+ });
+
+ it('should export spans setState(), shouldComponentUpdate, render, getSnapshotBeforeUpdate, and componentDidUpdate as children', () => {
+ const setStateSpan: ReadableSpan = exportSpy.args[3][0][0];
+ const shouldComponentUpdateSpan: ReadableSpan =
+ exportSpy.args[4][0][0];
+ const renderSpan: ReadableSpan = exportSpy.args[5][0][0];
+ const getSnapshotBeforeUpdateSpan: ReadableSpan =
+ exportSpy.args[6][0][0];
+ const componentDidUpdateSpan: ReadableSpan = exportSpy.args[7][0][0];
+ const updatingSpan: ReadableSpan = exportSpy.args[8][0][0];
+
+ assert.equal(
+ updatingSpan.parentSpanId,
+ undefined,
+ 'updating span is should not have a parent'
+ );
+ assert.equal(
+ setStateSpan.parentSpanId,
+ updatingSpan.spanContext.spanId,
+ 'setState span is not a child of the updating span'
+ );
+ assert.equal(
+ shouldComponentUpdateSpan.parentSpanId,
+ updatingSpan.spanContext.spanId,
+ 'shouldComponentUpdate span is not a child of the updating span'
+ );
+ assert.equal(
+ renderSpan.parentSpanId,
+ updatingSpan.spanContext.spanId,
+ 'render span is not a child of the updating span'
+ );
+ assert.equal(
+ getSnapshotBeforeUpdateSpan.parentSpanId,
+ updatingSpan.spanContext.spanId,
+ 'getSnapshotBeforeUpdate span is not a child of the updating span'
+ );
+ assert.equal(
+ componentDidUpdateSpan.parentSpanId,
+ updatingSpan.spanContext.spanId,
+ 'componentDidUpdate span is not a child of the updating span'
+ );
+
+ assert.strictEqual(
+ exportSpy.args.length,
+ 9,
+ 'total number of spans is wrong'
+ );
+ });
+
+ it('spans should have correct name', () => {
+ const setStateSpan: ReadableSpan = exportSpy.args[3][0][0];
+ const shouldComponentUpdateSpan: ReadableSpan =
+ exportSpy.args[4][0][0];
+ const renderSpan: ReadableSpan = exportSpy.args[5][0][0];
+ const getSnapshotBeforeUpdateSpan: ReadableSpan =
+ exportSpy.args[6][0][0];
+ const componentDidUpdateSpan: ReadableSpan = exportSpy.args[7][0][0];
+ const updatingSpan: ReadableSpan = exportSpy.args[8][0][0];
+
+ assert.equal(
+ updatingSpan.name,
+ 'reactLoad: updating',
+ 'updating span has wrong name'
+ );
+ assert.equal(
+ setStateSpan.name,
+ 'setState()',
+ 'setState span has wrong name'
+ );
+ assert.equal(
+ shouldComponentUpdateSpan.name,
+ 'shouldComponentUpdate',
+ 'shouldComponentUpdate span has wrong name'
+ );
+ assert.equal(renderSpan.name, 'render', 'render span has wrong name');
+ assert.equal(
+ getSnapshotBeforeUpdateSpan.name,
+ 'getSnapshotBeforeUpdate',
+ 'getSnapshotBeforeUpdate span has wrong name'
+ );
+ assert.equal(
+ componentDidUpdateSpan.name,
+ 'componentDidUpdate',
+ 'componentDidUpdate span has wrong name'
+ );
+ });
+
+ it('spans should have correct attributes', () => {
+ const spans: [] = exportSpy.args;
+ assert.strictEqual(spans.length, 9, 'number of spans is wrong');
+ spans.forEach(element => {
+ const span: ReadableSpan = element[0][0];
+ const attributes = span.attributes;
+ const keys = Object.keys(attributes);
+
+ assert.ok(
+ attributes[keys[0]] !== '',
+ `attributes ${GeneralAttribute.COMPONENT} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[1]] !== '',
+ `attributes ${AttributeNames.LOCATION_URL} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[2]] !== '',
+ `attributes ${AttributeNames.REACT_NAME} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[3]] !== '',
+ `attributes ${AttributeNames.REACT_STATE} is not defined for span "${span.name}"`
+ );
+
+ assert.strictEqual(
+ keys.length,
+ 4,
+ `number of attributes is wrong for span "${span.name}"`
+ );
+ });
+ });
+ });
+
+ describe('AND component is updated by calling forceUpdate()', () => {
+ beforeEach(() => {
+ rootContainer = document.createElement('div');
+ document.body.appendChild(rootContainer);
+ const reactElement = React.createElement(component, null, null);
+ act(() => {
+ const reactComponent = ReactDOM.render(reactElement, rootContainer);
+ reactComponent.forceUpdate();
+ });
+ });
+
+ afterEach(() => {
+ document.body.removeChild(rootContainer);
+ rootContainer = null;
+ });
+
+ it('should export spans forceUpdate(), render, getSnapshotBeforeUpdate, and componentDidUpdate as children', () => {
+ const forceUpdateSpan: ReadableSpan = exportSpy.args[3][0][0];
+ const renderSpan: ReadableSpan = exportSpy.args[4][0][0];
+ const getSnapshotBeforeUpdateSpan: ReadableSpan =
+ exportSpy.args[5][0][0];
+ const componentDidUpdateSpan: ReadableSpan = exportSpy.args[6][0][0];
+ const updatingSpan: ReadableSpan = exportSpy.args[7][0][0];
+
+ assert.equal(
+ updatingSpan.parentSpanId,
+ undefined,
+ 'updating span is should not have a parent'
+ );
+ assert.equal(
+ forceUpdateSpan.parentSpanId,
+ updatingSpan.spanContext.spanId,
+ 'forceUpdate span is not a child of the updating span'
+ );
+ assert.equal(
+ renderSpan.parentSpanId,
+ updatingSpan.spanContext.spanId,
+ 'render span is not a child of the updating span'
+ );
+ assert.equal(
+ getSnapshotBeforeUpdateSpan.parentSpanId,
+ updatingSpan.spanContext.spanId,
+ 'getSnapshotBeforeUpdate span is not a child of the updating span'
+ );
+ assert.equal(
+ componentDidUpdateSpan.parentSpanId,
+ updatingSpan.spanContext.spanId,
+ 'componentDidUpdate span is not a child of the updating span'
+ );
+
+ assert.strictEqual(
+ exportSpy.args.length,
+ 8,
+ 'total number of spans is wrong'
+ );
+ });
+
+ it('spans should have correct name', () => {
+ const forceUpdateSpan: ReadableSpan = exportSpy.args[3][0][0];
+ const renderSpan: ReadableSpan = exportSpy.args[4][0][0];
+ const getSnapshotBeforeUpdateSpan: ReadableSpan =
+ exportSpy.args[5][0][0];
+ const componentDidUpdateSpan: ReadableSpan = exportSpy.args[6][0][0];
+ const updatingSpan: ReadableSpan = exportSpy.args[7][0][0];
+
+ assert.equal(
+ updatingSpan.name,
+ 'reactLoad: updating',
+ 'updating span has wrong name'
+ );
+ assert.equal(
+ forceUpdateSpan.name,
+ 'forceUpdate()',
+ 'forceUpdate span has wrong name'
+ );
+ assert.equal(renderSpan.name, 'render', 'render span has wrong name');
+ assert.equal(
+ getSnapshotBeforeUpdateSpan.name,
+ 'getSnapshotBeforeUpdate',
+ 'getSnapshotBeforeUpdate span has wrong name'
+ );
+ assert.equal(
+ componentDidUpdateSpan.name,
+ 'componentDidUpdate',
+ 'componentDidUpdate span has wrong name'
+ );
+ });
+
+ it('spans should have correct attributes', () => {
+ const spans: [] = exportSpy.args;
+ assert.strictEqual(spans.length, 8, 'number of spans is wrong');
+ spans.forEach(element => {
+ const span: ReadableSpan = element[0][0];
+ const attributes = span.attributes;
+ const keys = Object.keys(attributes);
+
+ assert.ok(
+ attributes[keys[0]] !== '',
+ `attributes ${GeneralAttribute.COMPONENT} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[1]] !== '',
+ `attributes ${AttributeNames.LOCATION_URL} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[2]] !== '',
+ `attributes ${AttributeNames.REACT_NAME} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[3]] !== '',
+ `attributes ${AttributeNames.REACT_STATE} is not defined for span "${span.name}"`
+ );
+
+ assert.strictEqual(
+ keys.length,
+ 4,
+ `number of attributes is wrong for span "${span.name}"`
+ );
+ });
+ });
+ });
+
+ describe('AND component is unmounting', () => {
+ beforeEach(() => {
+ rootContainer = document.createElement('div');
+ document.body.appendChild(rootContainer);
+ const reactElement = React.createElement(component, null, null);
+ act(() => {
+ ReactDOM.render(reactElement, rootContainer);
+ ReactDOM.unmountComponentAtNode(rootContainer);
+ });
+ });
+
+ afterEach(() => {
+ document.body.removeChild(rootContainer);
+ rootContainer = null;
+ });
+
+ it('should export spans render and componentDidMount as children', () => {
+ const componentWillUnmountSpan: ReadableSpan =
+ exportSpy.args[3][0][0];
+ const unmountingSpan: ReadableSpan = exportSpy.args[4][0][0];
+
+ assert.equal(
+ unmountingSpan.parentSpanId,
+ undefined,
+ 'unmounting span is should not have a parent'
+ );
+ assert.equal(
+ componentWillUnmountSpan.parentSpanId,
+ unmountingSpan.spanContext.spanId,
+ 'componentWillUnmount span is not a child of the unmounting span'
+ );
+
+ assert.strictEqual(
+ exportSpy.args.length,
+ 5,
+ 'total number of spans is wrong'
+ );
+ });
+
+ it('spans should have correct name', () => {
+ const componentWillUnmountSpan: ReadableSpan =
+ exportSpy.args[3][0][0];
+ const unmountingSpan: ReadableSpan = exportSpy.args[4][0][0];
+
+ assert.equal(
+ unmountingSpan.name,
+ 'reactLoad: unmounting',
+ 'unmounting span has wrong name'
+ );
+ assert.equal(
+ componentWillUnmountSpan.name,
+ 'componentWillUnmount',
+ 'componentWillUnmount span has wrong name'
+ );
+ });
+
+ it('spans should have correct attributes', () => {
+ const spans: [] = exportSpy.args;
+ assert.strictEqual(spans.length, 5, 'number of spans is wrong');
+ spans.forEach(element => {
+ const span: ReadableSpan = element[0][0];
+ const attributes = span.attributes;
+ const keys = Object.keys(attributes);
+
+ assert.ok(
+ attributes[keys[0]] !== '',
+ `attributes ${GeneralAttribute.COMPONENT} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[1]] !== '',
+ `attributes ${AttributeNames.LOCATION_URL} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[2]] !== '',
+ `attributes ${AttributeNames.REACT_NAME} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[3]] !== '',
+ `attributes ${AttributeNames.REACT_STATE} is not defined for span "${span.name}"`
+ );
+
+ assert.strictEqual(
+ keys.length,
+ 4,
+ `number of attributes is wrong for span "${span.name}"`
+ );
+ });
+ });
+ });
+ });
+ });
+
+ describe('when updating AND shouldComponentUpdate returns false', () => {
+ beforeEach(() => {
+ rootContainer = document.createElement('div');
+ document.body.appendChild(rootContainer);
+ const reactElement = React.createElement(
+ ShouldComponentUpdateFalse,
+ null,
+ null
+ );
+ act(() => {
+ const reactComponent = ReactDOM.render(reactElement, rootContainer);
+ reactComponent.setState({ test: 'newState' });
+ });
+ });
+
+ afterEach(() => {
+ document.body.removeChild(rootContainer);
+ rootContainer = null;
+ });
+
+ it('should export spans setState() and shouldComponentUpdate as children', () => {
+ const setStateSpan: ReadableSpan = exportSpy.args[3][0][0];
+ const shouldComponentUpdateSpan: ReadableSpan = exportSpy.args[4][0][0];
+ const updatingSpan: ReadableSpan = exportSpy.args[5][0][0];
+
+ assert.equal(
+ updatingSpan.parentSpanId,
+ undefined,
+ 'updating span is should not have a parent'
+ );
+ assert.equal(
+ setStateSpan.parentSpanId,
+ updatingSpan.spanContext.spanId,
+ 'setState span is not a child of the updating span'
+ );
+ assert.equal(
+ shouldComponentUpdateSpan.parentSpanId,
+ updatingSpan.spanContext.spanId,
+ 'shouldComponentUpdate span is not a child of the updating span'
+ );
+
+ assert.strictEqual(
+ exportSpy.args.length,
+ 6,
+ 'total number of spans is wrong'
+ );
+ });
+
+ it('spans should have correct name', () => {
+ const setStateSpan: ReadableSpan = exportSpy.args[3][0][0];
+ const shouldComponentUpdateSpan: ReadableSpan = exportSpy.args[4][0][0];
+ const updatingSpan: ReadableSpan = exportSpy.args[5][0][0];
+
+ assert.equal(
+ updatingSpan.name,
+ 'reactLoad: updating',
+ 'updating span has wrong name'
+ );
+ assert.equal(
+ setStateSpan.name,
+ 'setState()',
+ 'setState span has wrong name'
+ );
+ assert.equal(
+ shouldComponentUpdateSpan.name,
+ 'shouldComponentUpdate',
+ 'shouldComponentUpdate span has wrong name'
+ );
+ });
+
+ it('spans should have correct attributes', () => {
+ const spans: [] = exportSpy.args;
+ spans.forEach(element => {
+ const span: ReadableSpan = element[0][0];
+ const attributes = span.attributes;
+ const keys = Object.keys(attributes);
+
+ assert.ok(
+ attributes[keys[0]] !== '',
+ `attributes ${GeneralAttribute.COMPONENT} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[1]] !== '',
+ `attributes ${AttributeNames.LOCATION_URL} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[2]] !== '',
+ `attributes ${AttributeNames.REACT_NAME} is not defined for span "${span.name}"`
+ );
+
+ assert.ok(
+ attributes[keys[3]] !== '',
+ `attributes ${AttributeNames.REACT_STATE} is not defined for span "${span.name}"`
+ );
+
+ assert.strictEqual(
+ keys.length,
+ 4,
+ `number of attributes is wrong for span "${span.name}"`
+ );
+ });
+ });
+ });
+});
diff --git a/plugins/web/opentelemetry-plugin-react-load/test/index-webpack.ts b/plugins/web/opentelemetry-plugin-react-load/test/index-webpack.ts
new file mode 100644
index 0000000000..061a48ccfa
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/test/index-webpack.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const testsContext = require.context('.', true, /test$/);
+testsContext.keys().forEach(testsContext);
+
+const srcContext = require.context('.', true, /src$/);
+srcContext.keys().forEach(srcContext);
diff --git a/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/AllLifecycles.tsx b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/AllLifecycles.tsx
new file mode 100644
index 0000000000..141b8a22a2
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/AllLifecycles.tsx
@@ -0,0 +1,28 @@
+import * as React from 'react';
+import { BaseOpenTelemetryComponent } from '../../src';
+
+export default class AllLifecycles extends BaseOpenTelemetryComponent {
+ constructor(props: Readonly){
+ super(props);
+ }
+
+ componentDidMount(){
+ }
+
+ componentDidUpdate(prevProps: any){
+ }
+
+ shouldComponentUpdate(nextProps: any, nextState: any){
+ return true;
+ }
+
+ getSnapshotBeforeUpdate(prevProps: any, prevState: any){
+ return null;
+ }
+
+ render() {
+ return(
+
+ );
+ }
+}
diff --git a/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingComponentDidMount.tsx b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingComponentDidMount.tsx
new file mode 100644
index 0000000000..a83121ed0c
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingComponentDidMount.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { BaseOpenTelemetryComponent } from '../../src';
+
+export default class MissingComponentDidMount extends BaseOpenTelemetryComponent {
+ constructor(props: Readonly){
+ super(props);
+ }
+
+ componentDidUpdate(prevProps: any){
+ }
+
+ shouldComponentUpdate(nextProps: any, nextState: any){
+ return true;
+ }
+
+ getSnapshotBeforeUpdate(prevProps: any, prevState: any){
+ return null;
+ }
+
+ render() {
+ return(
+
+ );
+ }
+}
diff --git a/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingComponentDidUpdate.tsx b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingComponentDidUpdate.tsx
new file mode 100644
index 0000000000..6c12ad7fab
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingComponentDidUpdate.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { BaseOpenTelemetryComponent } from '../../src';
+
+export default class MissingComponentDidUpdate extends BaseOpenTelemetryComponent {
+ constructor(props: Readonly){
+ super(props);
+ }
+
+ componentDidMount(){
+ }
+
+ shouldComponentUpdate(nextProps: any, nextState: any){
+ return true;
+ }
+
+ getSnapshotBeforeUpdate(prevProps: any, prevState: any){
+ return null;
+ }
+
+ render() {
+ return(
+
+ );
+ }
+}
diff --git a/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingGetSnapshotBeforeUpdate.tsx b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingGetSnapshotBeforeUpdate.tsx
new file mode 100644
index 0000000000..27578a78f3
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingGetSnapshotBeforeUpdate.tsx
@@ -0,0 +1,24 @@
+import * as React from 'react';
+import { BaseOpenTelemetryComponent } from '../../src';
+
+export default class MissingGetSnapshotBeforeUpdate extends BaseOpenTelemetryComponent {
+ constructor(props: Readonly){
+ super(props);
+ }
+
+ componentDidMount(){
+ }
+
+ componentDidUpdate(prevProps: any){
+ }
+
+ shouldComponentUpdate(nextProps: any, nextState: any){
+ return true;
+ }
+
+ render() {
+ return(
+
+ );
+ }
+}
diff --git a/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingRender.tsx b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingRender.tsx
new file mode 100644
index 0000000000..c1935f0d7d
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingRender.tsx
@@ -0,0 +1,22 @@
+import { BaseOpenTelemetryComponent } from '../../src';
+
+export default class MissingRender extends BaseOpenTelemetryComponent {
+ constructor(props: Readonly){
+ super(props);
+ }
+
+ componentDidMount(){
+ }
+
+ componentDidUpdate(prevProps: any){
+ }
+
+ shouldComponentUpdate(nextProps: any, nextState: any){
+ return true;
+ }
+
+ getSnapshotBeforeUpdate(prevProps: any, prevState: any){
+ return null;
+ }
+
+}
diff --git a/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingShouldComponentUpdate.tsx b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingShouldComponentUpdate.tsx
new file mode 100644
index 0000000000..92ac7c6511
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/MissingShouldComponentUpdate.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { BaseOpenTelemetryComponent } from '../../src';
+
+export default class MisingShouldComponentUpdate extends BaseOpenTelemetryComponent {
+ constructor(props: Readonly){
+ super(props);
+ }
+
+ componentDidMount(){
+ }
+
+ componentDidUpdate(prevProps: any){
+ }
+
+ getSnapshotBeforeUpdate(prevProps: any, prevState: any){
+ return null;
+ }
+
+
+ render() {
+ return(
+
+ );
+ }
+}
diff --git a/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/ShouldComponentUpdateFalse.tsx b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/ShouldComponentUpdateFalse.tsx
new file mode 100644
index 0000000000..10beac419b
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/test/test-react-components/ShouldComponentUpdateFalse.tsx
@@ -0,0 +1,28 @@
+import * as React from 'react';
+import { BaseOpenTelemetryComponent } from '../../src';
+
+export default class ShouldComponentUpdateFalse extends BaseOpenTelemetryComponent {
+ constructor(props: Readonly){
+ super(props);
+ }
+
+ componentDidMount(){
+ }
+
+ componentDidUpdate(prevProps: any){
+ }
+
+ shouldComponentUpdate(nextProps: any, nextState: any){
+ return false;
+ }
+
+ getSnapshotBeforeUpdate(prevProps: any, prevState: any){
+ return null;
+ }
+
+ render() {
+ return(
+
+ );
+ }
+}
diff --git a/plugins/web/opentelemetry-plugin-react-load/tsconfig.json b/plugins/web/opentelemetry-plugin-react-load/tsconfig.json
new file mode 100644
index 0000000000..74e0a2ef99
--- /dev/null
+++ b/plugins/web/opentelemetry-plugin-react-load/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../../../tsconfig.base",
+ "compilerOptions": {
+ "rootDir": ".",
+ "outDir": "build",
+ "jsx": "react",
+ },
+ "include": [
+ "src/**/*.ts",
+ "test/**/*.ts"
+ ]
+}