add log center
This commit is contained in:
parent
b54fbc1ccb
commit
0919ded628
@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
import '../errors/exceptions.dart';
|
import '../errors/exceptions.dart';
|
||||||
|
import '../services/log_center_service.dart';
|
||||||
import 'api_config.dart';
|
import 'api_config.dart';
|
||||||
import 'token_manager.dart';
|
import 'token_manager.dart';
|
||||||
|
|
||||||
@ -10,14 +11,16 @@ part 'api_client.g.dart';
|
|||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
ApiClient apiClient(Ref ref) {
|
ApiClient apiClient(Ref ref) {
|
||||||
final tokenManager = ref.watch(tokenManagerProvider);
|
final tokenManager = ref.watch(tokenManagerProvider);
|
||||||
return ApiClient(tokenManager);
|
final logCenter = ref.watch(logCenterServiceProvider);
|
||||||
|
return ApiClient(tokenManager, logCenter);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
final TokenManager _tokenManager;
|
final TokenManager _tokenManager;
|
||||||
|
final LogCenterService _logCenter;
|
||||||
late final Dio _dio;
|
late final Dio _dio;
|
||||||
|
|
||||||
ApiClient(this._tokenManager) {
|
ApiClient(this._tokenManager, this._logCenter) {
|
||||||
_dio = Dio(BaseOptions(
|
_dio = Dio(BaseOptions(
|
||||||
baseUrl: ApiConfig.fullBaseUrl,
|
baseUrl: ApiConfig.fullBaseUrl,
|
||||||
connectTimeout: ApiConfig.connectTimeout,
|
connectTimeout: ApiConfig.connectTimeout,
|
||||||
@ -26,6 +29,7 @@ class ApiClient {
|
|||||||
));
|
));
|
||||||
|
|
||||||
_dio.interceptors.add(_AuthInterceptor(_tokenManager, _dio));
|
_dio.interceptors.add(_AuthInterceptor(_tokenManager, _dio));
|
||||||
|
_dio.interceptors.add(_LogCenterInterceptor(_logCenter));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET 请求,返回 data 字段
|
/// GET 请求,返回 data 字段
|
||||||
@ -176,3 +180,46 @@ class _AuthInterceptor extends Interceptor {
|
|||||||
handler.next(err);
|
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<String, dynamic>) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -48,4 +48,4 @@ final class ApiClientProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$apiClientHash() => r'9d0cce119ded498b0bdf8ec8bb1ed5fc9fcfb8aa';
|
String _$apiClientHash() => r'03fa482085a0f74d1526b1a511e1b3c555269918';
|
||||||
|
|||||||
142
airhub_app/lib/core/services/log_center_service.dart
Normal file
142
airhub_app/lib/core/services/log_center_service.dart
Normal file
@ -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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic> payload) {
|
||||||
|
// Web 不支持 Isolate,用 compute / 直接 fire-and-forget
|
||||||
|
if (kIsWeb) {
|
||||||
|
_send(payload);
|
||||||
|
} else {
|
||||||
|
// 原生平台用 Isolate 完全不阻塞 UI 线程
|
||||||
|
Isolate.run(() => _sendStatic(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _send(Map<String, dynamic> payload) async {
|
||||||
|
try {
|
||||||
|
await _dio.post(_url, data: payload);
|
||||||
|
} catch (_) {
|
||||||
|
// 静默失败,不影响 App 运行
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Isolate 内使用的静态方法(独立 Dio 实例)
|
||||||
|
static Future<void> _sendStatic(Map<String, dynamic> 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 (_) {
|
||||||
|
// 静默失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
airhub_app/lib/core/services/log_center_service.g.dart
Normal file
56
airhub_app/lib/core/services/log_center_service.g.dart
Normal file
@ -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<LogCenterService> {
|
||||||
|
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<LogCenterService> $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<LogCenterService>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$logCenterServiceHash() => r'd32eef012bfcebde414b77bfc69fa9ffda09eb5e';
|
||||||
@ -1,13 +1,41 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui' show PlatformDispatcher;
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'core/router/app_router.dart';
|
import 'core/router/app_router.dart';
|
||||||
|
import 'core/services/log_center_service.dart';
|
||||||
import 'theme/app_theme.dart';
|
import 'theme/app_theme.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
runZonedGuarded(() {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
runApp(const ProviderScope(child: AirhubApp()));
|
|
||||||
|
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 {
|
class AirhubApp extends ConsumerWidget {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user