Skip to content

feat/sentry user implementation #910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions apps/backend/src/api/routes/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions apps/backend/src/services/auth/auth.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', '', {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/components/layout/logout.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 {
Expand Down
19 changes: 18 additions & 1 deletion apps/frontend/src/components/layout/user.context.tsx
Original file line number Diff line number Diff line change
@@ -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 & {
Expand Down Expand Up @@ -36,6 +37,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);
55 changes: 55 additions & 0 deletions libraries/nestjs-libraries/src/sentry/sentry.user.context.ts
Original file line number Diff line number Diff line change
@@ -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', null);
Sentry.setTag('user.provider', null);
Sentry.setTag('user.super_admin', null);
Comment on lines +52 to +54
Copy link
Preview

Copilot AI Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting tags to null may not properly clear them in Sentry. Consider using empty strings or calling Sentry.removeTag() if available, to ensure tags are properly cleared.

Suggested change
Sentry.setTag('user.activated', null);
Sentry.setTag('user.provider', null);
Sentry.setTag('user.super_admin', null);
Sentry.setTag('user.activated', '');
Sentry.setTag('user.provider', '');
Sentry.setTag('user.super_admin', '');

Copilot uses AI. Check for mistakes.

};
24 changes: 24 additions & 0 deletions libraries/nestjs-libraries/src/sentry/sentry.user.interceptor.ts
Original file line number Diff line number Diff line change
@@ -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<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();
}
}
67 changes: 67 additions & 0 deletions libraries/react-shared-libraries/src/sentry/sentry.user.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'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;
}

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);
}
};

/**
* 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.org_id', '');
Sentry.setTag('user.role', '');
Sentry.setTag('user.tier', '');
};