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 '../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<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/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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user