diff --git a/src/app/(authenticated)/layout.tsx b/src/app/(authenticated)/layout.tsx index abaa4b5867..866ff54c1c 100644 --- a/src/app/(authenticated)/layout.tsx +++ b/src/app/(authenticated)/layout.tsx @@ -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 ( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 56d4d4007e..d8d89a7d45 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( diff --git a/src/components/layout/AuthProvider.tsx b/src/components/layout/AuthProvider.tsx index f34f6da112..0ebd59ca20 100644 --- a/src/components/layout/AuthProvider.tsx +++ b/src/components/layout/AuthProvider.tsx @@ -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; @@ -24,6 +25,7 @@ export const AuthProvider = ({ localization={localization} afterSignOutUrl={'/sign-in'} > + {children} ); diff --git a/src/components/layout/SentryUserContext.tsx b/src/components/layout/SentryUserContext.tsx new file mode 100644 index 0000000000..35d445f058 --- /dev/null +++ b/src/components/layout/SentryUserContext.tsx @@ -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; +} diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index ffe95f146b..cd46bdb95f 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -11,3 +11,4 @@ export { DataTable } from './DataTable'; export { ThemeProvider } from './ThemeProvider'; export { AuthProvider } from './AuthProvider'; export { ReactQueryProvider } from './ReactQueryProvider'; +export { SentryUserContext } from './SentryUserContext'; diff --git a/src/instrumentation-client.ts b/src/instrumentation-client.ts index 296016dcb3..c9e1e3096a 100644 --- a/src/instrumentation-client.ts +++ b/src/instrumentation-client.ts @@ -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'; diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 30530683d7..99a2b81eab 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -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, diff --git a/src/lib/server/sentry-context.ts b/src/lib/server/sentry-context.ts new file mode 100644 index 0000000000..de30ba5f01 --- /dev/null +++ b/src/lib/server/sentry-context.ts @@ -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 }), + }); +}