-
Notifications
You must be signed in to change notification settings - Fork 548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add React Plugin #164
Conversation
- changed plugin model to component based approach - fixed tests
Codecov Report
@@ Coverage Diff @@
## master #164 +/- ##
==========================================
+ Coverage 94.06% 94.23% +0.16%
==========================================
Files 74 78 +4
Lines 3536 3744 +208
Branches 374 395 +21
==========================================
+ Hits 3326 3528 +202
- Misses 210 216 +6
|
BaseOpenTelemetryComponent.setTracer('name', 'version'); | ||
``` | ||
|
||
To instrument components, extend `BaseOpenTelemetryComponent`: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can I instrument a functional component? Would I just wrap it as a HOC?
const FuncComponent = ({...props}) => (<BaseOpenTelemetryComponent>
<div {...props}>Hello World</div>
</BaseOpenTelemetryComponent>);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now it doesn't support functional components, I was hoping this was okay because functional components don't have lifecycle methods. If we wanted to add tracing to it I was thinking that could be a separate strain of work to instrument useEffect
. I was also thinking this would be okay since functional components are typically less complicated than class components so it would likely be more important for a user to want the class component to be instrumented, what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of components that we write today are functional components since react hooks make class-based components unnecessary in most cases. As such, instrumentation of functional components will be a big plus.
parentData.name = AttributeNames.MOUNTING_SPAN; | ||
} | ||
return plugin._instrumentFunction(this, 'render', () => { | ||
return original!.apply(this, args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the assertion here required?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to the comment below! The type of the original function can be undefined as specified by React, but in my code I guarantee it to be a no-op when undefined. This patch function has to have the same signature as the actual React method so that the shimmer wrap function can take them as arguments
/* | ||
* method "componentDidMount" from React.Component | ||
*/ | ||
export type ComponentDidMountFunction = (() => void) | undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you are providing your own default no-ops for these, can the | undefined
and non-falsy assertions!
be removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to add | undefined
in order to match the function definition provided by React, otherwise when I tried to wrap a function using Shimmer it didn't compile since the types of the patch I'm supplying and actual function prototype passed in don't match
const apply = plugin._instrumentFunction( | ||
this, | ||
'componentDidMount', | ||
() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: these can all be written without defining new arrow functions as original.bind(this, args)
, but I'm not sure if it matters either way
private _getAttributes(react: React.Component) { | ||
let state = ''; | ||
try { | ||
state = JSON.stringify(react.state); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@open-telemetry/javascript-maintainers
Do attributes need to be non-object types? I wonder if this stringify is something that should be left to the exporters. Else I think the attribute value type should be changed from unknown
to clarify whether a stringify is necessary or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have an issue to track this on our side open-telemetry/opentelemetry-js#1340, and a spec PR which resolved this open-telemetry/opentelemetry-specification#722, but the simple answer is that this is not allowed:
- The attribute key, which MUST be a non-
null
and non-empty string.- The attribute value, which is either:
- A primitive type: string, boolean, double precision floating point (IEEE 754-1985) or signed > 64 bit integer.
- An array of primitive type values. The array MUST be homogeneous,
i.e. it MUST NOT contain values of different types. For protocols that do
not natively support array values such values SHOULD be represented as JSON strings.
plugins/web/opentelemetry-plugin-react-load/src/BaseOpenTelemetryComponent.ts
Outdated
Show resolved
Hide resolved
examples/react-load/src/Home.jsx
Outdated
import { Link } from 'react-router-dom'; | ||
import { BaseOpenTelemetryComponent } from '@opentelemetry/plugin-react-load'; | ||
|
||
class Home extends BaseOpenTelemetryComponent { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is another example using hooks/functional components also possible with this plugin?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to comment above, but currently my implementation doesn't support functional components
Has this been tested with preact? Would be interested to see if it works, and would be a nice addition to the README. |
[GeneralAttribute.COMPONENT]: this.moduleName, | ||
[AttributeNames.LOCATION_URL]: window.location.href, | ||
[AttributeNames.REACT_NAME]: react.constructor.name, | ||
[AttributeNames.REACT_STATE]: state, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a requirement? I think the state can become quite large in some cases and this may cause quite some overhead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added in state as an attribute because I found it a good way to decipher where/when spans were coming from in an application, would it be better to not include this attribute or only conditionally include it based on length?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A length heuristic is likely confusing for users. Maybe you could define a protected property on the base class like getCustomSpanAttributes: undefined | (state: State) => Attributes
, which is called with state iff it is defined, and its returned attributes are attached to the span? Or a simpler property like addStateAsSpanAttribute: boolean
might be easier... It depends what use-case you're actually trying to solve.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the use-case is to help you debug while building the module, I would say to strip it out before merge. If it is actually impossible to tell which component generated the span, then maybe there is some other attribute that could cover that use-case like the component's constructor.name
property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, I'm trying to solve the user side use case. I was thinking that since it's common in React to have multiple state updates in a row (for example setting setting the state to Loading, then setting it to Loaded once a request returns), these would all come from the same component so just having a name (which is an attribute right now too) might still make it hard for users to decipher the traces from each other.
Could you clarify about your suggestion I think I'm misunderstanding it a bit, is the suggestion to still allow long states to be added as attributes but not add the state label at all when state is undefined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't addressed this change yet, I was hoping to understand it a bit more and get some clarification first!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall great work and I really like it :). I have tested this heavily and I posted all issues / bugs that I found.
There are still things which doesn't work fully. The main problem is with context propagation between other plugins and this plugin. I have posted you some fixes and files which when you modify it will be easier to debug things.
Findings:
-
Preact
Without fixes
https://imgur.com/dBWRdmX
With fixes
https://imgur.com/xEVDoom -
React
Without fixes
https://imgur.com/i4tTpO4
With fixes
https://imgur.com/78z7lDo
Summary
- Preact looks like this is working fine after my fix with context propagation.
- React is still having some problem, although the XMLHttpRequest is getting a correct parent,
but as you can see you spans are splitted into 2 groups and also the click event is not a parent for start group.
Why those 2 groups are not connected whereas in preact it works fine
WIth regards to click event for me it looks like the react might either have some addition life cycle that the plugin is not taking into consideration or something else.
Finally the react should look like preact and I'm enclosing you the image how it should look like
https://imgur.com/SpLw3nQ
plugins/web/opentelemetry-plugin-react-load/src/BaseOpenTelemetryComponent.ts
Outdated
Show resolved
Hide resolved
Co-authored-by: Bartlomiej Obecny <bobecny@gmail.com>
After investigating a bit, I found that if the button handler is added directly using
instead of setting the handler using the As discussed, we think this is likely due to some React optimizations or a bug in zone.js, and out of scope of this PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great job
Which problem is this PR solving?
Short description of the changes
Note: Constructor & getDerivedStateFromProps are not instrumented because they are static methods. getDerivedStateFromProps is considered an extremely rare use case
Usage
Once the tracer is defined, statically set logger & tracer:
Component definitions:
Any component extending
BaseOpenTelemetryComponent
will be instrumented.Example Traces
Example Attributes:
Mounting Flow:
Updating Flow:
Unmounting Flow: