Skip to content

Sentry NodeJS setUser per request #13205

Closed

Description

@sbriceland

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/node

SDK Version

8.20.0

Framework Version

@sentry/node

Link to Sentry event

No response

Reproduction Example/SDK Setup

How to set User per request in Node?

From Scopes Documentation:

For instance, a web server might handle multiple requests at the same time, and each request may have different scope data to apply to its events.

The isolation scope is used to isolate events from each other. For example, each request in a web server might get its own isolation scope, so that events from one request don't interfere with events from another request. In most cases, you'll want to put data that should be applied to your events on the isolation scope - which is also why all Sentry.setXXX methods, like Sentry.setTag(), will write data onto the currently active isolation scope. A classic example for data that belongs on the isolation scope is a user - each request may have a different user, so you want to make sure that the user is set on the isolation scope

... The documentation matches exactly the behavior needed. However, it simply does not work.

  • make an authenticated request, lookup user details, call setUser
  • make an unauthenticated request, do not call to setUser for this request (scope) because we don't have one, and throw an error manually

Problems

  • The manually thrown error will report the User from the previous authenticated request.
  • The same problem exists for spans.
  • This is intentionally simple, but we've seen many errors report the wrong user

This example using Express should demonstrate...

import * as Sentry from '@sentry/node';
import bodyParser from 'body-parser';
import express, { Application, NextFunction, Request, Response, Router } from 'express';

Sentry.init({
    dsn: undefined, // not needed for local debugging to reveal the issue
    beforeSend: (event, hint, ...args) => {
        const { type, contexts, exception, extra, tags, message, user, request } = event;
        console.dir(
            {
                whoami: 'sentry:beforeSend',
                event: { type, contexts, exception, extra, tags, message, user, request },
                hint,
                args
            },
            { depth: null }
        );
        return event;
    },
    skipOpenTelemetrySetup: true
});

const app: Application = express();

const router = Router();

router.use(bodyParser.urlencoded({ extended: true, limit: '500kb' }));
router.use(bodyParser.json({ limit: '500kb' }));

const Users: { id: string; email: string; name: string }[] = [
    { id: '1', email: 'foo@example.com', name: 'foo example' },
    { id: '2', email: 'foo2@example.com', name: 'foo example2' },
    { id: '3', email: 'foo3@example.com', name: 'foo example3' },
    { id: '4', email: 'foo4@example.com', name: 'foo example4' }
];

router.use('/users', function (req, res, next) {
    try {
        const authUser = Users.find((u) => u.id === req.headers['authorization']);
        if (authUser) {
            Sentry.setTag('Authenticated', true);
            Sentry.setUser(authUser);
            res.json(Users);
        } else {
            throw new Error('Authentication Error');
        }
    } catch (err) {
        next(err);
    }
});

app.use('/api', router);

app.use(function (err: Error, req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl, params, query, body } = req;
    const { statusCode, locals } = res;

    Sentry.withScope((scope) => {
        scope.setExtras({
            request: { method, originalUrl, params, query, body },
            response: { statusCode, locals }
        });
        const eventId = Sentry.captureException(err);
        (res as { sentry?: string }).sentry = eventId;
    });

    next(err);
});

// Or just use this, which is identical to above, without `extras`
// Sentry.setupExpressErrorHandler(app);

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
    console.log(`API running @ http://localhost:${PORT}`);
});

Steps to Reproduce

Send the following requests:

# make an "authenticated" request
curl --header "authorization: 1" --header "content-type: application/json" http://localhost:3000/api/users

# subsequently make an unauthenticated request
curl --header "authorization: 798798798798" --header "content-type: application/json" http://localhost:3000/api/users

Expected Result

Logs show different user context. Also, this is only a very simple issue. We've seen many users mis reported in our Prod environment. So it would seem that Scopes are not isolated to a request.

Actual Result

same user for requests with different auth

Activity

  1. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Aug 2, 2024
  2. sbriceland commented on Aug 2, 2024

    @sbriceland
    Author

    Also, not sure why this has package:browser tag. I am pretty sure i typed @sentry/node 🤷

  3. andreiborza commented on Aug 5, 2024

    @andreiborza
    Member

    Hey, thanks for filing this and providing extensive reproduction steps. We are looking into this and will get back to you.

  4. sbriceland commented on Aug 5, 2024

    @sbriceland
    Author

    Thanks @andreiborza !

    To take the above example a step forward, I have tweaked the demo API and sample test script to illustrate that when authenticated requests are handled in parallel, the User reported for the Sentry issue is often incorrect.

    To confirm that Sentry reports an incorrect User when handling multiple authenticated requests, try running the following example API:

    import * as Sentry from '@sentry/node';
    import bodyParser from 'body-parser';
    import express, { Application, NextFunction, Request, Response, Router } from 'express';
    
    Sentry.init({
        dsn: 'XXX',
        release: 'sentry-scope-testing',
        environment: 'local',
        skipOpenTelemetrySetup: true
    });
    
    const app: Application = express();
    
    const router = Router();
    
    router.use(bodyParser.urlencoded({ extended: true, limit: '500kb' }));
    router.use(bodyParser.json({ limit: '500kb' }));
    
    const Users: { id: string; email: string; name: string }[] = [
        { id: '1', email: 'foo@example.com', name: 'foo example' },
        { id: '2', email: 'foo2@example.com', name: 'foo example2' },
        { id: '3', email: 'foo3@example.com', name: 'foo example3' },
        { id: '4', email: 'foo4@example.com', name: 'foo example4' }
    ];
    
    const sleep = async (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
    router.use('/users', function (req, res, next) {
        try {
            const reqUser = Users.find((u) => u.id === req.headers['authorization']);
            if (reqUser) {
                Sentry.setTag('Authenticated', true);
                Sentry.setUser(reqUser);
                const randomNum = Math.random() * 3000;
                sleep(randomNum).then(() => {
                    Sentry.withScope((scope) => {
                        // extras will include correct User.
                        // This allows a reference point to check the reported Issue User and the Additional Data reqUser
                        // Our tests confirm that this is often wrong when the api is dealing with requests in parallel
                        scope.setExtras({ reqUser, randomNum });
                        Sentry.captureMessage('sentry-scope-testing!');
                    });
                    res.json(Users);
                });
            } else {
                throw new Error('Authentication Error');
            }
        } catch (err) {
            next(err);
        }
    });
    
    app.use('/api', router);
    
    app.use(function (err: Error, req: Request, res: Response, next: NextFunction) {
        const { method, originalUrl, params, query, body } = req;
        const { statusCode, locals } = res;
    
        Sentry.withScope((scope) => {
            scope.setExtras({
                request: { method, originalUrl, params, query, body },
                response: { statusCode, locals }
            });
            const eventId = Sentry.captureException(err);
            (res as { sentry?: string }).sentry = eventId;
        });
    
        next(err);
    });
    
    // Or just use this, which is identical to above, without `extras`
    // Sentry.setupExpressErrorHandler(app);
    
    const PORT = process.env.PORT || 3000;
    app.listen(PORT, () => {
        console.log(`API running @ http://localhost:${PORT}`);
    });

    After starting the above server, create (& run) a shell script containing the following commands to invoke some authenticated requests in parallel:

    curl --header "authorization: 4" --header "content-type: application/json" http://localhost:3000/api/users &
    curl --header "authorization: 2" --header "content-type: application/json" http://localhost:3000/api/users &
    curl --header "authorization: 3" --header "content-type: application/json" http://localhost:3000/api/users &
    curl --header "authorization: 4" --header "content-type: application/json" http://localhost:3000/api/users &
    curl --header "authorization: 3" --header "content-type: application/json" http://localhost:3000/api/users &
    curl --header "authorization: 3" --header "content-type: application/json" http://localhost:3000/api/users &
    curl --header "authorization: 2" --header "content-type: application/json" http://localhost:3000/api/users &
    curl --header "authorization: 2" --header "content-type: application/json" http://localhost:3000/api/users &
    curl --header "authorization: 4" --header "content-type: application/json" http://localhost:3000/api/users &
    curl --header "authorization: 2" --header "content-type: application/json" http://localhost:3000/api/users &
    wait
  5. sbriceland commented on Aug 5, 2024

    @sbriceland
    Author

    Here's a repo for ease of running my above mentioned examples: https://github.com/sbriceland/sentry-debug

  6. andreiborza commented on Aug 6, 2024

    @andreiborza
    Member

    Awesome, thank you for providing even more detail :)

  7. 9 remaining items

  8. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Aug 13, 2024
  9. sbriceland commented on Aug 13, 2024

    @sbriceland
    Author

    @terasaka2k - I thought that could have been an issue. So, I ended up putting together a more robust reproduction example in a repo: https://github.com/sbriceland/sentry-debug. More importantly, I made the DSN configurable so that we could test the case of sending reports up to Sentry just in case: https://github.com/sbriceland/sentry-debug/blob/main/src/server.ts#L5. We still experienced the same issues with a valid DSN.

  10. andreiborza commented on Aug 13, 2024

    @andreiborza
    Member

    @sbriceland thank you for elaborating this (and thus documenting it for future users coming across this issue).

    As the issue isn't really with setUser, but OTEL + Sentry in general I'm going to close this issue. Further discussion can happen in the aforementioned issues.

  11. terasaka2k commented on Aug 13, 2024

    @terasaka2k

    Not sure this is the problem only in the sample codes, but just to be sure, the setup must follow https://docs.sentry.io/platforms/javascript/guides/node/install/esm/ (or similar) so that httpIntegration works correctly for incoming requests.
    c.f. https://gist.github.com/terasaka2k/214dcea3aeed4566b5e93a818025e9f1

  12. sbriceland commented on Aug 13, 2024

    @sbriceland
    Author

    @andreiborza - This is a bug with scopes though right? I'm not sure why opting out of Sentry's open telemetry would cause something like Scopes to be unreliable across requests.

  13. mydea commented on Sep 17, 2024

    @mydea
    Member

    FYI the problem here is most likely that the automatic request isolation happens after middlewares run. I added docs for this here: getsentry/sentry-docs#11378, we may investigate if we can somehow fix this but I don't know if we'll find a way -so for now you'll need to wrap this manually.

  14. 5d-jh commented on Dec 10, 2024

    @5d-jh

    It should be also noted that users should use witIsolationScope on WebSocket endpoint. It too mangles up user label.

    In Fastify, for example, withIsolationScope should be used onRequest hook.

    app.get(
      ‘/ws’,
      {
        websocket: true,
        onRequest: (_, __, done) => withIsolationScope(done),
      },
      (ws) => {...}
    
    )
  15. sagrawal31 commented on Apr 1, 2025

    @sagrawal31

    In our B2B application, the NestJS setup is something like

    All the endpoints being executed set some tags (if being passed by the front end) about the business who is accessing the platform. In cases where no tags/context/user is being set, I can see the tags/context/user from any previous request if an error is logged.

    For a few months, I have been spending hours investigating why my tags, context, and user are being leaked to other requests, re-reading the documentation repeatedly to check if I missed some configuration.

    I have read the https://docs.sentry.io/platforms/javascript/guides/nestjs/enriching-events/request-isolation/ many times but the documentation doesn't make sense until I read the comment from @mydea-

    FYI the problem here is most likely that the automatic request isolation happens after middlewares run.

    The question is: is this really true?

    Because I'm setting those tags in a NestJS interceptor-

    import {RequestUtils} from '@lib/web-core/utils/request/request-utils';
    import {AppCacheService} from '@libs/core/app/app-cache.service';
    import {Account} from '@libs/core/schemas/account/account.schema';
    import {MongooseUtils} from '@libs/core/utils/schema/mongoose-utils';
    import {CallHandler, ExecutionContext, Injectable, NestInterceptor} from '@nestjs/common';
    import * as Sentry from '@sentry/node';
    import {User} from '@sentry/node';
    import {Request} from 'express';
    import {CustomerAppService} from '../services/customer-app.service';
    
    /**
     * The NestInterceptor to interceptor the incoming request and add the {@link App} details in Sentry tags.
     *
     * @author Shashank Agrawal
     */
    @Injectable()
    export class SentryInterceptor implements NestInterceptor {
    
        constructor(
            private readonly appCacheService: AppCacheService,
            private readonly customerAppService: CustomerAppService,
        ) {
        }
    
        async intercept(context: ExecutionContext, next: CallHandler): Promise<any> {
            await this.trackInSentry(context);
            return next.handle();
        }
    
        private async trackInSentry(context: ExecutionContext): Promise<void> {
            const ctx = context.switchToHttp();
            const request = ctx.getRequest();
    
            this.trackAccount(request);
            await this.trackApp(request);
        }
    
        private trackAccount(request: Request): void {
            const account = request.user as Account;
            let user: User = {
                // eslint-disable-next-line @typescript-eslint/naming-convention,camelcase
                ip_address: RequestUtils.getIP(request),
            };
    
            if (account) {
                user = {
                    ...user,
                    id: account._id.toHexString(),
                    email: account.email,
                    username: account.firstName + ' ' + account.lastName,
                    enabled: account.enabled,
                    roles: account.roles,
                    mobile: account.mobile?.substring(0, 4),
                };
    
                Sentry.setUser(user);
            }
        }
    
        private async trackApp(request: Request): Promise<void> {
            const appID = this.customerAppService.getAppIDFromRequest(request);
            if (!MongooseUtils.isValid(appID)) {
                return;
            }
    
            const app = await this.appCacheService.getFromCache(appID);
            if (!app) {
                return;
            }
    
            Sentry.getCurrentScope().setTag('app.id', app.id);
            Sentry.getCurrentScope().setTag('app.name', app.name);
            Sentry.getCurrentScope().setContext('Cooee App', {
                id: app.id,
                name: app.name,
                platforms: app.getPlatformsAsString(),
                website: app.web?.domain,
            });
        }
    
    }
  16. mydea commented on Apr 1, 2025

    @mydea
    Member

    @sagrawal31 do you mind opening a new issue for this? Then we can investigate this specifically for nest.js!

  17. sagrawal31 commented on Apr 1, 2025

    @sagrawal31

    Sure @mydea. I didn't want to pollute the issue tracker, so thought of asking here first. Here you go #15940

  18. ghost added on Aug 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions