feat: 添加 Analytics 分析功能(行为追踪、错误码、协议更新)

This commit is contained in:
qzl
2026-04-02 11:52:23 +08:00
parent b101826de5
commit 7b6dbe72c3
24 changed files with 682 additions and 52 deletions
@@ -12,6 +12,7 @@ import 'package:social_app/core/chat/chat_list_item.dart';
import 'package:social_app/core/chat/chat_orchestrator.dart';
import 'package:social_app/core/chat/chat_history_repository.dart';
import 'package:social_app/core/chat/chat_timeline_reconciler.dart';
import 'package:social_app/core/analytics/tracker.dart';
import 'package:social_app/core/l10n/l10n.dart';
import 'chat_bloc_recovery_utils.dart';
@@ -20,6 +21,13 @@ part 'chat_bloc_send.dart';
part 'chat_bloc_history.dart';
part 'chat_bloc_attachments.dart';
typedef ChatCompletedCallback =
void Function({
required String conversationId,
required int messageCount,
required int responseTimeMs,
});
class ChatState implements ChatOrchestratorState {
@override
final List<ChatListItem> items;
@@ -115,12 +123,14 @@ class ChatBloc extends Cubit<ChatState> implements ChatOrchestrator {
required ChatApi chatApi,
ChatHistoryRepository? historyRepository,
Future<void> Function()? onCalendarMutated,
ChatCompletedCallback? onChatCompleted,
Duration recoveryPollInterval = const Duration(milliseconds: 700),
Duration recoveryTimeout = const Duration(seconds: 20),
}) : _service =
service ??
AgUiService(chatApi: chatApi, historyRepository: historyRepository),
_onCalendarMutated = onCalendarMutated,
_onChatCompleted = onChatCompleted,
_recoveryPollInterval = recoveryPollInterval,
_recoveryTimeout = recoveryTimeout,
super(const ChatState()) {
@@ -129,9 +139,14 @@ class ChatBloc extends Cubit<ChatState> implements ChatOrchestrator {
final AgUiService _service;
final Future<void> Function()? _onCalendarMutated;
final ChatCompletedCallback? _onChatCompleted;
final Duration _recoveryPollInterval;
final Duration _recoveryTimeout;
String? _activeUserId;
DateTime? _activeRunStartedAt;
DateTime? _activeRunFirstResponseAt;
String? _activeRunId;
String? _activeThreadId;
int _sessionEpoch = 0;
final Map<String, Uint8List> _attachmentPreviewCache = <String, Uint8List>{};
final Map<String, Future<Uint8List?>> _attachmentPreviewInflight =
@@ -259,4 +274,54 @@ class ChatBloc extends Cubit<ChatState> implements ChatOrchestrator {
emit(state.copyWith(error: error.toString()));
}
}
void _recordRunStarted({required String runId, required String threadId}) {
_activeRunStartedAt = DateTime.now();
_activeRunFirstResponseAt = null;
_activeRunId = runId;
_activeThreadId = threadId;
}
void _recordRunFirstResponse() {
_activeRunFirstResponseAt ??= DateTime.now();
}
void _trackChatCompleted() {
final startedAt = _activeRunStartedAt;
if (startedAt == null) {
return;
}
final firstResponseAt = _activeRunFirstResponseAt ?? DateTime.now();
final responseTimeMs = firstResponseAt.difference(startedAt).inMilliseconds;
final threadId = _activeThreadId?.trim();
final runId = _activeRunId?.trim();
final conversationId = (threadId != null && threadId.isNotEmpty)
? threadId
: runId;
if (conversationId == null || conversationId.isEmpty) {
return;
}
final onChatCompleted = _onChatCompleted;
if (onChatCompleted != null) {
onChatCompleted(
conversationId: conversationId,
messageCount: 1,
responseTimeMs: responseTimeMs,
);
return;
}
AnalyticsTracker.instance.trackAgentChatCompleted(
conversationId: conversationId,
scenario: 'assistant',
messageCount: 1,
responseTimeMs: responseTimeMs,
);
}
void _clearRunMetrics() {
_activeRunStartedAt = null;
_activeRunFirstResponseAt = null;
_activeRunId = null;
_activeThreadId = null;
}
}
@@ -6,6 +6,11 @@ extension _ChatBlocEvents on ChatBloc {
void _handleEvent(AgUiEvent event) {
switch (event.type) {
case AgUiEventType.runStarted:
final runStartedEvent = event as RunStartedEvent;
_recordRunStarted(
runId: runStartedEvent.runId,
threadId: runStartedEvent.threadId,
);
emit(
state.copyWith(
isSending: false,
@@ -17,11 +22,14 @@ extension _ChatBlocEvents on ChatBloc {
),
);
case AgUiEventType.runFinished:
_trackChatCompleted();
_clearRunMetrics();
emit(
_resetRunState().copyWith(items: _removeToolCallItems(state.items)),
);
case AgUiEventType.runError:
final errorEvent = event as RunErrorEvent;
_clearRunMetrics();
final isCanceledByUser = errorEvent.code == 'RUN_CANCELED';
emit(
_resetRunState(
@@ -72,6 +80,7 @@ extension _ChatBlocEvents on ChatBloc {
}
void _handleTextMessageEnd(TextMessageEndEvent event) {
_recordRunFirstResponse();
final timestamp = DateTime.now();
final items = _updateOrAddMessage(
state.items,