Initial commit
This commit is contained in:
439
skills/sentry-and-otel-setup/references/sentry-best-practices.md
Normal file
439
skills/sentry-and-otel-setup/references/sentry-best-practices.md
Normal file
@@ -0,0 +1,439 @@
|
||||
# Sentry Best Practices
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
### 1. Structured Error Handling
|
||||
|
||||
Use consistent error handling across the application:
|
||||
|
||||
```typescript
|
||||
import { logger } from '@/lib/logger';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
export async function performOperation() {
|
||||
try {
|
||||
// Operation logic
|
||||
const result = await someOperation();
|
||||
logger.info('Operation succeeded', { result });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
// Log with context
|
||||
logger.error('Operation failed', {
|
||||
error: error as Error,
|
||||
operation: 'performOperation',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Re-throw for caller to handle
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. User-Facing vs Internal Errors
|
||||
|
||||
Distinguish between errors shown to users and internal errors:
|
||||
|
||||
```typescript
|
||||
class UserFacingError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public userMessage: string,
|
||||
public statusCode: number = 400
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'UserFacingError';
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
try {
|
||||
await createEntity(data);
|
||||
} catch (error) {
|
||||
if (error instanceof UserFacingError) {
|
||||
// Show to user
|
||||
return { error: error.userMessage };
|
||||
}
|
||||
|
||||
// Internal error - log to Sentry, show generic message
|
||||
logger.error('Entity creation failed', { error });
|
||||
return { error: 'An unexpected error occurred' };
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Add Context to Errors
|
||||
|
||||
Enrich errors with relevant context:
|
||||
|
||||
```typescript
|
||||
export async function updateEntity(entityId: string, data: unknown) {
|
||||
// Set context for all errors in this scope
|
||||
Sentry.setContext('entity', {
|
||||
id: entityId,
|
||||
type: 'character',
|
||||
data,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await prisma.entity.update({
|
||||
where: { id: entityId },
|
||||
data,
|
||||
});
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
// Error automatically includes entity context
|
||||
logger.error('Entity update failed', { error, entityId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
### 1. Custom Transactions
|
||||
|
||||
Track important operations:
|
||||
|
||||
```typescript
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
export async function generateWorld(params: WorldParams) {
|
||||
const transaction = Sentry.startTransaction({
|
||||
name: 'generate-world',
|
||||
op: 'task',
|
||||
tags: {
|
||||
worldType: params.type,
|
||||
complexity: params.complexity,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Track each step
|
||||
const terrainSpan = transaction.startChild({
|
||||
op: 'generate-terrain',
|
||||
description: 'Generate terrain data',
|
||||
});
|
||||
const terrain = await generateTerrain(params);
|
||||
terrainSpan.finish();
|
||||
|
||||
const biomesSpan = transaction.startChild({
|
||||
op: 'generate-biomes',
|
||||
description: 'Generate biome data',
|
||||
});
|
||||
const biomes = await generateBiomes(terrain);
|
||||
biomesSpan.finish();
|
||||
|
||||
transaction.setStatus('ok');
|
||||
return { terrain, biomes };
|
||||
|
||||
} catch (error) {
|
||||
transaction.setStatus('internal_error');
|
||||
throw error;
|
||||
|
||||
} finally {
|
||||
transaction.finish();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Database Query Monitoring
|
||||
|
||||
Track slow queries:
|
||||
|
||||
```typescript
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
// Prisma automatically creates spans for queries when OTel is configured
|
||||
const users = await prisma.user.findMany({
|
||||
where: { active: true },
|
||||
include: { profile: true },
|
||||
});
|
||||
|
||||
// Shows up in Sentry as:
|
||||
// - db.query
|
||||
// - duration
|
||||
// - query details
|
||||
```
|
||||
|
||||
### 3. API Call Monitoring
|
||||
|
||||
Track external API calls:
|
||||
|
||||
```typescript
|
||||
export async function fetchExternalData(url: string) {
|
||||
return await Sentry.startSpan(
|
||||
{
|
||||
name: 'external-api-call',
|
||||
op: 'http.client',
|
||||
attributes: {
|
||||
'http.url': url,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
const response = await fetch(url);
|
||||
|
||||
// Add response details to span
|
||||
Sentry.getCurrentScope().setContext('http', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Quota Management
|
||||
|
||||
### 1. Sample Rates
|
||||
|
||||
Adjust sampling to control quota usage:
|
||||
|
||||
```typescript
|
||||
Sentry.init({
|
||||
// Errors
|
||||
sampleRate: 1.0, // 100% of errors
|
||||
|
||||
// Performance monitoring
|
||||
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
|
||||
|
||||
// Session replay
|
||||
replaysSessionSampleRate: 0.05, // 5% of normal sessions
|
||||
replaysOnErrorSampleRate: 1.0, // 100% of error sessions
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Filter Out Noise
|
||||
|
||||
Prevent known non-issues from consuming quota:
|
||||
|
||||
```typescript
|
||||
Sentry.init({
|
||||
beforeSend(event, hint) {
|
||||
const error = hint.originalException;
|
||||
|
||||
// Filter by error message
|
||||
if (error && typeof error === 'object' && 'message' in error) {
|
||||
const message = String(error.message);
|
||||
|
||||
const ignoredPatterns = [
|
||||
'ResizeObserver loop',
|
||||
'Non-Error promise rejection',
|
||||
'Loading chunk',
|
||||
'Script error.',
|
||||
];
|
||||
|
||||
if (ignoredPatterns.some((pattern) => message.includes(pattern))) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by URL
|
||||
if (event.request?.url?.includes('localhost')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return event;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Inbound Filters
|
||||
|
||||
Configure in Sentry dashboard:
|
||||
- Settings > Inbound Filters
|
||||
- Filter by:
|
||||
- Browser version
|
||||
- Error message
|
||||
- Release version
|
||||
- IP address
|
||||
|
||||
## User Context
|
||||
|
||||
### 1. Set User on Authentication
|
||||
|
||||
```typescript
|
||||
// In middleware or auth utility
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { getCurrentUser } from '@/lib/auth/utils';
|
||||
|
||||
export async function setUserContext() {
|
||||
const user = await getCurrentUser();
|
||||
|
||||
if (user) {
|
||||
Sentry.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Clear User on Logout
|
||||
|
||||
```typescript
|
||||
export async function logout() {
|
||||
// Clear Sentry context
|
||||
Sentry.setUser(null);
|
||||
logger.clearUser();
|
||||
|
||||
// Logout logic
|
||||
await supabase.auth.signOut();
|
||||
}
|
||||
```
|
||||
|
||||
## Breadcrumbs
|
||||
|
||||
Track user journey:
|
||||
|
||||
```typescript
|
||||
// Navigation
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'navigation',
|
||||
message: 'User navigated to world editor',
|
||||
level: 'info',
|
||||
data: { worldId: 'world-123' },
|
||||
});
|
||||
|
||||
// User actions
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'user.action',
|
||||
message: 'Created new character',
|
||||
level: 'info',
|
||||
data: {
|
||||
characterName: 'Hero',
|
||||
worldId: 'world-123',
|
||||
},
|
||||
});
|
||||
|
||||
// Data changes
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'data',
|
||||
message: 'Updated world settings',
|
||||
level: 'info',
|
||||
data: {
|
||||
worldId: 'world-123',
|
||||
settings: { theme: 'dark' },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Source Maps
|
||||
|
||||
### 1. Verify Upload
|
||||
|
||||
Check source maps are uploaded:
|
||||
|
||||
```bash
|
||||
# Build with source maps
|
||||
npm run build
|
||||
|
||||
# Verify in Sentry dashboard
|
||||
# Settings > Source Maps > [Release]
|
||||
```
|
||||
|
||||
### 2. Configure Properly
|
||||
|
||||
```typescript
|
||||
// next.config.js
|
||||
const { withSentryConfig } = require('@sentry/nextjs');
|
||||
|
||||
module.exports = withSentryConfig(
|
||||
nextConfig,
|
||||
{
|
||||
// Sentry webpack plugin options
|
||||
silent: true,
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
},
|
||||
{
|
||||
// Sentry SDK options
|
||||
hideSourceMaps: true, // Don't expose source maps to public
|
||||
widenClientFileUpload: true, // Upload more files for better stack traces
|
||||
disableLogger: true, // Reduce noise in build logs
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Troubleshooting
|
||||
|
||||
**Source maps not working:**
|
||||
- Verify `SENTRY_AUTH_TOKEN` is set
|
||||
- Check build logs for upload errors
|
||||
- Ensure release version matches between app and Sentry
|
||||
|
||||
## Alerting
|
||||
|
||||
### 1. Configure Alert Rules
|
||||
|
||||
In Sentry dashboard:
|
||||
- Alerts > Create Alert Rule
|
||||
- Set conditions (frequency, affected users)
|
||||
- Choose notification channels (email, Slack, PagerDuty)
|
||||
|
||||
**Recommended alerts:**
|
||||
- High error rate (>10 errors/minute)
|
||||
- New error type
|
||||
- Regression (error in new release)
|
||||
- Performance degradation
|
||||
|
||||
### 2. Alert Fatigue
|
||||
|
||||
Avoid alert fatigue:
|
||||
- Use appropriate thresholds
|
||||
- Filter out noisy errors
|
||||
- Set up different alerts for different severity
|
||||
- Use digest emails instead of immediate notifications
|
||||
|
||||
## Release Tracking
|
||||
|
||||
Associate errors with releases:
|
||||
|
||||
```typescript
|
||||
Sentry.init({
|
||||
release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || 'dev',
|
||||
environment: process.env.NODE_ENV,
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Track which release introduced errors
|
||||
- Compare error rates between releases
|
||||
- Verify if deploy fixed issues
|
||||
|
||||
## Testing
|
||||
|
||||
### 1. Test Error Capture
|
||||
|
||||
```typescript
|
||||
// Test error handling
|
||||
export async function testSentry() {
|
||||
try {
|
||||
throw new Error('Test error from Sentry setup');
|
||||
} catch (error) {
|
||||
logger.error('Testing Sentry integration', { error });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Verify in Dashboard
|
||||
|
||||
After testing:
|
||||
1. Go to Sentry dashboard
|
||||
2. Check Issues for test error
|
||||
3. Verify context, breadcrumbs, and user info
|
||||
4. Check source maps resolve correctly
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Not filtering development errors** - Always disable Sentry in development or filter out
|
||||
2. **Missing source maps** - Stack traces are unreadable without them
|
||||
3. **Not setting user context** - Makes debugging user-specific issues hard
|
||||
4. **Over-sampling in production** - Wastes quota and money
|
||||
5. **Ignoring performance monitoring** - Only tracking errors misses slow operations
|
||||
6. **Not reviewing regularly** - Set aside time weekly to review Sentry issues
|
||||
Reference in New Issue
Block a user