Add Sentry (#19)

* add sentry

* add sentry

* better log web3proxy

* Add managing and worker on sentry

* better log web3proxy
This commit is contained in:
Oda
2025-04-22 20:49:02 +02:00
committed by GitHub
parent df5f7185c8
commit 42a4cafd8d
40 changed files with 2959 additions and 146 deletions

View File

@@ -28,4 +28,6 @@ PRIVY_APP_ID=cm7u09v0u002zrkuf2yjjr58p
PRIVY_APP_SECRET=25wwYu5AgxArU7djgvQEuioc9YSdGY3WN3r1dmXftPfH33KfGVfzopW3vqoPFjy1b8wS2gkDDZ9iQ8yxSo9Vi4iN
PRIVY_AUTHORIZATION_KEY=wallet-auth:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggpJ65PCo4E6NYpY867AyE6p1KxOrs8LJqHZw+t+076yhRANCAAS2EM23CtIfQRmHWTxcqb1j5yfrVePjZyBOZZ2RoPZHb9bDGLos206fTuVA3zgLVomlOoHTeYifkBASCn9Mfg3b
API_URL=http://localhost:5000
ARBITRUM_RPC_URL=https://arbitrum-one.publicnode.com
ARBITRUM_RPC_URL=https://arbitrum-one.publicnode.com
SENTRY_DSN=https://4b88eba622584ab1af8d0611960e6a2f@bugcenter.apps.managing.live/3
SENTRY_ENVIRONMENT=production

View File

@@ -0,0 +1,73 @@
# Sentry Integration for Web3Proxy
This project includes Sentry for error monitoring and logging. Sentry helps track application errors in real-time and provides detailed information about exceptions that occur during runtime.
## Configuration
Sentry is configured via environment variables in your `.env` file:
```bash
# Sentry configuration
SENTRY_DSN=your-sentry-dsn
SENTRY_ENVIRONMENT=development # or production, staging, etc.
```
## How It Works
1. Sentry is initialized during application startup in `plugins/external/sentry.ts`
2. All unhandled exceptions and errors are automatically captured and sent to Sentry
3. The Fastify instance is decorated with a `sentry` property, which gives you access to the Sentry SDK
## Testing Endpoints
The application provides several endpoints to test Sentry functionality:
- `/api/sentry-diagnostics` - Shows the Sentry configuration status and sends test events
- `/test-sentry` - Triggers and captures a handled exception
- `/test-sentry-uncaught` - Triggers an unhandled exception (useful for testing error handlers)
## Using Sentry in Your Routes
You can manually capture events and exceptions in your routes:
```typescript
// Capture a message
fastify.get('/example', async (request, reply) => {
// Log a message to Sentry
fastify.sentry.captureMessage('User visited example page');
// Continue with your route logic
return { message: 'Example page' };
});
// Capture an exception
fastify.get('/example-error', async (request, reply) => {
try {
// Some code that might fail
throw new Error('Something went wrong');
} catch (error) {
// Capture the exception
fastify.sentry.captureException(error);
// Respond to the client
return { message: 'An error occurred, but we've logged it' };
}
});
```
## Troubleshooting
If events aren't appearing in your Sentry dashboard:
1. Verify your DSN is correct in your `.env` file
2. Ensure your network allows outbound HTTPS connections to sentry.io
3. Check that the environment is correctly set
4. Visit `/api/sentry-diagnostics` to run a diagnostic test
## NPM Installation
The Sentry SDK is installed as a dependency via:
```bash
npm install @sentry/node
```

File diff suppressed because it is too large Load Diff

View File

@@ -43,6 +43,7 @@
"@fastify/type-provider-typebox": "^5.0.0",
"@fastify/under-pressure": "^9.0.1",
"@privy-io/server-auth": "^1.18.12",
"@sentry/node": "^8.55.0",
"@sinclair/typebox": "^0.34.11",
"canonicalize": "^2.0.0",
"concurrently": "^9.0.1",

View File

@@ -22,10 +22,7 @@ export default async function serviceApp (
delete opts.skipOverride // This option only serves testing purpose
// This loads all external plugins defined in plugins/external
// those should be registered first as your custom plugins might depend on them
// await fastify.register(fastifyAutoload, {
// dir: path.join(import.meta.dirname, 'plugins/external'),
// options: { ...opts }
// })
// This loads all your custom plugins defined in plugins/custom
// those should be support plugins that are reused

View File

@@ -0,0 +1,75 @@
import * as Sentry from '@sentry/node';
import fp from 'fastify-plugin';
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
interface SentryPluginOptions {
dsn?: string;
environment?: string;
debug?: boolean;
}
export const autoConfig = {
// Set default options for the plugin
dsn: process.env.SENTRY_DSN,
environment: process.env.SENTRY_ENVIRONMENT || 'development',
debug: false
};
const sentryPlugin: FastifyPluginAsync<SentryPluginOptions> = async (fastify, options) => {
const {
dsn = fastify.config?.SENTRY_DSN || process.env.SENTRY_DSN,
environment = fastify.config?.SENTRY_ENVIRONMENT || process.env.SENTRY_ENVIRONMENT || 'development',
debug = false
} = options;
if (!dsn) {
fastify.log.warn('Sentry DSN not provided, skipping initialization');
return;
}
// Initialize Sentry with minimal configuration
Sentry.init({
dsn,
environment,
debug
});
// Add Sentry error handler, but don't override the existing one
const originalErrorHandler = fastify.errorHandler;
fastify.setErrorHandler((error, request, reply) => {
// Capture the exception with request details
Sentry.captureException(error, {
extra: {
method: request.method,
url: request.url,
params: request.params,
query: request.query,
ip: request.ip
}
});
if (originalErrorHandler) {
return originalErrorHandler(error, request, reply);
}
reply.status(500).send({ error: 'Internal Server Error' });
});
// Add Sentry to fastify instance
fastify.decorate('sentry', Sentry);
// Log initialization success
fastify.log.info(`Sentry initialized for environment: ${environment}`);
};
// Augment FastifyInstance to include sentry
declare module 'fastify' {
interface FastifyInstance {
sentry: typeof Sentry;
}
}
export default fp(sentryPlugin, {
name: 'fastify-sentry',
fastify: '5.x'
});

View File

@@ -12,6 +12,8 @@ declare module 'fastify' {
PRIVY_APP_ID: string;
PRIVY_APP_SECRET: string;
PRIVY_AUTHORIZATION_KEY: string;
SENTRY_DSN: string;
SENTRY_ENVIRONMENT: string;
};
}
}
@@ -20,6 +22,8 @@ const schema = {
type: 'object',
required: [
'PORT',
'COOKIE_SECRET',
'COOKIE_NAME'
],
properties: {
PORT: {
@@ -30,7 +34,8 @@ const schema = {
type: 'string'
},
COOKIE_NAME: {
type: 'string'
type: 'string',
default: 'web3proxy'
},
COOKIE_SECURED: {
type: 'boolean',
@@ -39,6 +44,13 @@ const schema = {
RATE_LIMIT_MAX: {
type: 'number',
default: 100
},
SENTRY_DSN: {
type: 'string'
},
SENTRY_ENVIRONMENT: {
type: 'string',
default: 'development'
}
}
}
@@ -72,7 +84,7 @@ export const autoConfig = {
export default fp(async (fastify) => {
const schema = {
type: 'object',
required: ['PRIVY_APP_ID', 'PRIVY_APP_SECRET', 'PRIVY_AUTHORIZATION_KEY'],
required: ['PRIVY_APP_ID', 'PRIVY_APP_SECRET', 'PRIVY_AUTHORIZATION_KEY', 'COOKIE_SECRET'],
properties: {
PRIVY_APP_ID: {
type: 'string'
@@ -82,6 +94,23 @@ export default fp(async (fastify) => {
},
PRIVY_AUTHORIZATION_KEY: {
type: 'string'
},
COOKIE_SECRET: {
type: 'string'
},
COOKIE_NAME: {
type: 'string',
default: 'web3proxy'
},
COOKIE_SECURED: {
type: 'boolean',
default: false
},
SENTRY_DSN: {
type: 'string'
},
SENTRY_ENVIRONMENT: {
type: 'string'
}
}
}

View File

@@ -15,16 +15,34 @@ declare module 'fastify' {
* @see {@link https://github.com/fastify/session}
*/
export default fp(async (fastify) => {
// Get cookie secret from config or use a default for development
const cookieSecret = fastify.config?.COOKIE_SECRET || process.env.COOKIE_SECRET || 'development-secret-for-session-do-not-use-in-production'
if (!cookieSecret) {
fastify.log.warn('No COOKIE_SECRET found in config or environment. Using an insecure default secret. DO NOT USE IN PRODUCTION!')
} else if (cookieSecret === 'development-secret-for-session-do-not-use-in-production') {
fastify.log.warn('Using the default insecure cookie secret. DO NOT USE IN PRODUCTION!')
}
// Get cookie name from config or use a default
const cookieName = fastify.config?.COOKIE_NAME || process.env.COOKIE_NAME || 'web3proxy'
// Get cookie secure setting or default to false for development
const cookieSecured = fastify.config?.COOKIE_SECURED !== undefined
? fastify.config.COOKIE_SECURED
: (process.env.COOKIE_SECURED === 'true' || false)
fastify.register(fastifyCookie)
fastify.register(fastifySession, {
secret: fastify.config.COOKIE_SECRET,
cookieName: fastify.config.COOKIE_NAME,
secret: cookieSecret,
cookieName: cookieName,
cookie: {
secure: fastify.config.COOKIE_SECURED,
secure: cookieSecured,
httpOnly: true,
maxAge: 1800000
maxAge: 1800000 // 30 minutes
}
})
}, {
name: 'session'
name: 'session',
dependencies: ['env-config'] // Make sure env-config runs first
})

View File

@@ -1,6 +1,7 @@
import {FastifyPluginAsyncTypebox} from '@fastify/type-provider-typebox'
import {Type} from '@sinclair/typebox'
import { TradeDirection } from '../../../generated/ManagingApiTypes'
import { handleError } from '../../../utils/errorHandler.js'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
// Define route to open a position
@@ -45,12 +46,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
return result
} catch (error) {
fastify.log.error(error)
reply.status(500)
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
}
return handleError(request, reply, error, 'gmx/open-position');
}
})
@@ -81,12 +77,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
return result
} catch (error) {
fastify.log.error(error)
reply.status(500)
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
}
return handleError(request, reply, error, 'gmx/cancel-orders');
}
})
@@ -120,12 +111,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
return result;
} catch (error) {
fastify.log.error(error)
reply.status(500)
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
}
return handleError(request, reply, error, 'gmx/close-position');
}
})
@@ -157,12 +143,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
return result
} catch (error) {
fastify.log.error(error)
reply.status(500)
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
}
return handleError(request, reply, error, 'gmx/trades');
}
})
@@ -192,12 +173,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
return result
} catch (error) {
fastify.log.error(error)
reply.status(500)
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
}
return handleError(request, reply, error, 'gmx/positions');
}
})
}

View File

@@ -1,10 +1,15 @@
import { FastifyInstance } from 'fastify'
import { handleError } from '../../utils/errorHandler.js'
export default async function (fastify: FastifyInstance) {
fastify.get('/', ({ protocol, hostname }) => {
return {
message:
`Hello ! See documentation at ${protocol}://${hostname}/documentation`
fastify.get('/', async (request, reply) => {
try {
return {
message:
`Hello ! See documentation at ${request.protocol}://${request.hostname}/documentation`
}
} catch (error) {
return handleError(request, reply, error, 'api-root');
}
})
}

View File

@@ -1,4 +1,5 @@
import {FastifyPluginAsyncTypebox, Type} from '@fastify/type-provider-typebox'
import { handleError } from '../../../utils/errorHandler.js'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.post(
@@ -29,8 +30,12 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
}
},
async function (request, reply) {
const { walletId, message, address } = request.body;
return request.signPrivyMessage(reply, walletId, message, address);
try {
const { walletId, message, address } = request.body;
return await request.signPrivyMessage(reply, walletId, message, address);
} catch (error) {
return handleError(request, reply, error, 'privy/sign-message');
}
}
)
@@ -62,8 +67,12 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
}
},
async function (request, reply) {
const { address } = request.body;
return request.initAddress(reply, address);
try {
const { address } = request.body;
return await request.initAddress(reply, address);
} catch (error) {
return handleError(request, reply, error, 'privy/init-address');
}
}
)
}

View File

@@ -1,24 +1,59 @@
import {FastifyPluginAsyncTypebox, Type} from '@fastify/type-provider-typebox'
import { handleError } from '../utils/errorHandler.js'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.get(
'/',
{
schema: {
tags: ['Home'],
description: 'Welcome endpoint that confirms the API is running',
response: {
200: Type.Object({
message: Type.String()
}),
500: Type.Object({
success: Type.Boolean(),
error: Type.String()
})
}
}
},
async function () {
return { message: 'Welcome to the official Web3 Proxy API!' }
async function (request, reply) {
try {
return { message: 'Welcome to the official Web3 Proxy API!' }
} catch (error) {
return handleError(request, reply, error, 'home-root');
}
}
)
// Add health check endpoint
fastify.get('/health', async function () {
return { status: 'ok' }
fastify.get('/health', {
schema: {
tags: ['Health'],
description: 'Health check endpoint that confirms the API is operational',
response: {
200: Type.Object({
status: Type.String(),
timestamp: Type.String(),
version: Type.String()
}),
500: Type.Object({
success: Type.Boolean(),
error: Type.String()
})
}
}
}, async function (request, reply) {
try {
return {
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '1.0.0'
}
} catch (error) {
return handleError(request, reply, error, 'health');
}
})
}

View File

@@ -0,0 +1,92 @@
import {FastifyPluginAsyncTypebox, Type} from '@fastify/type-provider-typebox'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
// Diagnostic endpoint for Sentry
fastify.get('/api/sentry-diagnostics', {
schema: {
tags: ['Sentry'],
description: 'Sentry diagnostics endpoint - tests Sentry configuration and connectivity',
response: {
200: Type.String()
}
}
}, async (request, reply) => {
let output = 'Sentry Diagnostics Report\n';
output += '========================\n';
output += `Timestamp: ${new Date().toISOString()}\n\n`;
output += '## Sentry SDK Status\n';
output += `Sentry Enabled: true\n`;
output += `Environment: ${process.env.SENTRY_ENVIRONMENT || 'development'}\n\n`;
output += '## Test Event\n';
try {
const eventId = fastify.sentry.captureMessage(`Diagnostics test from ${request.hostname} at ${new Date().toISOString()}`);
output += `Test Event ID: ${eventId}\n`;
output += 'Test event was sent to Sentry. Check your Sentry dashboard to confirm it was received.\n\n';
try {
throw new Error('Test exception from diagnostics endpoint');
} catch (ex) {
const exceptionId = fastify.sentry.captureException(ex);
output += `Test Exception ID: ${exceptionId}\n`;
}
} catch (ex) {
output += `Error sending test event: ${ex.message}\n`;
output += ex.stack || '';
}
output += '\n## Connectivity Check\n';
output += 'If events are not appearing in Sentry, check the following:\n';
output += '1. Verify your DSN is correct in your .env file\n';
output += '2. Ensure your network allows outbound HTTPS connections to sentry.io\n';
output += '3. Check Sentry server logs for any ingestion issues\n';
output += '4. Verify your Sentry project is correctly configured to receive events\n';
reply.type('text/plain').send(output);
});
// Test endpoint with explicit capture
fastify.get('/test-sentry', {
schema: {
tags: ['Sentry'],
description: 'Tests Sentry error reporting with a handled exception',
response: {
200: Type.Object({
message: Type.String(),
timestamp: Type.String()
})
}
}
}, async (request, reply) => {
try {
throw new Error(`Test exception for Sentry - ${new Date().toISOString()}`);
} catch (ex) {
fastify.sentry.captureException(ex);
console.log(`Captured exception in Sentry: ${ex.message}`);
fastify.sentry.captureMessage('This is a test message from Web3Proxy');
return {
message: 'Error captured by Sentry',
timestamp: new Date().toISOString()
};
}
});
// Test endpoint that throws an uncaught exception
fastify.get('/test-sentry-uncaught', {
schema: {
tags: ['Sentry'],
description: 'Tests Sentry error reporting with an unhandled exception',
response: {
200: Type.Object({
message: Type.String()
})
}
}
}, async () => {
console.log('About to throw an uncaught exception for Sentry to capture');
throw new Error(`Uncaught exception test for Sentry - ${new Date().toISOString()}`);
});
}
export default plugin;

View File

@@ -0,0 +1,78 @@
# Web3Proxy Error Handling Utilities
This directory contains utilities for standardized error handling across the application.
## Error Handling
The error handling utilities provide a consistent way to:
1. Log errors
2. Capture exceptions in Sentry with proper context
3. Return standardized error responses to clients
## Usage
### Using the Error Handler Directly
You can use the `handleError` function directly in your try/catch blocks:
```typescript
import { handleError } from '../utils/errorHandler'
fastify.get('/example', async (request, reply) => {
try {
// Your route logic here
return { success: true, data: result }
} catch (error) {
return handleError(request, reply, error, 'endpoint/path')
}
})
```
### Using the Route Wrapper
For more concise code, use the `createHandler` or `withErrorHandling` functions:
```typescript
import { createHandler } from '../utils/routeWrapper'
// Method 1: Using createHandler
fastify.get('/example', createHandler('endpoint/example', async (request, reply) => {
// Your route logic here - errors are automatically caught and handled
return { success: true, data: result }
}))
// Method 2: Using withErrorHandling
const originalHandler = async (request, reply) => {
// Your route logic here
return { success: true, data: result }
}
fastify.get('/example', withErrorHandling(originalHandler, 'endpoint/example'))
```
## Custom Error Types
The error handler provides several custom error types that map to appropriate HTTP status codes:
- `ValidationError`: For request validation errors (400)
- `NotFoundError`: For resource not found errors (404)
- `UnauthorizedError`: For authentication failures (401)
- `ForbiddenError`: For permission errors (403)
Example:
```typescript
import { ValidationError } from '../utils/errorHandler'
if (!isValid) {
throw new ValidationError('Invalid input parameters')
}
```
## Best Practices
1. Always provide a meaningful endpoint string for tracking in Sentry
2. Use the appropriate error type for better error classification
3. Prefer using the route wrapper for new routes
4. Include relevant context in error messages for easier debugging

View File

@@ -0,0 +1,107 @@
import { FastifyRequest, FastifyReply } from 'fastify';
/**
* Handles errors consistently across the application.
* Logs errors, captures them in Sentry, and returns consistent error responses.
*
* @param request - The FastifyRequest object
* @param reply - The FastifyReply object
* @param error - The error that occurred
* @param endpoint - The endpoint where the error occurred (for better tracing)
* @returns A standardized error response
*/
export async function handleError(
request: FastifyRequest,
reply: FastifyReply,
error: unknown,
endpoint: string
) {
// Ensure status code is set
const statusCode = error instanceof SyntaxError || error instanceof TypeError || error instanceof RangeError
? 400
: 500;
reply.status(statusCode);
// Log the error
request.log.error(error);
// Capture exception in Sentry with relevant context
request.server.sentry.captureException(error, {
extra: {
method: request.method,
url: request.url,
routeParams: request.params,
queryParams: request.query,
bodyParams: request.body,
ip: request.ip,
endpoint: endpoint
}
});
// Return a standardized error response
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
};
}
/**
* Custom error class for validation errors
*/
export class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
/**
* Custom error class for not found errors
*/
export class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'NotFoundError';
}
}
/**
* Custom error class for unauthorized errors
*/
export class UnauthorizedError extends Error {
constructor(message: string) {
super(message);
this.name = 'UnauthorizedError';
}
}
/**
* Custom error class for forbidden errors
*/
export class ForbiddenError extends Error {
constructor(message: string) {
super(message);
this.name = 'ForbiddenError';
}
}
/**
* Gets the appropriate status code for different error types
* @param error The error to analyze
* @returns The appropriate HTTP status code
*/
export function getStatusCodeForError(error: Error): number {
if (error instanceof ValidationError) return 400;
if (error instanceof NotFoundError) return 404;
if (error instanceof UnauthorizedError) return 401;
if (error instanceof ForbiddenError) return 403;
// Default error types
if (error instanceof SyntaxError) return 400;
if (error instanceof TypeError) return 400;
if (error instanceof RangeError) return 400;
// Default to 500 for unhandled errors
return 500;
}

View File

@@ -0,0 +1,35 @@
import { FastifyRequest, FastifyReply } from 'fastify'
import { handleError } from './errorHandler.js'
/**
* Type for route handler functions
*/
type RouteHandler<T = any> = (request: FastifyRequest, reply: FastifyReply) => Promise<T>
/**
* Wraps a route handler with error handling logic
*
* @param handler - The original route handler function
* @param endpoint - The endpoint identifier for tracking
* @returns A wrapped handler with error handling
*/
export function withErrorHandling<T = any>(handler: RouteHandler<T>, endpoint: string): RouteHandler<T> {
return async (request: FastifyRequest, reply: FastifyReply): Promise<T> => {
try {
return await handler(request, reply)
} catch (error) {
return handleError(request, reply, error, endpoint) as T
}
}
}
/**
* Creates a route handler with built-in error handling
*
* @param endpoint - The endpoint identifier for tracking
* @param handlerFn - The handler function logic
* @returns A route handler with error handling
*/
export function createHandler<T = any>(endpoint: string, handlerFn: RouteHandler<T>): RouteHandler<T> {
return withErrorHandling(handlerFn, endpoint)
}