Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
Expand Down
52 changes: 30 additions & 22 deletions libraries/nestjs-libraries/src/sentry/sentry.user.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,30 @@ export const setSentryUserContext = (user: User | null) => {
return;
}

Comment thread
egelhaus marked this conversation as resolved.
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
}
};

Expand All @@ -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
}
};
30 changes: 30 additions & 0 deletions libraries/nestjs-libraries/src/sentry/sentry.user.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
58 changes: 33 additions & 25 deletions libraries/react-shared-libraries/src/sentry/sentry.user.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
};

Expand All @@ -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
}
};