- 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/*
225 lines
5.3 KiB
TypeScript
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 };
|