UNPKG

@sentry/react

Version:
220 lines (195 loc) 6.94 kB
import { startInactiveSpan } from '@sentry/browser'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, timestampInSeconds, withActiveSpan, spanToJSON } from '@sentry/core'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; import { REACT_MOUNT_OP, REACT_UPDATE_OP, REACT_RENDER_OP } from './constants.js'; const UNKNOWN_COMPONENT = 'unknown'; /** * The Profiler component leverages Sentry's Tracing integration to generate * spans based on component lifecycles. */ class Profiler extends React.Component { /** * The span of the mount activity * Made protected for the React Native SDK to access */ /** * The span that represents the duration of time between shouldComponentUpdate and componentDidUpdate */ // eslint-disable-next-line @typescript-eslint/member-ordering static __initStatic() {this.defaultProps = { disabled: false, includeRender: true, includeUpdates: true, };} constructor(props) { super(props); const { name, disabled = false } = this.props; if (disabled) { return; } this._mountSpan = startInactiveSpan({ name: `<${name}>`, onlyIfParent: true, op: REACT_MOUNT_OP, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', 'ui.component_name': name, }, }); } // If a component mounted, we can finish the mount activity. componentDidMount() { if (this._mountSpan) { this._mountSpan.end(); } } shouldComponentUpdate({ updateProps, includeUpdates = true }) { // Only generate an update span if includeUpdates is true, if there is a valid mountSpan, // and if the updateProps have changed. It is ok to not do a deep equality check here as it is expensive. // We are just trying to give baseline clues for further investigation. if (includeUpdates && this._mountSpan && updateProps !== this.props.updateProps) { // See what props have changed between the previous props, and the current props. This is // set as data on the span. We just store the prop keys as the values could be potentially very large. const changedProps = Object.keys(updateProps).filter(k => updateProps[k] !== this.props.updateProps[k]); if (changedProps.length > 0) { const now = timestampInSeconds(); this._updateSpan = withActiveSpan(this._mountSpan, () => { return startInactiveSpan({ name: `<${this.props.name}>`, onlyIfParent: true, op: REACT_UPDATE_OP, startTime: now, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', 'ui.component_name': this.props.name, 'ui.react.changed_props': changedProps, }, }); }); } } return true; } componentDidUpdate() { if (this._updateSpan) { this._updateSpan.end(); this._updateSpan = undefined; } } // If a component is unmounted, we can say it is no longer on the screen. // This means we can finish the span representing the component render. componentWillUnmount() { const endTimestamp = timestampInSeconds(); const { name, includeRender = true } = this.props; if (this._mountSpan && includeRender) { const startTime = spanToJSON(this._mountSpan).timestamp; withActiveSpan(this._mountSpan, () => { const renderSpan = startInactiveSpan({ onlyIfParent: true, name: `<${name}>`, op: REACT_RENDER_OP, startTime, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', 'ui.component_name': name, }, }); if (renderSpan) { // Have to cast to Span because the type of _mountSpan is Span | undefined // and not getting narrowed properly renderSpan.end(endTimestamp); } }); } } render() { return this.props.children; } } Profiler.__initStatic(); /** * withProfiler is a higher order component that wraps a * component in a {@link Profiler} component. It is recommended that * the higher order component be used over the regular {@link Profiler} component. * * @param WrappedComponent component that is wrapped by Profiler * @param options the {@link ProfilerProps} you can pass into the Profiler */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function withProfiler( WrappedComponent, // We do not want to have `updateProps` given in options, it is instead filled through the HOC. options, ) { const componentDisplayName = (options && options.name) || WrappedComponent.displayName || WrappedComponent.name || UNKNOWN_COMPONENT; const Wrapped = (props) => ( React.createElement(Profiler, { ...options, name: componentDisplayName, updateProps: props,} , React.createElement(WrappedComponent, { ...props,} ) ) ); Wrapped.displayName = `profiler(${componentDisplayName})`; // Copy over static methods from Wrapped component to Profiler HOC // See: https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over hoistNonReactStatics(Wrapped, WrappedComponent); return Wrapped; } /** * * `useProfiler` is a React hook that profiles a React component. * * Requires React 16.8 or above. * @param name displayName of component being profiled */ function useProfiler( name, options = { disabled: false, hasRenderSpan: true, }, ) { const [mountSpan] = React.useState(() => { if (options && options.disabled) { return undefined; } return startInactiveSpan({ name: `<${name}>`, onlyIfParent: true, op: REACT_MOUNT_OP, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', 'ui.component_name': name, }, }); }); React.useEffect(() => { if (mountSpan) { mountSpan.end(); } return () => { if (mountSpan && options.hasRenderSpan) { const startTime = spanToJSON(mountSpan).timestamp; const endTimestamp = timestampInSeconds(); const renderSpan = startInactiveSpan({ name: `<${name}>`, onlyIfParent: true, op: REACT_RENDER_OP, startTime, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', 'ui.component_name': name, }, }); if (renderSpan) { // Have to cast to Span because the type of _mountSpan is Span | undefined // and not getting narrowed properly renderSpan.end(endTimestamp); } } }; // We only want this to run once. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); } export { Profiler, UNKNOWN_COMPONENT, useProfiler, withProfiler }; //# sourceMappingURL=profiler.js.map