Skip to content

Consider using the HOC for React OpenTelemtry manual instrumentation

Created by: valerybugakov

Context

From @vovakulikov

I'm curious to see other opinions on this, but it feels a bit wrong to me. I see that we have an explicit solution for tracing, but these hooks mixed with business logic make a bad first impression. I see how easy it would be to mix all this logic with spans in one huge React component. I'm not 100% sure, but I think this is a good call to use HOC and factory function instead of hooks here if we want to trace something standard like react rendering tree.

From @erzhtor

Btw, my two cents on composition vs HOC, I feel that both APIs could coexist, maybe HOC for simpler cases where there is no need to access child component calculated state for span attributes, smth like:

const MyTracedComponent = withTraceSpan('span name', spanOptions, mapPropsToSpanAttributes)(MyComponent)

History of the composition approach

Quote from the @valerybugakov's PR comment:


I started my exploration with the desire to implement a rough auto-instrumentation of React components, which would allow us to separate the tracing logic from business logic in components completely via leveraging the zone.js context propagation mechanism. Ideally, it would look similar to the history auto-instrumentation, where we hook into history methods and window events to instrument them and add required attributes to spans.

Profiler

It brought me to the React Profiler API, which looks like a thing that we could leverage for performance monitoring. But it comes with trade-offs:

  1. It's not meant to be used in production. It's possible to configure it so, but we would spend a lot of extra CPU cycles on measurements we don't care about because there's no way to disable the internal performance collection. E.g., we don't care about re-renders timings or interactions for every component we want to observe.
  2. There's no way to hook into React hooks (🙃) executed in the component, meaning that we will need to somehow propagate the span to the business logic we want to instrument. E.g., useObservable calls.

REACT_DEVTOOLS_GLOBAL_HOOK

That brought me to the only API available that gives these capabilities — __REACT_DEVTOOLS_GLOBAL_HOOK__ used by react-devtools. The global window property allows tapping into all React calls, from rendering to useEffect. It's an experimental API that can change at any time, and that's why it's not documented and not recommended for production environments.

HOC

Then I jumped into prototyping the HOC function that would wrap component render into a span. The API that I experimented with looked similar to this:

const MyTracedComponent = withTraceSpan('span name', spanOptions)(MyComponent)

After instrumenting a couple of components, I noticed that I do not use the HOC "power" to do anything valuable. Most of the work was done by the wrapper component created by HOC.

const withTraceSpan = (name, options) => WrappedComponent => props => (
  <TraceSpanProvider name={name} options={options}><WrappedComponent {...props /></TraceSpanProvider>
)

The "power" of the HOC comes from the fact that we can access props and execute some valuable logic based on their values. I didn't find applications of this pattern during my testing. Also, with this approach, we still cannot tap into WrappedComponent hooks execution, so we need to pass the created span down the tree.

Another non-technical trade-off I considered while evaluating the need for the HOC was that we rarely use HOCs in our codebase, which means that wrapping components can be cumbersome. E.g., in Redux heavy codebases, there's usually a widely adopted pattern of combining multiple HOCs into one and then exporting the wrapped component, making integrating new wrappers easy. Introducing the new export pattern to many individual components across the codebase will negatively affect the codebase consistency.

Composition

Because of these two concerns, I moved on to the component composition approach added in this PR. It's very similar to the Profiler API provided by react.

const Traced = () => (
  <TraceSpanProvider name="RepositoryFileTreePage">
      <RepositoryFileTreePage {...props} />,
  </TraceSpanProvider>
)

I'd love to hear your thoughts and ideas on how to leverage the HOC approach better.

Bright future

It is the first stab into the React instrumentation, and we will continue iterating on the current implementation for a long time. Right now, the key is to start spans collection to understand our needs better and adjust our logic based on that.

/cc @taylorsperry @jasongornall