修复错误报告审查中的次要问题
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / Repository checks (pull_request) Failing after 14s
Project CI / Frontend tests (pull_request) Failing after 3m16s
Project CI / Native shell tests (pull_request) Failing after 6m16s

修复后台错误报告刷新竞态、下载并发与查询参数构建复用。

修复错误报告清理分页、归档大小限制、分页总数和下载响应头异常。

修复 OSS 流式读取上限与 Codex 高频调试日志阻塞。

同步错误报告技术方案与项目决策记录,并在 review.txt 保留待决 breaking 项。
This commit is contained in:
2026-09-01 23:37:36 +08:00
parent 35ec049138
commit 3cde591b67
8 changed files with 152 additions and 90 deletions
@@ -45,6 +45,7 @@ const MAX_LOG_CHARS: usize = 2_000_000;
const MAX_BATCH_BYTES: usize = 20 * 1024 * 1024;
const MAX_REQUEST_BODY_BYTES: usize = 24 * 1024 * 1024;
const MAX_EVENT_FIELD_CHARS: usize = 512;
const MAX_EVENTS_JSONL_BYTES: usize = MAX_REQUEST_BODY_BYTES;
#[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
@@ -251,7 +252,7 @@ pub async fn admin_list_error_reports(
total,
offset,
limit,
has_more: offset.saturating_add(limit) < total as u32,
has_more: (offset as u64).saturating_add(limit as u64) < total,
},
))
}
@@ -391,7 +392,8 @@ pub async fn admin_download_error_report(
);
resp.headers_mut().insert(
header::CONTENT_DISPOSITION,
HeaderValue::from_str(&format!("attachment; filename=\"{batch_id}.zip\"")).unwrap(),
HeaderValue::from_str(&format!("attachment; filename=\"{batch_id}.zip\""))
.map_err(internal)?,
);
Ok(resp)
}
@@ -513,15 +515,26 @@ pub(crate) fn build_error_report_archive(
fn parse_archive(bytes: &[u8]) -> Result<(Vec<Event>, Option<String>, Vec<String>), String> {
let mut a = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| e.to_string())?;
let mut t = String::new();
a.by_name("events.jsonl")
.map_err(|_| "归档缺少 events.jsonl".to_string())?
.read_to_string(&mut t)
.map_err(|e| e.to_string())?;
{
let mut events_file = a
.by_name("events.jsonl")
.map_err(|_| "归档缺少 events.jsonl".to_string())?
.take((MAX_EVENTS_JSONL_BYTES + 1) as u64);
events_file
.read_to_string(&mut t)
.map_err(|e| e.to_string())?;
}
if t.len() > MAX_EVENTS_JSONL_BYTES {
return Err("events.jsonl 解压后超过大小上限".to_string());
}
let events = t
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| serde_json::from_str(l).map_err(|e| e.to_string()))
.collect::<Result<Vec<Event>, _>>()?;
if events.len() > MAX_EVENTS {
return Err("归档事件数量超过上限".to_string());
}
let desc = if let Ok(mut f) = a.by_name("user-description.txt") {
let mut s = String::new();
f.read_to_string(&mut s).map_err(|e| e.to_string())?;
@@ -590,23 +603,32 @@ pub fn spawn_cleanup_worker(state: AppState) {
}
async fn cleanup_expired(state: &AppState) -> Result<(), String> {
let cutoff = OffsetDateTime::now_utc() - time::Duration::days(30);
let (rows, _) = state
.spacetime_client()
.list_error_reports(ErrorReportListRecordInput {
status: None,
fingerprint: None,
source: None,
limit: 500,
offset: 0,
})
.await
.map_err(|e| e.to_string())?;
for r in rows {
if let Ok(ts) = OffsetDateTime::parse(&r.created_at, &Rfc3339) {
if ts < cutoff {
let Some(oss) = state.oss_client() else {
continue;
};
let Some(oss) = state.oss_client() else {
return Ok(());
};
let mut offset = 0_u32;
let mut first_error = None;
loop {
let (rows, _) = state
.spacetime_client()
.list_error_reports(ErrorReportListRecordInput {
status: None,
fingerprint: None,
source: None,
limit: 500,
offset,
})
.await
.map_err(|e| e.to_string())?;
if rows.is_empty() {
break;
}
let page_len = rows.len() as u32;
let mut deleted = 0_u32;
for r in rows {
if let Ok(ts) = OffsetDateTime::parse(&r.created_at, &Rfc3339)
&& ts < cutoff
{
if oss
.delete_object(
state.editor_oss_http_client(),
@@ -619,12 +641,23 @@ async fn cleanup_expired(state: &AppState) -> Result<(), String> {
{
continue;
}
let _ = state
if let Err(error) = state
.spacetime_client()
.delete_error_report(r.batch_id)
.await;
.await
{
first_error.get_or_insert_with(|| error.to_string());
} else {
deleted += 1;
}
}
}
if page_len < 500 {
break;
}
if deleted == 0 {
offset = offset.saturating_add(page_len);
}
}
Ok(())
first_error.map_or(Ok(()), Err)
}
+17 -7
View File
@@ -867,7 +867,7 @@ impl OssClient {
let key = normalize_internal_object_key(&request.object_key)?;
let target = build_object_url(&self.config.bucket, &self.config.endpoint, &key)
.map_err(|e| request_error(OssRequestOperation::Get, &e.to_string()))?;
let response = send_signed_request(
let mut response = send_signed_request(
client,
&self.config,
Method::GET,
@@ -886,14 +886,24 @@ impl OssClient {
format!("OSS GET Object 失败,状态码:{}", response.status()),
));
}
let bytes = response
.bytes()
.await
.map_err(|e| request_error_from_reqwest(OssRequestOperation::Get, e))?;
if bytes.len() > request.max_bytes {
if response
.content_length()
.is_some_and(|length| length > request.max_bytes as u64)
{
return Err(OssError::InvalidRequest("OSS 对象超过读取上限".to_string()));
}
Ok(bytes.to_vec())
let mut bytes = Vec::new();
while let Some(chunk) = response
.chunk()
.await
.map_err(|e| request_error_from_reqwest(OssRequestOperation::Get, e))?
{
if bytes.len().saturating_add(chunk.len()) > request.max_bytes {
return Err(OssError::InvalidRequest("OSS 对象超过读取上限".to_string()));
}
bytes.extend_from_slice(&chunk);
}
Ok(bytes)
}
pub async fn delete_object(
@@ -19,6 +19,7 @@ pub struct ErrorReportRecord {
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ErrorReportCreateRecordInput {
pub batch_id: String,
pub user_id: String,
@@ -33,6 +34,7 @@ pub struct ErrorReportCreateRecordInput {
pub first_source: Option<String>,
pub now_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ErrorReportListRecordInput {
pub status: Option<String>,
pub fingerprint: Option<String>,
@@ -40,6 +42,7 @@ pub struct ErrorReportListRecordInput {
pub limit: u32,
pub offset: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ErrorReportUpdateRecordInput {
pub batch_id: String,
pub status: String,