埋点分析系统 测试与优化方案

📑 目录
  1. 一、测试结论
  2. 二、P0 严重问题(必须修复)
  3. 三、P1 重要问题(建议修复)
  4. 四、P2 一般问题(建议修复)
  5. 五、P3 建议问题(可选优化)
  6. 六、搜索接口专项测试(补充)
  7. 七、执行优先级路线图
  8. 七、验证清单

埋点分析系统 测试与优化方案

测试日期: 2026-07-30

测试环境: http://127.0.0.1:5000

测试凭据: admin / clklog

测试范围: 6个前端页面 / 20个API端点 / 后端代码 / 前端代码

文档用途: 可喂给AI执行的优化清单


一、测试结论

维度状态说明
前端路由6/6 正常概览、事件、元素、用户、页面、明细均正常跳转
API认证正常登录/登出/Token校验正确
核心API14/14 返回200数据正确返回
数据一致性良好projects.usercount与users/kpi.totalusers一致(29026)
分页功能正确events/list total=166, detail/events total=255891

发现的问题统计

严重程度数量涉及模块
P0-严重2后端flow/overview数据全0, 前端错误拦截器
P1-重要3留存率trend为null, 缺少参数校验, 401响应Body为空
P2-一般54个页面无Loading/Error状态, ElementsContent未处理allSettled失败
P3-建议4导出限制, 死代码, 硬编码, alert弹窗

二、P0 严重问题(必须修复)

P0-1: flow/overview API 数据全部为0

文件: backend/api/flow.py 第22行

问题: flowmetrics() 函数中 AND eventsessionid != '' 条件过滤过于严格。ClickHouse 中 NULL != '' 结果为 NULL(非 TRUE),导致大量不含 eventsessionid 的行被排除,查询返回 0 行。

影响: 概览看板的"流量概览"区域所有指标(PV/UV/IP/访问次数/平均PV/跳出率)全部显示为0。

修复代码:

# 修改 backend/api/flow.py 第22行
# 原代码:
AND event_session_id != ''

# 修改为:
AND event_session_id != '' AND event_session_id IS NOT NULL

或者更合理的做法是直接去掉该过滤条件,因为流量指标不需要以 session 为前提:

# 删除整个 AND event_session_id != '' 条件
# 将第22行改为:
# (删除该行)

验证方法: 重启后端后,访问概览看板,确认"流量概览"区域显示正常数值。


P0-2: 前端错误拦截器返回 String 而非 Error 对象

文件: frontend/src/api/index.ts 第60-78行

问题: 响应拦截器和错误拦截器中 Promise.reject(msg) 直接传递字符串,导致所有调用方 .catch(err => err.message) 获取不到真实错误信息,始终显示默认文本。

影响: 所有页面的错误提示都是通用文本(如"数据加载失败"),用户无法知道具体错误原因。

修复代码:

// 修改 frontend/src/api/index.ts

// 第60-66行 响应拦截器
// 原代码:
const msg = response.data?.message || '请求失败'
return Promise.reject(msg)

// 修改为:
const msg = response.data?.message || '请求失败'
return Promise.reject(new Error(msg))

// 第68-79行 错误拦截器
// 原代码:
const msg = error.response?.data?.message || error.message || '网络错误'
return Promise.reject(msg)

// 修改为:
const msg = error.response?.data?.message || error.message || '网络错误'
return Promise.reject(new Error(msg))

验证方法: 修改后故意触发一个API错误(如临时改错端点),确认 toast 显示的是具体错误信息而非默认文本。


三、P1 重要问题(建议修复)

P1-1: 留存率趋势(trend)为 null

文件: backend/api/users.py 第148-154行

问题: 当上一队列周期(14天前)没有新增用户时,prevcohort = 0,导致 prevretentionrate = None,进而 retentiontrend = null。前端可能因此无法正确显示留存率的趋势箭头。

修复代码:

# 修改 backend/api/users.py 第152-154行
# 原代码:
retention_trend = None
if prev_retention_rate is not None and prev_retention_rate > 0:
    retention_trend = format_percent(safe_divide(retention_rate - prev_retention_rate, max(prev_retention_rate, 1)))

# 修改为:
retention_trend = None
if prev_retention_rate is not None:
    if prev_retention_rate > 0:
        retention_trend = format_percent(safe_divide(retention_rate - prev_retention_rate, prev_retention_rate))
    else:
        # 上一周期留存率为0但当前有留存,视为增长
        retention_trend = 100.0 if retention_rate > 0 else 0.0

验证方法: 重启后端,访问用户分析页面,确认留存率KPI卡片显示趋势箭头(而非空白)。


P1-2: API 缺少参数校验

文件: backend/api/overview.pyevents.pyusers.py 等所有API文件

问题: 缺少 startdate/enddate 或传入无效日期格式时,API返回200而非400,只是返回空数据。前端无法有效提示用户。

修复代码(在 backend/utils.py 中添加校验函数):

# 在 backend/utils.py 中新增函数
from datetime import datetime

def validate_date_params(start_date, end_date):
    """校验日期参数,返回 (错误信息, None) 或 (None, (start, end))"""
    if not start_date or not end_date:
        return '缺少日期参数 start_date 或 end_date', None
    try:
        start = datetime.strptime(start_date, '%Y-%m-%d')
        end = datetime.strptime(end_date, '%Y-%m-%d')
        if start > end:
            return '开始日期不能晚于结束日期', None
        return None, (start, end)
    except ValueError:
        return '日期格式无效,需为 YYYY-MM-DD', None

然后在各API端点开头调用:

# 在每个API函数开头添加
from utils import validate_date_params

start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
err, _ = validate_date_params(start_date, end_date)
if err:
    return error_response(err, code=400), 400

验证方法: 访问 http://127.0.0.1:5000/api/overview/kpi?start_date=abc 应返回400错误。


P1-3: 401响应Body为空

文件: backend/api/decorators.py 第68-69行、backend/api/auth.py

问题: 未认证访问时返回401状态码但Body为空,前端无法解析JSON格式的错误信息。

修复代码(确保Flask返回JSON):

# 修改 backend/app.py 或配置文件,添加全局错误处理器
from flask import jsonify

@app.errorhandler(401)
def unauthorized(error):
    return jsonify({'code': 401, 'message': '未登录或登录已过期,请重新登录', 'data': None}), 401

验证方法: 不带Token访问 http://127.0.0.1:5000/api/overview/kpi 应返回包含JSON Body的401。


四、P2 一般问题(建议修复)

P2-1: 4个页面无 Loading 状态

文件:

  • frontend/src/pages/Users.tsx
  • frontend/src/pages/Events.tsx
  • frontend/src/pages/ElementsContent.tsx
  • frontend/src/pages/Screens.tsx

问题: 页面初始渲染时数据为空,显示 '0' 或空白图表,用户无法区分"正在加载"和"无数据"。

修复方案: 为每个页面添加 loading 状态变量,在数据请求前设为 true,请求完成后(无论成功失败)设为 false,渲染时显示 组件。

示例代码(Users.tsx):

// 在 Users.tsx 中添加
const [loading, setLoading] = useState(true)

// 修改 useEffect
useEffect(() => {
  setLoading(true)
  Promise.allSettled([
    usersApi.kpi({ start_date: startDate, end_date: endDate }),
    usersApi.trend({ start_date: startDate, end_date: endDate }),
    // ... 其他请求
  ]).then((results) => {
    // 处理数据...
  }).finally(() => {
    setLoading(false)
  })
}, [startDate, endDate, projects])

// 在渲染中添加
if (loading) return <Loading text="加载中..." />

对其他3个页面(Events.tsx、ElementsContent.tsx、Screens.tsx)做相同修改。


P2-2: 4个页面静默吞噬错误

文件: 同 P2-1

问题: 所有API错误被 .catch(() => {}) 静默吞噬,用户无法感知数据加载失败。

修复方案: 添加 error 状态,在 .catch() 中设置错误信息,渲染时显示错误提示。

示例代码:

const [error, setError] = useState<string | null>(null)

// 在每个 .catch() 中
.catch((err: Error) => {
  setError(err.message || '数据加载失败')
  toast.error(err.message || '数据加载失败')
})

// 渲染
if (error && !loading) {
  return <Empty icon="error" title="加载失败" description={error}>
    <Button onClick={() => window.location.reload()}>重试</Button>
  </Empty>
}

P2-3: ElementsContent 未处理 Promise.allSettled 失败

文件: frontend/src/pages/ElementsContent.tsx

问题: 使用了 Promise.allSettled 但未检查 status === 'rejected',失败的请求静默被忽略。

修复代码:

const [results] = await Promise.allSettled([...])
const failedCount = results.filter(r => r.status === 'rejected').length
if (failedCount > 0) {
  toast.warning(`${failedCount} 个数据模块加载失败,部分数据可能不完整`)
}
// 处理成功的请求
const [kpiResult, trendResult, ...] = results
if (kpiResult.status === 'fulfilled') {
  setKpi(kpiResult.value.data)
}

P2-4: Users.tsx 和 Screens.tsx 缺少 projects 依赖

文件:

  • frontend/src/pages/Users.tsx 第33行
  • frontend/src/pages/Screens.tsx 第33行

问题: useEffect 依赖数组中缺少 projects,导致切换项目后数据不更新。

修复代码:

// Users.tsx 第33行
// 原代码:
}, [startDate, endDate])

// 修改为:
}, [startDate, endDate, projects])

// Screens.tsx 第33行
// 原代码:
}, [startDate, endDate, keyword, page])

// 修改为:
}, [startDate, endDate, keyword, page, projects])

验证方法: 在侧边栏切换项目后,确认用户分析和页面路径页面数据正确刷新。


P2-5: api/index.ts 中 401 处理不一致

文件: frontend/src/api/index.ts

问题:

  • 响应拦截器(第60-66行):code === 50008 || code === 401 时清除token但不跳转
  • 错误拦截器(第68-78行):status === 401 时全量跳转

两者行为不一致,可能导致某些401场景下不跳转登录页。

修复代码:

// 统一在响应拦截器中处理
http.interceptors.response.use(
  (response) => {
    const { code, message } = response.data
    if (code === 50008 || code === 401) {
      localStorage.removeItem('tracker_token')
      localStorage.removeItem('tracker_user')
      window.location.href = '/login'
      return Promise.reject(new Error(message || '登录已过期'))
    }
    return response
  },
  (error) => {
    if (error.response?.status === 401) {
      localStorage.removeItem('tracker_token')
      localStorage.removeItem('tracker_user')
      window.location.href = '/login'
    }
    const msg = error.response?.data?.message || error.message || '网络错误'
    return Promise.reject(new Error(msg))
  }
)

五、P3 建议问题(可选优化)

P3-1: CSV导出硬编码 LIMIT 10000

文件: backend/api/detail.py 第174行

问题: 导出上限固定为10000条,无法导出全部数据,且用户无感知。

修复方案:

# 方案A: 增加 limit 参数,允许前端指定
max_export = min(int(request.args.get('limit', 10000)), 50000)
params['limit'] = max_export

# 方案B: 移除 limit 限制,由数据库查询性能决定
# 删除第174行: params['limit'] = 10000

同时在前端导出按钮旁显示提示:"单次导出最多 50000 条"。


P3-2: csv_escape 函数不完善

文件: backend/api/detail.py 第7-11行

问题: 表头行未使用 csv_escape,且数据行的转义逻辑不严谨。

修复代码:

import csv
import io

def export_events():
    # 使用 Python 标准库 csv 模块替代手动拼接
    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow(headers)  # 自动处理转义
    for row in rows:
        writer.writerow([str(row.get(col, '')) for col in columns])
    
    csv_content = output.getvalue()
    output.close()
    return Response(
        csv_content,
        mimetype='text/csv',
        headers={'Content-Disposition': f'attachment; filename={filename}.csv'}
    )

P3-3: decorators.py 是死代码

文件: backend/api/decorators.py

问题: withdaterangeandproject 装饰器未被任何API端点使用,所有端点都是手动调用 getdaterangegetprojectfilterparamsgettable_name

修复方案:

  • 方案A: 删除 decorators.py 文件,清理死代码
  • 方案B: 将装饰器改为函数调用,统一各端点的样板代码

推荐方案A,保持代码简洁。


P3-4: Events.tsx filterTabs 硬编码

文件: frontend/src/pages/Events.tsx

问题: 事件筛选标签硬编码为 ['全部', '$AppClick', '$AppViewScreen', '$SignUp'],无法动态展示所有事件类型。

修复方案: 从 /api/events/types API 动态获取事件类型列表,渲染筛选标签。

const [eventTypes, setEventTypes] = useState<string[]>([])

useEffect(() => {
  eventsApi.types({ start_date: startDate, end_date: endDate })
    .then(res => setEventTypes(res.data || []))
    .catch(() => {})
}, [startDate, endDate])

// 渲染
{['全部', ...eventTypes.slice(0, 5)].map(type => (
  <button key={type} onClick={() => setActiveFilter(type)}>{type}</button>
))}

P3-5: App.tsx 使用 alert() 弹窗

文件: frontend/src/App.tsx

问题: handleEnvChange 中使用 alert(res.message) 弹窗报错,用户体验差。

修复代码:

// 原代码:
alert(res.message)

// 修改为:
toast.error(res.message || '环境切换失败')

P3-6: Detail.tsx 导出 try/catch 无效

文件: frontend/src/pages/Detail.tsx 第56-66行

问题: window.open() 不会抛异常,try/catch 块是死代码。

修复代码:

const handleExport = async () => {
  setExporting(true)
  try {
    // 改为使用 fetch + blob 下载,可以捕获错误
    const token = localStorage.getItem('tracker_token')
    const response = await fetch(`/api/detail/export?${params}`, {
      headers: { 'Authorization': `Bearer ${token}` }
    })
    if (!response.ok) throw new Error('导出失败')
    const blob = await response.blob()
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `事件明细_${Date.now()}.csv`
    a.click()
    URL.revokeObjectURL(url)
    toast.success('导出成功')
  } catch (e: any) {
    toast.error(e.message || '导出失败')
  } finally {
    setExporting(false)
  }
}

P3-7: health 接口 data 为 null

文件: backend/app.py

问题: /api/health 返回 {"code":0,"data":null,"message":"ok"},data 为 null 与其他API格式不一致。

修复代码:

@app.route('/api/health')
def health():
    return success_response({'status': 'running', 'timestamp': datetime.now().isoformat()})

六、搜索接口专项测试(补充)

测试范围

对系统中所有支持搜索/筛选的API进行了33个测试用例的全面测试,覆盖7个维度。

测试结果: 33/33 通过,发现3个新问题。

测试维度用例数通过发现问题
事件分析搜索770
元素分析搜索440
页面路径搜索550
数据明细搜索+筛选10100
概览搜索相关222(数据质量)
分页边界测试441(参数校验)
日期边界测试330

搜索功能验证结果

API端点搜索参数搜索字段结果
events/listkeywordevent, screenname, elementcontent, elementtype, elementname正常
events/listevent_typeevent正常,筛选结果准确
events/distributionevent_typeevent正常
elements/top-contentskeywordelementcontent, elementname正常
screens/listkeywordscreen_name, title (大小写不敏感)正常
detail/eventskeywordevent, screenname, elementcontent, elementtype, elementname, distinct_id正常
detail/eventsevent_typeevent正常,筛选结果准确
detail/eventsuser_iddistinct_id (LIKE模糊)正常
detail/eventsdevicemodel (LIKE模糊)正常
detail/eventsprovinceprovince (精确匹配)正常
detail/events组合筛选event_type + province正常

安全性测试

测试项结果
SQL注入 (events/list)参数化查询阻止,返回0条
SQL注入 (detail/events)参数化查询阻止,返回0条
特殊字符搜索未报错,正常处理

新发现问题

P2-6: page=0 和 page_size=0 未校验

文件: backend/api/events.py 第105-106行、backend/api/detail.py 第23-24行

问题: 当 page=0 时,OFFSET = (0-1)*pagesize = -pagesize,ClickHouse会返回空结果而非报错。当 page_size=0 时,LIMIT 0 也返回空结果。用户无法区分"无数据"和"无效分页参数"。

测试结果:

detail/events (page=0)     -> total=256185, returned=0 (page=0, 返回空列表)
detail/events (page_size=0) -> total=256185, returned=0 (page_size=0, 返回空列表)

修复代码:

# 在所有分页API开头添加校验
page = max(int(request.args.get('page', 1)), 1)        # 最小为1
page_size = max(int(request.args.get('page_size', 10)), 1)  # 最小为1
page_size = min(page_size, 200)  # 最大200,防止过大查询

或返回400错误:

page = int(request.args.get('page', 1))
page_size = int(request.args.get('page_size', 10))
if page < 1:
    return error_response('page参数必须大于0', code=400), 400
if page_size < 1 or page_size > 200:
    return error_response('page_size参数必须在1-200之间', code=400), 400

P3-8: flow/search-words 数据质量问题

文件: backend/api/flow.py 第137-173行

问题: 搜索词Top10的第一名是 "url的domain解析失败"(71个用户),这是数据采集层面的错误信息被当作搜索关键词存储了。

测试结果:

flow/search-words -> count=1, top1=url的domain解析失败 (users=71)

影响: 概览看板的"搜索词 TOP10"区域展示的是错误数据。

修复方案:

# 方案A: 后端过滤掉明显非搜索词的数据
query = f"""
    SELECT
        latest_search_keyword as keyword,
        count(distinct distinct_id) as user_count,
        count(*) as pv
    FROM {table}
    WHERE stat_date BETWEEN %(start_date)s AND %(end_date)s
      {pf_sql}
      AND latest_search_keyword != ''
      AND latest_search_keyword NOT LIKE '%解析失败%'
      AND latest_search_keyword NOT LIKE '%error%'
      AND latest_search_keyword NOT LIKE '%失败%'
    GROUP BY keyword
    ORDER BY user_count DESC
    LIMIT 10
"""

P3-9: flow/source-websites 返回空数据

文件: backend/api/flow.py 第176-212行

问题: 来源网站Top10返回空列表(count=0),说明 latestreferrerhost 字段在数据中全部为空。

测试结果:

flow/source-websites -> count=0

影响: 概览看板的"来源网站 TOP10"区域无数据展示。

排查建议: 检查埋点SDK是否正确采集 latestreferrerhost 字段,或数据入库时该字段被截断。


七、执行优先级路线图

第一阶段:紧急修复(1-2小时)

□ P0-1: 修复 flow/overview 数据全为0
□ P0-2: 修复前端错误拦截器返回类型
□ P2-4: 修复 Users.tsx/Screens.tsx 缺少 projects 依赖

第二阶段:核心优化(2-3小时)

□ P1-1: 修复留存率趋势 null
□ P1-2: 添加参数校验
□ P1-3: 修复401响应Body为空
□ P2-1: 4个页面添加 Loading 状态
□ P2-2: 4个页面添加 Error 状态
□ P2-6: 分页参数 page/page_size 边界校验

第三阶段:体验优化(1-2小时)

□ P2-3: 处理 Promise.allSettled 失败
□ P2-5: 统一401处理
□ P3-1: CSV导出限制优化
□ P3-2: csv_escape 使用标准库
□ P3-5: alert 改为 toast
□ P3-6: 导出使用 blob 下载

第四阶段:代码清理(1小时)

□ P3-3: 删除 decorators.py 死代码
□ P3-4: 事件类型动态加载
□ P3-7: health 接口格式统一
□ P3-8: 过滤搜索词中的错误数据
□ P3-9: 排查来源网站字段为空问题

七、验证清单

修复完成后,按以下清单逐一验证:

  • [ ] 概览看板 → 流量概览区域显示正常数值(PV/UV/IP等)
  • [ ] 概览看板 → 其他KPI卡片正常显示
  • [ ] 事件分析 → 事件分布图、趋势对比图正常渲染
  • [ ] 事件分析 → 事件列表分页正常
  • [ ] 事件分析 → 筛选标签切换正常
  • [ ] 元素分析 → KPI卡片、趋势图、热力图正常(含 Loading 状态)
  • [ ] 用户分析 → KPI卡片、留存率趋势箭头正常(含 Loading 状态)
  • [ ] 用户分析 → 切换项目后数据刷新
  • [ ] 页面路径 → 流程图、列表、入口/退出页面正常(含 Loading 状态)
  • [ ] 页面路径 → 切换项目后数据刷新
  • [ ] 数据明细 → 筛选、搜索、分页正常
  • [ ] 数据明细 → 导出按钮触发CSV下载
  • [ ] 数据明细 → 组合筛选(事件+省份+设备)正常
  • [ ] 数据明细 → 用户ID搜索正常
  • [ ] 事件分析 → 关键词搜索结果准确
  • [ ] 事件分析 → event_type筛选结果准确
  • [ ] 元素分析 → 关键词搜索结果准确
  • [ ] 页面路径 → 关键词搜索结果准确
  • [ ] 页面路径 → 大小写不敏感搜索正常
  • [ ] 分页参数 → page=0时返回400或修正为1
  • [ ] 分页参数 → page_size=0时返回400或修正为1
  • [ ] SQL注入 → 搜索参数被参数化查询阻止
  • [ ] 无Token访问 → 返回401 + JSON Body
  • [ ] 缺少日期参数 → 返回400错误
  • [ ] 无效日期格式 → 返回400错误
  • [ ] 环境切换 → toast提示而非alert弹窗
  • [ ] 登出/登录 → 正常跳转

文档生成时间: 2026-07-30

测试覆盖: 6个前端页面、20个API端点、14个后端Python文件、10个前端TSX文件、33个搜索接口测试用例

问题总计: P0×2 + P1×3 + P2×6 + P3×9 = 20个问题