-
I've been digging around in the react Profiler code, and it seems to me like the way this is structured, instrumented components can only create spans for updates that are caused by prop changes. If a component is re-rendered due to a context change or a state change (either a hook or a class setState), that render would happen only at the individual component's level, and the Profiler would never even know.
Am I understanding this correctly, or did I miss something in the code? This feels like a pretty big limitation, and something that should be mentioned in the docs. This could be worked around for functional components by invoking the component directly in the wrapper component, so that the wrapper is seen by fiber as being the actual component, not just a parent component. This wouldn't work for class components, however. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
I ended up writing my own HOC for functional components that addresses this. /**
* React App component wrapper to initialize Sentry instrumentation.
*
* @function withProfiling
* @param {React.Component} WrappedComponent App component to instrument
* @param {object} options
* @param {string} options.name The name of the component being profiled. If this is omitted,
* it will attempt to pull the name from the component's `displayName` or function name.
* @param {boolean} options.disabled
* @param {boolean} options.includeRender Defaults to true. This controls if a profiling
* span should be created for the initial rendering of the component.
* @param {boolean} options.includeUpdates Defaults to true. This controls if subsequent
* re-renders should also be profiled. Sentry recommends setting this to false for
* components with frequent updates, such as textfields or animations.
* @param {boolean} options.includeUnmount
* @param {boolean} options.includeReturn Defaults to false. This controls the dispatch of
* `ui.react.return` spans which track total execution time of a component function. This
* will be dispatched for _every_ evocation of the component function, including renders which
* do not make it to the DOM. This should be used sparingly, as it will produce a lot of spans.
*
* @returns {React.Component}
*/
export default function withProfiling(WrappedComponent, {
name,
disabled,
includeRender = true,
includeUpdates = true,
includeUnmount = false,
includeReturn = false
} = {}) {
if (typeof WrappedComponent !== 'function' || WrappedComponent?.prototype?.render) {
throw new Error(`withProfiling can only be used with functional components. Received ${typeof WrappedComponent} ${WrappedComponent}`);
}
const componentDisplayName = (
name
|| WrappedComponent.displayName
|| WrappedComponent.name
|| 'Anonymous'
);
// eslint-disable-next-line jsdoc/require-jsdoc
function Wrapped(props) {
// grab the active transaction at mount
const activeTransaction = useMemo(() => Sentry.getActiveTransaction(), []);
const description = `<${componentDisplayName}>`;
const [mountSpan] = useState(() => {
if (disabled) {
return undefined;
}
if (activeTransaction) {
return activeTransaction.startChild({
description,
op: 'react.mount'
});
}
return undefined;
});
const startTimestamp = timestampWithMs();
// evaluate at mount and unmount
useLayoutEffect(() => {
if (!mountSpan) {
return undefined;
}
mountSpan.finish();
if (mountSpan && includeRender) {
mountSpan.startChild({
description,
op: 'react.render',
startTimestamp,
endTimestamp: timestampWithMs()
});
}
if (!includeUnmount) return undefined;
return () => {
const now = timestampWithMs();
mountSpan.startChild({
description,
op: 'react.unmount',
startTimestamp: now,
endTimestamp: now
});
};
// We only want this to run once.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const previousPropsRef = useRef();
// evaluate every update
useEffect(() => {
if (!mountSpan || !includeUpdates) return;
const previousProps = previousPropsRef.current;
previousPropsRef.current = props;
// if this is the first render, do nothing.
if (!previousProps) return;
const changedProps = Object.keys(props)
.filter((k) => props[k] !== previousProps[k]);
mountSpan.startChild({
data: {
changedProps
},
description,
op: 'react.update',
startTimestamp,
endTimestamp: timestampWithMs()
});
});
try {
// execute the render function and return its results, logging render
return WrappedComponent(props);
} finally {
if (mountSpan && includeReturn) {
mountSpan.startChild({
description,
op: 'react.return',
startTimestamp,
endTimestamp: timestampWithMs()
});
}
}
}
Wrapped.displayName = `profiler(${componentDisplayName})`;
return Wrapped;
} |
Beta Was this translation helpful? Give feedback.
I ended up writing my own HOC for functional components that addresses this.