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

XMLHttpRequest #595

Merged
merged 31 commits into from
Dec 19, 2019
Merged

XMLHttpRequest #595

merged 31 commits into from
Dec 19, 2019

Conversation

obecny
Copy link
Member

@obecny obecny commented Dec 5, 2019

Which problem is this PR solving?

  1. Fixes Web browser plugin for XHRs #493
  2. Fixes Make XHR browser plugin create child spans for CORS pre-flight requests #604

Short description of the changes

It adds auto instrumentation for XMLHttpRequest including trace headers

@codecov-io
Copy link

codecov-io commented Dec 5, 2019

Codecov Report

Merging #595 into master will decrease coverage by 0.24%.
The diff coverage is 83.04%.

@@            Coverage Diff             @@
##           master     #595      +/-   ##
==========================================
- Coverage   90.48%   90.23%   -0.25%     
==========================================
  Files         180      189       +9     
  Lines        9109     9608     +499     
  Branches      814      877      +63     
==========================================
+ Hits         8242     8670     +428     
- Misses        867      938      +71
Impacted Files Coverage Δ
...ntelemetry-web/src/enums/PerformanceTimingNames.ts 100% <ø> (ø)
packages/opentelemetry-core/src/common/types.ts 100% <ø> (ø) ⬆️
packages/opentelemetry-web/src/WebTracer.ts 100% <100%> (ø) ⬆️
...telemetry-plugin-document-load/src/documentLoad.ts 97.93% <100%> (-0.2%) ⬇️
...lugin-xml-http-request/src/enums/AttributeNames.ts 100% <100%> (ø)
...ry-plugin-xml-http-request/src/enums/EventNames.ts 100% <100%> (ø)
packages/opentelemetry-core/src/utils/wrap.ts 100% <100%> (ø)
...emetry-plugin-xml-http-request/src/enums/Format.ts 100% <100%> (ø)
packages/opentelemetry-core/src/utils/url.ts 100% <100%> (ø)
packages/opentelemetry-core/test/utils/url.test.ts 50% <50%> (ø)
... and 27 more

@mayurkale22
Copy link
Member

/cc @draffensperger

Copy link
Member

@dyladan dyladan left a comment

Choose a reason for hiding this comment

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

Some first pass comments

packages/opentelemetry-core/src/common/time.ts Outdated Show resolved Hide resolved
protected patch() {
this._logger.debug('applying patch to', this.moduleName, this.version);

if (isWrapped(XMLHttpRequest.prototype.open)) {
Copy link
Member

Choose a reason for hiding this comment

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

Why would this already be patched? Shouldn't we just no-op if it is already wrapped?

Copy link
Member Author

Choose a reason for hiding this comment

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

with web there is always some probability that something can be destroyed and recreated again. This way it will always work correctly

Copy link
Member

Choose a reason for hiding this comment

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

Since this is patching a global, would it not patch if multiple components on the same page are instrumented with separate tracers/configs?

Copy link
Member Author

Choose a reason for hiding this comment

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

copy paste here my reply from below
We are using shimmer which doesn't support multiple wrap of the same function so if you even register multiple tracers with the same plugins, and try to unpatch just one, it will unpatch the global successfully anyway. So if the decision to use shimmer was done my assumption is that we are able to instrument using the plugins once only. Otherwise the code will be broken anyway.

): OpenFunction | any {
plugin._createSpan(this as XMLHttpRequestWrapped, url, method);

if (async) {
Copy link
Member

Choose a reason for hiding this comment

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

it is always safer to use original.apply(this, arguments) because we can't account for users passing incorrect values or the incorrect number of values. It also removes the need for this check.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is the same here.
You don't have arguments word in typescript , so you would still declare array of arguments and define type for it, which would be quite weird anyway.

Besides that check also the whole code to see more context. The async will always be set to true and there are reasons for that check here. It is also worth to mention that it was ported from open census.

Copy link
Member

Choose a reason for hiding this comment

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

You do have the arguments keyword in typescript.

It is possible to call open with async: false and user not-null. In this case you are changing the arguments the user has called the function with.

We cannot assume that users will use the API properly as documented and it is much easier and safer to directly pass their arguments. If the user incorrectly calls open with async: true I am suggesting something like this (tests pass with this version):

  protected _patchOpen() {
    return (original: Function): OpenFunction => {
      const plugin = this;
      return function patchOpen(
        this: XMLHttpRequest,
        method: string,
        url: string,
        async?: boolean,
        user?: string | null,
        pass?: string | null
      ): ReturnType<OpenFunction> {
        plugin._createSpan(this as XMLHttpRequestWrapped, url, method);
        return original.apply(this, arguments);
        /*
        If you would rather use `call` this can be done easily
        return original.call(this, ...arguments);
        */
      };
    };
  }

Regarding the performance difference between apply and call, it may be the case that call is faster than apply, but in this case the if statement would provide more overhead than the difference.

Copy link
Member Author

Choose a reason for hiding this comment

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

still cannot use apply anyway, as I mentioned previously please read the spec, and why it needs to force the async

Copy link
Member Author

Choose a reason for hiding this comment

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

If true, notification of a completed transaction is provided using event listeners.

Copy link
Member

Choose a reason for hiding this comment

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

What about that means it must be true? If it is false (synchronous), then you just finish the span after calling the original function. If user sets async false in a context that it is not allowed (some browsers only allow sync requests in workers), then

The way your code is currently, it changes the behavior of user code behind the scenes. If I am a user and I make a synchronous xhr request, I expect it to be made synchronously not transparently rewritten to be async behind the scenes by my telemetry provider.

Copy link
Member Author

@obecny obecny Dec 10, 2019

Copy link
Member Author

Choose a reason for hiding this comment

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

and one more Synchronous XHR is now in deprecation state.

Copy link
Member

Choose a reason for hiding this comment

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

I understand that it's deprecated, but that doesn't mean users can't or won't use it and the telemetry library shouldn't silently change the behavior of the program.

should we tallk about this in the SIG tomorrow?

};
plugin._addHeaders(this, currentSpan);
}
return original.call(this, body);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return original.call(this, body);
return original.apply(this, arguments);

Copy link
Member Author

Choose a reason for hiding this comment

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

  1. call is faster
  2. we already know there is just one para, we know it's type so we can use it and typescript will also validate the type of this is correcty
  3. You don't have arguments word in typescript , so you would still declare array of aguments and define type for it, which would be quite weird anyway

Copy link
Member Author

Choose a reason for hiding this comment

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

and the same here, thx
@draffensperger @mayurkale22 ^^

packages/opentelemetry-plugin-xml-http-request/src/xhr.ts Outdated Show resolved Hide resolved
packages/opentelemetry-plugin-xml-http-request/src/xhr.ts Outdated Show resolved Hide resolved
Copy link
Member

@OlivierAlbertini OlivierAlbertini left a comment

Choose a reason for hiding this comment

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

Nice work!

@obecny obecny added the WIP label Dec 7, 2019
@obecny
Copy link
Member Author

obecny commented Dec 7, 2019

marking temporary as WIP - working on better detection for cors

*/
export class XMLHttpRequestPlugin extends BasePlugin<XMLHttpRequest> {
readonly component: string = 'xml-http-request';
readonly version: string = '0.3.0';
Copy link
Member

Choose a reason for hiding this comment

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

That would assume that the user has the same version of the plugin and of core installed which may not always be the case. @obecny and I had talked about adding the autogenerated VERSION file to all lerna packages, but nothing has been done yet.

packages/opentelemetry-plugin-xml-http-request/src/xhr.ts Outdated Show resolved Hide resolved
packages/opentelemetry-plugin-xml-http-request/src/xhr.ts Outdated Show resolved Hide resolved
packages/opentelemetry-plugin-xml-http-request/src/xhr.ts Outdated Show resolved Hide resolved
): OpenFunction | any {
plugin._createSpan(this as XMLHttpRequestWrapped, url, method);

if (async) {
Copy link
Member

Choose a reason for hiding this comment

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

You do have the arguments keyword in typescript.

It is possible to call open with async: false and user not-null. In this case you are changing the arguments the user has called the function with.

We cannot assume that users will use the API properly as documented and it is much easier and safer to directly pass their arguments. If the user incorrectly calls open with async: true I am suggesting something like this (tests pass with this version):

  protected _patchOpen() {
    return (original: Function): OpenFunction => {
      const plugin = this;
      return function patchOpen(
        this: XMLHttpRequest,
        method: string,
        url: string,
        async?: boolean,
        user?: string | null,
        pass?: string | null
      ): ReturnType<OpenFunction> {
        plugin._createSpan(this as XMLHttpRequestWrapped, url, method);
        return original.apply(this, arguments);
        /*
        If you would rather use `call` this can be done easily
        return original.call(this, ...arguments);
        */
      };
    };
  }

Regarding the performance difference between apply and call, it may be the case that call is faster than apply, but in this case the if statement would provide more overhead than the difference.

@dyladan
Copy link
Member

dyladan commented Dec 10, 2019

You may also want to do the same with the other maps that have possibility of leaks:

_callbackToRemoveEvents
_resourcesCreatedInTheMiddle
_usedResources

which then removes the need for taskCount

@obecny
Copy link
Member Author

obecny commented Dec 10, 2019

@dyladan not all can be done and taskCount is still needed, but all things that depends on xhr has been moved.

Copy link
Contributor

@draffensperger draffensperger 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 on this.

I'm giving approval since to me this semantically captures what we should be doing in this plugin. I left a couple more minor comments/suggestions and in particular I think the issue around the cast vs. use of the span API is relevant to fix if we can. But this is great!!

As an aside, I saw the comment thread about this assuming there is a trace context set up, which to me is a relevant comment, and highlights the need for this plugin to work in conjunction with other plugins that set up a "root" span of sorts, e.g. a user interaction tracing plugin, a root span from document load, etc.

examples/tracer-web/examples/xml-http-request/index.js Outdated Show resolved Hide resolved
packages/opentelemetry-plugin-xml-http-request/README.md Outdated Show resolved Hide resolved
}
}
if (!addHeaderOnDifferentOrigin) {
this._logger.warn('Cannot set headers on different origin');
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this will be a pretty routine case that the user sends an XHR to a different origin that they don't want to pass along trace headers for (that is, at least until the browser spec can give an exception for CORS for the W3C trace headers)

Given that, would it make sense to remove this warning log and corresponding if block?

Copy link
Member Author

Choose a reason for hiding this comment

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

I might be wrong but my understanding is that we might break something with the request if we force to add headers. The headers are only added when origin is the same. If not then I check if user allows to add the headers based on config. If not then the headers are dropped and warning is being show. I cannot remove the if block (to prevent adding headers) but only warning if that is something you want me to do ?

packages/opentelemetry-plugin-xml-http-request/src/xhr.ts Outdated Show resolved Hide resolved
packages/opentelemetry-plugin-xml-http-request/src/xhr.ts Outdated Show resolved Hide resolved
Copy link
Member

@dyladan dyladan left a comment

Choose a reason for hiding this comment

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

This looks real good!

From my perspective I really only have one outstanding question about forcing all calls to open to be async even when the user passes false. I'd like to get @draffensperger and @mayurkale22 opinion on this.

It looks like the dependency on web in this version is limited to only functions that don't require casts to our sdk types so they should be compatible even with an sdk other than ours being used. 👍

): void {
plugin._createSpan(this, url, method);

return original.call(this, method, url, true, user, pass);
Copy link
Member

Choose a reason for hiding this comment

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

Creating a new thread on this line as the old one is deep in the history and on a line that has been outdated for a long time.

@draffensperger @mayurkale22

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I think I missed this in my review, but I definitely agree that we should preserve the original behavior of the XHR function including the async parameter.

const onClick = () => {
for (let i = 0, j = 5; i < j; i++) {
const span1 = webTracerWithZone.startSpan(`files-series-info-${i}`, {
parent: webTracerWithZone.getCurrentSpan()
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe if there is no current span, this span will just be a root span, which is the correct behavior. I think my comment about having a nice root span for XHRs is more like a future feature and not so much an issue with this code.

@obecny
Copy link
Member Author

obecny commented Dec 16, 2019

@draffensperger @mayurkale22 @dyladan changed the code with regards to our meeting

@obecny obecny requested a review from dyladan December 17, 2019 20:56
@mayurkale22 mayurkale22 added Merge:LGTM This PR is ready to be merged by a Maintainer (has enough valid approvals, successful build, etc.) and removed Awaiting reviewer feedback labels Dec 18, 2019
lerna.json Outdated Show resolved Hide resolved
@mayurkale22
Copy link
Member

@obecny just wanted to confirm, do you have any outstanding work here or PR is ready to merge?

@obecny
Copy link
Member Author

obecny commented Dec 19, 2019

@obecny just wanted to confirm, do you have any outstanding work here or PR is ready to merge?

Nothing left from my side

@mayurkale22 mayurkale22 merged commit bc583b8 into open-telemetry:master Dec 19, 2019
@obecny obecny deleted the xml-http-request branch July 8, 2020 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Merge:LGTM This PR is ready to be merged by a Maintainer (has enough valid approvals, successful build, etc.)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Make XHR browser plugin create child spans for CORS pre-flight requests Web browser plugin for XHRs
7 participants