Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "sentry-and-otel-setup",
|
||||
"description": "This skill should be used when adding error tracking and performance monitoring with Sentry and OpenTelemetry tracing to Next.js applications. Apply when setting up error monitoring, configuring tracing for Server Actions and routes, implementing logging wrappers, adding performance instrumentation, or establishing observability for debugging production issues.",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Hope Overture",
|
||||
"email": "support@worldbuilding-app-skills.dev"
|
||||
},
|
||||
"skills": [
|
||||
"./skills"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# sentry-and-otel-setup
|
||||
|
||||
This skill should be used when adding error tracking and performance monitoring with Sentry and OpenTelemetry tracing to Next.js applications. Apply when setting up error monitoring, configuring tracing for Server Actions and routes, implementing logging wrappers, adding performance instrumentation, or establishing observability for debugging production issues.
|
||||
81
plugin.lock.json
Normal file
81
plugin.lock.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:hopeoverture/worldbuilding-app-skills:plugins/sentry-and-otel-setup",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "bb9d6c73e4d4f9a3524bdebba9d39880dc641b10",
|
||||
"treeHash": "6bf1280ebbe2ac6fffe2f42927e8d774f40015bbd2a4a2d511c4599dd5eebf3d",
|
||||
"generatedAt": "2025-11-28T10:17:32.904133Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "sentry-and-otel-setup",
|
||||
"description": "This skill should be used when adding error tracking and performance monitoring with Sentry and OpenTelemetry tracing to Next.js applications. Apply when setting up error monitoring, configuring tracing for Server Actions and routes, implementing logging wrappers, adding performance instrumentation, or establishing observability for debugging production issues.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "ec0f2ff53bd491f2609820974de33eb0f29fe8299005e391d43e1e8e55d7a3bc"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "55623282ba58d99393247c3f4e253af1f901ecebf8a7ce03b8cc2ddfb9293cfa"
|
||||
},
|
||||
{
|
||||
"path": "skills/sentry-and-otel-setup/SKILL.md",
|
||||
"sha256": "e4c7683cc234874369e452452f94d6572a7aee0718fcf7649b9e3aee9b06be3e"
|
||||
},
|
||||
{
|
||||
"path": "skills/sentry-and-otel-setup/references/otel-integration.md",
|
||||
"sha256": "5502b6a769bedc96243453c6b730dc96d4b7169ab3a77671b935d6ef471dfbfb"
|
||||
},
|
||||
{
|
||||
"path": "skills/sentry-and-otel-setup/references/sentry-best-practices.md",
|
||||
"sha256": "2b7028e418f335fdba46e72836d717546639e9e98a0536463f8ff6420a6f29e1"
|
||||
},
|
||||
{
|
||||
"path": "skills/sentry-and-otel-setup/assets/error-boundary.tsx",
|
||||
"sha256": "7cd8d3284d3f40eafad77ba150d8f7fa66f61d050e231b9a6d244e1eafdfc467"
|
||||
},
|
||||
{
|
||||
"path": "skills/sentry-and-otel-setup/assets/sentry-server-config.ts",
|
||||
"sha256": "77d0f5041e142b4a5cb1dbe6f1786e85ee6a706e959369fe4145fea77b6d8d81"
|
||||
},
|
||||
{
|
||||
"path": "skills/sentry-and-otel-setup/assets/logger.ts",
|
||||
"sha256": "a823cd25ac212c95c9c8ce7c1e741c2a7397dafcb8f211681ac2594504e8a7cf"
|
||||
},
|
||||
{
|
||||
"path": "skills/sentry-and-otel-setup/assets/sentry-client-config.ts",
|
||||
"sha256": "0604e7afb0d11a24ff93a70de60ce401ccd864b2868d8a34c903de35ab9ac770"
|
||||
},
|
||||
{
|
||||
"path": "skills/sentry-and-otel-setup/assets/error-page.tsx",
|
||||
"sha256": "8acfbb7e64fe0a94e452cac4bdae59e9256dd831bdaad979bbcc88810b781f6f"
|
||||
},
|
||||
{
|
||||
"path": "skills/sentry-and-otel-setup/assets/instrumentation.ts",
|
||||
"sha256": "eaeb2f76eb61c89a8d32aa44bb48a651512c7767d7a45d37d8bcd62bf0ce2d9d"
|
||||
},
|
||||
{
|
||||
"path": "skills/sentry-and-otel-setup/assets/global-error.tsx",
|
||||
"sha256": "739f7c9064b3ab421c6646e80390d0bd23561458f438cd6e2f22a6b7eacd52c6"
|
||||
}
|
||||
],
|
||||
"dirSha256": "6bf1280ebbe2ac6fffe2f42927e8d774f40015bbd2a4a2d511c4599dd5eebf3d"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
383
skills/sentry-and-otel-setup/SKILL.md
Normal file
383
skills/sentry-and-otel-setup/SKILL.md
Normal file
@@ -0,0 +1,383 @@
|
||||
---
|
||||
name: sentry-and-otel-setup
|
||||
description: This skill should be used when adding error tracking and performance monitoring with Sentry and OpenTelemetry tracing to Next.js applications. Apply when setting up error monitoring, configuring tracing for Server Actions and routes, implementing logging wrappers, adding performance instrumentation, or establishing observability for debugging production issues.
|
||||
---
|
||||
|
||||
# Sentry and OpenTelemetry Setup
|
||||
|
||||
## Overview
|
||||
|
||||
Configure comprehensive error tracking and performance monitoring using Sentry with OpenTelemetry (OTel) instrumentation for Next.js applications, including automatic error capture, distributed tracing, and custom logging.
|
||||
|
||||
## Installation and Configuration
|
||||
|
||||
### 1. Install Sentry
|
||||
|
||||
Install Sentry Next.js SDK:
|
||||
|
||||
```bash
|
||||
npm install @sentry/nextjs
|
||||
```
|
||||
|
||||
Run Sentry wizard for automatic configuration:
|
||||
|
||||
```bash
|
||||
npx @sentry/wizard@latest -i nextjs
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `sentry.client.config.ts` - Client-side configuration
|
||||
- `sentry.server.config.ts` - Server-side configuration
|
||||
- `sentry.edge.config.ts` - Edge runtime configuration
|
||||
- `instrumentation.ts` - OpenTelemetry setup
|
||||
- Updates `next.config.js` with Sentry webpack plugin
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
|
||||
Add Sentry credentials to `.env.local`:
|
||||
|
||||
```env
|
||||
SENTRY_DSN=https://your-dsn@sentry.io/project-id
|
||||
SENTRY_ORG=your-org
|
||||
SENTRY_PROJECT=your-project
|
||||
NEXT_PUBLIC_SENTRY_DSN=https://your-dsn@sentry.io/project-id
|
||||
```
|
||||
|
||||
Get DSN from Sentry dashboard: Settings > Projects > [Your Project] > Client Keys (DSN)
|
||||
|
||||
**For production**, add these to deployment environment variables.
|
||||
|
||||
### 3. Update Sentry Configurations
|
||||
|
||||
Customize `sentry.server.config.ts` using the template from `assets/sentry-server-config.ts`:
|
||||
|
||||
- Set environment (development, staging, production)
|
||||
- Configure sample rates for performance monitoring
|
||||
- Enable tracing for Server Actions and API routes
|
||||
- Set up error filtering and breadcrumbs
|
||||
|
||||
Customize `sentry.client.config.ts` using the template from `assets/sentry-client-config.ts`:
|
||||
|
||||
- Configure replay sessions for debugging
|
||||
- Set error boundaries
|
||||
- Enable performance monitoring for user interactions
|
||||
|
||||
### 4. Add Instrumentation Hook
|
||||
|
||||
Create or update `instrumentation.ts` in project root using the template from `assets/instrumentation.ts`. This:
|
||||
|
||||
- Initializes OpenTelemetry before app starts
|
||||
- Registers Sentry as trace provider
|
||||
- Enables distributed tracing across services
|
||||
- Runs only once on server startup
|
||||
|
||||
**Note**: Requires `experimental.instrumentationHook` in `next.config.js` (added by Sentry wizard).
|
||||
|
||||
### 5. Create Logging Wrapper
|
||||
|
||||
Create `lib/logger.ts` using the template from `assets/logger.ts`. This provides:
|
||||
|
||||
- Structured logging with context
|
||||
- Automatic Sentry integration
|
||||
- Different log levels (debug, info, warn, error)
|
||||
- Request context capture
|
||||
|
||||
Use instead of `console.log` for better debugging:
|
||||
|
||||
```typescript
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
logger.info('User logged in', { userId: user.id });
|
||||
logger.error('Failed to save data', { error, userId });
|
||||
```
|
||||
|
||||
### 6. Add Error Boundary (Client Components)
|
||||
|
||||
Create `components/error-boundary.tsx` using the template from `assets/error-boundary.tsx`. This:
|
||||
|
||||
- Catches React errors in client components
|
||||
- Sends errors to Sentry
|
||||
- Shows fallback UI
|
||||
- Provides error recovery
|
||||
|
||||
Use in layouts or pages:
|
||||
|
||||
```typescript
|
||||
import { ErrorBoundary } from '@/components/error-boundary';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Create Custom Error Page
|
||||
|
||||
Update `app/error.tsx` using the template from `assets/error-page.tsx`. This:
|
||||
|
||||
- Shows user-friendly error messages
|
||||
- Captures errors in Server Components
|
||||
- Provides retry functionality
|
||||
- Sends errors to Sentry
|
||||
|
||||
### 8. Add Global Error Handler
|
||||
|
||||
Update `app/global-error.tsx` using the template from `assets/global-error.tsx`. This:
|
||||
|
||||
- Catches errors in root layout
|
||||
- Last resort error boundary
|
||||
- Required for catching layout errors
|
||||
|
||||
## Tracing Server Actions
|
||||
|
||||
### Manual Instrumentation
|
||||
|
||||
Wrap Server Actions with Sentry tracing:
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
|
||||
import { logger } from '@/lib/logger';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
return await Sentry.startSpan(
|
||||
{ name: 'createPost', op: 'server.action' },
|
||||
async () => {
|
||||
try {
|
||||
const title = formData.get('title') as string;
|
||||
|
||||
logger.info('Creating post', { title });
|
||||
|
||||
// Your logic here
|
||||
const post = await prisma.post.create({
|
||||
data: { title, content: '...' },
|
||||
});
|
||||
|
||||
logger.info('Post created', { postId: post.id });
|
||||
return { success: true, post };
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to create post', { error });
|
||||
Sentry.captureException(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Automatic Instrumentation
|
||||
|
||||
Sentry automatically instruments:
|
||||
- Next.js API routes
|
||||
- Server Components (partial)
|
||||
- Fetch requests
|
||||
- Database queries (with OTel)
|
||||
|
||||
## Monitoring Patterns
|
||||
|
||||
### 1. Capture User Context
|
||||
|
||||
Associate errors with users:
|
||||
|
||||
```typescript
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Call in layouts or middleware to track user context globally.
|
||||
|
||||
### 2. Add Custom Tags
|
||||
|
||||
Tag errors for filtering:
|
||||
|
||||
```typescript
|
||||
Sentry.setTag('feature', 'worldbuilding');
|
||||
Sentry.setTag('entity_type', 'character');
|
||||
|
||||
// Now errors are tagged and filterable in Sentry dashboard
|
||||
```
|
||||
|
||||
### 3. Add Breadcrumbs
|
||||
|
||||
Track user actions leading to errors:
|
||||
|
||||
```typescript
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'user_action',
|
||||
message: 'User clicked create entity',
|
||||
level: 'info',
|
||||
data: {
|
||||
entityType: 'character',
|
||||
worldId: 'world-123',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Performance Monitoring
|
||||
|
||||
Track custom operations:
|
||||
|
||||
```typescript
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
export async function complexOperation() {
|
||||
const transaction = Sentry.startTransaction({
|
||||
name: 'Complex World Generation',
|
||||
op: 'task',
|
||||
});
|
||||
|
||||
// Step 1
|
||||
const span1 = transaction.startChild({
|
||||
op: 'generate.terrain',
|
||||
description: 'Generate terrain data',
|
||||
});
|
||||
await generateTerrain();
|
||||
span1.finish();
|
||||
|
||||
// Step 2
|
||||
const span2 = transaction.startChild({
|
||||
op: 'generate.biomes',
|
||||
description: 'Generate biome data',
|
||||
});
|
||||
await generateBiomes();
|
||||
span2.finish();
|
||||
|
||||
transaction.finish();
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Database Query Tracing
|
||||
|
||||
Prisma automatically integrates with OTel:
|
||||
|
||||
```typescript
|
||||
// Queries are automatically traced if OTel is configured
|
||||
const users = await prisma.user.findMany();
|
||||
// Shows up in Sentry as a database span
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Sample Rates
|
||||
|
||||
Control how many events are sent to Sentry (avoid quota limits):
|
||||
|
||||
```typescript
|
||||
// sentry.server.config.ts
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
|
||||
// Percentage of errors to capture (1.0 = 100%)
|
||||
sampleRate: 1.0,
|
||||
|
||||
// Percentage of transactions to trace
|
||||
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
|
||||
|
||||
// Percentage of sessions to replay
|
||||
replaysSessionSampleRate: 0.1,
|
||||
|
||||
// Percentage of error sessions to replay
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
});
|
||||
```
|
||||
|
||||
### Environment Detection
|
||||
|
||||
Configure different settings per environment:
|
||||
|
||||
```typescript
|
||||
Sentry.init({
|
||||
environment: process.env.NODE_ENV,
|
||||
enabled: process.env.NODE_ENV !== 'development', // Disable in dev
|
||||
|
||||
beforeSend(event, hint) {
|
||||
// Filter out specific errors
|
||||
if (event.exception?.values?.[0]?.value?.includes('ResizeObserver')) {
|
||||
return null; // Don't send to Sentry
|
||||
}
|
||||
return event;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Source Maps
|
||||
|
||||
Ensure source maps are uploaded for readable stack traces:
|
||||
|
||||
```typescript
|
||||
// next.config.js (added by Sentry wizard)
|
||||
const { withSentryConfig } = require('@sentry/nextjs');
|
||||
|
||||
module.exports = withSentryConfig(
|
||||
nextConfig,
|
||||
{
|
||||
silent: true,
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
},
|
||||
{
|
||||
hideSourceMaps: true,
|
||||
widenClientFileUpload: true,
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use logger wrapper**: Centralize logging for consistency
|
||||
2. **Set user context**: Associate errors with users for debugging
|
||||
3. **Add breadcrumbs**: Track user journey before errors
|
||||
4. **Monitor performance**: Use tracing for slow operations
|
||||
5. **Filter noise**: Exclude known non-critical errors
|
||||
6. **Configure sample rates**: Balance visibility with quota
|
||||
7. **Test in staging**: Verify Sentry integration before production
|
||||
8. **Review regularly**: Check Sentry dashboard for patterns
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Sentry not capturing errors**: Check DSN is correct and Sentry is initialized. Verify `instrumentation.ts` exports `register()`.
|
||||
|
||||
**Source maps not working**: Ensure auth token is set and source maps are uploaded during build. Check Sentry dashboard > Settings > Source Maps.
|
||||
|
||||
**High quota usage**: Reduce sample rates in production. Filter out noisy errors with `beforeSend`.
|
||||
|
||||
**Traces not appearing**: Verify `tracesSampleRate` > 0. Check OpenTelemetry is initialized in `instrumentation.ts`.
|
||||
|
||||
**Client errors not captured**: Ensure `NEXT_PUBLIC_SENTRY_DSN` is set and accessible from browser.
|
||||
|
||||
## Resources
|
||||
|
||||
### scripts/
|
||||
|
||||
No executable scripts needed for this skill.
|
||||
|
||||
### references/
|
||||
|
||||
- `sentry-best-practices.md` - Error handling patterns, performance monitoring strategies, and quota management
|
||||
- `otel-integration.md` - OpenTelemetry concepts, custom instrumentation, and distributed tracing setup
|
||||
|
||||
### assets/
|
||||
|
||||
- `sentry-server-config.ts` - Server-side Sentry configuration with tracing and sampling
|
||||
- `sentry-client-config.ts` - Client-side Sentry configuration with replay and error boundaries
|
||||
- `instrumentation.ts` - OpenTelemetry initialization and Sentry integration
|
||||
- `logger.ts` - Structured logging wrapper with Sentry integration
|
||||
- `error-boundary.tsx` - React error boundary component for client-side error handling
|
||||
- `error-page.tsx` - Custom error page for Server Component errors
|
||||
- `global-error.tsx` - Global error handler for root layout errors
|
||||
71
skills/sentry-and-otel-setup/assets/error-boundary.tsx
Normal file
71
skills/sentry-and-otel-setup/assets/error-boundary.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { Component, type ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
// Log error to Sentry
|
||||
Sentry.captureException(error, {
|
||||
contexts: {
|
||||
react: {
|
||||
componentStack: errorInfo.componentStack,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// Render custom fallback UI
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<div className="max-w-md rounded-lg border border-red-200 bg-red-50 p-6">
|
||||
<h2 className="mb-2 text-xl font-semibold text-red-900">
|
||||
Something went wrong
|
||||
</h2>
|
||||
<p className="mb-4 text-red-700">
|
||||
An error occurred while rendering this component.
|
||||
</p>
|
||||
{process.env.NODE_ENV === 'development' && this.state.error && (
|
||||
<pre className="mb-4 overflow-auto rounded bg-red-100 p-2 text-xs text-red-900">
|
||||
{this.state.error.message}
|
||||
</pre>
|
||||
)}
|
||||
<button
|
||||
onClick={() => this.setState({ hasError: false, error: undefined })}
|
||||
className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
61
skills/sentry-and-otel-setup/assets/error-page.tsx
Normal file
61
skills/sentry-and-otel-setup/assets/error-page.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function ErrorPage({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// Log error to Sentry
|
||||
Sentry.captureException(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<div className="max-w-md rounded-lg border p-6">
|
||||
<h2 className="mb-2 text-2xl font-semibold">Something went wrong</h2>
|
||||
|
||||
<p className="mb-4 text-gray-600">
|
||||
An unexpected error occurred. Please try again.
|
||||
</p>
|
||||
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<details className="mb-4">
|
||||
<summary className="cursor-pointer text-sm text-gray-500">
|
||||
Error details
|
||||
</summary>
|
||||
<pre className="mt-2 overflow-auto rounded bg-gray-100 p-2 text-xs">
|
||||
{error.message}
|
||||
</pre>
|
||||
{error.digest && (
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Error ID: {error.digest}
|
||||
</p>
|
||||
)}
|
||||
</details>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
|
||||
<a
|
||||
href="/"
|
||||
className="rounded border border-gray-300 px-4 py-2 hover:bg-gray-50"
|
||||
>
|
||||
Go home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
skills/sentry-and-otel-setup/assets/global-error.tsx
Normal file
41
skills/sentry-and-otel-setup/assets/global-error.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<div className="max-w-md rounded-lg border p-6">
|
||||
<h2 className="mb-2 text-2xl font-semibold">Application Error</h2>
|
||||
|
||||
<p className="mb-4 text-gray-600">
|
||||
A critical error occurred. Please refresh the page.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||
>
|
||||
Reload page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
9
skills/sentry-and-otel-setup/assets/instrumentation.ts
Normal file
9
skills/sentry-and-otel-setup/assets/instrumentation.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
await import('./sentry.server.config');
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
await import('./sentry.edge.config');
|
||||
}
|
||||
}
|
||||
79
skills/sentry-and-otel-setup/assets/logger.ts
Normal file
79
skills/sentry-and-otel-setup/assets/logger.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
interface LogContext {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
class Logger {
|
||||
private log(level: LogLevel, message: string, context?: LogContext) {
|
||||
// Add context as Sentry breadcrumb
|
||||
if (context) {
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'log',
|
||||
message,
|
||||
level,
|
||||
data: context,
|
||||
});
|
||||
}
|
||||
|
||||
// Also log to console in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const logFn = console[level] || console.log;
|
||||
logFn(`[${level.toUpperCase()}] ${message}`, context || '');
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, context?: LogContext) {
|
||||
this.log('debug', message, context);
|
||||
}
|
||||
|
||||
info(message: string, context?: LogContext) {
|
||||
this.log('info', message, context);
|
||||
}
|
||||
|
||||
warn(message: string, context?: LogContext) {
|
||||
this.log('warn', message, context);
|
||||
// Set context for next error
|
||||
if (context) {
|
||||
Sentry.setContext('warning', context);
|
||||
}
|
||||
}
|
||||
|
||||
error(message: string, context?: LogContext & { error?: Error }) {
|
||||
this.log('error', message, context);
|
||||
|
||||
// Capture as Sentry error with context
|
||||
const error = context?.error || new Error(message);
|
||||
|
||||
Sentry.captureException(error, {
|
||||
level: 'error',
|
||||
contexts: {
|
||||
custom: context,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Set user context for all subsequent logs/errors
|
||||
setUser(user: { id: string; email?: string; username?: string }) {
|
||||
Sentry.setUser(user);
|
||||
}
|
||||
|
||||
// Clear user context (e.g., on logout)
|
||||
clearUser() {
|
||||
Sentry.setUser(null);
|
||||
}
|
||||
|
||||
// Add custom tags for filtering in Sentry
|
||||
setTag(key: string, value: string) {
|
||||
Sentry.setTag(key, value);
|
||||
}
|
||||
|
||||
// Add context that persists across logs
|
||||
setContext(key: string, context: LogContext) {
|
||||
Sentry.setContext(key, context);
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger();
|
||||
60
skills/sentry-and-otel-setup/assets/sentry-client-config.ts
Normal file
60
skills/sentry-and-otel-setup/assets/sentry-client-config.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
|
||||
// Set environment
|
||||
environment: process.env.NODE_ENV,
|
||||
|
||||
// Adjust sample rate for production
|
||||
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
|
||||
|
||||
// Capture 100% of errors
|
||||
sampleRate: 1.0,
|
||||
|
||||
// Disable in development
|
||||
enabled: process.env.NODE_ENV !== 'development',
|
||||
|
||||
// Session Replay - captures user interactions for debugging
|
||||
replaysSessionSampleRate: 0.1, // 10% of sessions
|
||||
replaysOnErrorSampleRate: 1.0, // 100% of sessions with errors
|
||||
|
||||
integrations: [
|
||||
Sentry.replayIntegration({
|
||||
maskAllText: true,
|
||||
blockAllMedia: true,
|
||||
}),
|
||||
],
|
||||
|
||||
// Filter out noise
|
||||
beforeSend(event, hint) {
|
||||
// Don't send in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Filter out specific client errors
|
||||
const error = hint.originalException;
|
||||
if (error && typeof error === 'object' && 'message' in error) {
|
||||
const message = String(error.message);
|
||||
|
||||
// Common browser errors to ignore
|
||||
if (
|
||||
message.includes('ResizeObserver') ||
|
||||
message.includes('Non-Error promise rejection') ||
|
||||
message.includes('ChunkLoadError')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
},
|
||||
|
||||
// Add custom tags
|
||||
initialScope: {
|
||||
tags: {
|
||||
runtime: 'client',
|
||||
},
|
||||
},
|
||||
});
|
||||
56
skills/sentry-and-otel-setup/assets/sentry-server-config.ts
Normal file
56
skills/sentry-and-otel-setup/assets/sentry-server-config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
|
||||
// Set environment
|
||||
environment: process.env.NODE_ENV,
|
||||
|
||||
// Adjust sample rate for production in production
|
||||
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
|
||||
|
||||
// Capture 100% of errors
|
||||
sampleRate: 1.0,
|
||||
|
||||
// Disable in development to reduce noise
|
||||
enabled: process.env.NODE_ENV !== 'development',
|
||||
|
||||
// Capture Server Actions and API routes
|
||||
integrations: [
|
||||
Sentry.rewriteFramesIntegration({
|
||||
root: process.cwd(),
|
||||
}),
|
||||
],
|
||||
|
||||
// Filter out noise
|
||||
beforeSend(event, hint) {
|
||||
// Don't send errors from development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Filter out specific errors
|
||||
const error = hint.originalException;
|
||||
if (error && typeof error === 'object' && 'message' in error) {
|
||||
const message = String(error.message);
|
||||
|
||||
// Ignore known non-critical errors
|
||||
if (
|
||||
message.includes('ResizeObserver') ||
|
||||
message.includes('NotFoundError') ||
|
||||
message.includes('AbortError')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
},
|
||||
|
||||
// Add custom tags
|
||||
initialScope: {
|
||||
tags: {
|
||||
runtime: 'server',
|
||||
},
|
||||
},
|
||||
});
|
||||
358
skills/sentry-and-otel-setup/references/otel-integration.md
Normal file
358
skills/sentry-and-otel-setup/references/otel-integration.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# OpenTelemetry Integration
|
||||
|
||||
## What is OpenTelemetry?
|
||||
|
||||
OpenTelemetry (OTel) is an observability framework for:
|
||||
- **Traces**: Track requests through distributed systems
|
||||
- **Metrics**: Measure system performance
|
||||
- **Logs**: Structured event logging
|
||||
|
||||
Sentry acts as an OTel backend, receiving and visualizing this data.
|
||||
|
||||
## How It Works with Sentry
|
||||
|
||||
1. **Instrumentation hook** (`instrumentation.ts`) initializes OTel
|
||||
2. **Automatic instrumentation** tracks HTTP, database, and framework operations
|
||||
3. **Custom instrumentation** adds application-specific tracing
|
||||
4. **Sentry integration** sends traces to Sentry for visualization
|
||||
|
||||
## Automatic Instrumentation
|
||||
|
||||
### What Gets Traced Automatically
|
||||
|
||||
With Sentry + Next.js:
|
||||
|
||||
**Server-side:**
|
||||
- HTTP requests (incoming/outgoing)
|
||||
- Database queries (Prisma, PostgreSQL)
|
||||
- File system operations
|
||||
- Next.js Server Components
|
||||
- API routes
|
||||
|
||||
**Client-side:**
|
||||
- Navigation
|
||||
- User interactions
|
||||
- Fetch requests
|
||||
- Component rendering (partial)
|
||||
|
||||
### Viewing Traces
|
||||
|
||||
In Sentry dashboard:
|
||||
1. Go to Performance
|
||||
2. Click on a transaction
|
||||
3. View waterfall of spans (operations)
|
||||
4. Identify bottlenecks
|
||||
|
||||
## Custom Instrumentation
|
||||
|
||||
### 1. Creating Spans
|
||||
|
||||
Track specific operations:
|
||||
|
||||
```typescript
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
export async function processWorldData(worldId: string) {
|
||||
return await Sentry.startSpan(
|
||||
{
|
||||
name: 'process-world-data',
|
||||
op: 'task',
|
||||
attributes: {
|
||||
worldId,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
// Your logic here
|
||||
const data = await fetchWorldData(worldId);
|
||||
const processed = await processData(data);
|
||||
return processed;
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Nested Spans
|
||||
|
||||
Track sub-operations:
|
||||
|
||||
```typescript
|
||||
export async function generateWorldContent(worldId: string) {
|
||||
return await Sentry.startSpan(
|
||||
{ name: 'generate-world-content', op: 'task' },
|
||||
async (span) => {
|
||||
// Child span 1
|
||||
const entities = await Sentry.startSpan(
|
||||
{ name: 'fetch-entities', op: 'db.query' },
|
||||
async () => {
|
||||
return await prisma.entity.findMany({
|
||||
where: { worldId },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Child span 2
|
||||
const content = await Sentry.startSpan(
|
||||
{ name: 'generate-content', op: 'ai' },
|
||||
async () => {
|
||||
return await generateContent(entities);
|
||||
}
|
||||
);
|
||||
|
||||
span.setStatus('ok');
|
||||
return content;
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Adding Attributes
|
||||
|
||||
Enrich spans with metadata:
|
||||
|
||||
```typescript
|
||||
Sentry.startSpan(
|
||||
{
|
||||
name: 'export-world',
|
||||
op: 'export',
|
||||
attributes: {
|
||||
worldId: 'world-123',
|
||||
format: 'json',
|
||||
includeAssets: true,
|
||||
},
|
||||
},
|
||||
async (span) => {
|
||||
const result = await exportWorld();
|
||||
|
||||
// Add result metadata
|
||||
span.setAttributes({
|
||||
'export.size': result.size,
|
||||
'export.entityCount': result.entityCount,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Distributed Tracing
|
||||
|
||||
### Propagating Context
|
||||
|
||||
Trace requests across services:
|
||||
|
||||
```typescript
|
||||
// Service A - initiates request
|
||||
export async function callServiceB() {
|
||||
return await Sentry.startSpan(
|
||||
{ name: 'call-service-b', op: 'http.client' },
|
||||
async () => {
|
||||
// Context automatically propagated via headers
|
||||
const response = await fetch('https://service-b.com/api', {
|
||||
headers: {
|
||||
// Sentry automatically adds tracing headers
|
||||
},
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Service B - receives request
|
||||
// Continues the same trace automatically if instrumented
|
||||
```
|
||||
|
||||
### Trace Visualization
|
||||
|
||||
In Sentry:
|
||||
- See complete request flow across services
|
||||
- Identify slow external calls
|
||||
- Understand service dependencies
|
||||
|
||||
## Common Span Operations
|
||||
|
||||
Use semantic operation names:
|
||||
|
||||
```typescript
|
||||
// Database operations
|
||||
{ op: 'db.query' }
|
||||
{ op: 'db.insert' }
|
||||
{ op: 'db.update' }
|
||||
|
||||
// HTTP operations
|
||||
{ op: 'http.client' }
|
||||
{ op: 'http.server' }
|
||||
|
||||
// Business logic
|
||||
{ op: 'task' }
|
||||
{ op: 'function' }
|
||||
|
||||
// External services
|
||||
{ op: 'ai' }
|
||||
{ op: 'cache' }
|
||||
{ op: 'storage' }
|
||||
```
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
### 1. Identify N+1 Queries
|
||||
|
||||
Traces reveal repeated database queries:
|
||||
|
||||
```typescript
|
||||
// Bad - N+1 problem shows in trace
|
||||
for (const world of worlds) {
|
||||
const entities = await prisma.entity.findMany({
|
||||
where: { worldId: world.id },
|
||||
});
|
||||
}
|
||||
// Trace shows: db.query x N times
|
||||
|
||||
// Good - Single query
|
||||
const entities = await prisma.entity.findMany({
|
||||
where: {
|
||||
worldId: { in: worlds.map(w => w.id) },
|
||||
},
|
||||
});
|
||||
// Trace shows: db.query x 1 time
|
||||
```
|
||||
|
||||
### 2. Parallel Operations
|
||||
|
||||
Visualize parallel vs sequential:
|
||||
|
||||
```typescript
|
||||
// Sequential - slow
|
||||
const terrain = await generateTerrain();
|
||||
const biomes = await generateBiomes();
|
||||
const climate = await generateClimate();
|
||||
// Trace: 3 sequential spans
|
||||
|
||||
// Parallel - faster
|
||||
const [terrain, biomes, climate] = await Promise.all([
|
||||
generateTerrain(),
|
||||
generateBiomes(),
|
||||
generateClimate(),
|
||||
]);
|
||||
// Trace: 3 parallel spans
|
||||
```
|
||||
|
||||
### 3. Caching Effectiveness
|
||||
|
||||
Track cache hits/misses:
|
||||
|
||||
```typescript
|
||||
export async function getCachedData(key: string) {
|
||||
return await Sentry.startSpan(
|
||||
{ name: 'get-cached-data', op: 'cache' },
|
||||
async (span) => {
|
||||
const cached = await redis.get(key);
|
||||
|
||||
if (cached) {
|
||||
span.setAttributes({ 'cache.hit': true });
|
||||
return cached;
|
||||
}
|
||||
|
||||
span.setAttributes({ 'cache.hit': false });
|
||||
const data = await fetchFreshData();
|
||||
await redis.set(key, data);
|
||||
return data;
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Sampling Strategies
|
||||
|
||||
### 1. Transaction-Based Sampling
|
||||
|
||||
Sample different operations differently:
|
||||
|
||||
```typescript
|
||||
Sentry.init({
|
||||
tracesSampler: (samplingContext) => {
|
||||
// High-value operations - trace all
|
||||
if (samplingContext.name?.includes('checkout')) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Background tasks - sample less
|
||||
if (samplingContext.name?.includes('background')) {
|
||||
return 0.01;
|
||||
}
|
||||
|
||||
// Default
|
||||
return 0.1;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Head-Based Sampling
|
||||
|
||||
Decision made at trace start (Sentry default).
|
||||
|
||||
**Pros**: Simple, consistent per trace
|
||||
**Cons**: Can't sample based on outcome
|
||||
|
||||
### 3. Performance Impact
|
||||
|
||||
- Minimal overhead (<5% typically)
|
||||
- Sampling reduces overhead
|
||||
- Most cost is in data transmission
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Traces Not Appearing
|
||||
|
||||
1. Check `tracesSampleRate` > 0
|
||||
2. Verify instrumentation.ts exports `register()`
|
||||
3. Ensure Next.js experimental.instrumentationHook enabled
|
||||
4. Check Sentry DSN is correct
|
||||
|
||||
### Missing Spans
|
||||
|
||||
1. Verify automatic instrumentation is enabled
|
||||
2. Check custom spans use correct Sentry API
|
||||
3. Ensure spans finish (call `.finish()` or use callback form)
|
||||
|
||||
### Incomplete Traces
|
||||
|
||||
1. Check trace context is propagated (especially cross-service)
|
||||
2. Verify all services use compatible Sentry SDK versions
|
||||
3. Review sampling - some spans may be dropped
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use semantic operation names** - Makes filtering/grouping easier
|
||||
2. **Add relevant attributes** - Enrich spans with context
|
||||
3. **Don't over-instrument** - Focus on meaningful operations
|
||||
4. **Keep spans short** - Track discrete operations, not long processes
|
||||
5. **Set span status** - Mark success/failure explicitly
|
||||
6. **Sample appropriately** - Balance visibility with cost
|
||||
7. **Review regularly** - Check for performance regressions
|
||||
|
||||
## Advanced: Custom Metrics
|
||||
|
||||
Track custom measurements:
|
||||
|
||||
```typescript
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
// Record custom metric
|
||||
Sentry.metrics.distribution('world.entity.count', entityCount, {
|
||||
tags: {
|
||||
worldType: 'fantasy',
|
||||
},
|
||||
});
|
||||
|
||||
// Track over time in Sentry dashboard
|
||||
```
|
||||
|
||||
## Integration with Other Tools
|
||||
|
||||
Sentry can forward traces to:
|
||||
- **DataDog** - Full observability platform
|
||||
- **Jaeger** - Distributed tracing
|
||||
- **Prometheus** - Metrics collection
|
||||
|
||||
Configure via Sentry integrations or OpenTelemetry Collector.
|
||||
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