Skip to content
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

Merged
merged 30 commits into from
Aug 13, 2020
Merged

feat: Add React Plugin #164

merged 30 commits into from
Aug 13, 2020

Conversation

thgao
Copy link
Contributor

@thgao thgao commented Jul 27, 2020

Which problem is this PR solving?

Short description of the changes

Usage

Once the tracer is defined, statically set logger & tracer:

BaseOpenTelemetryComponent.setLogger(logger);
BaseOpenTelemetryComponent.setTracer("name", "version");

Component definitions:

class export Component1 extend BaseOpenTelemetryComponent { ...}
class export Component2 extend BaseOpenTelemetryComponent { ...}

Any component extending BaseOpenTelemetryComponent will be instrumented.

Example Traces

Example Attributes:
Screenshot 2020-07-27 at 4 01 43 PM

Mounting Flow:
Screenshot 2020-07-27 at 4 01 26 PM

Updating Flow:
Screenshot 2020-07-27 at 4 01 59 PM

Unmounting Flow:
Screenshot 2020-07-27 at 4 04 23 PM

thgao added 20 commits July 23, 2020 14:37
- changed plugin model to component based approach
- fixed tests
@thgao thgao requested a review from a team July 27, 2020 20:38
@codecov
Copy link

codecov bot commented Jul 27, 2020

Codecov Report

Merging #164 into master will increase coverage by 0.16%.
The diff coverage is 97.11%.

@@            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     
Impacted Files Coverage Δ
...s/web/opentelemetry-plugin-react-load/.eslintrc.js 0.00% <0.00%> (ø)
...lugin-react-load/src/BaseOpenTelemetryComponent.ts 97.47% <97.47%> (ø)
...etry-plugin-react-load/src/enums/AttributeNames.ts 100.00% <100.00%> (ø)
...web/opentelemetry-plugin-react-load/src/version.ts 100.00% <100.00%> (ø)

@thgao thgao changed the title React plugin feat: Add React Plugin Jul 27, 2020
BaseOpenTelemetryComponent.setTracer('name', 'version');
```

To instrument components, extend `BaseOpenTelemetryComponent`:
Copy link
Member

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>);

Copy link
Contributor Author

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?

Copy link

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.

plugins/web/opentelemetry-plugin-react-load/package.json Outdated Show resolved Hide resolved
parentData.name = AttributeNames.MOUNTING_SPAN;
}
return plugin._instrumentFunction(this, 'render', () => {
return original!.apply(this, args);
Copy link
Member

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?

Copy link
Contributor Author

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;
Copy link
Member

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?

Copy link
Contributor Author

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',
() => {
Copy link
Member

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);
Copy link
Member

@markwolff markwolff Jul 29, 2020

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.

Copy link
Member

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.

import { Link } from 'react-router-dom';
import { BaseOpenTelemetryComponent } from '@opentelemetry/plugin-react-load';

class Home extends BaseOpenTelemetryComponent {
Copy link
Member

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?

Copy link
Contributor Author

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

@markwolff markwolff requested a review from a team July 29, 2020 15:23
@dyladan
Copy link
Member

dyladan commented Jul 29, 2020

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,
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dyladan @thgao was this addressed already or there are still things that left ?

Copy link
Contributor Author

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!

Copy link
Member

@obecny obecny left a 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:

  1. Preact
    Without fixes
    https://imgur.com/dBWRdmX
    With fixes
    https://imgur.com/xEVDoom

  2. React
    Without fixes
    https://imgur.com/i4tTpO4
    With fixes
    https://imgur.com/78z7lDo

Summary

  1. Preact looks like this is working fine after my fix with context propagation.
  2. 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

examples/react-load/preact/.gitignore Outdated Show resolved Hide resolved
examples/react-load/preact/package.json Show resolved Hide resolved
examples/react-load/preact/public/index.html Show resolved Hide resolved
examples/react-load/preact/size-plugin.json Outdated Show resolved Hide resolved
examples/react-load/preact/src/web-tracer.js Show resolved Hide resolved
examples/react-load/preact/package.json Show resolved Hide resolved
examples/react-load/preact/src/components/app.js Outdated Show resolved Hide resolved
examples/react-load/react/package.json Outdated Show resolved Hide resolved
examples/react-load/react/README.md Show resolved Hide resolved
@thgao
Copy link
Contributor Author

thgao commented Aug 4, 2020

  1. 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

After investigating a bit, I found that if the button handler is added directly using addEventListener like:

 document.getElementById('id').addEventListener('click', () => this.buttonHandler(this))

instead of setting the handler using the onClick method as part of the button, the React traces get nested under the the click span as expected.

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.

@thgao thgao requested review from obecny and markwolff August 4, 2020 20:33
Copy link
Member

@obecny obecny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great job

@dyladan dyladan added the enhancement New feature or request label Aug 5, 2020
@obecny obecny merged commit 5be5217 into open-telemetry:master Aug 13, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants