修复错误报告审查中的次要问题
修复后台错误报告刷新竞态、下载并发与查询参数构建复用。 修复错误报告清理分页、归档大小限制、分页总数和下载响应头异常。 修复 OSS 流式读取上限与 Codex 高频调试日志阻塞。 同步错误报告技术方案与项目决策记录,并在 review.txt 保留待决 breaking 项。
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user