Files
social-app/docs/plans/2026-04-01-analytics-design.md
T

18 KiB
Raw Blame History

Analytics 数据埋点与可视化方案

1. 概述

为 Flutter 移动应用实现数据埋点系统,采集用户行为数据,通过异步方式发送至后端存储,并提供可视化网站展示。

目标

  • 监测每日用户登录时间
  • 记录 Agent 对话次数
  • 统计每个页面的点击次数和停留时长
  • 统一数据格式,参照 OpenTelemetry 简化版规范
  • 数据持久化到本地文件,供可视化网站读取

2. 系统架构

┌─────────────────┐    异步 POST     ┌─────────────────┐   taskiq    ┌─────────────────────────┐
│   Flutter App   │ ───────────────► │   FastAPI       │ ──────────► │ backend/data/analytics/ │
│   (埋点 SDK)     │   /v1/analytics  │   /v1/analytics │   async     │ {YYYY-MM-DD}.jsonl      │
└─────────────────┘                 └─────────────────┘              └─────────────────────────┘
                                    │     │
                                    │     │ taskiq worker (general queue)
                                    ▼     ▼
                              ┌─────────────────┐
                              │   Redis Queue   │
                              └─────────────────┘

3. 数据格式规范

3.1 统一事件信封结构

所有埋点事件使用统一顶层结构:

{
  "event_id": "01JQ2G5Q3N6Y5N8R7M4K2P1T9A",
  "event_type": "session.login",
  "timestamp": "2026-04-01T10:30:00.123Z",

  "user_id": "user_123",
  "device_id": "install_a93f5d7c21",
  "session_id": "sess_7c2d5e8f91",

  "platform": "android",
  "app_version": "1.0.0",
  "app_build": "100",
  "env": "prod",

  "page_name": "login",
  "trace_id": "trace_8d2f6c1a",
  "request_id": null,

  "attributes": {},
  "metrics": {},

  "context": {
    "network_type": "wifi",
    "os_version": "Android 14",
    "device_model": "Xiaomi 13",
    "locale": "zh-CN",
    "timezone": "Asia/Taipei"
  }
}

3.2 顶层字段定义

字段 类型 必填 示例 说明
event_id string 01JQ2G... 事件唯一 ID,ULID/UUID,用于幂等去重
event_type string page.view 事件类型,dot 命名法
timestamp string 2026-04-01T10:30:00.123Z 事件发生时间,UTC ISO8601
user_id string user_123 用户 ID(必填)
device_id string install_xxx 设备标识
session_id string sess_xxx App 一次启动会话 ID
platform string android android / ios / web
app_version string 1.0.0 App 版本号
app_build string | null 100 构建号
env string prod dev / staging / prod
page_name string | null home 当前页面名
trace_id string | null trace_xxx 用于串联日志、接口、错误
request_id string | null req_xxx 与某次请求关联时可传
attributes object {} 离散属性、分类信息
metrics object {} 可聚合数值指标
context object {...} 客户端环境上下文

3.3 attributes 规范

只放离散属性、分类信息、上下文标签。

允许内容: string, boolean, number, null, 一层简单 object 或 string list

适合放这里:

  • method: "password"
  • page_from: "home"
  • conversation_id: "conv_123"
  • element_id: "send_button"
  • logout_reason: "manual"

不适合放这里(应进 metrics):

  • 停留时长、点击次数、响应时间、消息数、会话时长

3.4 metrics 规范

只放数值型、可聚合、可统计指标。

允许内容: int, float

适合放这里:

  • stay_duration_ms: 停留时长(毫秒)
  • click_count: 点击次数
  • response_time_ms: 响应耗时
  • message_count: 消息数量
  • session_duration_s: 会话时长(秒)

3.5 context 规范

通用环境上下文:

{
  "network_type": "wifi",
  "os_version": "Android 14",
  "device_model": "Xiaomi 13",
  "locale": "zh-CN",
  "timezone": "Asia/Taipei"
}
字段 类型 说明
network_type string | null wifi / cellular / offline / unknown
os_version string | null 操作系统版本
device_model string | null 设备型号
locale string | null 当前语言地区
timezone string | null 时区标识

3.6 事件类型定义

event_type 触发时机 attributes metrics
session.login 用户登录成功 method (password/phone_code/oauth) -
session.logout 用户登出 reason (manual/expired/kickout) session_duration_s
agent.chat_completed Agent 对话完成 conversation_id, scenario message_count, response_time_ms
page.view 页面退出时 page_from stay_duration_ms, click_count
ui.click 元素点击时 element_id, element_type -

3.7 事件数据结构

session.login

{
  "event_id": "01JQ2G5Q3N6Y5N8R7M4K2P1T9A",
  "event_type": "session.login",
  "timestamp": "2026-04-01T10:30:00.123Z",
  "user_id": "user_123",
  "device_id": "install_a93f5d7c21",
  "session_id": "sess_7c2d5e8f91",
  "platform": "android",
  "app_version": "1.0.0",
  "app_build": "100",
  "env": "prod",
  "page_name": "login",
  "trace_id": "trace_login_001",
  "request_id": "req_login_001",
  "attributes": { "method": "password" },
  "metrics": {},
  "context": { "network_type": "wifi", "os_version": "Android 14", "device_model": "Xiaomi 13", "locale": "zh-CN", "timezone": "Asia/Taipei" }
}

session.logout

{
  "event_id": "01JQ2G6A7X9N2T4R8K1M5P3Q7B",
  "event_type": "session.logout",
  "timestamp": "2026-04-01T12:00:00.000Z",
  "user_id": "user_123",
  "device_id": "install_a93f5d7c21",
  "session_id": "sess_7c2d5e8f91",
  "platform": "android",
  "app_version": "1.0.0",
  "app_build": "100",
  "env": "prod",
  "page_name": "settings",
  "trace_id": "trace_logout_001",
  "request_id": null,
  "attributes": { "reason": "manual" },
  "metrics": { "session_duration_s": 5400 },
  "context": { "network_type": "wifi", "os_version": "Android 14", "device_model": "Xiaomi 13", "locale": "zh-CN", "timezone": "Asia/Taipei" }
}

agent.chat_completed

{
  "event_id": "01JQ2G7B9V4K8N3R1T6M2P5Q8C",
  "event_type": "agent.chat_completed",
  "timestamp": "2026-04-01T13:20:15.456Z",
  "user_id": "user_123",
  "device_id": "install_a93f5d7c21",
  "session_id": "sess_7c2d5e8f91",
  "platform": "android",
  "app_version": "1.0.0",
  "app_build": "100",
  "env": "prod",
  "page_name": "chat",
  "trace_id": "trace_agent_001",
  "request_id": "req_agent_001",
  "attributes": { "conversation_id": "conv_987", "scenario": "assistant" },
  "metrics": { "message_count": 4, "response_time_ms": 1320 },
  "context": { "network_type": "wifi", "os_version": "Android 14", "device_model": "Xiaomi 13", "locale": "zh-CN", "timezone": "Asia/Taipei" }
}

page.view

{
  "event_id": "01JQ2G8C1M7R4T9K2P6N5Q3D8E",
  "event_type": "page.view",
  "timestamp": "2026-04-01T14:05:30.000Z",
  "user_id": "user_123",
  "device_id": "install_a93f5d7c21",
  "session_id": "sess_7c2d5e8f91",
  "platform": "android",
  "app_version": "1.0.0",
  "app_build": "100",
  "env": "prod",
  "page_name": "home",
  "trace_id": "trace_page_home_001",
  "request_id": null,
  "attributes": { "page_from": "login" },
  "metrics": { "stay_duration_ms": 18234, "click_count": 7 },
  "context": { "network_type": "wifi", "os_version": "Android 14", "device_model": "Xiaomi 13", "locale": "zh-CN", "timezone": "Asia/Taipei" }
}

ui.click

{
  "event_id": "01JQ2G9D4P8N1R6T3K5M7Q2E9F",
  "event_type": "ui.click",
  "timestamp": "2026-04-01T14:00:12.000Z",
  "user_id": "user_123",
  "device_id": "install_a93f5d7c21",
  "session_id": "sess_7c2d5e8f91",
  "platform": "android",
  "app_version": "1.0.0",
  "app_build": "100",
  "env": "prod",
  "page_name": "home",
  "trace_id": "trace_click_001",
  "request_id": null,
  "attributes": { "element_id": "create_task_button", "element_type": "button" },
  "metrics": {},
  "context": { "network_type": "wifi", "os_version": "Android 14", "device_model": "Xiaomi 13", "locale": "zh-CN", "timezone": "Asia/Taipei" }
}

4. 批量上报请求结构

4.1 请求体

{
  "client_time": "2026-04-01T14:10:00.000Z",
  "sdk_version": "1.0.0",
  "events": [
    { /* 事件对象 */ }
  ]
}

4.2 字段定义

字段 类型 必填 说明
client_time string 本次上报请求发起时间
sdk_version string 埋点 SDK 版本
events array 事件数组,建议 1~100 条

5. 后端落盘结构

5.1 路径结构

按日期分桶,便于后续聚合查询:

backend/data/analytics/
├── 2026-04-01.jsonl
├── 2026-04-02.jsonl
└── 2026-04-03.jsonl

5.2 JSONL 单行格式

每行一个完整事件对象:

{"event_id":"01JQ2G5Q3N6Y5N8R7M4K2P1T9A","event_type":"session.login","timestamp":"2026-04-01T10:30:00.123Z",...}
{"event_id":"01JQ2G6A7X9N2T4R8K1M5P3Q7B","event_type":"session.logout","timestamp":"2026-04-01T12:00:00.000Z",...}

6. Python 类型定义

from typing import Any, Literal
from pydantic import BaseModel, Field
from datetime import datetime


class AnalyticsContext(BaseModel):
    network_type: str | None = None
    os_version: str | None = None
    device_model: str | None = None
    locale: str | None = None
    timezone: str | None = None


class AnalyticsEvent(BaseModel):
    event_id: str
    event_type: str
    timestamp: datetime

    user_id: str
    device_id: str
    session_id: str

    platform: Literal["android", "ios", "web"]
    app_version: str
    app_build: str | None = None
    env: Literal["dev", "staging", "prod"]

    page_name: str | None = None
    trace_id: str | None = None
    request_id: str | None = None

    attributes: dict[str, Any] = Field(default_factory=dict)
    metrics: dict[str, int | float] = Field(default_factory=dict)
    context: AnalyticsContext | None = None


class AnalyticsBatchRequest(BaseModel):
    client_time: datetime | None = None
    sdk_version: str | None = None
    events: list[AnalyticsEvent]

7. Flutter 埋点 SDK 设计

4.1 目录结构

apps/lib/core/analytics/
├── tracker.dart           # 埋点入口,单例
├── config.dart            # 配置(endpoint、flush 策略)
├── events/                # 事件定义
│   ├── base_event.dart   # 基础事件类
│   ├── login_event.dart
│   ├── logout_event.dart
│   ├── conversation_event.dart
│   └── page_view_event.dart
├── queue/
│   └── event_queue.dart  # 内存队列
└── sender.dart           # 异步 HTTP 发送器

4.2 核心流程

  1. 事件采集:调用 AnalyticsTracker.track(event) 记录事件
  2. 队列缓冲:事件先入内存队列
  3. 批量发送:队列满(50条)或定时(30秒)触发批量发送
  4. 失败重试:发送失败则落盘本地,下次启动时重试

4.3 初始化

apps/lib/main.dart 的 App 初始化阶段:

await AnalyticsTracker.init(
  endpoint: '${Env.apiBaseUrl}/v1/analytics/events',
  deviceId: deviceInfo.id,
);

4.4 页面停留时长计算

  • page.view 事件在页面 initState 时发送 enter_time
  • 在页面 dispose 时补充 stay_duration_ms
  • 点击次数由各页面手动调用 trackClick(pageName, elementId)

4.5 Agent 对话统计

  • 在 Agent 对话完成回调中触发 agent.conversation 事件
  • message_count 为用户发送的消息数
  • response_time_ms 为首次响应耗时

5. 后端接收路由

5.1 路由定义

端点: POST /v1/analytics/events

5.2 请求格式

{
  "events": [
    { /* 事件对象 */ },
    { /* 事件对象 */ }
  ]
}

5.3 响应格式

{
  "received": 10,
  "queued": true
}

5.4 异步写入流程

请求 → 内存队列缓冲 → [队列满100条 或 超时5秒] → taskiq 任务 → 批量写入文件
  • API 接收事件后立即放入内存队列,返回 202 Accepted
  • 后台 taskiq worker 消费队列,批量写入 JSONL 文件
  • 使用文件锁保证多 worker 并发写入安全

5.5 Taskiq 任务定义

@taskify
async def write_analytics_events(batch: list[AnalyticsEvent], date: str):
    """批量写入事件到 JSONL 文件"""
    # 按 event_type 分组写入对应文件
    # 每条事件追加到当日文件末尾

5.6 文件写入

路径:backend/data/analytics/{event_type}/{YYYY-MM-DD}.jsonl

写入策略:

  • 每条事件追加到当日文件末尾
  • 使用文件锁保证并发写入安全
  • 目录不存在时自动创建

5.7 目录结构

backend/data/analytics/
├── session.login/
│   ├── 2026-04-01.jsonl
│   └── 2026-04-02.jsonl
├── session.logout/
│   └── 2026-04-01.jsonl
├── agent.conversation/
│   └── 2026-04-01.jsonl
└── page.view/
    └── 2026-04-01.jsonl

6. 可视化网站

6.1 技术栈

  • 框架React 18 + Vite
  • 图表ECharts
  • 样式TailwindCSS
  • HTTPAxios

6.2 项目结构

web/
├── src/
│   ├── main.jsx
│   ├── App.jsx
│   ├── pages/
│   │   ├── Login.jsx
│   │   └── Dashboard.jsx
│   ├── components/
│   │   ├── StatCard.jsx
│   │   ├── LoginTrendChart.jsx
│   │   ├── PageClickChart.jsx
│   │   └── StayDurationChart.jsx
│   ├── services/
│   │   └── api.js
│   └── styles/
├── public/
├── package.json
├── vite.config.js
└── .env

6.3 访问方式

与后端打包在一起,通过子路径访问:

http://域名/analytics/          # 静态网站
http://域名/api/v1/analytics/   # API 路由

实现方式:FastAPI mount 静态文件目录

from fastapi.staticfiles import StaticFiles

app.mount("/analytics", StaticFiles(directory="web/dist", html=True), name="analytics")

6.4 认证方式

推荐:后端验证密码,返回 JWT Token

  1. 前端登录页输入密码
  2. 调用 POST /api/v1/analytics/login 验证
  3. 后端读取 .envANALYTICS_PASSWORD 验证
  4. 验证成功返回 JWT Token,前端存 sessionStorage
  5. 后续请求带 Token,后端验证

6.5 页面设计

登录页 (/login)

  • 简单密码输入框
  • 调用后端 API 验证
  • 登录成功后写入 sessionStorage

仪表盘 (/dashboard)

  • 顶部:日期范围选择器(默认最近7天)
  • 统计卡片:
    • DAU(当日独立用户数)
    • 累计对话次数
    • 累计页面点击量
    • 平均停留时长
  • 图表:
    • 折线图:每日登录趋势
    • 柱状图:各页面点击量 Top 10
    • 热力图:页面停留时长分布

6.6 数据读取

  • 前端通过 GET /api/v1/analytics/summary 获取聚合数据
  • 后端解析 backend/data/analytics/*.jsonl 文件并聚合
  • 提供 GET /api/v1/analytics/daily 等查询接口

7. 环境变量

7.1 backend/.env 新增

# Analytics 数据存储路径
ANALYTICS_DATA_PATH=backend/data/analytics

# 可视化网站密码(用于 /api/v1/analytics/login 验证)
ANALYTICS_PASSWORD=your-secure-password

7.2 后端登录验证 API

端点: POST /api/v1/analytics/login

请求:

{
  "password": "your-password"
}

响应(成功):

{
  "token": "jwt-token-here"
}

响应(失败):

{
  "error": "invalid_password"
}

7.3 Web 环境变量

web 构建时可通过环境变量注入 API 地址:

VITE_API_BASE_URL=http://localhost:5775

注:密码不在前端存储,改为后端验证方式。


8. Worker Queue 重构

8.1 背景

现有 automation 队列仅用于将任务转发到 agent 队列,职责单一。为支持 analytics 异步写入,将 automation 重构为 general,用于承载所有非实时任务。

8.2 重构范围

文件 改动内容
backend/src/core/taskiq/app.py worker_automation_brokerworker_general_brokerqueue name "automation""general"
backend/src/core/taskiq/__init__.py 更新 export 名称
backend/src/core/agentscope/runtime/tasks.py import 和 @worker_automation_broker.task@worker_general_broker.task
backend/src/v1/agent/service.py:145 queue = "automation"queue = "general"
backend/tests/unit/core/taskiq/test_app.py 更新引用
infra/scripts/app.sh WORKER_AUTOMATION_CMDWORKER_GENERAL_CMDtmux window 改名
deploy/docker-compose.prod.yml worker-automation service → worker-general

8.3 环境变量改动

# 当前
SOCIAL_WORKER__GROUPS__AUTOMATION__CONCURRENCY=1

# 改为
SOCIAL_WORKER__GROUPS__GENERAL__CONCURRENCY=1

涉及文件:

  • .env
  • .env.example
  • deploy/.env.prod
  • deploy/.env.prod.example

9. 实施计划

阶段 任务 产出
0 Worker Queue 重构 (automationgeneral) 所有 queue 相关文件
1 创建 docs/plans/2026-04-01-analytics-design.md 设计文档
2 实现后端 POST /v1/analytics/events 路由 backend/src/v1/analytics/
3 实现 Flutter 埋点 SDK apps/lib/core/analytics/
4 集成埋点到现有 App 页面 修改 home_screenauth
5 搭建 React 项目框架 web/
6 实现 Dashboard 页面和图表 ECharts 组件
7 端到端测试验证 测试报告

10. 风险与限制

  • 数据丢失:批量发送失败时落盘本地,最多保留次日重试
  • 并发写入:多实例部署时需使用分布式锁,当前设计仅支持单实例
  • 密码安全:简单密码校验,生产环境建议使用 JWT

11. 后续扩展方向

  • 支持更多事件类型(分享、搜索、错误)
  • 接入 OpenTelemetry Collector
  • 接入 ClickHouse 进行 OLAP 查询
  • 支持数据导出功能