Skip to content
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
6 changes: 5 additions & 1 deletion packages/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { PlanProvider } from "@/features/entitlements/planProvider";
import { getEntitlements } from "@/lib/entitlements";
import { IdentityProvidersProvider } from "@/features/auth/identityProvidersProvider";
import { getIdentityProviderMetadata } from "@/lib/identityProviders";
import { SentryUserProvider } from "./sentryUserProvider";

export const metadata: Metadata = {
metadataBase: env.AUTH_URL ? new URL(env.AUTH_URL) : undefined,
Expand Down Expand Up @@ -74,6 +75,9 @@ export default async function RootLayout({
<body>
<Toaster />
<SessionProvider>
<SentryUserProvider
isPiiEnabled={env.SOURCEBOT_TELEMETRY_PII_COLLECTION_ENABLED === 'true'}
/>
<PlanProvider entitlements={entitlements}>
<IdentityProvidersProvider providers={identityProviders}>
<PostHogProvider
Expand Down Expand Up @@ -106,4 +110,4 @@ export default async function RootLayout({
</body>
</html>
);
}
}
52 changes: 52 additions & 0 deletions packages/web/src/app/sentryUserProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { cleanup, render } from '@testing-library/react';
import { afterEach, describe, expect, test, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
setSentryUser: vi.fn(),
useSession: vi.fn(),
}));

vi.mock('@/lib/sentryUser', () => ({
setSentryUser: mocks.setSentryUser,
}));

vi.mock('next-auth/react', () => ({
useSession: mocks.useSession,
}));

const { SentryUserProvider } = await import('./sentryUserProvider');

afterEach(() => {
cleanup();
vi.clearAllMocks();
});

describe('SentryUserProvider', () => {
test('waits for the session before changing the Sentry user', () => {
mocks.useSession.mockReturnValue({ data: undefined, status: 'loading' });

render(<SentryUserProvider isPiiEnabled={false} />);

expect(mocks.setSentryUser).not.toHaveBeenCalled();
});

test('sets and clears the Sentry user as authentication changes', () => {
const user = {
id: 'user-1',
email: 'user@example.com',
name: 'Example User',
};
mocks.useSession.mockReturnValue({
data: { user },
status: 'authenticated',
});

const { rerender } = render(<SentryUserProvider isPiiEnabled={true} />);
expect(mocks.setSentryUser).toHaveBeenLastCalledWith(user, true);

mocks.useSession.mockReturnValue({ data: null, status: 'unauthenticated' });
rerender(<SentryUserProvider isPiiEnabled={true} />);

expect(mocks.setSentryUser).toHaveBeenLastCalledWith(null, true);
});
});
23 changes: 23 additions & 0 deletions packages/web/src/app/sentryUserProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import { setSentryUser } from '@/lib/sentryUser';
import { useSession } from 'next-auth/react';
import { useEffect } from 'react';

interface SentryUserProviderProps {
isPiiEnabled: boolean;
}

export function SentryUserProvider({ isPiiEnabled }: SentryUserProviderProps) {
const { data: session, status } = useSession();

useEffect(() => {
if (status === 'loading') {
return;
}

setSentryUser(session?.user ?? null, isPiiEnabled);
}, [isPiiEnabled, session, status]);

return null;
}
8 changes: 7 additions & 1 deletion packages/web/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { getAnonymousId } from '@/lib/anonymousId';
import { captureEvent } from '@/lib/posthog';
import { isEmailCodeLoginEnabled, isCredentialsLoginEnabled } from '@sourcebot/shared'
import { onCreateUser } from './features/membership/onCreateUser';
import { setSentryUser } from './lib/sentryUser';

export const runtime = 'nodejs';

Expand Down Expand Up @@ -452,7 +453,12 @@ export const { handlers, signIn, signOut } = nextAuthResult;
* without re-running the upstream resolver.
*/
export const auth = cache(async (): Promise<Session | null> => {
return nextAuthResult.auth();
const session = await nextAuthResult.auth();
setSentryUser(
session?.user ?? null,
env.SOURCEBOT_TELEMETRY_PII_COLLECTION_ENABLED === 'true',
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

API auth skips Sentry user

Medium Severity

Server-side setSentryUser runs only inside the NextAuth auth() wrapper. getAuthenticatedUser always calls auth() first, so API key and OAuth Bearer requests clear the Sentry user to null and never re-associate after the real principal is resolved. Server errors on those paths (for example MCP and chat via withOptionalAuth + sew) stay anonymous in Sentry.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 49d0429. Configure here.

return session;
});

/**
Expand Down
45 changes: 45 additions & 0 deletions packages/web/src/lib/sentryUser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';

const setUser = vi.fn();

vi.mock('@sentry/nextjs', () => ({
setUser,
}));

const { setSentryUser } = await import('./sentryUser');

describe('setSentryUser', () => {
beforeEach(() => {
setUser.mockClear();
});

test('associates errors with the user id without PII by default', () => {
setSentryUser({
id: 'user-1',
email: 'user@example.com',
name: 'Example User',
}, false);

expect(setUser).toHaveBeenCalledWith({ id: 'user-1' });
});

test('includes user details when PII collection is enabled', () => {
setSentryUser({
id: 'user-1',
email: 'user@example.com',
name: 'Example User',
}, true);

expect(setUser).toHaveBeenCalledWith({
id: 'user-1',
email: 'user@example.com',
username: 'Example User',
});
});

test('clears the user for unauthenticated requests', () => {
setSentryUser(null, true);

expect(setUser).toHaveBeenCalledWith(null);
});
});
22 changes: 22 additions & 0 deletions packages/web/src/lib/sentryUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as Sentry from "@sentry/nextjs";

type SentryUser = {
id: string;
email?: string | null;
name?: string | null;
};

export function setSentryUser(user: SentryUser | null, isPiiEnabled: boolean) {
if (!user) {
Sentry.setUser(null);
return;
}

Sentry.setUser({
id: user.id,
...(isPiiEnabled ? {
email: user.email ?? undefined,
username: user.name ?? undefined,
} : {}),
});
}
Loading