Concurrent requests; separate transaction IDs

Hi,

  • Relates to SDK: Javascript browser

Question

Is there a way use multiple different scopes at the same time? I’ve been looking through the docs and GitHub issues today, but haven’t found anything that fits my use case.

The short description of what I want to do is as follows:

We have an app that fires off an arbitrary number of network requests concurrently. For each request, we’d like to create and set a transaction id, both on the request, and in the scope (as per the docs on error tracing). I’d like to have the transaction ID as part of the scope so that it’s automatically logged for everything related to a given request.

What I’ve tried

From what I understand, because the default scope is on a global object, using configureScope will cause all kinds of race conditions here. My first thought was to perform each request with withScope, but that doesn’t work, because I need the return value from the function, and from what I understand, withScope doesn’t support async callbacks.

Based on this GitHub issue at /sentry-javascript/issues/2023 (sorry, only 2 links in a post) and the advanced usage section of the docs, it seems that creating a new hub per request could work. However, it also mentions that you need to run something in the context of hub.run, which is another function that returns void, so that also doesn’t work in this case.

What’s the ‘right’ way of doing this? I’m very happy to accept that I’m going about this in the wrong manner here, because it doesn’t seem like a particularly uncommon use case to me. The easiest way would probably be to not use scope at all, but passing around an object that has the corresponding data on it. If that’s the case, then all I need to do is set the tag on all events, which, while tedious, should be pretty doable.


Anyway, appreciate any and all input on this. I’m still really new to Sentry (only started playing around with it yesterday), so I’m probably missing something obvious.

Cheers!

Hi Thomas,

This highly depends on the specifics you’re trying to do.
If you’re using a recent version of the SDK, you can pass a scope explicitly to captureException calls, and that should work fine in a withScope block.

Here’s a rather hackish example, but has the building blocks you can use for greater good:

<html>
<head>
<script
src="https://browser.sentry-cdn.com/5.20.0/bundle.min.js"
integrity="sha384-mmBh0B1uYbzmP0q9QcqWr4el7gVXBSY0hWB2U8cNhNZi/RIuVQRp7pm1NlPOa644"
crossorigin="anonymous"></script>
<script>
  (() => {
    Sentry.init({
      dsn: '__YOUR_DSN__',
      debug: true,
      beforeSend: event => {
        // workaround: take transaction from tags
        event.transaction = (event.tags || {})["txn"];
        delete (event.tags || {})["txn"];
        console.log(event.event_id, event.transaction, event.tags);
        return event;
      },
    });

    function doRequest(i) {
      let p;
      Sentry.withScope(scope => {
        const transactionID = 't'+i;
        // Note: as of the 5.20.0 release, transaction is not supported when
        // passing context directly to captureException. This may change in a
        // future release.
        // As a workaround for now, we set it as a tag and update the event in
        // beforeSend.
        // https://docs.sentry.io/sdks/javascript/errors/context/#passing-context-directly
        scope.setTransaction(transactionID);
        scope.setTag("txn", transactionID);
        p = fetch(location.pathname+'?'+i, {
          headers: {'X-Custom-Transaction-ID': transactionID},
        })
        .then(response => response.text())
        .then(text => {
          if (Math.random() < 0.3) {
            Sentry.captureException(new Error("error"), scope);
          }
          return [text.length, transactionID];
        });
      });
      return p;
    }

    const promises = new Array(10);
    for (let i = 0; i < promises.length; i++) {
      promises[i] = doRequest(i);
    }

    console.log(Promise.allSettled(promises));
  })();
</script>
</head>
<body>
<h1 onclick="javascript:foo()">Hello</h1>
</body>
</html>

This example will start 10 requests concurrently, each with its own scope. Some of them will randomly send an error to Sentry. All outgoing requests have a transaction id, and all error events have the corresponding transaction id.

You mentioned multiple hubs, that should also work.

If you have a code snippet representing what you’re trying to do, someone may be able to give more specific advice.

Cheers,

Rodolfo

I think you could achieve that with https://www.npmjs.com/package/cls-hooked (only on node.js though). By wrapping your async code with a cls namespace that contains reference to sentry’s hub / scope. You’d probably also have to replace Sentry.captureException calls if you have some code using those directly.

Does Sentry have any plans for incorporating that cls-hooked usage (or making it easier), so I can actually have a Sentry.withScope function that works for async code?

Also, why are scopes a stack? I think we should have a way to create a new scope without having to append them to the global stack.

I want to be able to have n scopes at the same time, without them being related to each other.

Right now if I create n+1th scope, I have to make sure it completely overwrites the data from nth scope, which is messy.