Files
eSIM-Tools/netlify/functions/_shared/sentry.js
Abner 0d99f1f600 feat: 集成 Sentry 错误监控系统
前端监控 (src/js/modules/sentry-init.js):
- 使用 @sentry/browser SDK v10+ 和 browserTracingIntegration()
- 智能环境检测,开发环境自动禁用
- 敏感数据过滤 (authKey, token, password, cookie)
- 性能追踪采样率 10%

后端监控 (netlify/functions/_shared/sentry.js):
- 使用 @sentry/node 用于 Netlify Functions
- 仅上报 5xx 服务端错误,节省配额
- Serverless 环境 flush() 确保错误发送

构建集成 (webpack.config.js):
- @sentry/webpack-plugin 自动上传 Source Maps
- 自动使用 Netlify COMMIT_REF 作为 release 版本
- 上传后自动删除本地 source maps

环境变量:
- SENTRY_DSN: 错误上报端点
- SENTRY_AUTH_TOKEN: Source Maps 上传认证
- SENTRY_ORG / SENTRY_PROJECT: 组织和项目标识
2025-12-13 22:03:02 +08:00

206 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Sentry 后端监控模块 (Netlify Functions)
*
* 职责:
* - 环境检测(仅生产环境启用)
* - Functions 错误追踪
* - 敏感数据过滤
* - Handler 包装器
*
* @module sentry
*/
'use strict';
const Sentry = require('@sentry/node');
// ===================================
// 环境检测
// ===================================
const isDev = process.env.NODE_ENV !== 'production';
// ===================================
// Sentry 初始化(仅生产环境)
// ===================================
let sentryInitialized = false;
if (!isDev) {
const sentryDsn = process.env.SENTRY_DSN || '';
// SENTRY_RELEASE: 优先环境变量,其次 Netlify COMMIT_REF (短哈希),最后回退默认值
const commitRef = process.env.COMMIT_REF;
const sentryRelease = process.env.SENTRY_RELEASE ||
(commitRef ? `esim-tools@${commitRef.slice(0, 7)}` : 'esim-tools@unknown');
const sentryEnvironment = process.env.SENTRY_ENVIRONMENT || 'production';
if (!sentryDsn) {
console.warn('[Sentry] SENTRY_DSN 未配置,跳过后端初始化');
} else {
try {
Sentry.init({
dsn: sentryDsn,
environment: sentryEnvironment,
release: sentryRelease,
// 性能采样率
tracesSampleRate: 0.1,
// 错误采样率
sampleRate: 1.0,
// ===================================
// 错误过滤
// ===================================
ignoreErrors: [
// 网络相关错误
'ECONNREFUSED',
'ETIMEDOUT',
'ENOTFOUND',
'socket hang up',
],
// ===================================
// 事件前处理(移除敏感数据)
// ===================================
beforeSend(event) {
// 移除请求中的敏感数据
if (event.request) {
delete event.request.cookies;
if (event.request.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['x-esim-key'];
delete event.request.headers['cookie'];
}
// 脱敏请求体中的敏感字段
if (event.request.data && typeof event.request.data === 'string') {
try {
const body = JSON.parse(event.request.data);
if (body.authKey) body.authKey = '***';
if (body.token) body.token = '***';
if (body.password) body.password = '***';
if (body.cookie) body.cookie = '***';
event.request.data = JSON.stringify(body);
} catch (_) {
// 不是 JSON跳过
}
}
}
// 脱敏异常消息
if (event.exception?.values) {
event.exception.values.forEach(exception => {
if (exception.value) {
exception.value = exception.value
.replace(/token[=:]\s*[^\s,]+/gi, 'token=***')
.replace(/key[=:]\s*[^\s,]+/gi, 'key=***')
.replace(/password[=:]\s*[^\s,]+/gi, 'password=***')
.replace(/cookie[=:]\s*[^\s,]+/gi, 'cookie=***');
}
});
}
return event;
},
});
sentryInitialized = true;
console.log('[Sentry] 后端初始化成功');
} catch (error) {
console.error('[Sentry] 后端初始化失败:', error);
sentryInitialized = false;
}
}
}
// ===================================
// Mock 对象(开发环境使用)
// ===================================
const SentryMock = {
captureException: (error) => {
console.error('[Sentry Dev] Exception:', error);
},
captureMessage: (message, level) => {
console.log(`[Sentry Dev] ${level || 'info'}:`, message);
},
setTag: () => {},
setContext: () => {},
setUser: () => {},
withScope: (callback) => callback({ setContext: () => {}, setTag: () => {} }),
flush: () => Promise.resolve(true),
};
// 导出 Sentry 实例
const SentryInstance = (isDev || !sentryInitialized) ? SentryMock : Sentry;
/**
* 捕获异常并上报到 Sentry
* @param {Error} error - 错误对象
* @param {object} [context] - 额外上下文
*/
function captureException(error, context) {
if (context) {
SentryInstance.withScope(scope => {
Object.entries(context).forEach(([key, value]) => {
scope.setContext(key, value);
});
SentryInstance.captureException(error);
});
} else {
SentryInstance.captureException(error);
}
}
/**
* 捕获消息并上报到 Sentry
* @param {string} message - 消息内容
* @param {string} [level] - 级别
*/
function captureMessage(message, level = 'info') {
SentryInstance.captureMessage(message, level);
}
/**
* 刷新 Sentry 队列(确保错误发送完成)
* @param {number} [timeout=2000] - 超时时间(毫秒)
* @returns {Promise<boolean>}
*/
async function flush(timeout = 2000) {
if (isDev || !sentryInitialized) {
return true;
}
return Sentry.flush(timeout);
}
/**
* 设置标签
* @param {string} key - 标签名
* @param {string} value - 标签值
*/
function setTag(key, value) {
SentryInstance.setTag(key, value);
}
/**
* 设置上下文
* @param {string} name - 上下文名称
* @param {object} context - 上下文数据
*/
function setContext(name, context) {
SentryInstance.setContext(name, context);
}
module.exports = {
Sentry: SentryInstance,
captureException,
captureMessage,
flush,
setTag,
setContext,
isDev,
sentryInitialized,
};