import { Injectable, Logger, OnModuleInit, OnModuleDestroy, } from '@nestjs/common'; import * as Sentry from '@sentry/node'; import { ExceptionReporterInterface, ExceptionStatsInterface, ExceptionType, ExceptionSeverity, } from '../../exception/exception.interface'; /** * Sentry 服务 * 基于 NestJS 官方示例实现 * 参考: https://docs.nestjs.cn/fundamentals/dependency-injection * 对应 Java: 错误追踪服务 */ @Injectable() export class SentryService implements ExceptionReporterInterface, ExceptionStatsInterface, OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(SentryService.name); private initialized = false; private stats = new Map(); constructor(private readonly config: any) { this.initializeSentry(); } async onModuleInit() { if (this.config?.enabled && !this.initialized) { try { Sentry.init({ dsn: this.config.dsn, environment: this.config.environment || process.env.NODE_ENV, release: this.config.release || process.env.npm_package_version, tracesSampleRate: this.config.tracesSampleRate || 0.1, profilesSampleRate: this.config.profilesSampleRate || 0.1, beforeSend: this.beforeSend.bind(this), beforeBreadcrumb: this.beforeBreadcrumb.bind(this), integrations: [ // 使用默认集成 ], }); this.initialized = true; this.logger.log('Sentry initialized successfully'); } catch (error) { this.logger.error('Failed to initialize Sentry', error); } } } async onModuleDestroy() { if (this.initialized) { try { await Sentry.close(2000); this.initialized = false; this.logger.log('Sentry closed successfully'); } catch (error) { this.logger.error('Failed to close Sentry', error); } } } private initializeSentry() { // Sentry 初始化在 onModuleInit 中进行 } // ==================== 异常上报接口 ==================== /** * 上报异常 */ async report(exception: any, context?: any): Promise { if (!this.initialized || !this.shouldReport(exception)) { return false; } try { Sentry.withScope((scope) => { // 设置标签 this.setTags(scope, exception, context); // 设置上下文 this.setContext(scope, exception, context); // 设置用户信息 this.setUser(scope, context); // 设置额外信息 this.setExtra(scope, exception, context); // 设置级别 this.setLevel(scope, exception); // 捕获异常 Sentry.captureException(exception); }); return true; } catch (error) { this.logger.error('Failed to report exception to Sentry', error); return false; } } /** * 批量上报异常 */ async reportBatch( exceptions: Array<{ exception: any; context?: any }>, ): Promise { const results: boolean[] = []; for (const { exception, context } of exceptions) { const result = await this.report(exception, context); results.push(result); } return results; } /** * 检查是否应该上报 */ shouldReport(exception: any): boolean { if (!this.config?.enabled) { return false; } // 根据异常类型和严重程度判断 const type = this.getExceptionType(exception); const severity = this.getExceptionSeverity(exception); // 只上报高严重程度的异常 if ( severity === ExceptionSeverity.HIGH || severity === ExceptionSeverity.CRITICAL ) { return true; } // 某些类型的异常总是上报 if ( type === ExceptionType.SYSTEM || type === ExceptionType.INTERNAL_SERVER_ERROR ) { return true; } return false; } // ==================== 异常统计接口 ==================== /** * 记录异常统计 */ record(exception: any, context?: any): void { const type = this.getExceptionType(exception); const severity = this.getExceptionSeverity(exception); const key = `${type}:${severity}`; const count = this.stats.get(key) || 0; this.stats.set(key, count + 1); } /** * 获取异常统计 */ getStats(timeRange?: { start: Date; end: Date }): any { const stats: any = { total: 0, byType: {}, bySeverity: {}, byTime: [], topExceptions: [], rate: 0, trend: 'stable', }; // 计算统计信息 for (const [key, count] of this.stats) { const [type, severity] = key.split(':'); stats.total += count; stats.byType[type] = (stats.byType[type] || 0) + count; stats.bySeverity[severity] = (stats.bySeverity[severity] || 0) + count; } return stats; } /** * 重置统计 */ reset(): void { this.stats.clear(); } // ==================== 工具方法 ==================== /** * 设置标签 */ private setTags(scope: Sentry.Scope, exception: any, context?: any): void { const type = this.getExceptionType(exception); const severity = this.getExceptionSeverity(exception); scope.setTag('exception.type', type); scope.setTag('exception.severity', severity); if (context?.request) { scope.setTag('http.method', context.request.method); scope.setTag('http.url', context.request.url); scope.setTag('http.status_code', context.response?.statusCode); } if (context?.user) { scope.setTag('user.id', context.user.id); scope.setTag('user.role', context.user.role); } if (context?.environment) { scope.setTag('environment', context.environment.nodeEnv); scope.setTag('version', context.environment.version); } } /** * 设置上下文 */ private setContext(scope: Sentry.Scope, exception: any, context?: any): void { if (context?.request) { scope.setContext('request', { method: context.request.method, url: context.request.url, headers: this.sanitizeHeaders(context.request.headers), body: this.sanitizeBody(context.request.body), query: context.request.query, params: context.request.params, ip: context.request.ip, userAgent: context.request.userAgent, }); } if (context?.response) { scope.setContext('response', { statusCode: context.response.statusCode, headers: this.sanitizeHeaders(context.response.headers), body: this.sanitizeBody(context.response.body), size: context.response.size, }); } if (context?.environment) { scope.setContext('environment', { nodeEnv: context.environment.nodeEnv, version: context.environment.version, hostname: context.environment.hostname, pid: context.environment.pid, }); } if (context?.trace) { scope.setContext('trace', { traceId: context.trace.traceId, spanId: context.trace.spanId, correlationId: context.trace.correlationId, }); } } /** * 设置用户信息 */ private setUser(scope: Sentry.Scope, context?: any): void { if (context?.user) { scope.setUser({ id: context.user.id, username: context.user.username, email: context.user.email, role: context.user.role, permissions: context.user.permissions, }); } } /** * 设置额外信息 */ private setExtra(scope: Sentry.Scope, exception: any, context?: any): void { if (exception.code) { scope.setExtra('error.code', exception.code); } if (exception.details) { scope.setExtra('error.details', exception.details); } if (exception.cause) { scope.setExtra('error.cause', exception.cause); } if (context?.meta) { scope.setExtra('meta', context.meta); } } /** * 设置级别 */ private setLevel(scope: Sentry.Scope, exception: any): void { const severity = this.getExceptionSeverity(exception); switch (severity) { case ExceptionSeverity.CRITICAL: scope.setLevel('fatal'); break; case ExceptionSeverity.HIGH: scope.setLevel('error'); break; case ExceptionSeverity.MEDIUM: scope.setLevel('warning'); break; case ExceptionSeverity.LOW: scope.setLevel('info'); break; default: scope.setLevel('error'); } } /** * 获取异常类型 */ private getExceptionType(exception: any): ExceptionType { if (exception.name) { const name = exception.name.toLowerCase(); if (name.includes('validation') || name.includes('badrequest')) { return ExceptionType.VALIDATION; } if (name.includes('unauthorized') || name.includes('authentication')) { return ExceptionType.AUTHENTICATION; } if (name.includes('forbidden') || name.includes('authorization')) { return ExceptionType.AUTHORIZATION; } if (name.includes('notfound')) { return ExceptionType.NOT_FOUND; } if (name.includes('conflict')) { return ExceptionType.CONFLICT; } if (name.includes('timeout')) { return ExceptionType.TIMEOUT; } if (name.includes('ratelimit')) { return ExceptionType.RATE_LIMIT; } if (name.includes('database') || name.includes('db')) { return ExceptionType.DATABASE; } if (name.includes('cache')) { return ExceptionType.CACHE; } if (name.includes('network') || name.includes('connection')) { return ExceptionType.NETWORK; } if (name.includes('external') || name.includes('api')) { return ExceptionType.EXTERNAL_API; } if (name.includes('business')) { return ExceptionType.BUSINESS; } if (name.includes('system')) { return ExceptionType.SYSTEM; } } if (exception.statusCode) { switch (exception.statusCode) { case 400: return ExceptionType.BAD_REQUEST; case 401: return ExceptionType.AUTHENTICATION; case 403: return ExceptionType.AUTHORIZATION; case 404: return ExceptionType.NOT_FOUND; case 409: return ExceptionType.CONFLICT; case 429: return ExceptionType.RATE_LIMIT; case 500: return ExceptionType.INTERNAL_SERVER_ERROR; case 503: return ExceptionType.SERVICE_UNAVAILABLE; default: return ExceptionType.UNKNOWN; } } return ExceptionType.UNKNOWN; } /** * 获取异常严重程度 */ private getExceptionSeverity(exception: any): ExceptionSeverity { const type = this.getExceptionType(exception); switch (type) { case ExceptionType.INTERNAL_SERVER_ERROR: case ExceptionType.SYSTEM: return ExceptionSeverity.CRITICAL; case ExceptionType.AUTHENTICATION: case ExceptionType.AUTHORIZATION: case ExceptionType.DATABASE: case ExceptionType.NETWORK: return ExceptionSeverity.HIGH; case ExceptionType.VALIDATION: case ExceptionType.BAD_REQUEST: case ExceptionType.CONFLICT: case ExceptionType.CACHE: case ExceptionType.EXTERNAL_API: return ExceptionSeverity.MEDIUM; case ExceptionType.NOT_FOUND: case ExceptionType.TIMEOUT: case ExceptionType.RATE_LIMIT: case ExceptionType.BUSINESS: return ExceptionSeverity.LOW; default: return ExceptionSeverity.MEDIUM; } } /** * 清理请求头 */ private sanitizeHeaders( headers: Record, ): Record { const sanitized: Record = {}; const sensitiveHeaders = [ 'authorization', 'cookie', 'x-api-key', 'x-auth-token', ]; for (const [key, value] of Object.entries(headers)) { if (sensitiveHeaders.includes(key.toLowerCase())) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = value; } } return sanitized; } /** * 清理请求体 */ private sanitizeBody(body: any): any { if (!body) return body; if (typeof body === 'string') { try { const parsed = JSON.parse(body); return this.sanitizeObject(parsed); } catch { return body; } } if (typeof body === 'object') { return this.sanitizeObject(body); } return body; } /** * 清理对象 */ private sanitizeObject(obj: any): any { if (!obj || typeof obj !== 'object') return obj; const sanitized = { ...obj }; const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth']; for (const field of sensitiveFields) { if (sanitized[field]) { sanitized[field] = '[REDACTED]'; } } return sanitized; } /** * 发送前处理 */ private beforeSend(event: Sentry.Event): Sentry.Event | null { // 过滤敏感信息 if (event.extra) { event.extra = this.sanitizeObject(event.extra); } if (event.contexts) { event.contexts = this.sanitizeObject(event.contexts); } if (event.tags) { event.tags = this.sanitizeObject(event.tags); } return event; } /** * 面包屑前处理 */ private beforeBreadcrumb( breadcrumb: Sentry.Breadcrumb, ): Sentry.Breadcrumb | null { // 过滤敏感信息 if (breadcrumb.data) { breadcrumb.data = this.sanitizeObject(breadcrumb.data); } return breadcrumb; } // ==================== 公共方法 ==================== /** * 手动捕获异常 */ captureException(exception: any, context?: any): void { if (this.initialized) { Sentry.withScope((scope) => { this.setTags(scope, exception, context); this.setContext(scope, exception, context); this.setUser(scope, context); this.setExtra(scope, exception, context); this.setLevel(scope, exception); Sentry.captureException(exception); }); } } /** * 手动捕获消息 */ captureMessage( message: string, level: Sentry.SeverityLevel = 'info', context?: any, ): void { if (this.initialized) { Sentry.withScope((scope) => { if (context) { this.setContext(scope, null, context); this.setUser(scope, context); this.setExtra(scope, null, context); } scope.setLevel(level); Sentry.captureMessage(message); }); } } /** * 开始事务 */ startTransaction(name: string, op: string): any { if (this.initialized) { // 新版本 Sentry API 已变更,暂时返回 null return null; } return null; } /** * 获取当前事务 */ getCurrentTransaction(): any { if (this.initialized) { // 新版本 Sentry API 已变更,暂时返回 undefined return undefined; } return undefined; } /** * 设置用户上下文 */ setUserContext(user: { id: string; username?: string; email?: string; role?: string; }): void { if (this.initialized) { Sentry.setUser(user); } } /** * 设置标签 */ setTag(key: string, value: string): void { if (this.initialized) { Sentry.setTag(key, value); } } /** * 设置上下文(公共方法) */ setContextPublic(key: string, context: any): void { if (this.initialized) { Sentry.setContext(key, context); } } /** * 设置额外信息(公共方法) */ setExtraPublic(key: string, value: any): void { if (this.initialized) { Sentry.setExtra(key, value); } } /** * 设置级别(公共方法) */ setLevelPublic(level: Sentry.SeverityLevel): void { if (this.initialized) { // 新版本 Sentry API 已变更,暂时不执行 // Sentry.setLevel(level); } } /** * 添加面包屑 */ addBreadcrumb(breadcrumb: Sentry.Breadcrumb): void { if (this.initialized) { Sentry.addBreadcrumb(breadcrumb); } } /** * 配置作用域 */ configureScope(callback: (scope: Sentry.Scope) => void): void { if (this.initialized) { // 新版本 Sentry API 已变更,暂时不执行 // Sentry.configureScope(callback); } } /** * 使用作用域 */ withScope(callback: (scope: Sentry.Scope) => void): void { if (this.initialized) { Sentry.withScope(callback); } } }