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

Sentry.setUser concurrency issues #4372

Closed
4 of 5 tasks
danechitoaie opened this issue Jan 6, 2022 · 9 comments
Closed
4 of 5 tasks

Sentry.setUser concurrency issues #4372

danechitoaie opened this issue Jan 6, 2022 · 9 comments

Comments

@danechitoaie
Copy link

Package + Version

  • @sentry/browser
  • @sentry/node

Description

I'm trying to integrate Sentry with Node.js serverside application and the problem is that Sentry.setUser(); is global. And this causes concurrency issues. The Node.js application handles multiple requests, each associated with different user.

Is there a way to set the user per request, scoped to current transaction and not globally, so that we don't have concurrency issues?

@AbhiPrasad
Copy link
Member

We recommend isolating async resources using domains or async hooks. As an example, the Sentry express integration uses domains under the hood to accomplish this.

If you don't want to use domains or async hooks, we recommend you manually pass around an explicit scope object so you control exactly what is being set and sent.

If you can provide a small demo or reproduction of what you're trying to accomplish we can help debug further.

@danechitoaie
Copy link
Author

danechitoaie commented Jan 10, 2022

At the moment my code is something like this:

Hook1

    fastify.addHook("onRequest", (request: FastifyRequest, _reply: FastifyReply, done) => {
        const currentHub = Sentry.getCurrentHub();
        currentHub.configureScope((scope) => {
            scope.addEventProcessor((event) => {
                event.request = {
                    method: request.method,
                    ...event.request,
                };

                return event;
            });
        });

        const tx = Sentry.startTransaction({
            name: `${request.method} ${request.url}`,
            op: "http.server",
            description: "HTTP request",
        });

        currentHub.configureScope((scope) => {
            scope.setSpan(tx);
        });

        request.sentryTx = tx;
        done();
    });

Hook2

    fastify.addHook("onRequest", async (request: FastifyRequest, reply: FastifyReply) => {
        Sentry.getCurrentHub().configureScope((scope) => {
            // eslint-disable-next-line @typescript-eslint/naming-convention
            scope.setUser({ id: oid, username: upn, email: upn, ip_address: ipaddr });
        });
    });

Handler

    fastify.get("/", async (_request: FastifyRequest, _reply: FastifyReply) => {
        const sentryTxSpan = request.sentryTx.startChild({
            op: "db.do_stuff",
            description: "Do stuff",
        });

        // stuff

        sentryTxSpan.finish();
        return "Test";
    });

Hook3

    fastify.addHook("onResponse", (request: FastifyRequest, reply: FastifyReply, done) => {
        request.sentryTx.setHttpStatus(reply.statusCode);
        request.sentryTx.finish();
        done();
    });

The way each request is executed is: Hook1 -> Hook2 -> Handler -> Hook3

Is this the right way to track/associate the current user with its own request?
Same issue reported here as well by someone else (maybe this helps to give more context) https://forum.sentry.io/t/correct-way-to-setuser-in-node-sdk/14355

@danechitoaie
Copy link
Author

I see the express.js handler is using domains: https://github.com/getsentry/sentry-javascript/blob/master/packages/node/src/handlers.ts#L420-L421

But they are deprecated: https://nodejs.org/dist/latest-v16.x/docs/api/domain.html#domain

Not sure why sentry went with this architecture of making everything global, data should've been instance scoped, not global. Or at last allow to set some data on the instance level as well.

For browser it made sense maybe to have everything global, but for server side, for nodejs everything should be request scoped as each request handles data related to different user.

We need to be able to associate a transaction with a specific user.

@danechitoaie
Copy link
Author

Regarding "passing around an explicit scope object" I would do that as well if it would be possible. But it seems you can only set an explicit scope to Sentry.captureException/captureMessage/etc. You can't set an scope object to a transaction. At least I was not able to find out how this can be done.

@wereHamster
Copy link

I'm having the same issue when trying to capture transactions during server-side rendering of a Next.js app. Not being able to configure the scope per-transaction is… unfortunate. In Node.js there are many situations where multiple transactions are running concurrently.

@dwd
Copy link

dwd commented Mar 28, 2022

Your problem isn't so much passing around an explicit scope; it's handling things such that Sentry.getCurrentHub() returns the right hub for the current request. The reason this is needed is because you're trying to capture errors from all over the app, and you simply can't easily pass a local around on the stack for this without potentially altering every function, which is painful.

Sentry's own integration for Express uses a Node.js Domain - these are indeed deprecated, but they're in limbo as far as I can tell, awaiting a replacement architecture for the same thing. So I reckon use them for now. Domain usage is encapsulated almost completely within Sentry.getCurrentHub(), so you needn't actually do much more than create and enter them.

In principle, you could hook into the async scheduler, which is what Domain does under the hood, and I believe the lower-level replacement APIs are around now, but I think you'd then need to handle Sentry.getCurrentHub() in a different way which sounds like a lot of hard work to me.

I'm not all that familiar with Fastify, but my gut feeling is that you could hook into the request early, and create a domain there, and enter it immediately:

fastify.addHook("onRequest", (request: FastifyRequest, _reply: FastifyReply, done) => {
  request.__sentry_domain = domain.create()
  request.__sentry_domain.enter()
  Sentry.getCurrentHub() // Actually creates the current hub on demand, nice to have to debug
}

fastify.addHook("onResponseOrSomethingRightAtTheEnd", ... => {
  // Do normal Sentry cleanup work like ending transaction.
  if (request.__sentry_domain) {
    request.__sentry_domain.exit()
    request.__sentry_domain = null
  }
}

Then everything else in the API should mostly work. You'll need to capture exceptions at sensible places too (and finish transactions, and so on), but it looks like you have that covered.

@vladanpaunovic
Copy link
Contributor

Hey @danechitoaie, it seems like @dwd provided a good answer here (Thanks @dwd !). Did you give it a try? Is this resolving the error for you?

@github-actions
Copy link
Contributor

This issue has gone three weeks without activity. In another week, I will close it.

But! If you comment or otherwise update it, I will reset the clock, and if you label it Status: Backlog or Status: In Progress, I will leave it alone ... forever!


"A weed is but an unloved flower." ― Ella Wheeler Wilcox 🥀

@schankam
Copy link

schankam commented Apr 4, 2023

I'm having the same issue today with my Apollo graphql server. Why can't we set a user directly on a transaction ? Wouldn't that solve the problem ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants