refactor: remove analytics module, update tool postprocessor tests

This commit is contained in:
qzl
2026-04-23 15:55:35 +08:00
parent 1052e19134
commit f708bce585
34 changed files with 294 additions and 1490 deletions
@@ -1,111 +0,0 @@
import 'package:equatable/equatable.dart';
class AnalyticsContext extends Equatable {
final String? networkType;
final String? osVersion;
final String? deviceModel;
final String? locale;
final String? timezone;
const AnalyticsContext({
this.networkType,
this.osVersion,
this.deviceModel,
this.locale,
this.timezone,
});
Map<String, dynamic> toJson() => {
'network_type': networkType,
'os_version': osVersion,
'device_model': deviceModel,
'locale': locale,
'timezone': timezone,
};
@override
List<Object?> get props => [
networkType,
osVersion,
deviceModel,
locale,
timezone,
];
}
class BaseAnalyticsEvent extends Equatable {
final String eventId;
final String eventType;
final DateTime timestamp;
final String userId;
final String deviceId;
final String sessionId;
final String platform;
final String appVersion;
final String? appBuild;
final String env;
final String? pageName;
final String? traceId;
final String? requestId;
final Map<String, dynamic> attributes;
final Map<String, num> metrics;
final AnalyticsContext? context;
const BaseAnalyticsEvent({
required this.eventId,
required this.eventType,
required this.timestamp,
required this.userId,
required this.deviceId,
required this.sessionId,
required this.platform,
required this.appVersion,
this.appBuild,
required this.env,
this.pageName,
this.traceId,
this.requestId,
this.attributes = const {},
this.metrics = const {},
this.context,
});
Map<String, dynamic> toJson() => {
'event_id': eventId,
'event_type': eventType,
'timestamp': timestamp.toUtc().toIso8601String(),
'user_id': userId,
'device_id': deviceId,
'session_id': sessionId,
'platform': platform,
'app_version': appVersion,
'app_build': appBuild,
'env': env,
'page_name': pageName,
'trace_id': traceId,
'request_id': requestId,
'attributes': attributes,
'metrics': metrics,
'context': context?.toJson(),
};
@override
List<Object?> get props => [
eventId,
eventType,
timestamp,
userId,
deviceId,
sessionId,
platform,
appVersion,
appBuild,
env,
pageName,
traceId,
requestId,
attributes,
metrics,
context,
];
}
@@ -1,27 +0,0 @@
import 'base_event.dart';
class UiClickEvent extends BaseAnalyticsEvent {
UiClickEvent({
required super.eventId,
required super.timestamp,
required super.userId,
required super.deviceId,
required super.sessionId,
required super.platform,
required super.appVersion,
super.appBuild,
required super.env,
required super.pageName,
super.traceId,
super.requestId,
required String elementId,
String? elementType,
super.context,
}) : super(
eventType: 'ui.click',
attributes: {
'element_id': elementId,
if (elementType != null) 'element_type': elementType,
},
);
}
@@ -1,34 +0,0 @@
import 'base_event.dart';
class AgentChatCompletedEvent extends BaseAnalyticsEvent {
AgentChatCompletedEvent({
required super.eventId,
required super.timestamp,
required super.userId,
required super.deviceId,
required super.sessionId,
required super.platform,
required super.appVersion,
super.appBuild,
required super.env,
super.pageName,
super.traceId,
super.requestId,
required String conversationId,
String? scenario,
int? messageCount,
int? responseTimeMs,
AnalyticsContext? context,
}) : super(
eventType: 'agent.chat_completed',
attributes: {
'conversation_id': conversationId,
if (scenario != null) 'scenario': scenario,
},
metrics: {
if (messageCount != null) 'message_count': messageCount,
if (responseTimeMs != null) 'response_time_ms': responseTimeMs,
},
context: context,
);
}
@@ -1,6 +0,0 @@
export 'base_event.dart';
export 'login_event.dart';
export 'logout_event.dart';
export 'conversation_event.dart';
export 'page_view_event.dart';
export 'click_event.dart';
@@ -1,24 +0,0 @@
import 'base_event.dart';
class SessionLoginEvent extends BaseAnalyticsEvent {
SessionLoginEvent({
required super.eventId,
required super.timestamp,
required super.userId,
required super.deviceId,
required super.sessionId,
required super.platform,
required super.appVersion,
super.appBuild,
required super.env,
super.pageName,
super.traceId,
super.requestId,
required String method,
AnalyticsContext? context,
}) : super(
eventType: 'session.login',
attributes: {'method': method},
context: context,
);
}
@@ -1,28 +0,0 @@
import 'base_event.dart';
class SessionLogoutEvent extends BaseAnalyticsEvent {
SessionLogoutEvent({
required super.eventId,
required super.timestamp,
required super.userId,
required super.deviceId,
required super.sessionId,
required super.platform,
required super.appVersion,
super.appBuild,
required super.env,
super.pageName,
super.traceId,
super.requestId,
String? reason,
int? sessionDurationS,
AnalyticsContext? context,
}) : super(
eventType: 'session.logout',
attributes: reason != null ? {'reason': reason} : const {},
metrics: sessionDurationS != null
? {'session_duration_s': sessionDurationS}
: const {},
context: context,
);
}
@@ -1,29 +0,0 @@
import 'base_event.dart';
class PageViewEvent extends BaseAnalyticsEvent {
PageViewEvent({
required super.eventId,
required super.timestamp,
required super.userId,
required super.deviceId,
required super.sessionId,
required super.platform,
required super.appVersion,
super.appBuild,
required super.env,
required super.pageName,
super.traceId,
super.requestId,
String? pageFrom,
int? stayDurationMs,
int? clickCount,
super.context,
}) : super(
eventType: 'page.view',
attributes: pageFrom != null ? {'page_from': pageFrom} : const {},
metrics: {
if (stayDurationMs != null) 'stay_duration_ms': stayDurationMs,
if (clickCount != null) 'click_count': clickCount,
},
);
}
@@ -1,46 +0,0 @@
import 'dart:async';
import '../events/events.dart';
class EventQueue {
final List<BaseAnalyticsEvent> _queue = [];
final int maxSize;
final Duration flushInterval;
final void Function(List<BaseAnalyticsEvent>) onFlush;
Timer? _timer;
EventQueue({
this.maxSize = 50,
this.flushInterval = const Duration(seconds: 30),
required this.onFlush,
});
void start() {
_timer?.cancel();
_timer = Timer.periodic(flushInterval, (_) => _tryFlush());
}
void stop() {
_timer?.cancel();
_timer = null;
}
void add(BaseAnalyticsEvent event) {
_queue.add(event);
if (_queue.length >= maxSize) {
_tryFlush();
}
}
void _tryFlush() {
if (_queue.isEmpty) return;
final events = List<BaseAnalyticsEvent>.from(_queue);
_queue.clear();
onFlush(events);
}
List<BaseAnalyticsEvent> get pendingEvents => List.unmodifiable(_queue);
int get pendingCount => _queue.length;
}
-45
View File
@@ -1,45 +0,0 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'events/events.dart';
class AnalyticsSender {
final Dio _dio;
final String endpoint;
AnalyticsSender({required this.endpoint, Dio? dio}) : _dio = dio ?? Dio();
Future<void> send(List<BaseAnalyticsEvent> events) async {
if (events.isEmpty) return;
final body = {'events': events.map((e) => e.toJson()).toList()};
try {
await _dio.post(
endpoint,
data: jsonEncode(body),
options: Options(
headers: {'Content-Type': 'application/json'},
sendTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
),
);
} on DioException catch (e) {
throw AnalyticsSendException(
'Failed to send analytics events: ${e.message}',
events: events,
);
}
}
}
class AnalyticsSendException implements Exception {
final String message;
final List<BaseAnalyticsEvent> events;
AnalyticsSendException(this.message, {required this.events});
@override
String toString() => 'AnalyticsSendException: $message';
}
-247
View File
@@ -1,247 +0,0 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
import 'events/events.dart';
import 'queue/event_queue.dart';
import 'sender.dart';
class AnalyticsTracker {
static AnalyticsTracker? _instance;
late final AnalyticsSender _sender;
late final EventQueue _queue;
late final String _deviceId;
late final String _sessionId;
late final String _platform;
late final String _appVersion;
late final String? _appBuild;
late final String _env;
String? _userId;
AnalyticsTracker._();
static AnalyticsTracker get instance {
if (_instance == null) {
throw StateError('AnalyticsTracker not initialized. Call init() first.');
}
return _instance!;
}
static Future<void> init({
required String endpoint,
required String deviceId,
}) async {
if (_instance != null) return;
final packageInfo = await PackageInfo.fromPlatform();
final sessionId = await _getOrCreateSessionId();
final platform = Platform.isAndroid ? 'android' : 'ios';
final env = kDebugMode ? 'dev' : 'prod';
final tracker = AnalyticsTracker._();
tracker._sender = AnalyticsSender(endpoint: endpoint);
tracker._queue = EventQueue(
maxSize: 50,
flushInterval: const Duration(seconds: 30),
onFlush: tracker._handleFlush,
);
tracker._deviceId = deviceId;
tracker._sessionId = sessionId;
tracker._platform = platform;
tracker._appVersion = packageInfo.version;
tracker._appBuild = packageInfo.buildNumber.isNotEmpty
? packageInfo.buildNumber
: null;
tracker._env = env;
tracker._queue.start();
_instance = tracker;
}
static Future<String> _getOrCreateSessionId() async {
const uuid = Uuid();
final prefs = await SharedPreferences.getInstance();
var sessionId = prefs.getString('_analytics_session_id');
if (sessionId == null) {
sessionId = 'sess_${uuid.v4()}';
await prefs.setString('_analytics_session_id', sessionId);
}
return sessionId;
}
void setUserId(String? userId) {
_userId = userId;
}
String get userId => _userId ?? 'anonymous';
String get sessionId => _sessionId;
void track(BaseAnalyticsEvent event) {
_queue.add(event);
}
void trackLogin({
required String method,
String? traceId,
String? requestId,
AnalyticsContext? context,
}) {
track(
SessionLoginEvent(
eventId: _generateEventId(),
timestamp: DateTime.now(),
userId: userId,
deviceId: _deviceId,
sessionId: _sessionId,
platform: _platform,
appVersion: _appVersion,
appBuild: _appBuild,
env: _env,
pageName: 'login',
traceId: traceId,
requestId: requestId,
method: method,
context: context,
),
);
}
void trackLogout({
String? reason,
int? sessionDurationS,
String? pageName,
String? traceId,
AnalyticsContext? context,
}) {
track(
SessionLogoutEvent(
eventId: _generateEventId(),
timestamp: DateTime.now(),
userId: userId,
deviceId: _deviceId,
sessionId: _sessionId,
platform: _platform,
appVersion: _appVersion,
appBuild: _appBuild,
env: _env,
pageName: pageName,
traceId: traceId,
reason: reason,
sessionDurationS: sessionDurationS,
context: context,
),
);
}
void trackAgentChatCompleted({
required String conversationId,
String? scenario,
int? messageCount,
int? responseTimeMs,
String? traceId,
String? requestId,
AnalyticsContext? context,
}) {
track(
AgentChatCompletedEvent(
eventId: _generateEventId(),
timestamp: DateTime.now(),
userId: userId,
deviceId: _deviceId,
sessionId: _sessionId,
platform: _platform,
appVersion: _appVersion,
appBuild: _appBuild,
env: _env,
pageName: 'chat',
traceId: traceId,
requestId: requestId,
conversationId: conversationId,
scenario: scenario,
messageCount: messageCount,
responseTimeMs: responseTimeMs,
context: context,
),
);
}
void trackPageView({
required String pageName,
String? pageFrom,
int? stayDurationMs,
int? clickCount,
String? traceId,
AnalyticsContext? context,
}) {
track(
PageViewEvent(
eventId: _generateEventId(),
timestamp: DateTime.now(),
userId: userId,
deviceId: _deviceId,
sessionId: _sessionId,
platform: _platform,
appVersion: _appVersion,
appBuild: _appBuild,
env: _env,
pageName: pageName,
pageFrom: pageFrom,
stayDurationMs: stayDurationMs,
clickCount: clickCount,
traceId: traceId,
context: context,
),
);
}
void trackClick({
required String pageName,
required String elementId,
String? elementType,
String? traceId,
AnalyticsContext? context,
}) {
track(
UiClickEvent(
eventId: _generateEventId(),
timestamp: DateTime.now(),
userId: userId,
deviceId: _deviceId,
sessionId: _sessionId,
platform: _platform,
appVersion: _appVersion,
appBuild: _appBuild,
env: _env,
pageName: pageName,
elementId: elementId,
elementType: elementType,
traceId: traceId,
context: context,
),
);
}
String _generateEventId() {
return const Uuid().v4();
}
Future<void> _handleFlush(List<BaseAnalyticsEvent> events) async {
try {
await _sender.send(events);
} catch (e) {
// TODO: 失败时落盘本地,下次启动重试
debugPrint('Analytics send failed: $e');
}
}
void dispose() {
_queue.stop();
}
}
-2
View File
@@ -16,8 +16,6 @@ class Env {
return 'http://localhost:5775';
}
static String get analyticsEndpoint => '$apiUrl/api/v1/analytics/events';
static String version = '0.1.0';
static int build = 1;
static String deviceId = '';
@@ -1,5 +1,4 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/analytics/tracker.dart';
import '../../../../core/logging/logger.dart';
import '../../data/repositories/auth_repository.dart';
import 'auth_event.dart';
@@ -8,7 +7,6 @@ import 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository _repository;
final Logger _logger = getLogger('features.auth.bloc');
DateTime? _loginTime;
AuthBloc(this._repository) : super(AuthInitial()) {
on<AuthStarted>(_onStarted);
@@ -23,8 +21,6 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
final refreshToken = await _repository.getRefreshToken();
if (refreshToken != null) {
final response = await _repository.refreshSession(refreshToken);
_loginTime = DateTime.now();
AnalyticsTracker.instance.setUserId(response.user.id);
emit(
AuthAuthenticated(
user: AuthUser(id: response.user.id, phone: response.user.phone),
@@ -60,7 +56,6 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
void _onLoggedIn(AuthLoggedIn event, Emitter<AuthState> emit) {
_loginTime = DateTime.now();
_logger.info(message: 'User logged in', extra: {'user_id': event.user.id});
emit(AuthAuthenticated(user: event.user));
}
@@ -69,9 +64,6 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthLoggedOut event,
Emitter<AuthState> emit,
) async {
final sessionDuration = _loginTime != null
? DateTime.now().difference(_loginTime!).inSeconds
: null;
try {
await _repository.deleteSession();
_logger.info(message: 'User logged out');
@@ -82,11 +74,6 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
stackTrace: stackTrace,
);
} finally {
AnalyticsTracker.instance.trackLogout(
reason: 'manual',
sessionDurationS: sessionDuration,
);
_loginTime = null;
emit(
const AuthUnauthenticated(reason: AuthUnauthenticatedReason.signedOut),
);
@@ -97,9 +84,6 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthSessionInvalidated event,
Emitter<AuthState> emit,
) async {
final sessionDuration = _loginTime != null
? DateTime.now().difference(_loginTime!).inSeconds
: null;
_logger.warning(message: 'Session invalidated by server');
try {
await _repository.clearSessionLocalOnly();
@@ -110,11 +94,6 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
stackTrace: stackTrace,
);
} finally {
AnalyticsTracker.instance.trackLogout(
reason: 'expired',
sessionDurationS: sessionDuration,
);
_loginTime = null;
emit(
const AuthUnauthenticated(reason: AuthUnauthenticatedReason.expired),
);
@@ -6,7 +6,6 @@ import 'package:go_router/go_router.dart';
import '../../../../app/di/injection.dart';
import '../../../../app/router/app_routes.dart';
import '../../../../core/analytics/tracker.dart';
import '../../../../core/l10n/l10n.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_button.dart';
@@ -70,8 +69,6 @@ class _LoginViewState extends State<LoginView> {
final response = await cubit.submit();
if (response != null && mounted) {
AnalyticsTracker.instance.trackLogin(method: 'phone_code');
AnalyticsTracker.instance.setUserId(response.user.id);
context.read<AuthBloc>().add(AuthLoggedIn(user: response.user));
context.go(AppRoutes.homeMain);
}
@@ -12,7 +12,6 @@ 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';
@@ -318,14 +317,7 @@ class ChatBloc extends Cubit<ChatState> implements ChatOrchestrator {
messageCount: 1,
responseTimeMs: responseTimeMs,
);
return;
}
AnalyticsTracker.instance.trackAgentChatCompleted(
conversationId: conversationId,
scenario: 'assistant',
messageCount: 1,
responseTimeMs: responseTimeMs,
);
}
void _clearRunMetrics() {
@@ -12,7 +12,6 @@ import '../../../../app/di/injection.dart';
import '../../../../app/router/app_route_observer.dart';
import '../../../../app/router/app_routes.dart';
import '../../../../core/l10n/l10n.dart';
import '../../../../core/analytics/tracker.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../core/inbox/inbox_sync_store.dart';
import '../../../chat/presentation/bloc/chat_bloc.dart';
@@ -99,8 +98,6 @@ class _HomeScreenState extends State<HomeScreen>
int _previousItemCount = 0;
bool _previousIsLoadingHistory = false;
bool _routeAwareSubscribed = false;
late final DateTime _pageEnteredAt;
int _pageClickCount = 0;
double? _historyViewportPixels;
double? _historyViewportMaxExtent;
final GlobalKey<HomeInputHostState> _inputHostKey =
@@ -124,7 +121,6 @@ class _HomeScreenState extends State<HomeScreen>
duration: const Duration(milliseconds: _rippleDurationMs),
);
_selectedImages.addAll(widget.initialSelectedImages);
_pageEnteredAt = DateTime.now();
final initialUserId = widget.initialUserId?.trim();
if (initialUserId != null && initialUserId.isNotEmpty) {
unawaited(_chatBloc.switchUser(initialUserId));
@@ -152,14 +148,6 @@ class _HomeScreenState extends State<HomeScreen>
@override
void dispose() {
final stayDurationMs = DateTime.now()
.difference(_pageEnteredAt)
.inMilliseconds;
AnalyticsTracker.instance.trackPageView(
pageName: 'home',
stayDurationMs: stayDurationMs,
clickCount: _pageClickCount,
);
_messageController.dispose();
_scrollController.removeListener(_handleScrollChanged);
_scrollController.dispose();
@@ -294,15 +282,15 @@ class _HomeScreenState extends State<HomeScreen>
return HomeFloatingHeader(
unreadCount: _unreadCount,
onTapSettings: () {
_trackClick('header_settings');
context.push(AppRoutes.settingsMain);
},
onTapCalendar: () {
_trackClick('header_calendar');
context.push('${AppRoutes.calendarDayWeek}?from=home');
},
onTapMessages: () {
_trackClick('header_messages');
context.push(AppRoutes.messageInviteList);
},
);
@@ -414,7 +402,7 @@ class _HomeScreenState extends State<HomeScreen>
child: HomeUnreadBadge(
count: _chatUnreadBadgeCount,
onTap: () {
_trackClick('unread_badge');
_scheduleAutoScroll(animated: true);
if (mounted) {
setState(() => _chatUnreadBadgeCount = 0);
@@ -467,7 +455,7 @@ class _HomeScreenState extends State<HomeScreen>
}
Future<void> _onLoadMore(BuildContext context) async {
_trackClick('history_load_more');
final chatBloc = context.read<ChatBloc>();
await _loadMoreHistoryPreservingViewport(chatBloc);
}
@@ -681,15 +669,15 @@ class _HomeScreenState extends State<HomeScreen>
messageController: _messageController,
onTapPlus: _isRecording
? () {
_trackClick('record_stop');
_stopRecording(autoSendAfterTranscribe: false);
}
: () {
_trackClick('input_plus');
_showBottomSheet(context);
},
onStopGenerating: () {
_trackClick('stop_generating');
_onStopGenerating();
},
onHoldToSpeakStart: _onHoldToSpeakStart,
@@ -701,15 +689,6 @@ class _HomeScreenState extends State<HomeScreen>
);
}
void _trackClick(String elementId) {
_pageClickCount += 1;
AnalyticsTracker.instance.trackClick(
pageName: 'home',
elementId: elementId,
elementType: 'button',
);
}
void _removeImage(int index) {
setState(() {
_selectedImages.removeAt(index);
@@ -53,7 +53,6 @@ extension _HomeScreenInteractions on _HomeScreenState {
});
try {
_trackClick('send_message');
await _chatBloc.sendMessage(content, images: images);
} finally {
if (mounted) {
-6
View File
@@ -3,7 +3,6 @@ import 'core/config/env.dart';
import 'core/logging/logger.dart';
import 'core/logging/log_service.dart';
import 'core/logging/error_handler.dart';
import 'core/analytics/tracker.dart';
import 'app/di/injection.dart';
import 'app/app.dart';
@@ -18,11 +17,6 @@ void main() async {
await configureDependencies();
await Env.init();
await AnalyticsTracker.init(
endpoint: Env.analyticsEndpoint,
deviceId: Env.deviceId,
);
getLogger(
'app',
).info(message: 'App starting...', extra: {'version': Env.version});