Skip to content
Draft
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
151 changes: 151 additions & 0 deletions packages/workshop-app/SENTRY_USER_CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Sentry User Context Integration

This document explains how to use the enhanced Sentry error reporting with user context information.

## Overview

The Sentry integration now automatically includes user information when capturing errors, providing better context for debugging and monitoring. User information is retrieved from:

1. **Authenticated users**: User ID, email, and name from the authentication system
2. **Anonymous users**: Client ID for tracking anonymous sessions
3. **Fallback**: No user context if neither is available

## How It Works

### Server-Side (Automatic)

User context is automatically included in server-side error reports through:

- **`handleError` function** in `entry.server.tsx` - automatically captures user context for loader/action errors
- **Database error handling** in `db.server.ts` - includes user context for database corruption errors

### Client-Side (Manual)

For client-side error reporting, use the provided utility functions:

```typescript
import { captureClientError } from '#app/utils/sentry.client'
import { useOptionalUser } from '#app/components/user'
import { useRequestInfo } from '#app/utils/request-info'

function MyComponent() {
const user = useOptionalUser()
const requestInfo = useRequestInfo()

const handleError = (error: Error) => {
// Capture error with user context
captureClientError(error, user, requestInfo.clientId)
}

// ... rest of component
}
```

## Available Functions

### `captureClientError(error: Error, user?: any, clientId?: string)`

Captures a client-side error with user context:

- `error`: The error to capture
- `user`: User object from `useOptionalUser()` (optional)
- `clientId`: Client ID from `useRequestInfo().clientId` (optional)

### `captureReactError(error: Error, errorInfo?: any)`

Captures React component errors (useful for error boundaries):

```typescript
import { captureReactError } from '#app/utils/sentry.client'

class ErrorBoundary extends React.Component {
componentDidCatch(error: Error, errorInfo: any) {
captureReactError(error, errorInfo)
}
}
```

## User Context Information

When user information is available, Sentry will include:

- **User ID**: Unique identifier for the user
- **Email**: User's email address
- **Username**: User's display name or email
- **IP Address**: Excluded for privacy reasons

When only client ID is available:

- **User ID**: `client-{clientId}` format
- **Username**: "Anonymous User"

## Privacy Considerations

- IP addresses are never captured
- User information is only included when explicitly provided
- Client IDs are anonymized with a "client-" prefix

## Example Usage

### In a React Component

```typescript
import { captureClientError } from '#app/utils/sentry.client'
import { useOptionalUser } from '#app/components/user'
import { useRequestInfo } from '#app/utils/request-info'

function MyComponent() {
const user = useOptionalUser()
const requestInfo = useRequestInfo()

const handleAsyncOperation = async () => {
try {
// ... async operation
} catch (error) {
// Capture error with full user context
captureClientError(error, user, requestInfo.clientId)
}
}

return <div>...</div>
}
```

### In an Error Boundary

```typescript
import { captureReactError } from '#app/utils/sentry.client'

class MyErrorBoundary extends React.Component {
componentDidCatch(error: Error, errorInfo: any) {
// React will provide error and errorInfo
captureReactError(error, errorInfo)
}

render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>
}

return this.props.children
}
}
```

## Benefits

1. **Better Error Context**: Errors include user information for easier debugging
2. **User Impact Tracking**: Identify which users are affected by specific errors
3. **Anonymous User Tracking**: Track errors for users who haven't authenticated
4. **Privacy Conscious**: No sensitive information like IP addresses are captured
5. **Automatic Integration**: Server-side errors automatically include user context
6. **Simple API**: Easy-to-use functions for client-side error reporting

## Troubleshooting

If user context is not being captured:

1. Ensure the user is properly authenticated
2. Check that `clientId` is available in `requestInfo`
3. Verify Sentry is properly initialized
4. Check browser console for any Sentry-related errors
33 changes: 30 additions & 3 deletions packages/workshop-app/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PassThrough } from 'stream'
import { getAuthInfo, getClientId } from '@epic-web/workshop-utils/db.server'
import { createReadableStreamFromReadable } from '@react-router/node'
// Dynamic import of Sentry with error handling
const Sentry = await import('@sentry/react-router').catch((error) => {
Expand All @@ -22,13 +23,39 @@ import {
export const streamTimeout = 60000
const ABORT_DELAY = streamTimeout + 1000

export function handleError(
export async function handleError(
error: unknown,
{ request }: LoaderFunctionArgs | ActionFunctionArgs,
): void {
): Promise<void> {
if (request.signal.aborted) return
if (ENV.EPICSHOP_IS_PUBLISHED) {
Sentry?.captureException(error)
try {
// Get user information for Sentry context
const authInfo = await getAuthInfo()
const clientId = await getClientId()

if (Sentry) {
if (authInfo) {
Sentry.setUser({
id: authInfo.id,
email: authInfo.email,
username: authInfo.name || authInfo.email,
ip_address: undefined, // Don't capture IP for privacy
})
} else if (clientId) {
Sentry.setUser({
id: `client-${clientId}`,
username: 'Anonymous User',
})
}

Sentry.captureException(error)
}
} catch (sentryError) {
console.error('Failed to capture error in Sentry:', sentryError)
// Fallback to basic error capture
Sentry?.captureException(error)
}
}
}

Expand Down
11 changes: 7 additions & 4 deletions packages/workshop-app/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getPreferences,
readOnboardingData,
getMutedNotifications,
getClientId,
} from '@epic-web/workshop-utils/db.server'
import { getEnv } from '@epic-web/workshop-utils/env.server'
import {
Expand Down Expand Up @@ -128,6 +129,7 @@ export async function loader({ request }: Route.LoaderArgs) {
repoUpdates: checkForUpdatesCached(),
unmutedNotifications: getUnmutedNotifications(),
exerciseChanges: checkForExerciseChanges(),
clientId: getClientId(),
})

const presentUsers = await getPresentUsers({
Expand Down Expand Up @@ -171,6 +173,7 @@ export async function loader({ request }: Route.LoaderArgs) {
session: { theme },
separator: path.sep,
online: await isOnlinePromise,
clientId: asyncStuff.clientId,
},
toast,
confettiId,
Expand Down Expand Up @@ -230,13 +233,13 @@ function Document({
function App() {
const data = useLoaderData<typeof loader>()
const navigation = useNavigation()
const showSpinner = useSpinDelay(navigation.state !== 'idle', {
delay: 400,
const isNavigating = useSpinDelay(navigation.state !== 'idle', {
delay: 500,
minDuration: 200,
})
const theme = useTheme()
const altDown = useAltDown()

const theme = useTheme()
return (
<Document
style={
Expand All @@ -247,7 +250,7 @@ function App() {
className={cn(
'antialiased h-screen-safe',
theme,
{ 'cursor-progress': showSpinner },
{ 'cursor-progress': isNavigating },
altDown ? 'alt-down' : null,
)}
env={data.ENV}
Expand Down
29 changes: 29 additions & 0 deletions packages/workshop-app/app/utils/monitoring.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,32 @@ export function init() {
},
})
}

// Helper function to capture errors with user context
export function captureExceptionWithUser(
error: Error,
user?: any,
clientId?: string,
) {
if (!Sentry) return

const userContext = user
? {
id: user.id,
email: user.email,
username: user.name || user.email,
ip_address: undefined, // Don't capture IP for privacy
}
: clientId
? {
id: `client-${clientId}`,
username: 'Anonymous User',
}
: null

if (userContext) {
Sentry.setUser(userContext)
}

Sentry.captureException(error)
}
24 changes: 24 additions & 0 deletions packages/workshop-app/app/utils/sentry.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { captureExceptionWithUser } from './monitoring.client'

// Helper function to capture client-side errors with user context
export function captureClientError(
error: Error,
user?: any,
clientId?: string,
) {
captureExceptionWithUser(error, user, clientId)
}

// Helper function to capture client-side errors from React components
export function captureReactError(error: Error, _errorInfo?: any) {
// Try to get user context from the current route data
try {
// This will be called from React components where we have access to hooks
// The user and clientId should be passed in when calling this function
captureExceptionWithUser(error, undefined, undefined)
} catch (sentryError) {
console.error('Failed to capture error in Sentry:', sentryError)
// Fallback to basic error capture
captureExceptionWithUser(error)
}
}
19 changes: 19 additions & 0 deletions packages/workshop-utils/src/db.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,25 @@ async function readDb() {
if (process.env.SENTRY_DSN && process.env.EPICSHOP_IS_PUBLISHED) {
try {
const Sentry = await import('@sentry/react-router')

// Get user information for Sentry context
const authInfo = await getAuthInfo()
const clientId = await getClientId()

if (authInfo) {
Sentry.setUser({
id: authInfo.id,
email: authInfo.email,
username: authInfo.name || authInfo.email,
ip_address: undefined, // Don't capture IP for privacy
})
} else if (clientId) {
Sentry.setUser({
id: `client-${clientId}`,
username: 'Anonymous User',
})
}

Sentry.captureException(error, {
tags: {
error_type: 'corrupted_database_file',
Expand Down
Loading