From 0919ded628380ef41bb936eeb3a47d214db01b88 Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Mon, 9 Feb 2026 18:24:35 +0800 Subject: [PATCH] add log center --- airhub_app/lib/core/network/api_client.dart | 51 ++++++- airhub_app/lib/core/network/api_client.g.dart | 2 +- .../lib/core/services/log_center_service.dart | 142 ++++++++++++++++++ .../core/services/log_center_service.g.dart | 56 +++++++ airhub_app/lib/main.dart | 32 +++- 5 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 airhub_app/lib/core/services/log_center_service.dart create mode 100644 airhub_app/lib/core/services/log_center_service.g.dart diff --git a/airhub_app/lib/core/network/api_client.dart b/airhub_app/lib/core/network/api_client.dart index 8aebc65..5a8f961 100644 --- a/airhub_app/lib/core/network/api_client.dart +++ b/airhub_app/lib/core/network/api_client.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../errors/exceptions.dart'; +import '../services/log_center_service.dart'; import 'api_config.dart'; import 'token_manager.dart'; @@ -10,14 +11,16 @@ part 'api_client.g.dart'; @Riverpod(keepAlive: true) ApiClient apiClient(Ref ref) { final tokenManager = ref.watch(tokenManagerProvider); - return ApiClient(tokenManager); + final logCenter = ref.watch(logCenterServiceProvider); + return ApiClient(tokenManager, logCenter); } class ApiClient { final TokenManager _tokenManager; + final LogCenterService _logCenter; late final Dio _dio; - ApiClient(this._tokenManager) { + ApiClient(this._tokenManager, this._logCenter) { _dio = Dio(BaseOptions( baseUrl: ApiConfig.fullBaseUrl, connectTimeout: ApiConfig.connectTimeout, @@ -26,6 +29,7 @@ class ApiClient { )); _dio.interceptors.add(_AuthInterceptor(_tokenManager, _dio)); + _dio.interceptors.add(_LogCenterInterceptor(_logCenter)); } /// GET 请求,返回 data 字段 @@ -176,3 +180,46 @@ class _AuthInterceptor extends Interceptor { handler.next(err); } } + +/// Log Center 拦截器:自动上报 API 错误 +class _LogCenterInterceptor extends Interceptor { + final LogCenterService _logCenter; + + _LogCenterInterceptor(this._logCenter); + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + final req = err.requestOptions; + final statusCode = err.response?.statusCode; + + // 跳过 401(token 过期是正常流程,不上报) + if (statusCode == 401) { + handler.next(err); + return; + } + + // 提取后端返回的业务错误信息 + String errorMessage = err.message ?? err.type.name; + String errorType = 'DioException.${err.type.name}'; + final body = err.response?.data; + if (body is Map) { + errorType = 'ApiError(${body['code'] ?? statusCode})'; + errorMessage = body['message'] as String? ?? errorMessage; + } + + _logCenter.reportError( + Exception(errorMessage), + context: { + 'type': 'api_error', + 'error_type': errorType, + 'url': '${req.method} ${req.path}', + 'status_code': statusCode, + 'query': req.queryParameters.isNotEmpty + ? req.queryParameters.toString() + : null, + }, + ); + + handler.next(err); + } +} diff --git a/airhub_app/lib/core/network/api_client.g.dart b/airhub_app/lib/core/network/api_client.g.dart index 2d28050..2e0dd3c 100644 --- a/airhub_app/lib/core/network/api_client.g.dart +++ b/airhub_app/lib/core/network/api_client.g.dart @@ -48,4 +48,4 @@ final class ApiClientProvider } } -String _$apiClientHash() => r'9d0cce119ded498b0bdf8ec8bb1ed5fc9fcfb8aa'; +String _$apiClientHash() => r'03fa482085a0f74d1526b1a511e1b3c555269918'; diff --git a/airhub_app/lib/core/services/log_center_service.dart b/airhub_app/lib/core/services/log_center_service.dart new file mode 100644 index 0000000..192cab2 --- /dev/null +++ b/airhub_app/lib/core/services/log_center_service.dart @@ -0,0 +1,142 @@ +import 'dart:isolate'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'log_center_service.g.dart'; + +@Riverpod(keepAlive: true) +LogCenterService logCenterService(Ref ref) { + return LogCenterService(); +} + +class LogCenterService { + static const String _url = + 'https://qiyuan-log-center-api.airlabs.art/api/v1/logs/report'; + static const String _projectId = 'airhub_app'; + + late final Dio _dio; + + LogCenterService() { + _dio = Dio(BaseOptions( + connectTimeout: const Duration(seconds: 3), + receiveTimeout: const Duration(seconds: 3), + headers: {'Content-Type': 'application/json'}, + )); + } + + /// 上报 Flutter 框架错误(FlutterError) + void reportFlutterError(FlutterErrorDetails details) { + _report( + errorType: 'FlutterError', + message: details.exceptionAsString(), + stackTrace: details.stack, + context: { + 'library': details.library ?? 'unknown', + 'context': details.context?.toString() ?? '', + }, + ); + } + + /// 上报未捕获的异步异常(Zone / PlatformDispatcher) + void reportUncaughtError(Object error, StackTrace? stack) { + _report( + errorType: error.runtimeType.toString(), + message: error.toString(), + stackTrace: stack, + ); + } + + /// 上报业务层手动捕获的异常 + void reportError( + Object error, { + StackTrace? stackTrace, + Map? context, + }) { + _report( + errorType: error.runtimeType.toString(), + message: error.toString(), + stackTrace: stackTrace, + context: context, + ); + } + + /// 核心上报逻辑 — 异步、静默、不阻塞 UI + void _report({ + required String errorType, + required String message, + StackTrace? stackTrace, + Map? context, + }) { + // 解析堆栈第一帧 + String filePath = 'unknown'; + int lineNumber = 0; + final frames = stackTrace?.toString().split('\n') ?? []; + if (frames.isNotEmpty) { + final match = + RegExp(r'(?:package:airhub_app/|lib/)(.+?):(\d+)').firstMatch( + frames.firstWhere( + (f) => f.contains('package:airhub_app/') || f.contains('lib/'), + orElse: () => frames.first, + ), + ); + if (match != null) { + filePath = 'lib/${match.group(1)}'; + lineNumber = int.tryParse(match.group(2) ?? '0') ?? 0; + } + } + + final payload = { + 'project_id': _projectId, + 'environment': kDebugMode ? 'development' : 'production', + 'level': 'ERROR', + 'error': { + 'type': errorType, + 'message': message.length > 2000 ? message.substring(0, 2000) : message, + 'file_path': filePath, + 'line_number': lineNumber, + 'stack_trace': frames.take(30).toList(), + }, + 'context': { + 'platform': defaultTargetPlatform.name, + 'is_web': kIsWeb, + ...?context, + }, + }; + + // 异步发送,不阻塞调用方 + _sendAsync(payload); + } + + void _sendAsync(Map payload) { + // Web 不支持 Isolate,用 compute / 直接 fire-and-forget + if (kIsWeb) { + _send(payload); + } else { + // 原生平台用 Isolate 完全不阻塞 UI 线程 + Isolate.run(() => _sendStatic(payload)); + } + } + + Future _send(Map payload) async { + try { + await _dio.post(_url, data: payload); + } catch (_) { + // 静默失败,不影响 App 运行 + } + } + + /// Isolate 内使用的静态方法(独立 Dio 实例) + static Future _sendStatic(Map payload) async { + try { + final dio = Dio(BaseOptions( + connectTimeout: const Duration(seconds: 3), + receiveTimeout: const Duration(seconds: 3), + headers: {'Content-Type': 'application/json'}, + )); + await dio.post(_url, data: payload); + } catch (_) { + // 静默失败 + } + } +} diff --git a/airhub_app/lib/core/services/log_center_service.g.dart b/airhub_app/lib/core/services/log_center_service.g.dart new file mode 100644 index 0000000..c74dd97 --- /dev/null +++ b/airhub_app/lib/core/services/log_center_service.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'log_center_service.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(logCenterService) +const logCenterServiceProvider = LogCenterServiceProvider._(); + +final class LogCenterServiceProvider + extends + $FunctionalProvider< + LogCenterService, + LogCenterService, + LogCenterService + > + with $Provider { + const LogCenterServiceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'logCenterServiceProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$logCenterServiceHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + LogCenterService create(Ref ref) { + return logCenterService(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(LogCenterService value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$logCenterServiceHash() => r'd32eef012bfcebde414b77bfc69fa9ffda09eb5e'; diff --git a/airhub_app/lib/main.dart b/airhub_app/lib/main.dart index aaffd05..21a7dc8 100644 --- a/airhub_app/lib/main.dart +++ b/airhub_app/lib/main.dart @@ -1,13 +1,41 @@ +import 'dart:async'; +import 'dart:ui' show PlatformDispatcher; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'core/router/app_router.dart'; +import 'core/services/log_center_service.dart'; import 'theme/app_theme.dart'; void main() { - WidgetsFlutterBinding.ensureInitialized(); - runApp(const ProviderScope(child: AirhubApp())); + runZonedGuarded(() { + WidgetsFlutterBinding.ensureInitialized(); + + final container = ProviderContainer(); + final logCenter = container.read(logCenterServiceProvider); + + // 捕获 Flutter 框架错误(Widget build 异常等) + FlutterError.onError = (details) { + FlutterError.presentError(details); // 保留控制台输出 + logCenter.reportFlutterError(details); + }; + + // 捕获非 Flutter 框架的平台异常 + PlatformDispatcher.instance.onError = (error, stack) { + logCenter.reportUncaughtError(error, stack); + return true; + }; + + runApp(UncontrolledProviderScope( + container: container, + child: const AirhubApp(), + )); + }, (error, stack) { + // Zone 兜底:捕获 runZonedGuarded 区域内的未处理异步异常 + // 此处 container 不可用,直接静态发送 + LogCenterService().reportUncaughtError(error, stack); + }); } class AirhubApp extends ConsumerWidget {