diff --git a/app/core/compare_alert_worker.py b/app/core/compare_alert_worker.py index 80e3d21..dfd5001 100644 --- a/app/core/compare_alert_worker.py +++ b/app/core/compare_alert_worker.py @@ -89,6 +89,16 @@ def _fmt_stuck(sp: trace_stuck.StuckPoint) -> str: return s +def _fmt_last(sp: trace_stuck.StuckPoint) -> str: + """末帧路径(failed/兜底):环节·页面 + 末段停留时长,不显总帧数(总帧数配末段时长会误导)。 + dwell_ms 为 None(缺 ts/时钟回退) → 只显环节。""" + s = sp.label() + if sp.dwell_ms is not None: + sec = round(sp.dwell_ms / 1000) + s += f" 停留{sec}s" if sec >= 1 else " 停留<1s" + return s + + def build_hits( records: list, *, @@ -134,9 +144,9 @@ def build_hits( cancelled_ms_threshold=cancelled_ms_threshold, cancelled_step_threshold=cancelled_step_threshold, ) - # 没卡点但命中(超阈值放弃)→ 用末帧标「退出前在哪屏」(平台·环节·页面) + # 没卡点但命中(超阈值放弃)→ 末帧标「退出前在哪屏 + 在那屏停多久」(dwell-only,不显总帧数) if hit is not None and res is not None and res.last is not None: - hit = _dc_replace(hit, stuck_point=res.last.label()) + hit = _dc_replace(hit, stuck_point=_fmt_last(res.last)) if hit is not None: hits.append(hit) else: @@ -159,9 +169,9 @@ def build_hits( sp = trace_stuck.last_step(td, max_tail=max_tail) reads += 1 if sp is not None: - # failed 是「末帧停在哪」:last_step 返回的 frames 是该平台总帧数(非"卡住"帧数)、 - # stuck_ms=None,带上帧数/时长会误导,故只用 label() 显示环节 - hit = _dc_replace(hit, stuck_point=sp.label()) + # failed 是「末帧停在哪 + 在那屏停多久」:只显环节·页面 + 末段停留 dwell, + # 不显总帧数(总帧数配末段时长会误导);缺 ts 时 _fmt_last 自动退化为只显环节 + hit = _dc_replace(hit, stuck_point=_fmt_last(sp)) if hit is not None: hits.append(hit) return hits diff --git a/tests/test_compare_alert_stuck_worker.py b/tests/test_compare_alert_stuck_worker.py index d3a835d..0cab10b 100644 --- a/tests/test_compare_alert_stuck_worker.py +++ b/tests/test_compare_alert_stuck_worker.py @@ -2,7 +2,7 @@ import json from pathlib import Path -from app.core.compare_alert_worker import _fmt_stuck, build_hits +from app.core.compare_alert_worker import _fmt_last, _fmt_stuck, build_hits from app.services.trace_stuck import StuckPoint @@ -28,6 +28,15 @@ def _frame(pdir: Path, idx: int, step: str, page: str) -> None: ) +def _frame_ts(pdir: Path, idx: int, step: str, page: str, ts: str) -> None: + pdir.mkdir(parents=True, exist_ok=True) + body = {"pipeline_step": step, "detected_page": page, "timestamp": ts, + "windows": [{"n": ["x" * 200]}]} + (pdir / f"step_{idx:03d}.json").write_text( + json.dumps(body, ensure_ascii=False), encoding="utf-8" + ) + + _KW = dict( stuck_threshold=15, max_tail=40, max_trace_reads=30, cancelled_ms_threshold=90000, cancelled_step_threshold=30, @@ -144,3 +153,54 @@ def test_fmt_stuck_with_ms(): def test_fmt_stuck_without_ms(): sp = StuckPoint(platform="meituan", pipeline_step="add_one_dish", frames=110, stuck_ms=None) assert _fmt_stuck(sp) == "美团·加菜 110帧" + + +# ---- _fmt_last 单测(末帧路径:环节·页面 + 停留,不显总帧数)---- + +def test_fmt_last_with_dwell(): + sp = StuckPoint("meituan", "checkout", frames=480, + detected_page="checkout_page", dwell_ms=8000) + assert _fmt_last(sp) == "美团·结算·checkout_page 停留8s" + + +def test_fmt_last_sub_second(): + sp = StuckPoint("meituan", "checkout", frames=480, + detected_page="checkout_page", dwell_ms=300) + assert _fmt_last(sp) == "美团·结算·checkout_page 停留<1s" + + +def test_fmt_last_without_dwell(): + sp = StuckPoint("meituan", "checkout", frames=480, + detected_page="checkout_page", dwell_ms=None) + assert _fmt_last(sp) == "美团·结算·checkout_page" + + +# ---- 末帧路径带 dwell 集成 ---- + +def test_failed_stuck_point_has_dwell(tmp_path): + # failed 末帧 5 帧都在 checkout,ts :00→:08 → stuck_point 附「停留8s」 + p = tmp_path / "20260807_f" / "meituan" + for i in range(5): + _frame_ts(p, i, "checkout", "checkout_page", + f"2026-08-07T12:00:{i * 2:02d}.000000") + rec = _Rec(status="failed", fail_reason="启动超时", + trace_url="https://x/traces/20260807_f/") + hits = build_hits([rec], work_log_dir=str(tmp_path), **_KW) + assert len(hits) == 1 + assert hits[0].alert_type == "T2" + assert hits[0].stuck_point == "美团·结算·checkout_page 停留8s" + + +def test_cancelled_fallback_stuck_point_has_dwell(tmp_path): + # cancelled 超阈值(95s)但末段 5<15 不卡死 → 兜底,末帧带 ts → 附「停留8s」 + p = tmp_path / "20260807_c" / "eleme" + _frame_ts(p, 0, "set_address", "home", "2026-08-07T12:00:00.000000") + for i in range(1, 6): # enter_store 5 帧 :02→:10 + _frame_ts(p, i, "enter_store", "store", + f"2026-08-07T12:00:{i * 2:02d}.000000") + rec = _Rec(status="cancelled", trace_url="https://x/traces/20260807_c/", + total_ms=95000, step_count=40) + hits = build_hits([rec], work_log_dir=str(tmp_path), **_KW) + assert len(hits) == 1 + assert hits[0].alert_type == "T5" + assert hits[0].stuck_point == "饿了么·进店·store 停留8s"