# 测试指南

> 本文档提供 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"
      }
    }
  ]
}
```
