feat: 集成 Sentry 错误监控 + 前端错误上报中继
后端: - Cargo.toml 添加 sentry/sentry-actix/sentry-tracing (v0.48) - main.rs 条件初始化 Sentry(SENTRY_DSN 环境变量控制) - 新增 handlers/sentry.rs — POST /api/sentry/events 前端中继端点 - sentry-actix 全局中间件 + sentry-tracing 自动捕获 tracing::error! - .env.example 添加 SENTRY_DSN / SENTRY_TRACES_SAMPLE_RATE 配置 前端: - 新增 utils/sentryReporter.ts (批量上报/防抖/开发环境跳过) - app.ts 全局错误处理接入 Sentry (wx.onError/onUnhandledRejection/onPageNotFound) Sentry 未配置 DSN 时完全无操作,无性能开销
This commit is contained in:
925
Cargo.lock
generated
925
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,9 @@ dotenvy = "0.15.7"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
tracing-appender = "0.2"
|
||||
sentry = "0.48"
|
||||
sentry-actix = "0.48"
|
||||
sentry-tracing = "0.48"
|
||||
include_dir = "0.7.4"
|
||||
jsonwebtoken = "9.3.1"
|
||||
openssl = "0.10.73"
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod favorites;
|
||||
pub mod health;
|
||||
pub mod meta;
|
||||
pub mod payment;
|
||||
pub mod sentry;
|
||||
pub mod static_files;
|
||||
pub mod user;
|
||||
pub mod weather;
|
||||
@@ -47,3 +48,4 @@ pub use payment::payment_index;
|
||||
pub use payment::payment_login_status;
|
||||
pub use payment::payment_page;
|
||||
pub use payment::payment_success;
|
||||
pub use sentry::report_frontend_error;
|
||||
|
||||
45
src/handlers/sentry.rs
Normal file
45
src/handlers/sentry.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use actix_web::{web, HttpResponse, post};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SentryEventRequest {
|
||||
pub level: Option<String>,
|
||||
pub message: String,
|
||||
pub stack: Option<String>,
|
||||
pub page: Option<String>,
|
||||
pub user_id: Option<i32>,
|
||||
pub extra: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[post("/api/sentry/events")]
|
||||
pub async fn report_frontend_error(body: web::Json<SentryEventRequest>) -> HttpResponse {
|
||||
let body = body.into_inner();
|
||||
tracing::info!("前端错误上报: {}", body.message);
|
||||
|
||||
sentry::with_scope(
|
||||
|scope| {
|
||||
scope.set_tag("source", "wechat-miniprogram");
|
||||
if let Some(page) = &body.page {
|
||||
scope.set_tag("page", page);
|
||||
}
|
||||
if let Some(user_id) = body.user_id {
|
||||
scope.set_user(Some(sentry::User {
|
||||
id: Some(user_id.to_string()),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
if let Some(extra) = &body.extra {
|
||||
if let Some(obj) = extra.as_object() {
|
||||
for (k, v) in obj {
|
||||
scope.set_extra(k, sentry::protocol::Value::from(v.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|| {
|
||||
sentry::capture_message(&body.message, sentry::Level::Error);
|
||||
},
|
||||
);
|
||||
|
||||
HttpResponse::Ok().json(serde_json::json!({"success": true}))
|
||||
}
|
||||
31
src/main.rs
31
src/main.rs
@@ -22,7 +22,7 @@ use handlers::{
|
||||
create_order, delete_weather, generate_code, generate_temp_token_handler, get_current_user_profile,
|
||||
get_favorites, get_user_quota, get_weather_brief, get_weather_details,
|
||||
get_user_orders, health_check, login, mock_login, mock_confirm, sync_order, payment_index, payment_login_status, payment_page, payment_success,
|
||||
post_weather_data,
|
||||
post_weather_data, report_frontend_error,
|
||||
refresh_token, remove_favorite, root, save_user_profile, serve_static_files,
|
||||
web_generate_login_code, web_login_confirm, web_login_auto_confirm,
|
||||
};
|
||||
@@ -64,13 +64,15 @@ fn create_server_config(
|
||||
.app_data(web::Data::new(app_state))
|
||||
// 安全响应头(全局中间件)
|
||||
.wrap(
|
||||
actix_web::middleware::DefaultHeaders::new()
|
||||
DefaultHeaders::new()
|
||||
.add(("X-Content-Type-Options", "nosniff"))
|
||||
.add(("X-Frame-Options", "DENY"))
|
||||
.add(("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self'"))
|
||||
.add(("Referrer-Policy", "no-referrer-when-downgrade"))
|
||||
.add(("Permissions-Policy", "geolocation=(), microphone=(), camera=()"))
|
||||
)
|
||||
// Sentry 请求监控(在 DSN 未配置时无操作)
|
||||
.wrap(sentry_actix::Sentry::new())
|
||||
// 根路径(无需认证)
|
||||
.service(root) // #[get("/")] - 返回服务信息
|
||||
// 支付页面(无需认证,外部浏览器访问)
|
||||
@@ -92,6 +94,7 @@ fn create_server_config(
|
||||
.service(web_generate_login_code)
|
||||
.service(web_login_confirm)
|
||||
.service(web_login_auto_confirm)
|
||||
.service(report_frontend_error) // POST /api/sentry/events
|
||||
.service(
|
||||
web::scope("")
|
||||
.wrap(from_fn(jwt_middleware))
|
||||
@@ -137,6 +140,30 @@ async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("FREE_USER_DATA_LIMIT", app_config.free_user_data_limit.to_string());
|
||||
}
|
||||
|
||||
// Sentry 初始化(仅在 SENTRY_DSN 环境变量已设置时启用)
|
||||
let _sentry_guard = if let Ok(dsn) = std::env::var("SENTRY_DSN") {
|
||||
if !dsn.is_empty() {
|
||||
info!("Sentry 已初始化");
|
||||
Some(sentry::init((
|
||||
dsn,
|
||||
sentry::ClientOptions {
|
||||
release: sentry::release_name!(),
|
||||
traces_sample_rate: std::env::var("SENTRY_TRACES_SAMPLE_RATE")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<f32>().ok())
|
||||
.unwrap_or(0.2),
|
||||
..Default::default()
|
||||
},
|
||||
)))
|
||||
} else {
|
||||
info!("SENTRY_DSN 为空,跳过 Sentry 初始化");
|
||||
None
|
||||
}
|
||||
} else {
|
||||
info!("未设置 SENTRY_DSN,跳过 Sentry 初始化");
|
||||
None
|
||||
};
|
||||
|
||||
// 初始化文件日志(JSON 格式,带轮转)
|
||||
let log_dir = std::path::Path::new("./logs");
|
||||
std::fs::create_dir_all(log_dir).ok(); // 确保日志目录存在
|
||||
|
||||
Reference in New Issue
Block a user