提交 9ea984a8 authored 作者: 陈泽健's avatar 陈泽健

feat(performance): 混合场景超时误读消除与脱敏前缀保留及业务断言指引

- 请求明细页 (RequestDetailPanel.vue):在 errors_only 采集模式下顶部展示黄色提示条,防止「全是超时」误读
- 报告页 (ReportPanel.vue):执行汇总补充展示明细采集模式(on/errors_only/off)
- 后端脱敏 (performance_executor.py):Authorization 头保留前 20 字符 + ***,便于核对 token 已携带,其余敏感字段仍整体脱敏
- 后端服务 (performance_service.py/schemas):报告摘要序列化输出 request_detail_enabled 字段
- 需求与问题处理文档:新增排查报告、执行计划、业务断言指引,更新 HANDOFF 交接记录
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 ae4b9c68
# HANDOFF — 性能测试模块会话交接文档
> **生成时间**: 2026-09-02
> **生成时间**: 2026-09-07
> **当前分支**: `platform-auto-test`
> **最近提交**: `03ee164c` docs(performance): HANDOFF 补充 5.60 部署 md5 复核与双 commit 记录(已同步 origin/platform-auto-test)
> **会话窗口**: 性能测试 — 混合场景(mix)执行失败修复:CSV 强制校验(无 CSV 不允许执行)+ 登录计入 api_summary 监控,已部署 5.60 并端到端验证通过
> **状态**: ✅ 修复已实现、部署 5.60、E2E 验收通过(A/B 拒绝 + C 实跑含「登录」行);**改动未提交**(见待办)
> **会话窗口(上一窗口)**: 修复 Java 进程 CPU 监控失真(ps 生命周期平均 → JSTAT 瞬时采样)`ad4163bb`
> **最近提交**: `0aa655a5` fix(performance): 混合场景CSV强制校验与登录监控记录+目标机监控开启与监控图表X轴修复(已同步 origin/platform-auto-test)
> **会话窗口**: 性能测试 — 目标机监控开启(TARGET_MONITOR_ENABLED=true + 运行前校验)+ 监控图表 X 轴修复(dataZoom/零尺寸 resize/空态自动展开),提交推送 `0aa655a5` 并部署 5.60(容器重建)
> **状态**: ✅ 已提交推送 + 部署 5.60 验证通过(env 生效 / 代码标记命中);**待真实混合场景任务端到端确认目标机曲线**
> **会话窗口(上一窗口)**: 2026-09-02 混合场景 CSV 强制校验 + 登录计入监控(该窗口改动随本窗口一并提交进 `0aa655a5`)
---
## ⚡ 最新会话更新(2026-09-02)— 混合场景执行失败修复:CSV 强制校验 + 登录计入监控 ✅
## ⚡ 最新会话更新(2026-09-07)— 目标机监控开启 + 监控图表 X 轴修复 ✅
### A. 会话背景
用户在 5.60 观察运行中的混合场景任务,反馈两个问题:
1. **监控页目标机资源无数据**(CPU/内存/磁盘曲线空白,空态提示「不可用」);
2. **执行机资源图显示异常**,X 轴被压成极窄一条。
另:混合场景运行后仍观察到 CSV 报错文案不精确(列映射配置错误时报的是账号数错误)。
### B. 根因
| # | 问题 | 根因 |
|---|------|------|
| 1 | 目标机无监控数据 | `deploy/docker-compose.yml` 未设置 `TARGET_MONITOR_ENABLED``config.py` 默认 `false``545ee9b4` 改的默认值)→ 容器内监控开关关闭,快照 `targetResource` 恒为空 |
| 2 | 代码无提醒 | 服务层运行前不检查监控开关,用户跑完全程才发现无数据 |
| 3 | X 轴窄条 | 折叠面板内 `echarts.init` 得到**零尺寸画布**,展开后未 `resize()`;且无 `dataZoom`,长时压测点位全挤在一屏 |
| 4 | 空态默默无数据 | 目标机面板持续无数据时不展开,用户看不到空态原因 |
| 5 | CSV 报错顺序 | `_run_mix()` 中列映射校验在 `per_vu_login` 分支内且账号数校验先于列校验,配置错误被数量错误掩盖 |
### C. 修复内容
| 文件 | 改动 |
|------|------|
| `deploy/docker-compose.yml` | 环境变量新增 `TARGET_MONITOR_ENABLED=true`(含根因注释) |
| `backend/app/services/performance_service.py` | `_validate_run_preconditions()` 增加目标机监控开关检查:开关关闭时任务启动前直接报错提示(避免白跑) |
| `backend/app/executors/performance_executor.py` | `_run_mix()` 列映射/缺列校验**先于**账号数校验(缺列是配置错误,优先报告);映射缺失 `logger.warning` 保留 |
| `frontend/src/views/performance/MonitorPanel.vue` | ① 资源/网络/目标机三组图增加 `dataZoom`(inside + slider),刷新时用 `getCurrentZoom()` 保留用户缩放窗口;② 零尺寸画布(width/height=0)时兜底 `resize()`;③ 图表更新改 `nextTick` 包装(等折叠面板 v-show 生效);④ 运行中连续 10 个快照(约 10s)无目标机数据 → 自动展开空态面板展示原因(每次连接重置);⑤ `handleWindowResize()` 监听窗口尺寸变化全部图表自适应;⑥ 空态文案补充「未开启 TARGET_MONITOR_ENABLED」 |
| `frontend/src/views/performance/TaskList.vue` | `handleRun()` mix 无 CSV 前置拦截(ElMessage.warning,不发请求);CSV 模板改为前端 XLSX 动态生成(`downloadCsvTemplate()`,不再依赖部署静态文件);schema + `types/performance.ts` 新增 `csvParameterizationEnabled` / `csvContent` / `perVuLogin` 三字段 |
| `backend/app/schemas/performance.py` | `PerformanceTaskListItem` 新增上述 3 字段(支撑前端拦截) |
### D. 提交推送 ✅(/GitCommit 三重门控)
- Commit `0aa655a5` fix(performance): 混合场景CSV强制校验与登录监控记录+目标机监控开启与监控图表X轴修复
- **10 个文件**(8 修改 + 2 新增文档),+655/-44,已推送 `origin/platform-auto-test`
- 本提交同时包含上一窗口(2026-09-02)未提交的 CSV 强制校验 + 登录监控改动
- 审查中排除:项目管理模块 14 个未跟踪文件(其他窗口半成品)、`backend/tests/test_screenshot_cleanup.py`**坏测试**,引用不存在的 `cleanup_screenshots_loop`,提交会弄挂 pytest 收集)、`backend/tmp/` + `tmp/` 临时产物
### E. 部署 5.60 ✅(脚本 `tmp/deploy_monitor_fix_560.py`)
1. 后端 3 文件 + docker-compose.yml + 前端 dist 94 文件上传;
2. **容器重建** `docker compose up -d app`(⚠️ env 变更必须重建,`restart` 不会重读 compose env —— 上一窗口用 `restart` 对代码生效但 env 类改动不适用);
3. 验证全过:`/health` 200;容器内 `TARGET_MONITOR_ENABLED='true'``_validate_run_preconditions`×3、`metrics_api_name`×6 代码标记命中;启动日志无 error/monitor 异常。
### F. 待办(交接给下一窗口)
| # | 任务 | 优先级 | 说明 |
|---|------|--------|------|
| 1 | **真实混合场景任务端到端确认** | P1 | 跑一次 mix 任务,监控页确认:目标机 CPU/内存/磁盘曲线出现、执行机资源图 X 轴正常且可缩放(会在 5.44 产生真实会议数据,需用户同意) |
| 2 | **修复/删除 `backend/tests/test_screenshot_cleanup.py`** | P1 | 坏测试:import 不存在的 `cleanup_screenshots_loop` / `start_screenshot_cleanup_loop`,任何 `pytest tests/` 收集即中断;5.60 启动日志同样报 `SCREENSHOT_RETENTION_DAYS` 属性缺失(同一未完成功能:Settings 缺配置 + cleanup_service 缺循环函数) |
| 3 | 清理 5.60 验证克隆任务 | P2 | 上一窗口遗留 A/B/C 三个 `*_A_no_csv` 等克隆任务 |
| 4 | 项目管理模块等 14 个未跟踪文件 | — | 属其他窗口工作,勿在本窗口提交 |
---
## ⚡ 会话更新(2026-09-02)— 混合场景执行失败修复:CSV 强制校验 + 登录计入监控 ✅
### A. 会话背景
......
# 执行计划:混合场景「全量超时」误读消除 —— 明细采集模式标注 + 脱敏前缀保留 + 业务断言
> 生成时间:2026-09-07
> 关联文档:`_问题处理_混合场景全量超时_请求明细token脱敏误读.md`
> 状态:📋 方案已定,待实施
> 性质:**体验优化,非缺陷修复**(本次排查结论:平台无缺陷,误读源于采集模式与脱敏展示)
---
## 问题概述
混合场景 100 并发执行完成后,报告页「察看结果树」看起来「全部超时」,
请求头 token 显示 `***`,用户误判为平台没传 token / 执行全部失败。
排查确认(详见问题处理文档):
1. 真实成功率 **90.7%**(2xx 11448 / 总 12615,4xx/5xx 均为 0);
2. 明细表只有 1166 条失败记录 —— 任务配置 `request_detail_enabled=errors_only`
**成功请求根本不入库**,明细列表天然「全是超时」;
3. `***``_desensitize()` 入库前脱敏,真实请求带有效 Bearer token
(11448 个 2xx + 0 个 401 为证)。
需要消除两类误读,并提升业务成功率真实度。
---
## 执行步骤
### Step 1: 请求明细页标注采集模式(errors_only / off / on)
**文件**`frontend/src/views/performance/RequestDetailPanel.vue`
**现状**:组件已读取采集模式(约 417 行
`enabled.value = res.request_detail_enabled || 'off'`),
仅在 `off` 时显示空态「请求详情采集未开启…」(11-14 行);
`errors_only` 模式**无任何提示**,用户看到列表全是失败即误读为「全部超时」。
**改动**:页面顶部(`enabled !== 'off'` 且有数据时)增加一条 `el-alert`
```vue
<!-- 采集模式提示:errors_only 下列表只含失败请求,防止「全是超时」误读(2026-09-07) -->
<el-alert
v-if="enabled === 'errors_only' && total > 0"
type="warning"
:closable="true"
show-icon
title="当前采集模式为「仅失败请求」(errors_only):此处仅展示超时/断言失败等失败样本,成功请求未入库。执行真实成功率请看报告页执行汇总(本次成功率 = 成功数 / 总请求数)。"
/>
```
同时把 `off` 空态文案补充采集模式说明(可选:
「可在任务配置中切换 全量采集(on) / 仅失败(errors_only) / 关闭(off)」)。
**验收标准**
- errors_only 任务明细页顶部出现黄色提示条;
- on / off 模式页面行为不变。
### Step 2: 报告页执行汇总补充采集模式标识
**文件**`frontend/src/views/performance/ReportPanel.vue`
**现状**:报告数据中已含 `request_detail_enabled`
`backend/app/models/performance.py:392` 序列化已输出),前端未展示。
**改动**:执行汇总信息卡片(状态/总请求/成功率附近)追加一个只读字段:
「请求明细采集:仅失败(errors_only)/ 全量(on)/ 关闭(off)」,
取值映射与 Step 1 一致。
**验收标准**:报告页可见本次执行的采集模式;三种取值均正确显示。
### Step 3: Authorization 脱敏保留前缀(可核对 token 已携带)
**文件**`backend/app/executors/performance_executor.py`
**现状**`_desensitize()`(约 2781-2810 行)对所有命中
`_SENSITIVE_KEYS`(含 authorization/token/password/secret/sign)的键整体替换 `"***"`
**改动**:dict 分支对 `authorization` 键特判 —— 保留前 20 字符 + `"***"`
其余敏感键维持整体脱敏:
```python
_SENSITIVE_PREFIX_KEEP = {"authorization"} # 保留前缀便于核对 token 已携带
@staticmethod
def _desensitize(data: Any) -> Any:
...
if isinstance(data, dict):
result = {}
for k, v in data.items():
kl = k.lower()
if any(s in kl for s in PerformanceExecutor._SENSITIVE_KEYS):
# Authorization 保留前 20 字符(如 "Bearer eyJhbGciOiJIUzI1NiIs"),
# 既防泄露又可核对请求确实携带 token(2026-09-07)
if any(s in kl for s in PerformanceExecutor._SENSITIVE_PREFIX_KEEP) \
and isinstance(v, str) and len(v) > 20:
result[k] = v[:20] + "***"
else:
result[k] = "***"
else:
result[k] = PerformanceExecutor._desensitize(v)
return result
```
> 注意:历史已入库的 `***` 明细不回填(无原始值);
> password / token / sign 等仍整体脱敏。
**验收标准**
- 新执行的明细中 `Authorization` 显示形如
`Bearer eyJhbGciOiJIUzI1NiIs***``password` 等仍为 `***`
- 5.60 部署后跑一次带明细采集的小任务验证。
### Step 4: 混合场景写接口业务断言指引(成功率真实度)
**现状**`ResultValidator` 已支持 `response_body` 断言
(JSON Path / 文本包含,`performance_executor.py:428-520`),
但当前混合场景任务未配置 —— 预约接口返回 `A0023 该时段已有会议安排`
(HTTP 200 业务失败)被计入成功,成功率虚高。
**改动**(配置指引 + 文档,不改代码):
1. 在混合场景任务编辑页为三个接口补断言:
- 预约 PUT / 模板创建 POST:
`{"type": "response_body", "path": "$.success", "operator": "equals", "value": true}`
- 模板查询 GET:
`{"type": "response_body", "path": "$.code", "operator": "equals", "value": "200"}`
2.`Docs/PRD/性能测试/` 使用指南补一节
「业务断言:HTTP 200 ≠ 业务成功」,给出上述示例与 A0023 案例。
**验收标准**
- 配置断言后执行,`A0023` 响应计入断言失败(fail_count / assertion_fail),
api_summary 成功率反映业务真实结果。
### Step 5: 压测方法论落地建议(输出给被测系统团队,非本仓库代码)
整理为结论反馈(已含在问题处理文档 §3.2 / §4.4):
1. `getTemplatePage` 大结果集/深分页性能排查(单发 58KB × 权重 40%);
2. 压测数据隔离:压测前清理模板测试数据,或压测租户使用独立 companyNumber;
3. 连接池水位核查(目标机监控 max_used_connections 93 / max 151);
4. 混合场景建议开启思考时间(think_time),贴近真实用户节奏。
**验收标准**:问题处理文档 + 本计划归档,结论可直接转发被测系统团队。
---
## 部署与验证
| 项 | 方式 |
|----|------|
| 前端 Step 1/2 | 本地 `npm run build` → scp dist/ → 5.60 volume 热更新 |
| 后端 Step 3 | scp `performance_executor.py``docker restart plat-auto-test-app` |
| 端到端验证 | 5.60 跑一次小并发 mix 任务(2 VU / 60s / 开明细采集),核对 Step 1-3 效果 |
## 回归清单
- [ ] errors_only 明细页提示条显示、关闭按钮可用
- [ ] on / off 模式明细页行为不变(无提示条 / 空态文案)
- [ ] 报告页汇总显示采集模式
- [ ] 新明细 Authorization 前缀保留、password 仍整体脱敏
- [ ] 配置业务断言后 A0023 计为失败
- [ ] 非 mix 场景(单接口/并发/长稳)回归无影响
---
*本文档由 Claude Code 生成*
......@@ -264,6 +264,29 @@ mix → <el-tag type="primary" size="small">混合场景 Mix</el-tag>
2. 长时间运行时如果前端 WS 断连,监控页自动切回放模式(已有机制)
3. 单次 8h 执行的内存/磁盘需在生产 5.60 提前确认(数据目录容量、快照膨胀)
### 5.4 业务断言指引:HTTP 200 ≠ 业务成功(2026-09-07 补充)
混合场景写接口在并发冲突时常见「HTTP 200 + 业务错误码」响应,
如预约接口返回 `{"success":false,"code":"A0023","message":"该时段已有会议安排,请重新选择"}`
无断言时这类响应计入成功,**成功率虚高**
**建议为每个接口配置业务断言**(任务编辑 → 接口 → 断言):
| 接口 | 断言配置 | 作用 |
|------|---------|------|
| 预约 PUT / 模板创建 POST | `response_body``$.success` equals `true` | A0023 等业务失败计入断言失败 |
| 模板查询 GET | `response_body``$.code` equals `"200"` | 业务码校验 |
| 通用兜底 | `status_code` equals `200` | HTTP 层校验 |
断言失败计入 `fail_count` / `error_type_assertion`
报告「接口级统计明细」成功率即反映业务真实结果。
> 排查案例:2026-09-07 混合场景 100 并发执行被误读为「全部超时」,
> 真相是 errors_only 采集模式只入库失败样本(真实成功率 90.7%)+ token 脱敏显示 `***`。
> 详见 `_问题处理_混合场景全量超时_请求明细token脱敏误读.md`。
> 同批压测还确认:持续满负荷下模板查询接口随测试数据膨胀逐步劣化(单发 254ms → 压测 avg 19s),
> **压测数据隔离(独立 companyNumber / 压测前清库)是保证结果可比的前提**。
---
## 六、验收标准
......
......@@ -2732,8 +2732,9 @@ class PerformanceExecutor:
"""
构建请求明细记录(JMeter 察看结果树,2026-08-25 新增)
脱敏规则:请求头/请求体中的 Authorization / password / token / secret / sign
等敏感字段在入库前替换为 `***`。
脱敏规则:请求头/请求体中的 password / token / secret / sign 等敏感字段
在入库前替换为 `***`;Authorization 头保留前 20 字符 + `***`
(便于核对请求确实携带 token,2026-09-07)。
响应体截断:默认保留前 8KB(防大响应撑爆数据库)。
错误消息截断:保留前 500 字符。
......@@ -2785,6 +2786,7 @@ class PerformanceExecutor:
将 dict/list/str 递归中的敏感字段值替换为 `***`。
敏感键名匹配(不区分大小写):authorization / password / token / secret / sign
及其子串组合(如 X-SIGN、access_token、client_secret)。
例外:authorization 值为长字符串时保留前 20 字符(核对 token 已携带)。
Args:
data: 请求头 / 请求体 / 响应头 / 响应体(任意可 JSON 序列化结构)
......@@ -2793,13 +2795,23 @@ class PerformanceExecutor:
Any: 脱敏后的数据
"""
_SENSITIVE_KEYS = ("authorization", "password", "token", "secret", "sign")
# Authorization 头保留前 20 字符(如 "Bearer eyJhbGciOiJIUzI1NiIs"),
# 既防泄露又可核对请求确实携带 token(2026-09-07 混合场景超时误读排查)
_PREFIX_KEEP_KEYS = ("authorization",)
if isinstance(data, dict):
return {
k: ("***" if any(s in k.lower() for s in _SENSITIVE_KEYS)
else PerformanceExecutor._desensitize(v))
for k, v in data.items()
}
result = {}
for k, v in data.items():
kl = k.lower()
if any(s in kl for s in _SENSITIVE_KEYS):
if (any(s in kl for s in _PREFIX_KEEP_KEYS)
and isinstance(v, str) and len(v) > 20):
result[k] = v[:20] + "***"
else:
result[k] = "***"
else:
result[k] = PerformanceExecutor._desensitize(v)
return result
if isinstance(data, list):
return [PerformanceExecutor._desensitize(v) for v in data]
if isinstance(data, str):
......
......@@ -517,6 +517,8 @@ class PerformanceReportSummary(BaseModel):
start_time: Optional[str] = None
end_time: Optional[str] = None
account_key: str = ""
# 请求明细采集模式(off/on/errors_only),报告页展示防止「全是超时」误读(2026-09-07)
request_detail_enabled: str = "off"
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
......
......@@ -688,6 +688,7 @@ class PerformanceService:
"start_time": task.start_time.isoformat() if task.start_time else None,
"end_time": task.end_time.isoformat() if task.end_time else None,
"account_key": task.account_key,
"request_detail_enabled": task.request_detail_enabled or "off",
},
"task_config": task_config,
"metrics": {
......@@ -818,6 +819,8 @@ class PerformanceService:
"start_time": execution.start_time.isoformat() if execution.start_time else None,
"end_time": execution.end_time.isoformat() if execution.end_time else None,
"account_key": task.account_key if task else "",
# 请求明细采集模式(off/on/errors_only),报告页展示防止「全是超时」误读(2026-09-07)
"request_detail_enabled": execution.request_detail_enabled or "off",
},
"task_config": task_config,
"metrics": {
......
......@@ -693,6 +693,8 @@ export interface PerformanceReportSummary {
startTime: string | null
endTime: string | null
accountKey: string | null
/** 请求明细采集模式(off / on / errors_only) */
requestDetailEnabled?: string
/** 场景类型 */
scenarioType?: PerfScenarioType
}
......
......@@ -188,6 +188,11 @@
<el-descriptions-item label="开始时间 Start">{{ formatTime(report.summary.startTime) }}</el-descriptions-item>
<el-descriptions-item label="结束时间 End">{{ formatTime(report.summary.endTime) }}</el-descriptions-item>
<el-descriptions-item label="实际时长 Duration">{{ report.summary.durationActual != null ? report.summary.durationActual.toFixed(1) + 's' : '-' }}</el-descriptions-item>
<el-descriptions-item label="明细采集 Detail Mode">
<el-tag v-if="report.summary.requestDetailEnabled === 'on'" type="success" size="small">全量采集 on</el-tag>
<el-tag v-else-if="report.summary.requestDetailEnabled === 'errors_only'" type="warning" size="small">仅失败 errors_only</el-tag>
<el-tag v-else type="info" size="small">关闭 off</el-tag>
</el-descriptions-item>
</el-descriptions>
<!-- 事务标记(仅事务任务) -->
......
......@@ -11,12 +11,22 @@
<!-- 未开启采集 且 确实无数据(避免后端 flag 误置时掩盖已采集数据) -->
<el-empty
v-if="enabled === 'off' && total === 0 && !loading"
description="请求详情采集未开启,请在任务配置中开启「请求详情采集」"
description="请求详情采集未开启,可在任务配置中开启「请求详情采集」(支持全量 on / 仅失败 errors_only)"
:image-size="80"
/>
<!-- 有数据时强制显示(后端 flag 误置时仍显示数据) -->
<template v-else-if="items.length > 0 || total > 0">
<!-- 采集模式提示:errors_only 下列表只含失败请求,防止「全是超时/失败」误读(2026-09-07) -->
<el-alert
v-if="enabled === 'errors_only'"
type="warning"
:closable="false"
show-icon
class="mode-alert"
title="当前采集模式为「仅记录失败请求」(errors_only):此列表仅展示超时、断言失败、异常等失败样本,成功请求未入库。执行真实成功率请以报告页顶部的「执行汇总」为准。"
/>
<!-- 筛选栏 -->
<div class="filter-bar">
<el-select
......@@ -460,6 +470,10 @@ onMounted(() => {
padding: 4px 0;
}
.mode-alert {
margin-bottom: 12px;
}
.filter-bar {
display: flex;
align-items: center;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论