diff --git a/.env.example b/.env.example index 61d2a02067..da1488d080 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 476d163f92..df3146942a 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', 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/nestjs-libraries/src/sentry/sentry.user.interceptor.ts b/libraries/nestjs-libraries/src/sentry/sentry.user.interceptor.ts index 80f458e1ee..1dae1c9949 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 874f24677c..dd0206467d 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', 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 + } };