Compare commits

...
Sign in to create a new pull request.

18 commits

Author SHA1 Message Date
Enno Gelhaus
6e1c152e5b
Merge branch 'main' into sentry-user
Some checks failed
Build / build (22.12.0) (push) Has been cancelled
2026-03-21 17:58:18 +01:00
Enno Gelhaus
4bec77bb91
Merge branch 'main' into sentry-user 2025-11-26 16:10:21 +01:00
Enno Gelhaus
bbdaf7dc6c
Merge branch 'main' into sentry-user 2025-11-25 16:20:27 +01:00
Enno Gelhaus
01d1c2e03a
Comment out Sentry DSN in .env.example
Comment out the Sentry DSN variable in the example file.
2025-11-25 16:20:12 +01:00
Enno Gelhaus
af0542b20d
Merge pull request #1076 from gitroomhq/copilot/sub-pr-910
feat: Sentry user context integration with error handling
2025-11-24 22:51:10 +01:00
copilot-swe-agent[bot]
b5bbf878a2 Use null instead of empty strings to clear Sentry tags
Co-authored-by: egelhaus <156946629+egelhaus@users.noreply.github.com>
2025-11-24 21:46:37 +00:00
copilot-swe-agent[bot]
a66a053ca9 Improve Sentry user context implementation with error handling and documentation
Co-authored-by: egelhaus <156946629+egelhaus@users.noreply.github.com>
2025-11-24 21:41:05 +00:00
copilot-swe-agent[bot]
d8e6759bbf Initial plan 2025-11-24 21:33:22 +00:00
Enno Gelhaus
a21087ea25
Merge branch 'main' into sentry-user 2025-11-24 18:52:09 +01:00
Enno Gelhaus
4a21ebae66
Update sentry.user.context.ts 2025-08-01 15:00:52 +02:00
Enno Gelhaus
d8f790b2e4
Update user.context.tsx 2025-08-01 14:59:53 +02:00
Enno Gelhaus
894dfdbd34
Update user.context.tsx 2025-08-01 14:56:53 +02:00
Enno Gelhaus
47e1795503
Merge branch 'main' into sentry-user 2025-08-01 11:29:50 +02:00
Enno Gelhaus
8932ed712f
Update libraries/react-shared-libraries/src/sentry/sentry.user.context.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-31 22:12:30 +02:00
Enno Gelhaus
9364012118
Make "admin" user contexr optional
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-31 22:11:40 +02:00
Enno Gelhaus
3dd49e2b77
Update libraries/nestjs-libraries/src/sentry/sentry.user.context.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-31 22:11:25 +02:00
Enno Gelhaus
a997bec147
Update libraries/react-shared-libraries/src/sentry/sentry.user.context.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-31 22:10:40 +02:00
Enno Gelhaus
cf03b9d0d6 feat/sentry user implementation 2025-07-31 22:02:10 +02:00
8 changed files with 235 additions and 2 deletions

View file

@ -86,6 +86,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=""
# Payment settings
FEE_AMOUNT=0.05
STRIPE_PUBLISHABLE_KEY=""

View file

@ -31,6 +31,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')
@ -238,6 +239,9 @@ export class UsersController {
@Post('/logout')
logout(@Res({ passthrough: true }) response: Response) {
// Clear Sentry user context on logout
clearSentryUserContext();
response.header('logout', 'true');
response.cookie('auth', '', {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),

View file

@ -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';
import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service';
export const removeAuth = (res: Response) => {
@ -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();

View file

@ -6,6 +6,8 @@ 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 = () => {
export const LogoutComponent: FC<{ isIcon?: boolean }> = ({ isIcon }) => {
const fetch = useFetch();
const { isGeneral, isSecured } = useVariables();
@ -21,6 +23,9 @@ export const LogoutComponent: FC<{ isIcon?: boolean }> = ({ isIcon }) => {
t('yes_logout', 'Yes logout')
)
) {
// Clear Sentry user context on logout
clearSentryUserContext();
if (!isSecured) {
setCookie('auth', '', -10);
} else {

View file

@ -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 & {
@ -37,6 +38,22 @@ 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,
});
} else {
setSentryUserContext(null);
}
}, [user]);
return <UserContext.Provider value={values}>{children}</UserContext.Provider>;
};
export const useUser = () => useContext(UserContext);

View file

@ -0,0 +1,63 @@
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;
}
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
});
// 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
}
};
/**
* 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;
}
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
}
};

View file

@ -0,0 +1,54 @@
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.
*
* 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 {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
// 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();
}
}

View file

@ -0,0 +1,75 @@
'use client';
import * as Sentry from '@sentry/nextjs';
interface UserInfo {
id: string;
email: string;
orgId?: string;
role?: string;
tier?: string;
}
/**
* 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;
}
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
});
// 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
}
};
/**
* 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;
}
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
}
};