localgreenchain/lib/monitoring/sentry.ts
Claude 5ea8bab5c3
Add production deployment infrastructure (Agent 4)
- Docker: Multi-stage Dockerfile with security hardening, docker-compose
  for production and development environments
- Environment: Comprehensive .env.example with all config options,
  lib/config/env.ts for typed environment validation
- Logging: Structured JSON logging with request/response middleware
- Monitoring: Prometheus metrics endpoint, Grafana dashboard, health
  checks (liveness/readiness probes)
- Security: Security headers, rate limiting, CORS middleware
- CI/CD: GitHub Actions workflows for CI, production deploy, and
  preview deployments
- Error tracking: Sentry integration foundation

Files created:
- Docker: Dockerfile, docker-compose.yml, docker-compose.dev.yml, .dockerignore
- Config: lib/config/env.ts, lib/config/index.ts
- Logging: lib/logging/logger.ts, lib/logging/middleware.ts
- Monitoring: lib/monitoring/sentry.ts, lib/monitoring/metrics.ts,
  lib/monitoring/health.ts
- Security: lib/security/headers.ts, lib/security/rateLimit.ts,
  lib/security/cors.ts
- API: pages/api/health/*, pages/api/metrics.ts
- Infra: infra/prometheus/prometheus.yml, infra/grafana/*
2025-11-23 03:54:03 +00:00

225 lines
5.3 KiB
TypeScript

/**
* Sentry Error Tracking Integration
* Agent 4: Production Deployment
*
* Provides error tracking and reporting with Sentry.
* Note: Requires @sentry/nextjs package when implementing full integration.
*/
import { env } from '../config';
import { logger } from '../logging';
interface SentryContext {
user?: {
id?: string;
email?: string;
username?: string;
};
tags?: Record<string, string>;
extra?: Record<string, unknown>;
}
interface BreadcrumbData {
category: string;
message: string;
level?: 'debug' | 'info' | 'warning' | 'error';
data?: Record<string, unknown>;
}
/**
* Sentry error handler (stub implementation)
* Replace with actual @sentry/nextjs integration when package is installed
*/
class SentryHandler {
private isEnabled: boolean;
private dsn: string | undefined;
private breadcrumbs: BreadcrumbData[] = [];
private maxBreadcrumbs = 100;
constructor() {
this.dsn = env.sentryDsn;
this.isEnabled = !!this.dsn && env.isProduction;
if (this.isEnabled) {
logger.info('Sentry error tracking enabled', {
environment: env.nodeEnv,
});
} else if (env.isProduction && !this.dsn) {
logger.warn('Sentry DSN not configured - error tracking disabled');
}
}
/**
* Capture an exception and send to Sentry
*/
captureException(error: Error, context?: SentryContext): string {
const eventId = this.generateEventId();
// Log the error
logger.error('Error captured', error, {
eventId,
...context?.tags,
...context?.extra,
});
if (this.isEnabled) {
// In a real implementation, this would send to Sentry
// For now, we log the error details
this.logToSentry('exception', {
eventId,
error: {
name: error.name,
message: error.message,
stack: error.stack,
},
context,
breadcrumbs: this.breadcrumbs.slice(-20),
});
}
return eventId;
}
/**
* Capture a message and send to Sentry
*/
captureMessage(
message: string,
level: 'debug' | 'info' | 'warning' | 'error' = 'info',
context?: SentryContext
): string {
const eventId = this.generateEventId();
// Log the message
const logFn = level === 'error' ? logger.error : level === 'warning' ? logger.warn : logger.info;
logFn.call(logger, message, { eventId, ...context?.tags });
if (this.isEnabled) {
this.logToSentry('message', {
eventId,
message,
level,
context,
});
}
return eventId;
}
/**
* Set user context for error tracking
*/
setUser(user: SentryContext['user'] | null): void {
if (this.isEnabled && user) {
logger.debug('Sentry user context set', { userId: user.id });
}
}
/**
* Set extra context for error tracking
*/
setContext(name: string, context: Record<string, unknown>): void {
if (this.isEnabled) {
logger.debug('Sentry context set', { name, ...context });
}
}
/**
* Add a breadcrumb for debugging
*/
addBreadcrumb(breadcrumb: BreadcrumbData): void {
this.breadcrumbs.push({
...breadcrumb,
level: breadcrumb.level || 'info',
});
// Keep only the last N breadcrumbs
if (this.breadcrumbs.length > this.maxBreadcrumbs) {
this.breadcrumbs = this.breadcrumbs.slice(-this.maxBreadcrumbs);
}
}
/**
* Create a scope for isolated error tracking
*/
withScope(callback: (scope: SentryScope) => void): void {
const scope = new SentryScope();
callback(scope);
}
/**
* Flush pending events (for serverless environments)
*/
async flush(timeout = 2000): Promise<boolean> {
// In a real implementation, this would flush pending events
return true;
}
/**
* Check if Sentry is enabled
*/
isActive(): boolean {
return this.isEnabled;
}
private generateEventId(): string {
return `evt_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 11)}`;
}
private logToSentry(type: string, data: Record<string, unknown>): void {
// Placeholder for actual Sentry API call
// In production with @sentry/nextjs, this would use the Sentry SDK
logger.debug(`[Sentry ${type}]`, data);
}
}
/**
* Scope class for isolated error context
*/
class SentryScope {
private tags: Record<string, string> = {};
private extra: Record<string, unknown> = {};
private user: SentryContext['user'] | null = null;
private level: string = 'error';
setTag(key: string, value: string): void {
this.tags[key] = value;
}
setExtra(key: string, value: unknown): void {
this.extra[key] = value;
}
setUser(user: SentryContext['user'] | null): void {
this.user = user;
}
setLevel(level: string): void {
this.level = level;
}
getContext(): SentryContext {
return {
user: this.user || undefined,
tags: this.tags,
extra: this.extra,
};
}
}
// Export singleton instance
export const sentry = new SentryHandler();
// Export for API error handling
export function captureApiError(error: Error, req?: { url?: string; method?: string }): string {
return sentry.captureException(error, {
tags: {
api: 'true',
path: req?.url || 'unknown',
method: req?.method || 'unknown',
},
});
}
// Export types
export type { SentryContext, BreadcrumbData };