Add user and org context to Sentry (#92)

* Add user and org context to Sentry

* PR feedback
This commit is contained in:
Matt Rubens 2025-06-10 16:26:50 -04:00 committed by GitHub
parent 6922e4b3ac
commit 4d40eb6e2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 125 additions and 1 deletions

View file

@ -1,6 +1,10 @@
import { redirect } from 'next/navigation';
import { auth } from '@clerk/nextjs/server';
import {
setSentryUserContext,
setSentryOrganizationContext,
} from '@/lib/server/sentry-context';
import { NavbarHeader, NavbarMenu, Section } from '@/components/layout';
import { Usage } from './usage/Usage';
@ -15,6 +19,19 @@ export default async function AuthenticatedLayout({
redirect('/select-org');
}
// Set enhanced Sentry context for authenticated users
if (userId) {
setSentryUserContext({
id: userId,
orgId,
orgRole,
});
if (orgId) {
setSentryOrganizationContext(orgId, orgRole);
}
}
// Members get access to usage page only, filtered to their own data
if (orgRole === 'org:member' || (orgRole && orgRole !== 'org:admin')) {
return (

View file

@ -6,6 +6,7 @@ import { auth } from '@clerk/nextjs/server';
import { getClerkLocale } from '@/i18n/locale';
import { syncAuth } from '@/actions/sync';
import { setSentryUserContext } from '@/lib/server/sentry-context';
import { Toaster } from '@/components/ui';
import {
ThemeProvider,
@ -52,7 +53,17 @@ export default async function RootLayout({
const locale = await getLocale();
setRequestLocale(locale);
await syncAuth(await auth());
const authData = await auth();
await syncAuth(authData);
// Set Sentry user context for server-side error tracking
if (authData.userId) {
setSentryUserContext({
id: authData.userId,
orgId: authData.orgId,
orgRole: authData.orgRole,
});
}
return (
<html lang={locale} suppressHydrationWarning>

View file

@ -4,6 +4,7 @@ import { ClerkProvider } from '@clerk/nextjs';
import { dark } from '@clerk/themes';
import { getClerkLocale } from '@/i18n/locale';
import { SentryUserContext } from './SentryUserContext';
import { useTheme } from 'next-themes';
type Localization = ReturnType<typeof getClerkLocale>;
@ -24,6 +25,7 @@ export const AuthProvider = ({
localization={localization}
afterSignOutUrl={'/sign-in'}
>
<SentryUserContext />
{children}
</ClerkProvider>
);

View file

@ -0,0 +1,53 @@
'use client';
import { useEffect } from 'react';
import { useUser, useOrganization } from '@clerk/nextjs';
import * as Sentry from '@sentry/nextjs';
/**
* Client-side component that sets Sentry user context based on Clerk authentication
*/
export function SentryUserContext() {
const { user, isLoaded: userLoaded } = useUser();
const { organization, isLoaded: orgLoaded } = useOrganization();
useEffect(() => {
// Wait for both user and organization data to be loaded
if (!userLoaded || !orgLoaded) {
return;
}
if (user) {
// Set user context with organization information if available
Sentry.setUser({
id: user.id,
...(organization && { orgId: organization.id }),
...(organization &&
user.organizationMemberships?.[0]?.role && {
orgRole: user.organizationMemberships[0].role,
}),
});
// Set organization context if available
if (organization) {
Sentry.setContext('organization', {
id: organization.id,
name: organization.name,
slug: organization.slug,
...(user.organizationMemberships?.[0]?.role && {
role: user.organizationMemberships[0].role,
}),
});
} else {
Sentry.setContext('organization', null);
}
} else {
// Clear user context when not authenticated
Sentry.setUser(null);
Sentry.setContext('organization', null);
}
}, [user, organization, userLoaded, orgLoaded]);
// This component doesn't render anything
return null;
}

View file

@ -11,3 +11,4 @@ export { DataTable } from './DataTable';
export { ThemeProvider } from './ThemeProvider';
export { AuthProvider } from './AuthProvider';
export { ReactQueryProvider } from './ReactQueryProvider';
export { SentryUserContext } from './SentryUserContext';

View file

@ -1,6 +1,7 @@
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
// Note: User context (userId, orgId, orgRole) is set via SentryUserContext component
import * as Sentry from '@sentry/nextjs';
// import * as Spotlight from '@spotlightjs/spotlight';

View file

@ -22,6 +22,7 @@ export async function register() {
if (process.env.NEXT_RUNTIME === 'edge') {
// Edge Sentry configuration
// Note: User context (userId, orgId, orgRole) is set in layouts via setSentryUserContext()
Sentry.init({
// Sentry DSN
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,

View file

@ -0,0 +1,38 @@
import * as Sentry from '@sentry/nextjs';
export interface SentryUserContext {
id: string;
orgId?: string | null;
orgRole?: string | null;
}
/**
* Sets the Sentry user context for server-side error tracking
*/
export function setSentryUserContext(context: SentryUserContext) {
Sentry.setUser({
id: context.id,
...(context.orgId && { orgId: context.orgId }),
...(context.orgRole && { orgRole: context.orgRole }),
});
}
/**
* Clears the Sentry user context
*/
export function clearSentryUserContext() {
Sentry.setUser(null);
}
/**
* Sets Sentry context with additional organization information
*/
export function setSentryOrganizationContext(
orgId: string,
orgRole?: string | null,
) {
Sentry.setContext('organization', {
id: orgId,
...(orgRole && { role: orgRole }),
});
}