@opentelemetry/plugin-react-load
Version:
OpenTelemetry instrumentation for React class component lifecycle methods
374 lines • 14.4 kB
JavaScript
/*
* 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/instrumentation';
import * as shimmer from 'shimmer';
import { AttributeNames } from './enums/AttributeNames';
import * as React from 'react';
/** @knipignore */
import { PACKAGE_VERSION } from './version';
/**
* This class is the base component for a React component with lifecycle instrumentation
*/
class BaseOpenTelemetryComponent extends React.Component {
component = 'react-load';
moduleName = this.component;
_parentSpanMap;
static _tracer;
static _logger = api.diag;
/**
* @param props Props of the React component
*/
constructor(props) {
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, version) {
BaseOpenTelemetryComponent._tracer = api.trace.getTracer(name, version ? version : PACKAGE_VERSION);
}
/**
* Sets the logger for all components being instrumented
* @param logger
*/
static setLogger(logger) {
api.diag.setLogger(logger);
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
* @param parentSpan parent span
*/
_createSpanWithParent(react, name, parentSpan) {
return BaseOpenTelemetryComponent._tracer.startSpan(name, {
attributes: this._getAttributes(react),
}, parentSpan
? api.trace.setSpan(api.context.active(), parentSpan)
: undefined);
}
/**
* Creates a new span
* @param react React component currently being instrumented
* @param name Name of span
*/
_createSpan(react, name) {
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.
*/
_instrumentFunction(react, spanName, parent, original) {
const span = this._createSpanWithParent(react, spanName, parent);
let wasError = false;
try {
return api.context.with(api.trace.setSpan(api.context.active(), 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.
*/
_endParentSpan(react) {
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
**/
_getAttributes(react) {
let state;
try {
state = JSON.stringify(react.state);
}
catch {
state = '{"message": "state could not be turned into string"}';
}
return {
[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.
*/
_getParentSpan(react, parentName) {
const parentSpan = 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
*/
_patchRender() {
return (original) => {
const plugin = this;
return function patchRender(...args) {
// 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;
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
*/
_patchComponentDidMount() {
return (original) => {
const plugin = this;
return function patchComponentDidMount(...args) {
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
*/
_patchSetState() {
return (original) => {
const plugin = this;
return function patchSetState(...args) {
const parentSpan = plugin._getParentSpan(this, AttributeNames.UPDATING_SPAN);
return plugin._instrumentFunction(this, 'setState()', parentSpan, () => {
return original.apply(this, args);
});
};
};
}
/**
* Patches the forceUpdate function
*/
_patchForceUpdate() {
return (original) => {
const plugin = this;
return function patchForceUpdate(...args) {
const parentSpan = plugin._getParentSpan(this, AttributeNames.UPDATING_SPAN);
return plugin._instrumentFunction(this, 'forceUpdate()', parentSpan, () => {
return original.apply(this, args);
});
};
};
}
/**
* Patches the shouldComponentUpdate lifecycle method
*/
_patchShouldComponentUpdate() {
return (original) => {
const plugin = this;
return function patchShouldComponentUpdate(...args) {
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
*/
_patchGetSnapshotBeforeUpdate() {
return (original) => {
const plugin = this;
return function patchGetSnapshotBeforeUpdate(...args) {
const parentSpan = plugin._getParentSpan(this, AttributeNames.UPDATING_SPAN);
return plugin._instrumentFunction(this, 'getSnapshotBeforeUpdate', parentSpan, () => {
return original.apply(this, args);
});
};
};
}
/**
* Patches the componentDidUpdate lifecycle method
*/
_patchComponentDidUpdate() {
return (original) => {
const plugin = this;
return function patchComponentDidUpdate(...args) {
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
*/
_patchComponentWillUnmount() {
return (original) => {
const plugin = this;
return function patchComponentWillUnmount(...args) {
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
*/
patch() {
BaseOpenTelemetryComponent._logger.debug('applying patch to', this.moduleName, PACKAGE_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
*/
unpatch() {
BaseOpenTelemetryComponent._logger.debug('removing patch from', this.moduleName, PACKAGE_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();
}
}
export { BaseOpenTelemetryComponent };
//# sourceMappingURL=BaseOpenTelemetryComponent.js.map