mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-06 07:47:27 +08:00
前端监控 (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: 组织和项目标识
206 lines
5.5 KiB
JavaScript
206 lines
5.5 KiB
JavaScript
/**
|
||
* 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,
|
||
};
|