rwadurian/frontend/mobile-app/docs/testing_guide.md

1097 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 测试指南
> 本文档提供 APK 升级模块、遥测模块的手动测试和自动化测试方法。
---
## 目录
1. [测试环境准备](#1-测试环境准备)
2. [APK 升级模块测试](#2-apk-升级模块测试)
3. [遥测模块测试](#3-遥测模块测试)
4. [自动化单元测试](#4-自动化单元测试)
5. [Mock 服务器搭建](#5-mock-服务器搭建)
---
## 1. 测试环境准备
### 1.1 开发环境
```bash
# 确保 Flutter 环境正常
flutter doctor
# 获取依赖
cd c:\Users\dong\Desktop\rwadurian\frontend\mobile-app
flutter pub get
```
### 1.2 测试设备
| 测试项 | 推荐设备 |
|-------|---------|
| APK 安装 | Android 真机 (API 24+) |
| 遥测功能 | Android 真机或模拟器 |
| 心跳服务 | Android 真机 (测试前后台切换) |
### 1.3 构建测试 APK
```bash
# Debug 版本 (用于开发测试)
flutter build apk --debug
# Release 版本 (用于正式测试)
flutter build apk --release
# 输出路径
# build/app/outputs/flutter-apk/app-debug.apk
# build/app/outputs/flutter-apk/app-release.apk
```
---
## 2. APK 升级模块测试
### 2.1 手动测试清单
#### 测试用例 1: 版本检测 - 有新版本
**前置条件**:
- 当前安装版本: 1.0.0 (versionCode: 1)
- 服务器配置新版本: 1.1.0 (versionCode: 2)
**测试步骤**:
1. 启动应用,进入主页面(龙虎榜)
2. 等待 3 秒后观察是否弹出更新对话框
**预期结果**:
- 弹出更新对话框
- 显示新版本号 "1.1.0"
- 显示文件大小
- 显示更新日志(如有)
**验证日志**:
```
flutter: 📊 Checking for update...
flutter: 📊 New version available: 1.1.0
```
---
#### 测试用例 2: 版本检测 - 已是最新版本
**前置条件**:
- 当前安装版本与服务器最新版本一致
**测试步骤**:
1. 启动应用,进入主页面
2. 等待 3 秒
**预期结果**:
- 不弹出更新对话框
**验证日志**:
```
flutter: Already latest version
```
---
#### 测试用例 3: 强制更新
**前置条件**:
- 服务器配置 `forceUpdate: true`
**测试步骤**:
1. 启动应用,触发版本检测
2. 尝试点击对话框外部关闭
3. 尝试按返回键关闭
**预期结果**:
- 对话框标题显示"发现重要更新"
- 无法通过点击外部或返回键关闭对话框
- 没有"稍后"按钮
---
#### 测试用例 4: APK 下载与安装
**前置条件**:
- 服务器已配置有效的 APK 下载链接
- APK 已正确签名
**测试步骤**:
1. 点击"立即更新"按钮
2. 观察下载进度
3. 下载完成后观察安装流程
**预期结果**:
- 显示下载进度对话框
- 进度百分比实时更新
- 下载完成后自动弹出系统安装界面
**验证日志**:
```
flutter: 📥 Downloading APK from: https://...
flutter: 📥 Download progress: 50%
flutter: ✅ APK downloaded successfully
flutter: 🔐 SHA-256 verification passed
flutter: 📲 Installing APK...
```
---
#### 测试用例 5: 下载取消
**测试步骤**:
1. 开始下载更新
2. 在下载过程中点击"取消"按钮
**预期结果**:
- 下载立即停止
- 显示"下载已取消"
- 临时文件被清理
---
#### 测试用例 6: 下载失败重试
**前置条件**:
- 模拟网络不稳定或服务器错误
**测试步骤**:
1. 开始下载更新
2. 断开网络或服务器返回错误
3. 观察错误提示
4. 点击"重试"按钮
**预期结果**:
- 显示"下载失败,请稍后重试"
- 出现"重试"按钮
- 点击重试后重新开始下载
---
#### 测试用例 7: SHA-256 校验失败
**前置条件**:
- 服务器返回的 SHA-256 与实际文件不匹配
**测试步骤**:
1. 下载 APK
2. 等待校验完成
**预期结果**:
- 显示下载失败
- 日志显示校验失败信息
**验证日志**:
```
flutter: ❌ SHA-256 verification failed
flutter: Expected: abc123...
flutter: Actual: def456...
```
---
#### 测试用例 8: 应用市场来源检测
**前置条件**:
- 从 Google Play 安装的应用
**测试步骤**:
1. 启动应用
2. 触发版本检测
**预期结果**:
- 显示"检测到您的应用来自应用市场"提示
- 按钮变为"前往应用市场"
---
### 2.2 调试技巧
#### 强制触发版本检测
在代码中临时修改版本号进行测试:
```dart
// lib/core/updater/version_checker.dart
// 临时修改 checkForUpdate() 方法,强制返回测试数据
Future<VersionInfo?> checkForUpdate() async {
// 测试用: 返回模拟的新版本信息
return VersionInfo(
version: '1.1.0',
versionCode: 999,
downloadUrl: 'https://your-test-server.com/test.apk',
fileSize: 52428800,
fileSizeFriendly: '50.0 MB',
sha256: 'your-test-sha256-hash',
forceUpdate: false,
updateLog: '测试更新日志',
releaseDate: DateTime.now(),
);
}
```
#### 使用 Charles/Fiddler 抓包
1. 配置手机代理到电脑
2. 监控 `/api/app/version/check` 请求
3. 可修改响应数据测试不同场景
---
## 3. 遥测模块测试
### 3.1 手动测试清单
#### 测试用例 1: 设备信息采集
**测试步骤**:
1. 首次启动应用
2. 等待 Splash 页面完成
**预期结果**:
- 控制台输出设备信息采集日志
**验证日志**:
```
flutter: 📊 Collecting device info...
flutter: 📊 Device: Samsung SM-G991B
flutter: 📊 OS: Android 13 (SDK 33)
flutter: 📊 App: 1.0.0 (1)
flutter: 📊 Screen: 1080x2400 @3.0x
flutter: 📊 Network: wifi
flutter: 📊 Locale: zh_CN
```
---
#### 测试用例 2: 会话开始事件
**测试步骤**:
1. 完全关闭应用
2. 重新启动应用
**预期结果**:
- 生成新的 sessionId
- 触发 `session_start` 事件
**验证日志**:
```
flutter: 📊 New session started: session-abc123
flutter: 📊 Event tracked: session_start
```
---
#### 测试用例 3: 会话结束事件
**测试步骤**:
1. 应用在前台运行
2. 按 Home 键将应用切到后台
**预期结果**:
- 触发 `session_end` 事件
- 事件包含会话时长
**验证日志**:
```
flutter: 📊 Session paused, duration: 120 seconds
flutter: 📊 Event tracked: session_end
```
---
#### 测试用例 4: 会话恢复 (30分钟内)
**测试步骤**:
1. 将应用切到后台
2. 5 分钟内切回前台
**预期结果**:
- 恢复原有 session
- 触发 `session_resume` 事件
**验证日志**:
```
flutter: 📊 Session resumed: session-abc123
flutter: 📊 Event tracked: session_resume
```
---
#### 测试用例 5: 新会话 (超过30分钟)
**测试步骤**:
1. 将应用切到后台
2. 等待超过 30 分钟(或修改超时配置为较短时间测试)
3. 切回前台
**预期结果**:
- 生成新的 sessionId
- 触发 `session_start` 事件(非 resume
---
#### 测试用例 6: 心跳发送
**测试步骤**:
1. 保持应用在前台
2. 观察 60 秒内的日志
**预期结果**:
- 每 60 秒发送一次心跳
**验证日志**:
```
flutter: 💓 Heartbeat sent
flutter: 💓 Heartbeat sent
...
```
---
#### 测试用例 7: 心跳暂停 (后台)
**测试步骤**:
1. 应用在前台,心跳正常发送
2. 将应用切到后台
3. 等待 2 分钟
4. 切回前台
**预期结果**:
- 后台期间不发送心跳
- 切回前台后立即发送心跳并恢复定时器
**验证日志**:
```
flutter: 💓 Heartbeat paused (app in background)
... (后台期间无心跳日志)
flutter: 💓 Heartbeat resumed (app in foreground)
flutter: 💓 Heartbeat sent
```
---
#### 测试用例 8: 事件批量上传
**测试步骤**:
1. 在应用中执行多个操作(页面切换、按钮点击等)
2. 等待 30 秒或事件队列达到 10 条
**预期结果**:
- 触发批量上传
- 上传成功后清理本地队列
**验证日志**:
```
flutter: 📊 Queue size: 10, triggering upload
flutter: 📊 Uploading 10 events...
flutter: ✅ Uploaded 10 telemetry events
```
---
#### 测试用例 9: 上传失败重试
**前置条件**:
- 模拟网络断开或服务器错误
**测试步骤**:
1. 积累一些事件
2. 断开网络
3. 等待上传触发
**预期结果**:
- 上传失败但事件保留在本地队列
- 网络恢复后下次触发时重新上传
**验证日志**:
```
flutter: ❌ Upload error (DioException): Connection refused
flutter: 📊 Events kept in queue for retry
```
---
#### 测试用例 10: 远程配置同步
**测试步骤**:
1. 启动应用
2. 修改服务器配置(如 `samplingRate: 0.5`
3. 等待配置同步周期(默认 1 小时,测试时可改短)
**预期结果**:
- 获取并应用新配置
**验证日志**:
```
flutter: 📊 Fetching telemetry config...
flutter: 📊 Config updated: samplingRate=0.5
```
---
#### 测试用例 11: 采样率过滤
**前置条件**:
- 配置 `samplingRate: 0.0`
**测试步骤**:
1. 应用新配置
2. 尝试触发事件
**预期结果**:
- 所有事件被过滤,不进入队列
**验证日志**:
```
flutter: 📊 Event filtered by sampling rate
```
---
#### 测试用例 12: 事件类型过滤
**前置条件**:
- 配置 `enabledEventTypes: ['error', 'crash']`
**测试步骤**:
1. 触发 `page_view` 事件
2. 触发 `error` 事件
**预期结果**:
- `page_view` 被过滤
- `error` 被记录
---
### 3.2 调试技巧
#### 查看本地事件队列
```dart
// 临时添加调试代码
final storage = TelemetryStorage();
final events = storage.peekEvents(100);
for (var e in events) {
debugPrint('Event: ${e.name} - ${e.type} - ${e.timestamp}');
}
```
#### 强制触发上传
```dart
// 在需要的地方调用
await TelemetryService().forceUpload();
```
#### 缩短配置同步周期
```dart
// lib/bootstrap.dart
await TelemetryService().initialize(
apiBaseUrl: _apiBaseUrl,
context: context,
configSyncInterval: const Duration(minutes: 1), // 测试用
);
```
---
## 4. 自动化单元测试
### 4.1 测试文件结构
```
test/
├── unit/
│ ├── core/
│ │ ├── updater/
│ │ │ ├── version_checker_test.dart
│ │ │ ├── download_manager_test.dart
│ │ │ └── update_service_test.dart
│ │ └── telemetry/
│ │ ├── device_info_collector_test.dart
│ │ ├── telemetry_storage_test.dart
│ │ ├── telemetry_uploader_test.dart
│ │ ├── session_manager_test.dart
│ │ └── heartbeat_service_test.dart
│ └── mocks/
│ ├── mock_dio.dart
│ └── mock_storage.dart
```
### 4.2 VersionChecker 单元测试
```dart
// test/unit/core/updater/version_checker_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:dio/dio.dart';
import 'package:rwa_android_app/core/updater/version_checker.dart';
import 'package:rwa_android_app/core/updater/models/version_info.dart';
class MockDio extends Mock implements Dio {}
void main() {
late VersionChecker versionChecker;
late MockDio mockDio;
setUp(() {
mockDio = MockDio();
versionChecker = VersionChecker(
apiBaseUrl: 'https://api.test.com',
dio: mockDio, // 需要修改 VersionChecker 支持注入 Dio
);
});
group('VersionChecker', () {
test('should return VersionInfo when new version available', () async {
// Arrange
when(() => mockDio.get(
any(),
queryParameters: any(named: 'queryParameters'),
)).thenAnswer((_) async => Response(
data: {
'code': 0,
'data': {
'hasUpdate': true,
'version': '1.1.0',
'versionCode': 2,
'downloadUrl': 'https://cdn.test.com/app.apk',
'fileSize': 52428800,
'fileSizeFriendly': '50.0 MB',
'sha256': 'abc123',
'forceUpdate': false,
'updateLog': 'Bug fixes',
'releaseDate': '2024-01-15T10:00:00Z',
}
},
statusCode: 200,
requestOptions: RequestOptions(path: ''),
));
// Act
final result = await versionChecker.checkForUpdate();
// Assert
expect(result, isNotNull);
expect(result!.version, '1.1.0');
expect(result.versionCode, 2);
expect(result.forceUpdate, false);
});
test('should return null when no update available', () async {
// Arrange
when(() => mockDio.get(
any(),
queryParameters: any(named: 'queryParameters'),
)).thenAnswer((_) async => Response(
data: {
'code': 0,
'data': {'hasUpdate': false}
},
statusCode: 200,
requestOptions: RequestOptions(path: ''),
));
// Act
final result = await versionChecker.checkForUpdate();
// Assert
expect(result, isNull);
});
test('should return null on network error', () async {
// Arrange
when(() => mockDio.get(
any(),
queryParameters: any(named: 'queryParameters'),
)).thenThrow(DioException(
type: DioExceptionType.connectionError,
requestOptions: RequestOptions(path: ''),
));
// Act
final result = await versionChecker.checkForUpdate();
// Assert
expect(result, isNull);
});
});
}
```
### 4.3 TelemetryStorage 单元测试
```dart
// test/unit/core/telemetry/telemetry_storage_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:rwa_android_app/core/telemetry/storage/telemetry_storage.dart';
import 'package:rwa_android_app/core/telemetry/models/telemetry_event.dart';
void main() {
late TelemetryStorage storage;
setUp(() {
storage = TelemetryStorage();
storage.clear(); // 清理之前的数据
});
group('TelemetryStorage', () {
test('should enqueue and dequeue events', () {
// Arrange
final event = TelemetryEvent(
eventId: 'test-1',
name: 'test_event',
type: EventType.userAction,
level: EventLevel.info,
installId: 'install-1',
deviceContextId: 'ctx-1',
timestamp: DateTime.now(),
);
// Act
storage.enqueueEvent(event);
final events = storage.dequeueEvents(10);
// Assert
expect(events.length, 1);
expect(events.first.eventId, 'test-1');
});
test('should respect max queue size', () {
// Arrange
storage.setMaxQueueSize(5);
// Act
for (int i = 0; i < 10; i++) {
storage.enqueueEvent(TelemetryEvent(
eventId: 'event-$i',
name: 'test_event',
type: EventType.userAction,
level: EventLevel.info,
installId: 'install-1',
deviceContextId: 'ctx-1',
timestamp: DateTime.now(),
));
}
// Assert
expect(storage.getQueueSize(), 5);
});
test('should dequeue in FIFO order', () {
// Arrange
for (int i = 0; i < 5; i++) {
storage.enqueueEvent(TelemetryEvent(
eventId: 'event-$i',
name: 'test_event',
type: EventType.userAction,
level: EventLevel.info,
installId: 'install-1',
deviceContextId: 'ctx-1',
timestamp: DateTime.now(),
));
}
// Act
final events = storage.dequeueEvents(3);
// Assert
expect(events[0].eventId, 'event-0');
expect(events[1].eventId, 'event-1');
expect(events[2].eventId, 'event-2');
});
});
}
```
### 4.4 SessionManager 单元测试
```dart
// test/unit/core/telemetry/session_manager_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:rwa_android_app/core/telemetry/session/session_manager.dart';
void main() {
late SessionManager sessionManager;
setUp(() {
sessionManager = SessionManager(
sessionTimeout: const Duration(minutes: 30),
);
});
group('SessionManager', () {
test('should generate new session on start', () {
// Act
sessionManager.startSession();
// Assert
expect(sessionManager.currentSessionId, isNotNull);
expect(sessionManager.currentSessionId, isNotEmpty);
});
test('should keep same session on resume within timeout', () {
// Arrange
sessionManager.startSession();
final originalSessionId = sessionManager.currentSessionId;
sessionManager.pauseSession();
// Act
sessionManager.resumeSession();
// Assert
expect(sessionManager.currentSessionId, originalSessionId);
});
test('should generate new session on resume after timeout', () async {
// Arrange
sessionManager = SessionManager(
sessionTimeout: const Duration(milliseconds: 100),
);
sessionManager.startSession();
final originalSessionId = sessionManager.currentSessionId;
sessionManager.pauseSession();
// Wait for timeout
await Future.delayed(const Duration(milliseconds: 150));
// Act
sessionManager.resumeSession();
// Assert
expect(sessionManager.currentSessionId, isNot(originalSessionId));
});
test('should calculate session duration correctly', () async {
// Arrange
sessionManager.startSession();
// Wait some time
await Future.delayed(const Duration(seconds: 1));
// Act
final duration = sessionManager.getSessionDuration();
// Assert
expect(duration.inSeconds, greaterThanOrEqualTo(1));
});
});
}
```
### 4.5 运行测试
```bash
# 运行所有单元测试
flutter test
# 运行指定测试文件
flutter test test/unit/core/updater/version_checker_test.dart
# 运行测试并生成覆盖率报告
flutter test --coverage
# 查看覆盖率报告 (需要安装 lcov)
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
```
---
## 5. Mock 服务器搭建
### 5.1 使用 JSON Server (快速搭建)
**安装**:
```bash
npm install -g json-server
```
**创建 db.json**:
```json
{
"app": {
"version": {
"check": {
"code": 0,
"data": {
"hasUpdate": true,
"version": "1.1.0",
"versionCode": 2,
"downloadUrl": "http://192.168.1.100:3000/releases/app.apk",
"fileSize": 52428800,
"fileSizeFriendly": "50.0 MB",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"forceUpdate": false,
"updateLog": "1. 新功能\\n2. Bug修复",
"releaseDate": "2024-01-15T10:00:00Z"
}
}
}
},
"analytics": {
"events": []
},
"presence": {
"heartbeat": { "code": 0, "message": "success" }
},
"telemetry": {
"config": {
"code": 0,
"data": {
"enabled": true,
"samplingRate": 1.0,
"enabledEventTypes": ["page_view", "user_action", "error", "crash", "session", "presence"],
"maxQueueSize": 1000,
"uploadBatchSize": 20,
"uploadIntervalSeconds": 30,
"heartbeatIntervalSeconds": 60,
"sessionTimeoutMinutes": 30
}
}
}
}
```
**启动服务器**:
```bash
json-server --watch db.json --port 3000 --host 0.0.0.0
```
**修改应用配置**:
```dart
// lib/bootstrap.dart
const String _apiBaseUrl = 'http://192.168.1.100:3000'; // 你的电脑IP
```
### 5.2 使用 Python Flask (更灵活)
```python
# mock_server.py
from flask import Flask, request, jsonify
from datetime import datetime
import hashlib
app = Flask(__name__)
# 存储上报的事件
events_log = []
@app.route('/api/app/version/check', methods=['GET'])
def check_version():
current_version = request.args.get('currentVersion', '1.0.0')
current_code = int(request.args.get('currentVersionCode', '1'))
# 模拟新版本
if current_code < 2:
return jsonify({
'code': 0,
'data': {
'hasUpdate': True,
'version': '1.1.0',
'versionCode': 2,
'downloadUrl': 'http://192.168.1.100:5000/releases/app.apk',
'fileSize': 52428800,
'fileSizeFriendly': '50.0 MB',
'sha256': 'your-sha256-hash',
'forceUpdate': False,
'updateLog': '1. 新功能\n2. Bug修复',
'releaseDate': datetime.now().isoformat()
}
})
else:
return jsonify({
'code': 0,
'data': {'hasUpdate': False}
})
@app.route('/api/v1/analytics/events', methods=['POST'])
def receive_events():
data = request.json
events = data.get('events', [])
events_log.extend(events)
print(f"Received {len(events)} events. Total: {len(events_log)}")
for e in events:
print(f" - {e['name']} ({e['type']}) at {e['timestamp']}")
return jsonify({'code': 0, 'message': 'success'})
@app.route('/api/v1/presence/heartbeat', methods=['POST'])
def heartbeat():
data = request.json
print(f"Heartbeat from {data.get('installId')} at {data.get('timestamp')}")
return jsonify({'code': 0, 'message': 'success'})
@app.route('/api/telemetry/config', methods=['GET'])
def telemetry_config():
return jsonify({
'code': 0,
'data': {
'enabled': True,
'samplingRate': 1.0,
'enabledEventTypes': ['page_view', 'user_action', 'error', 'crash', 'session', 'presence'],
'maxQueueSize': 1000,
'uploadBatchSize': 20,
'uploadIntervalSeconds': 30,
'heartbeatIntervalSeconds': 60,
'sessionTimeoutMinutes': 30
}
})
@app.route('/releases/<filename>', methods=['GET'])
def download_apk(filename):
# 返回测试APK文件
return app.send_static_file(filename)
@app.route('/events', methods=['GET'])
def view_events():
"""调试用: 查看所有上报的事件"""
return jsonify({
'total': len(events_log),
'events': events_log[-100:] # 返回最近100条
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
```
**启动**:
```bash
pip install flask
python mock_server.py
```
### 5.3 网络调试工具
| 工具 | 用途 |
|-----|------|
| Charles | HTTPS 抓包、修改响应 |
| Fiddler | Windows 抓包工具 |
| mitmproxy | 命令行抓包工具 |
| Wireshark | 底层网络分析 |
**Charles 配置要点**:
1. 手机和电脑在同一局域网
2. 手机设置代理到电脑 IP:8888
3. 安装 Charles 根证书到手机
4. 使用 Map Remote 或 Rewrite 修改响应
---
## 6. 常见问题排查
### 6.1 APK 安装失败
**症状**: 下载成功但安装界面不弹出
**排查步骤**:
1. 检查 `REQUEST_INSTALL_PACKAGES` 权限
2. 检查 FileProvider 配置
3. 查看 Logcat 中的安装错误
```bash
adb logcat | grep -i "install"
```
### 6.2 心跳不发送
**症状**: 控制台没有心跳日志
**排查步骤**:
1. 确认遥测服务已初始化
2. 确认应用在前台
3. 检查远程配置是否禁用了 presence 类型
### 6.3 事件不上传
**症状**: 事件积累但不上传
**排查步骤**:
1. 检查网络连接
2. 检查队列大小是否达到阈值 (默认10)
3. 检查远程配置 `enabled` 是否为 true
4. 查看上传错误日志
### 6.4 设备信息采集失败
**症状**: DeviceContext 部分字段为空
**排查步骤**:
1. 确认使用真机测试(模拟器部分信息不可用)
2. 检查 device_info_plus 插件版本
3. 查看采集错误日志
---
## 附录: 测试数据示例
### 版本检测响应
```json
{
"code": 0,
"data": {
"hasUpdate": true,
"version": "1.1.0",
"versionCode": 2,
"downloadUrl": "https://cdn.rwadurian.com/releases/app-v1.1.0.apk",
"fileSize": 52428800,
"fileSizeFriendly": "50.0 MB",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"forceUpdate": false,
"updateLog": "1. 新增挖矿动画\n2. 优化性能\n3. 修复已知问题",
"releaseDate": "2024-01-15T10:00:00Z"
}
}
```
### 遥测事件示例
```json
{
"events": [
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"name": "session_start",
"type": "session",
"level": "info",
"installId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"deviceContextId": "ctx-xyz789",
"sessionId": "sess-abc123",
"userId": null,
"timestamp": "2024-01-15T10:00:00.000Z",
"properties": {
"trigger": "app_launch",
"is_first_launch": false
}
},
{
"eventId": "550e8400-e29b-41d4-a716-446655440001",
"name": "page_view",
"type": "page_view",
"level": "info",
"installId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"deviceContextId": "ctx-xyz789",
"sessionId": "sess-abc123",
"userId": null,
"timestamp": "2024-01-15T10:00:01.000Z",
"properties": {
"page": "/ranking",
"referrer": "/splash"
}
}
]
}
```