add log center

This commit is contained in:
zyc 2026-02-09 18:24:35 +08:00
parent b54fbc1ccb
commit 0919ded628
5 changed files with 278 additions and 5 deletions

View File

@ -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;
// 401token
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);
}
}

View File

@ -48,4 +48,4 @@ final class ApiClientProvider
}
}
String _$apiClientHash() => r'9d0cce119ded498b0bdf8ec8bb1ed5fc9fcfb8aa';
String _$apiClientHash() => r'03fa482085a0f74d1526b1a511e1b3c555269918';

View 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 (_) {
//
}
}
}

View 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';

View File

@ -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 {