From cf03b9d0d6accf3cd0ac926813d466bd9b7dece8 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Thu, 31 Jul 2025 22:02:10 +0200 Subject: [PATCH 01/12] feat/sentry user implementation --- .../src/api/routes/users.controller.ts | 4 + .../src/services/auth/auth.middleware.ts | 12 +++ .../components/layout/logout.component.tsx | 4 + .../src/components/layout/user.context.tsx | 22 +++++- .../src/sentry/sentry.user.context.ts | 55 +++++++++++++ .../src/sentry/sentry.user.interceptor.ts | 24 ++++++ .../src/sentry/sentry.user.context.ts | 79 +++++++++++++++++++ 7 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 libraries/nestjs-libraries/src/sentry/sentry.user.context.ts create mode 100644 libraries/nestjs-libraries/src/sentry/sentry.user.interceptor.ts create mode 100644 libraries/react-shared-libraries/src/sentry/sentry.user.context.ts diff --git a/apps/backend/src/api/routes/users.controller.ts b/apps/backend/src/api/routes/users.controller.ts index 601ba6556..2a4e66f2d 100644 --- a/apps/backend/src/api/routes/users.controller.ts +++ b/apps/backend/src/api/routes/users.controller.ts @@ -29,6 +29,7 @@ import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum'; import { TrackService } from '@gitroom/nestjs-libraries/track/track.service'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class'; +import { clearSentryUserContext } from '@gitroom/nestjs-libraries/sentry/sentry.user.context'; @ApiTags('User') @Controller('/user') @@ -199,6 +200,9 @@ export class UsersController { @Post('/logout') logout(@Res({ passthrough: true }) response: Response) { + // Clear Sentry user context on logout + clearSentryUserContext(); + response.cookie('auth', '', { domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!), ...(!process.env.NOT_SECURED diff --git a/apps/backend/src/services/auth/auth.middleware.ts b/apps/backend/src/services/auth/auth.middleware.ts index 0ef8377ad..f4879d737 100644 --- a/apps/backend/src/services/auth/auth.middleware.ts +++ b/apps/backend/src/services/auth/auth.middleware.ts @@ -6,6 +6,7 @@ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/o import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service'; import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management'; import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter'; +import { setSentryUserContext } from '@gitroom/nestjs-libraries/sentry/sentry.user.context'; export const removeAuth = (res: Response) => { res.cookie('auth', '', { @@ -33,6 +34,8 @@ export class AuthMiddleware implements NestMiddleware { async use(req: Request, res: Response, next: NextFunction) { const auth = req.headers.auth || req.cookies.auth; if (!auth) { + // Clear Sentry user context when no auth token is present + setSentryUserContext(null); throw new HttpForbiddenException(); } try { @@ -70,6 +73,10 @@ export class AuthMiddleware implements NestMiddleware { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error req.org = loadImpersonate.organization; + + // Set Sentry user context for impersonated user + setSentryUserContext(user); + next(); return; } @@ -97,7 +104,12 @@ export class AuthMiddleware implements NestMiddleware { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error req.org = setOrg; + + // Set Sentry user context for this request + setSentryUserContext(user); } catch (err) { + // Clear Sentry user context on authentication failure + setSentryUserContext(null); throw new HttpForbiddenException(); } next(); diff --git a/apps/frontend/src/components/layout/logout.component.tsx b/apps/frontend/src/components/layout/logout.component.tsx index 3128f522c..6debe0718 100644 --- a/apps/frontend/src/components/layout/logout.component.tsx +++ b/apps/frontend/src/components/layout/logout.component.tsx @@ -6,6 +6,7 @@ import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { useVariables } from '@gitroom/react/helpers/variable.context'; import { setCookie } from '@gitroom/frontend/components/layout/layout.context'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; +import { clearSentryUserContext } from '@gitroom/react/sentry/sentry.user.context'; export const LogoutComponent = () => { const fetch = useFetch(); const { isGeneral, isSecured } = useVariables(); @@ -21,6 +22,9 @@ export const LogoutComponent = () => { t('yes_logout', 'Yes logout') ) ) { + // Clear Sentry user context on logout + clearSentryUserContext(); + if (!isSecured) { setCookie('auth', '', -10); } else { diff --git a/apps/frontend/src/components/layout/user.context.tsx b/apps/frontend/src/components/layout/user.context.tsx index 9e5ec48d2..e1cbf0532 100644 --- a/apps/frontend/src/components/layout/user.context.tsx +++ b/apps/frontend/src/components/layout/user.context.tsx @@ -1,11 +1,12 @@ 'use client'; -import { createContext, FC, ReactNode, useContext } from 'react'; +import { createContext, FC, ReactNode, useContext, useEffect } from 'react'; import { User } from '@prisma/client'; import { pricing, PricingInnerInterface, } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; +import { setSentryUserContext } from '@gitroom/react/sentry/sentry.user.context'; export const UserContext = createContext< | undefined | (User & { @@ -18,6 +19,7 @@ export const UserContext = createContext< impersonate: boolean; allowTrial: boolean; isTrailing: boolean; + admin: boolean; // Add admin field from backend response }) >(undefined); export const ContextWrapper: FC<{ @@ -27,6 +29,7 @@ export const ContextWrapper: FC<{ role: 'USER' | 'ADMIN' | 'SUPERADMIN'; publicApi: string; totalChannels: number; + admin: boolean; // Add admin field from backend response }; children: ReactNode; }> = ({ user, children }) => { @@ -36,6 +39,23 @@ export const ContextWrapper: FC<{ tier: pricing[user.tier], } : ({} as any); + + // Set Sentry user context whenever user changes + useEffect(() => { + if (user) { + setSentryUserContext({ + id: user.id, + email: user.email, + orgId: user.orgId, + role: user.role, + tier: user.tier, + admin: user.admin, // Use admin field from backend response + }); + } else { + setSentryUserContext(null); + } + }, [user]); + return {children}; }; export const useUser = () => useContext(UserContext); diff --git a/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts b/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts new file mode 100644 index 000000000..deb5d08b2 --- /dev/null +++ b/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts @@ -0,0 +1,55 @@ +import * as Sentry from '@sentry/nestjs'; +import { User } from '@prisma/client'; + +/** + * Sets user context for Sentry for the current request. + * This will include user information in all error reports and events. + * Only executes if Sentry DSN is configured. + * + * @param user - The user object from the database + */ +export const setSentryUserContext = (user: User | null) => { + // Only set context if Sentry is configured + if (!process.env.NEXT_PUBLIC_SENTRY_DSN) { + return; + } + + if (!user) { + // Clear user context when no user is present + Sentry.setUser(null); + return; + } + + Sentry.setUser({ + id: user.id, + email: user.email, + username: user.email, // Use email as username since that's the primary identifier + // Add additional useful context + ip_address: undefined, // Let Sentry auto-detect IP + }); + + // Also set additional tags for better filtering in Sentry + Sentry.setTag('user.activated', user.activated); + Sentry.setTag('user.provider', user.providerName || 'local'); + + if (user.isSuperAdmin) { + Sentry.setTag('user.super_admin', true); + } +}; + +/** + * Clears the Sentry user context. + * Useful when logging out or switching users. + * Only executes if Sentry DSN is configured. + */ +export const clearSentryUserContext = () => { + // Only clear context if Sentry is configured + if (!process.env.NEXT_PUBLIC_SENTRY_DSN) { + return; + } + + Sentry.setUser(null); + Sentry.setTag('user.activated', undefined); + Sentry.setTag('user.provider', undefined); + Sentry.setTag('user.super_admin', undefined); +}; diff --git a/libraries/nestjs-libraries/src/sentry/sentry.user.interceptor.ts b/libraries/nestjs-libraries/src/sentry/sentry.user.interceptor.ts new file mode 100644 index 000000000..80f458e1e --- /dev/null +++ b/libraries/nestjs-libraries/src/sentry/sentry.user.interceptor.ts @@ -0,0 +1,24 @@ +import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { Request } from 'express'; +import { User } from '@prisma/client'; +import { setSentryUserContext } from './sentry.user.context'; + +/** + * Interceptor that automatically sets Sentry user context for all requests. + * This interceptor runs after authentication middleware has set req.user. + */ +@Injectable() +export class SentryUserInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + + // Get user from request (set by auth middleware) + const user = (request as any).user as User | undefined; + + // Set Sentry user context for this request + setSentryUserContext(user || null); + + return next.handle(); + } +} diff --git a/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts b/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts new file mode 100644 index 000000000..4c0883641 --- /dev/null +++ b/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts @@ -0,0 +1,79 @@ +'use client'; + +import * as Sentry from '@sentry/nextjs'; + +interface UserInfo { + id: string; + email: string; + orgId?: string; + role?: string; + tier?: string; + admin?: boolean; +} + +/** + * Sets user context for Sentry in the frontend. + * This will include user information in all error reports and events. + * Only executes if Sentry DSN is configured. + * + * @param user - The user object from the API + */ +export const setSentryUserContext = (user: UserInfo | null) => { + // Only set context if Sentry is configured + if (!process.env.NEXT_PUBLIC_SENTRY_DSN) { + return; + } + + if (!user) { + // Clear user context when no user is present + Sentry.setUser(null); + return; + } + + Sentry.setUser({ + id: user.id, + email: user.email, + username: user.email, // Use email as username since that's the primary identifier + }); + + // Also set additional tags for better filtering in Sentry + if (user.orgId) { + Sentry.setTag('user.org_id', user.orgId); + } + + if (user.role) { + Sentry.setTag('user.role', user.role); + } + + if (user.tier) { + Sentry.setTag('user.tier', user.tier); + } + + if (user.admin) { + Sentry.setTag('user.admin', true); + } +}; + +/** + * Clears the Sentry user context. + * Useful when logging out or switching users. + * Only executes if Sentry DSN is configured. + */ +export const clearSentryUserContext = () => { + // Only clear context if Sentry is configured (check at runtime for frontend) + if (typeof window !== 'undefined' && !window.location.origin.includes('localhost')) { + // For production, check if Sentry DSN exists in environment or is initialized + if (!process.env.NEXT_PUBLIC_SENTRY_DSN) { + return; + } + } else if (typeof process !== 'undefined' && !process.env.NEXT_PUBLIC_SENTRY_DSN) { + // For server-side or development + return; + } + + Sentry.setUser(null); + Sentry.setTag('user.org_id', undefined); + Sentry.setTag('user.role', undefined); + Sentry.setTag('user.tier', undefined); + Sentry.setTag('user.admin', undefined); +}; From a997bec14720fab8dcdcca47bfacc90293b03809 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Thu, 31 Jul 2025 22:10:40 +0200 Subject: [PATCH 02/12] Update libraries/react-shared-libraries/src/sentry/sentry.user.context.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/sentry/sentry.user.context.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts b/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts index 4c0883641..758138c39 100644 --- a/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts +++ b/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts @@ -60,14 +60,8 @@ export const setSentryUserContext = (user: UserInfo | null) => { * Only executes if Sentry DSN is configured. */ export const clearSentryUserContext = () => { - // Only clear context if Sentry is configured (check at runtime for frontend) - if (typeof window !== 'undefined' && !window.location.origin.includes('localhost')) { - // For production, check if Sentry DSN exists in environment or is initialized - if (!process.env.NEXT_PUBLIC_SENTRY_DSN) { - return; - } - } else if (typeof process !== 'undefined' && !process.env.NEXT_PUBLIC_SENTRY_DSN) { - // For server-side or development + // Only clear context if Sentry is configured + if (!process.env.NEXT_PUBLIC_SENTRY_DSN) { return; } From 3dd49e2b7754d2f32f0bd688bc6fc2115c52d9e7 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Thu, 31 Jul 2025 22:11:25 +0200 Subject: [PATCH 03/12] Update libraries/nestjs-libraries/src/sentry/sentry.user.context.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../nestjs-libraries/src/sentry/sentry.user.context.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts b/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts index deb5d08b2..476d163f9 100644 --- a/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts +++ b/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts @@ -49,7 +49,7 @@ export const clearSentryUserContext = () => { } Sentry.setUser(null); - Sentry.setTag('user.activated', undefined); - Sentry.setTag('user.provider', undefined); - Sentry.setTag('user.super_admin', undefined); + Sentry.setTag('user.activated', null); + Sentry.setTag('user.provider', null); + Sentry.setTag('user.super_admin', null); }; From 936401211859d5006101726284e80bc1dce9ecc1 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Thu, 31 Jul 2025 22:11:40 +0200 Subject: [PATCH 04/12] Make "admin" user contexr optional Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- apps/frontend/src/components/layout/user.context.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/components/layout/user.context.tsx b/apps/frontend/src/components/layout/user.context.tsx index e1cbf0532..40acf380d 100644 --- a/apps/frontend/src/components/layout/user.context.tsx +++ b/apps/frontend/src/components/layout/user.context.tsx @@ -19,7 +19,7 @@ export const UserContext = createContext< impersonate: boolean; allowTrial: boolean; isTrailing: boolean; - admin: boolean; // Add admin field from backend response + admin?: boolean; // Add admin field from backend response }) >(undefined); export const ContextWrapper: FC<{ From 8932ed712f9617f8003dd5f37f1bd28239967431 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Thu, 31 Jul 2025 22:12:30 +0200 Subject: [PATCH 05/12] Update libraries/react-shared-libraries/src/sentry/sentry.user.context.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/sentry/sentry.user.context.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts b/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts index 758138c39..cb70ab2a0 100644 --- a/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts +++ b/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts @@ -66,8 +66,8 @@ export const clearSentryUserContext = () => { } Sentry.setUser(null); - Sentry.setTag('user.org_id', undefined); - Sentry.setTag('user.role', undefined); - Sentry.setTag('user.tier', undefined); - Sentry.setTag('user.admin', undefined); + Sentry.setTag('user.org_id', ''); + Sentry.setTag('user.role', ''); + Sentry.setTag('user.tier', ''); + Sentry.setTag('user.admin', ''); }; From 894dfdbd34a9017b86d07df8aa47ea0422b8a05a Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Fri, 1 Aug 2025 14:56:53 +0200 Subject: [PATCH 06/12] Update user.context.tsx --- apps/frontend/src/components/layout/user.context.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/frontend/src/components/layout/user.context.tsx b/apps/frontend/src/components/layout/user.context.tsx index 40acf380d..6dcb053bd 100644 --- a/apps/frontend/src/components/layout/user.context.tsx +++ b/apps/frontend/src/components/layout/user.context.tsx @@ -19,7 +19,6 @@ export const UserContext = createContext< impersonate: boolean; allowTrial: boolean; isTrailing: boolean; - admin?: boolean; // Add admin field from backend response }) >(undefined); export const ContextWrapper: FC<{ @@ -49,7 +48,6 @@ export const ContextWrapper: FC<{ orgId: user.orgId, role: user.role, tier: user.tier, - admin: user.admin, // Use admin field from backend response }); } else { setSentryUserContext(null); From d8f790b2e495b5d92be6838f7650f04d9e2569bb Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Fri, 1 Aug 2025 14:59:53 +0200 Subject: [PATCH 07/12] Update user.context.tsx --- apps/frontend/src/components/layout/user.context.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/frontend/src/components/layout/user.context.tsx b/apps/frontend/src/components/layout/user.context.tsx index 6dcb053bd..8ec59335e 100644 --- a/apps/frontend/src/components/layout/user.context.tsx +++ b/apps/frontend/src/components/layout/user.context.tsx @@ -28,7 +28,6 @@ export const ContextWrapper: FC<{ role: 'USER' | 'ADMIN' | 'SUPERADMIN'; publicApi: string; totalChannels: number; - admin: boolean; // Add admin field from backend response }; children: ReactNode; }> = ({ user, children }) => { From 4a21ebae66678072e3ca35ab4947ef6c54557997 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Fri, 1 Aug 2025 15:00:52 +0200 Subject: [PATCH 08/12] Update sentry.user.context.ts --- .../src/sentry/sentry.user.context.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts b/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts index cb70ab2a0..874f24677 100644 --- a/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts +++ b/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts @@ -8,7 +8,6 @@ interface UserInfo { orgId?: string; role?: string; tier?: string; - admin?: boolean; } /** @@ -48,10 +47,6 @@ export const setSentryUserContext = (user: UserInfo | null) => { if (user.tier) { Sentry.setTag('user.tier', user.tier); } - - if (user.admin) { - Sentry.setTag('user.admin', true); - } }; /** @@ -69,5 +64,4 @@ export const clearSentryUserContext = () => { Sentry.setTag('user.org_id', ''); Sentry.setTag('user.role', ''); Sentry.setTag('user.tier', ''); - Sentry.setTag('user.admin', ''); }; From d8e6759bbf7f811d809208b334abe6463e4b668b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:33:22 +0000 Subject: [PATCH 09/12] Initial plan From a66a053ca9c06f326ebf73f8a87d83404f51a38a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:41:05 +0000 Subject: [PATCH 10/12] Improve Sentry user context implementation with error handling and documentation Co-authored-by: egelhaus <156946629+egelhaus@users.noreply.github.com> --- .env.example | 3 + .../src/sentry/sentry.user.context.ts | 52 ++++++++++------- .../src/sentry/sentry.user.interceptor.ts | 30 ++++++++++ .../src/sentry/sentry.user.context.ts | 58 +++++++++++-------- 4 files changed, 96 insertions(+), 47 deletions(-) diff --git a/.env.example b/.env.example index 61d2a0206..da1488d08 100644 --- a/.env.example +++ b/.env.example @@ -83,6 +83,9 @@ NEXT_PUBLIC_POLOTNO="" # NOT_SECURED=false API_LIMIT=30 # The limit of the public API hour limit +# Sentry Error Tracking Settings (optional) +# NEXT_PUBLIC_SENTRY_DSN="" # Your Sentry DSN for error tracking. User context will be automatically associated with errors. + # Payment settings FEE_AMOUNT=0.05 STRIPE_PUBLISHABLE_KEY="" diff --git a/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts b/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts index 476d163f9..932bc1583 100644 --- a/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts +++ b/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts @@ -14,26 +14,30 @@ export const setSentryUserContext = (user: User | null) => { return; } - if (!user) { - // Clear user context when no user is present - Sentry.setUser(null); - return; - } + try { + if (!user) { + // Clear user context when no user is present + Sentry.setUser(null); + return; + } - Sentry.setUser({ - id: user.id, - email: user.email, - username: user.email, // Use email as username since that's the primary identifier - // Add additional useful context - ip_address: undefined, // Let Sentry auto-detect IP - }); + Sentry.setUser({ + id: user.id, + email: user.email, + username: user.email, // Use email as username since that's the primary identifier + // Add additional useful context + ip_address: undefined, // Let Sentry auto-detect IP + }); - // Also set additional tags for better filtering in Sentry - Sentry.setTag('user.activated', user.activated); - Sentry.setTag('user.provider', user.providerName || 'local'); - - if (user.isSuperAdmin) { - Sentry.setTag('user.super_admin', true); + // Also set additional tags for better filtering in Sentry + Sentry.setTag('user.activated', user.activated); + Sentry.setTag('user.provider', user.providerName || 'local'); + + if (user.isSuperAdmin) { + Sentry.setTag('user.super_admin', true); + } + } catch { + // Silently fail if Sentry throws an error - we don't want to break the app } }; @@ -48,8 +52,12 @@ export const clearSentryUserContext = () => { return; } - Sentry.setUser(null); - Sentry.setTag('user.activated', null); - Sentry.setTag('user.provider', null); - Sentry.setTag('user.super_admin', null); + try { + Sentry.setUser(null); + Sentry.setTag('user.activated', ''); + Sentry.setTag('user.provider', ''); + Sentry.setTag('user.super_admin', ''); + } catch { + // Silently fail if Sentry throws an error - we don't want to break the app + } }; diff --git a/libraries/nestjs-libraries/src/sentry/sentry.user.interceptor.ts b/libraries/nestjs-libraries/src/sentry/sentry.user.interceptor.ts index 80f458e1e..1dae1c994 100644 --- a/libraries/nestjs-libraries/src/sentry/sentry.user.interceptor.ts +++ b/libraries/nestjs-libraries/src/sentry/sentry.user.interceptor.ts @@ -7,6 +7,36 @@ import { setSentryUserContext } from './sentry.user.context'; /** * Interceptor that automatically sets Sentry user context for all requests. * This interceptor runs after authentication middleware has set req.user. + * + * Usage Options: + * + * 1. Global interceptor (recommended for APIs with consistent auth): + * In your app.module.ts: + * ```typescript + * import { APP_INTERCEPTOR } from '@nestjs/core'; + * import { SentryUserInterceptor } from '@gitroom/nestjs-libraries/sentry/sentry.user.interceptor'; + * + * @Module({ + * providers: [ + * { provide: APP_INTERCEPTOR, useClass: SentryUserInterceptor }, + * ], + * }) + * export class AppModule {} + * ``` + * + * 2. Controller-level (for specific controllers): + * ```typescript + * @UseInterceptors(SentryUserInterceptor) + * @Controller('users') + * export class UsersController {} + * ``` + * + * 3. Method-level (for specific routes): + * ```typescript + * @UseInterceptors(SentryUserInterceptor) + * @Get('profile') + * getProfile() {} + * ``` */ @Injectable() export class SentryUserInterceptor implements NestInterceptor { diff --git a/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts b/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts index 874f24677..e27de97aa 100644 --- a/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts +++ b/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts @@ -23,29 +23,33 @@ export const setSentryUserContext = (user: UserInfo | null) => { return; } - if (!user) { - // Clear user context when no user is present - Sentry.setUser(null); - return; - } + try { + if (!user) { + // Clear user context when no user is present + Sentry.setUser(null); + return; + } - Sentry.setUser({ - id: user.id, - email: user.email, - username: user.email, // Use email as username since that's the primary identifier - }); + Sentry.setUser({ + id: user.id, + email: user.email, + username: user.email, // Use email as username since that's the primary identifier + }); - // Also set additional tags for better filtering in Sentry - if (user.orgId) { - Sentry.setTag('user.org_id', user.orgId); - } - - if (user.role) { - Sentry.setTag('user.role', user.role); - } - - if (user.tier) { - Sentry.setTag('user.tier', user.tier); + // Also set additional tags for better filtering in Sentry + if (user.orgId) { + Sentry.setTag('user.org_id', user.orgId); + } + + if (user.role) { + Sentry.setTag('user.role', user.role); + } + + if (user.tier) { + Sentry.setTag('user.tier', user.tier); + } + } catch { + // Silently fail if Sentry throws an error - we don't want to break the app } }; @@ -60,8 +64,12 @@ export const clearSentryUserContext = () => { return; } - Sentry.setUser(null); - Sentry.setTag('user.org_id', ''); - Sentry.setTag('user.role', ''); - Sentry.setTag('user.tier', ''); + try { + Sentry.setUser(null); + Sentry.setTag('user.org_id', ''); + Sentry.setTag('user.role', ''); + Sentry.setTag('user.tier', ''); + } catch { + // Silently fail if Sentry throws an error - we don't want to break the app + } }; From b5bbf878a25d4b960b402f199e1d402018b07590 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:46:37 +0000 Subject: [PATCH 11/12] Use null instead of empty strings to clear Sentry tags Co-authored-by: egelhaus <156946629+egelhaus@users.noreply.github.com> --- .../nestjs-libraries/src/sentry/sentry.user.context.ts | 6 +++--- .../src/sentry/sentry.user.context.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts b/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts index 932bc1583..df3146942 100644 --- a/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts +++ b/libraries/nestjs-libraries/src/sentry/sentry.user.context.ts @@ -54,9 +54,9 @@ export const clearSentryUserContext = () => { try { Sentry.setUser(null); - Sentry.setTag('user.activated', ''); - Sentry.setTag('user.provider', ''); - Sentry.setTag('user.super_admin', ''); + Sentry.setTag('user.activated', null); + Sentry.setTag('user.provider', null); + Sentry.setTag('user.super_admin', null); } catch { // Silently fail if Sentry throws an error - we don't want to break the app } diff --git a/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts b/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts index e27de97aa..dd0206467 100644 --- a/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts +++ b/libraries/react-shared-libraries/src/sentry/sentry.user.context.ts @@ -66,9 +66,9 @@ export const clearSentryUserContext = () => { try { Sentry.setUser(null); - Sentry.setTag('user.org_id', ''); - Sentry.setTag('user.role', ''); - Sentry.setTag('user.tier', ''); + Sentry.setTag('user.org_id', null); + Sentry.setTag('user.role', null); + Sentry.setTag('user.tier', null); } catch { // Silently fail if Sentry throws an error - we don't want to break the app } From 01d1c2e03a2f79122fbd5f31e9745dc47f73fcb3 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Tue, 25 Nov 2025 16:20:12 +0100 Subject: [PATCH 12/12] Comment out Sentry DSN in .env.example Comment out the Sentry DSN variable in the example file. --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index da1488d08..443d598e9 100644 --- a/.env.example +++ b/.env.example @@ -84,7 +84,7 @@ NEXT_PUBLIC_POLOTNO="" API_LIMIT=30 # The limit of the public API hour limit # Sentry Error Tracking Settings (optional) -# NEXT_PUBLIC_SENTRY_DSN="" # Your Sentry DSN for error tracking. User context will be automatically associated with errors. +# NEXT_PUBLIC_SENTRY_DSN="" # Payment settings FEE_AMOUNT=0.05 @@ -120,4 +120,4 @@ POSTIZ_OAUTH_CLIENT_SECRET="" # LINK_DRIP_API_KEY="" # Your LinkDrip API key # LINK_DRIP_API_ENDPOINT="https://api.linkdrip.com/v1/" # Your self-hosted LinkDrip API endpoint -# LINK_DRIP_SHORT_LINK_DOMAIN="dripl.ink" # Your self-hosted LinkDrip domain \ No newline at end of file +# LINK_DRIP_SHORT_LINK_DOMAIN="dripl.ink" # Your self-hosted LinkDrip domain