完成封装打印和推送功能日志方法和使用
完成添加绑定设备接口的日志便于追溯
This commit is contained in:
4
lib/core/env/env_config.dart
vendored
4
lib/core/env/env_config.dart
vendored
@@ -7,9 +7,9 @@ class EnvConfig {
|
||||
static String get sentryDsn {
|
||||
// 根据环境切换 DSN(去 Sentry 后台创建两个项目:Test 和 Prod)
|
||||
if (environment == 'prod') {
|
||||
return 'https://your_prod_dsn@sentry.io/123';
|
||||
return 'https://d5fdda78335793935593efa3f4ed11bb@o4510984107261952.ingest.de.sentry.io/4510984527937616';
|
||||
}
|
||||
return 'https://your_test_dsn@sentry.io/456';
|
||||
return 'https://1a72051f8ac6c21cde96ded18a318217@o4510984107261952.ingest.de.sentry.io/4510984468824144';
|
||||
}
|
||||
|
||||
static bool get isProduction => environment == 'prod';
|
||||
|
||||
@@ -10,17 +10,26 @@ import 'package:intl/intl.dart';
|
||||
|
||||
class SentryLoggerImpl implements ILoggerService {
|
||||
File? _localLogFile;
|
||||
//调试模式开关
|
||||
bool _isDebugModeEnabled = false;
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
// await SentryFlutter.init((options) {
|
||||
// options.dsn = EnvConfig.sentryDsn;
|
||||
// options.environment = EnvConfig.environment;
|
||||
// // 生产环境只采样 20% 的性能数据,测试环境全开
|
||||
// options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0;
|
||||
// });
|
||||
await _collectDeviceInfo();
|
||||
_isDebugModeEnabled = false;
|
||||
await SentryFlutter.init((options) {
|
||||
options.dsn = EnvConfig.sentryDsn;
|
||||
options.environment = EnvConfig.environment;
|
||||
// 生产环境只采样 20% 的性能数据,测试环境全开
|
||||
options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0;
|
||||
|
||||
// 动态设置 Sentry 接收的日志级别
|
||||
// 如果开启调试模式,接收 DEBUG;否则生产环境只接收 INFO 以上
|
||||
|
||||
options.debug = _isDebugModeEnabled; // 开启 SDK 内部调试
|
||||
|
||||
});
|
||||
|
||||
await _collectDeviceInfo();
|
||||
await _prepareLocalFile();
|
||||
}
|
||||
|
||||
@@ -93,6 +102,54 @@ class SentryLoggerImpl implements ILoggerService {
|
||||
// 本地写入失败不应崩溃
|
||||
}
|
||||
}
|
||||
@override
|
||||
void logWithLevel(
|
||||
String message, {
|
||||
String level = 'INFO', // 'DEBUG', 'INFO', 'WARNING', 'ERROR'
|
||||
Map<String, dynamic>? data,
|
||||
}) {
|
||||
final timestamp = DateTime.now().toIso8601String();
|
||||
final logEntry = "[$timestamp] [$level] $message ${data ?? ''}\n";
|
||||
|
||||
// --- 策略判断 ---
|
||||
// 1. 开发环境:全部允许
|
||||
// 2. 生产环境:
|
||||
// - 如果是 DEBUG 级别:只有 _isDebugModeEnabled 为 true 时才允许
|
||||
// - 其他级别:始终允许
|
||||
bool shouldProcess = !EnvConfig.isProduction ||
|
||||
level != 'DEBUG' ||
|
||||
_isDebugModeEnabled;
|
||||
|
||||
if (shouldProcess) {
|
||||
// 写入本地文件
|
||||
_writeToLocalFile(logEntry);
|
||||
|
||||
// 上报 Sentry
|
||||
SentryLevel sentryLevel;
|
||||
switch (level) {
|
||||
case 'DEBUG': sentryLevel = SentryLevel.debug; break;
|
||||
case 'WARNING': sentryLevel = SentryLevel.warning; break;
|
||||
case 'ERROR': sentryLevel = SentryLevel.error; break;
|
||||
default: sentryLevel = SentryLevel.info;
|
||||
}
|
||||
|
||||
// 使用 addBreadcrumb 记录轨迹
|
||||
Sentry.addBreadcrumb(Breadcrumb(
|
||||
message: message,
|
||||
level: sentryLevel,
|
||||
data: data,
|
||||
));
|
||||
|
||||
// 如果是 ERROR,额外捕获为一个事件
|
||||
if (level == 'ERROR') {
|
||||
Sentry.captureMessage(message, level: sentryLevel);
|
||||
}
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint(logEntry);
|
||||
}
|
||||
}
|
||||
|
||||
// 增加一个获取本地日志文件路径的方法,方便你以后做“导出日志”功能
|
||||
Future<String?> getLocalLogPath() async {
|
||||
|
||||
@@ -6,6 +6,11 @@ abstract class ILoggerService {
|
||||
bool isWarning = false,
|
||||
Map<String, dynamic>? data,
|
||||
});
|
||||
void logWithLevel(
|
||||
String message, {
|
||||
String level = 'INFO', // 'DEBUG', 'INFO', 'WARNING', 'ERROR'
|
||||
Map<String, dynamic>? data,
|
||||
});
|
||||
void addBreadcrumb(String message, {String? category});
|
||||
void setUser(String userId);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
|
||||
|
||||
import '../app/app_user_cubit.dart';
|
||||
import '../di/injection.dart';
|
||||
import '../infrastructure/logging/sentry_logger_impl.dart';
|
||||
import '../logging/i_logger_service.dart';
|
||||
|
||||
class DioClient {
|
||||
static Dio create() {
|
||||
@@ -14,26 +18,110 @@ class DioClient {
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
),
|
||||
);
|
||||
dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
|
||||
dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
// 1. 从 GetIt 中获取全局的 AppUserCubit
|
||||
if (kDebugMode) {
|
||||
dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
|
||||
}
|
||||
// dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
|
||||
// dio.interceptors.add(
|
||||
// InterceptorsWrapper(
|
||||
// onRequest: (options, handler) {
|
||||
// // 1. 从 GetIt 中获取全局的 AppUserCubit
|
||||
// final userCubit = sl<AppUserCubit>();
|
||||
//
|
||||
// // 2. 从 Cubit 的状态中提取 Token
|
||||
// final token = userCubit.state.user?.token;
|
||||
//
|
||||
// // 3. 如果 Token 存在,则添加到请求头
|
||||
// if (token != null && token.isNotEmpty) {
|
||||
// options.headers['Authorization'] = 'Bearer $token';
|
||||
// }
|
||||
//
|
||||
// // 继续发送请求
|
||||
// return handler.next(options);
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// return dio;
|
||||
// }
|
||||
dio.interceptors.add(InterceptorsWrapper(
|
||||
onError: (DioException error, ErrorInterceptorHandler handler) async {
|
||||
// 捕获所有网络异常 (超时、无网络、DNS 错误等)
|
||||
await _reportToSentry(error);
|
||||
return handler.next(error);
|
||||
},
|
||||
onResponse: (Response response, ResponseInterceptorHandler handler) async {
|
||||
// 捕获业务错误 (如 HTTP 4xx, 5xx)
|
||||
if (response.statusCode != null && response.statusCode! >= 400) {
|
||||
await _reportHttpError(response);
|
||||
}
|
||||
return handler.next(response);
|
||||
},
|
||||
onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
|
||||
// 原有 Token 逻辑保持不变
|
||||
try {
|
||||
final userCubit = sl<AppUserCubit>();
|
||||
|
||||
// 2. 从 Cubit 的状态中提取 Token
|
||||
final token = userCubit.state.user?.token;
|
||||
|
||||
// 3. 如果 Token 存在,则添加到请求头
|
||||
if (token != null && token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
} catch (e) {
|
||||
// 防止获取 Token 时出错阻断请求
|
||||
debugPrint('获取 Token 失败: $e');
|
||||
}
|
||||
return handler.next(options);
|
||||
},
|
||||
));
|
||||
|
||||
// 继续发送请求
|
||||
return handler.next(options);
|
||||
},
|
||||
),
|
||||
);
|
||||
return dio;
|
||||
}
|
||||
// 上报 HTTP 状态码错误 (404, 500 等)
|
||||
static Future<void> _reportHttpError(Response response) async {
|
||||
try {
|
||||
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
|
||||
|
||||
// 记录详细的错误上下文
|
||||
logger.logWithLevel(
|
||||
'HTTP 请求失败: ${response.statusCode}',
|
||||
level: 'ERROR',
|
||||
data: {
|
||||
'url': response.requestOptions.uri.toString(),
|
||||
'method': response.requestOptions.method,
|
||||
'statusCode': response.statusCode,
|
||||
'responseData': response.data, // 注意:如果响应包含敏感信息,请脱敏
|
||||
},
|
||||
);
|
||||
|
||||
// 可选:如果希望每个 4xx/5xx 都生成一个独立 Issue,取消下面注释
|
||||
// await Sentry.captureMessage(
|
||||
// 'HTTP Error ${response.statusCode}: ${response.requestOptions.path}',
|
||||
// level: SentryLevel.error,
|
||||
// extra: {'data': response.data},
|
||||
// );
|
||||
} catch (e) {
|
||||
debugPrint('Sentry 上报 HTTP 错误失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 上报网络异常 (超时、断开等)
|
||||
static Future<void> _reportToSentry(DioException error) async {
|
||||
try {
|
||||
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
|
||||
|
||||
logger.logWithLevel(
|
||||
'网络异常: ${error.type}',
|
||||
level: 'ERROR',
|
||||
data: {
|
||||
'url': error.requestOptions.uri.toString(),
|
||||
'method': error.requestOptions.method,
|
||||
'message': error.message,
|
||||
'type': error.type.name,
|
||||
},
|
||||
);
|
||||
|
||||
// 捕获完整堆栈
|
||||
logger.captureException(error, stackTrace: error.stackTrace);
|
||||
} catch (e) {
|
||||
debugPrint('Sentry 上报网络异常失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
|
||||
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/net_message_dispatcher.dart';
|
||||
|
||||
import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../../core/infrastructure/logging/sentry_logger_impl.dart';
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../core/network/tcp/tcp_client.dart';
|
||||
import '../../../../core/storage/user_storage.dart';
|
||||
import '../../../devices/domain/usecases/get_user_device_usecase.dart';
|
||||
@@ -51,7 +54,9 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
|
||||
/// 当 HTTP 登录/注册成功后调用
|
||||
Future<void> loginSuccess(UserEntity user) async {
|
||||
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
|
||||
await storage.saveUser(user);
|
||||
logger.setUser(user.userId);
|
||||
await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数
|
||||
tcp.startHeartbeat(interval: const Duration(seconds: 4)); //启动心跳
|
||||
@@ -62,6 +67,8 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
/// 退出登录 (主动或被动)
|
||||
Future<void> logout() async {
|
||||
print("退出登录");
|
||||
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
|
||||
logger.setUser('');// 清除用户信息
|
||||
await storage.deleteUser();
|
||||
tcp.disconnect();
|
||||
appCubit.clearAuth();
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/data/datasources/device_http_datasource.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
|
||||
|
||||
import '../../../../../core/infrastructure/logging/sentry_logger_impl.dart';
|
||||
import '../../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../../core/storage/user_storage.dart';
|
||||
import '../../../domain/entities/device_location_entity.dart';
|
||||
|
||||
@@ -77,7 +80,15 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
|
||||
@override
|
||||
Future<int> switchDevice(String platform, String deviceId) async {
|
||||
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
|
||||
final token = await _getToken();
|
||||
print('用户点击switchDevice方法开始触发: $token');
|
||||
logger.logWithLevel('用户点击switchDevice方法开始触发', level: 'DEBUG');
|
||||
logger.logWithLevel(
|
||||
'用户点击switchDevice方法API 请求开始',
|
||||
level: 'INFO',
|
||||
data: {'url': 'https://serviceri.satabot.com/iot/device/switchDevice'}
|
||||
);
|
||||
var response = await dio.post(
|
||||
HttpApiConsts.switchDevice,
|
||||
data: {'platform': platform, 'deviceId': deviceId},
|
||||
@@ -88,16 +99,38 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
},
|
||||
),
|
||||
);
|
||||
logger.logWithLevel(
|
||||
'请求详细参数',
|
||||
level: 'DEBUG',
|
||||
data: {
|
||||
'url': 'https://serviceri.satabot.com/iot/device/switchDevice',
|
||||
'platform': platform,
|
||||
'deviceId': deviceId
|
||||
}
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
logger.logWithLevel(
|
||||
'API 请求失败',
|
||||
level: 'ERROR',
|
||||
data: {'statusCode': response.statusCode, 'data': response.data}
|
||||
);
|
||||
throw Exception('网络请求失败:${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseData = response.data;
|
||||
|
||||
logger.logWithLevel(
|
||||
'API 响应成功',
|
||||
level: 'INFO',
|
||||
data: {'statusCode': response.statusCode, 'data': response.data}
|
||||
);
|
||||
if (responseData['code'] != 200 || responseData['data'] != true) {
|
||||
logger.logWithLevel(
|
||||
'API 响应失败',
|
||||
level: 'ERROR',
|
||||
data: {'statusCode': response.statusCode, 'data': response.data}
|
||||
);
|
||||
throw Exception(responseData['msg'] ?? '业务异常');
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -142,9 +175,19 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
|
||||
@override
|
||||
Future<dynamic> getDeviceLocation(String deviceame) async {
|
||||
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
|
||||
// TODO: implement getDeviceLocation
|
||||
try {
|
||||
// 发起 GET 请求
|
||||
logger.logWithLevel('用户点击getDeviceLocation方法开始触发', level: 'INFO');
|
||||
logger.logWithLevel(
|
||||
'请求详细参数',
|
||||
level: 'DEBUG',
|
||||
data: {
|
||||
'url': 'https://serviceri.satabot.com/iot/device/userDevice',
|
||||
'tenantName': deviceame
|
||||
}
|
||||
);
|
||||
final response = await dio.get(
|
||||
'https://serviceri.satabot.com/iot/device/userDevice',
|
||||
queryParameters: {'tenantName': deviceame},
|
||||
@@ -159,21 +202,35 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
// 检查响应状态码
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
logger.logWithLevel(
|
||||
'API 响应接收',
|
||||
level: 'DEBUG',
|
||||
data: {'statusCode': response.statusCode, 'data': response.data}
|
||||
);
|
||||
if (data['code'] == 200) {
|
||||
//return data['data']; // 返回设备数据
|
||||
final locationData = data['data']; // 假设数据结构,测试时候如有不妥就修改结果的实例化
|
||||
logger.logWithLevel(
|
||||
'成功获取设备位置',
|
||||
level: 'INFO',
|
||||
data: {'lat': locationData['latitude'], 'lng': locationData['longitude']}
|
||||
);
|
||||
return DeviceLocationEntity(
|
||||
deviceName: locationData['deviceName'],
|
||||
latitude: locationData['latitude'],
|
||||
longitude: locationData['longitude'],
|
||||
);
|
||||
} else {
|
||||
logger.logWithLevel(data['msg'] ?? '业务异常', level: 'ERROR');
|
||||
throw Exception(data['msg'] ?? '业务异常');
|
||||
}
|
||||
} else {
|
||||
logger.logWithLevel('网络请求失败: ${response.statusCode}', level: 'ERROR');
|
||||
throw Exception('网络请求失败: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.logWithLevel('获取设备位置失败: $e', level: 'ERROR');
|
||||
logger.captureException(e, stackTrace: e);
|
||||
throw Exception('获取设备位置失败: $e');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user