Flutter接入sentry流程

前言

  • sentry,一个线上异常监控平台,类似国内的Bugly,但要收费,Bugly用免费版就基本足够

导入依赖

dependencies:
  sentry_flutter: ^8.12.0

初始化SDK

import 'package:sentry_flutter/sentry_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  FlutterError.onError = (errorDetails) {
    Sentry.captureException(
      errorDetails,
      stackTrace: errorDetails.stack,
    );
  };

  PlatformDispatcher.instance.onError = (error, stack) {
    Sentry.captureException(
      error,
      stackTrace: stack,
    );
    return true;
  };

  await SentryFlutter.init(
    (options) {
      options.dsn = 'https://8e0d1dc077f7530d6e3d8f89b417ec18@o4508619471716352.ingest.us.sentry.io/4508645179588608';
      options.tracesSampleRate = 1.0;
      options.profilesSampleRate = 1.0;
    },
    appRunner: () => {
      // 执行原本的初始化逻辑
      runApp(const MyApp());
    }),
  ;
}

捕获Dio日志

  Future<dynamic> post(String url,
      {dynamic body,
      Map<String, dynamic>? query,
      bool isBodyEncrypt = true,
      bool isFileUpload = false,
      Function? callback}) async {
  LoggerUtils.w('send url:$url');
  LoggerUtils.w('send body:$body');
  if (isBodyEncrypt) {
      String bodyJson = jsonEncode(body);
      body = DecryptUtils.getInstance().encrypt(bodyJson);
  }
  _dio.options.method = 'POST';
  if (isFileUpload) {
      _dio.options.connectTimeout = const Duration(seconds: zip6in1Timeout);
      _dio.options.receiveTimeout = const Duration(seconds: zip6in1Timeout);
  } else {
      _dio.options.connectTimeout = const Duration(seconds: commonTimeout);
      _dio.options.receiveTimeout = const Duration(seconds: commonTimeout);
  }
  Response response;
  try {
      url = DecryptUtils.getInstance().encrypt(url, isUrl: true);
      response = await _dio.post(url, data: body, queryParameters: query);
  } on DioException catch (error) {
      // Sentry上报异常
      captureException(error, null);

      // 请求超时
      if (error.type == DioExceptionType.connectionTimeout ||
          error.type == DioExceptionType.sendTimeout ||
          error.type == DioExceptionType.receiveTimeout) {
      callback?.call(TextConfig.networkTimeout);
      } else {
      callback?.call(TextConfig.networkError);
      }
      return null;
  } catch (error) {
      callback?.call(TextConfig.networkError);
      return null;
  }
  return parseResponse(response, callback: callback);
}

/// Sentry捕获日志
void captureException(DioException error, Map? extra) {
  String decrypt16tPath = "";
  try {
    if (error.requestOptions.path.isNotEmpty &&
        !error.requestOptions.path.contains("/")) {
      decrypt16tPath =
          DecryptUtils.getInstance().encrypt(error.requestOptions.path);
      // encryptUtil.decryptDataReturnStr(error.requestOptions.path);
    } else {
      decrypt16tPath = error.requestOptions.path;
    }
  } catch (_) {
    decrypt16tPath = error.requestOptions.path;
  }
  int code = -99999;
  Map<dynamic, dynamic>? map;
  Map? responseData;
  if (error.requestOptions.data != null) {
    if (error.requestOptions.data is! Map) {
      try {
        // if (NetConfig.isOpenEncrypt) {
        var data = error.requestOptions.data;
        map = jsonDecode(DecryptUtils.getInstance().decrypt(data.toString()))
            as Map<dynamic, dynamic>;
        // map = jsonDecode(encryptUtil.decryptDataReturnStr(data.toString()));
        // }
      } catch (e) {
        print(e.toString());
      }
    } else {
      map = error.requestOptions.data as Map<dynamic, dynamic>;
    }
  }
  if (error.response != null && error.response!.statusCode != null) {
    code = error.response!.statusCode ?? -99999;
  }
  if (error.response != null && error.response!.data != null) {
    responseData = error.response!.data as Map;
  }
  print('httpCode=$code\n'
      'errorMessage=${error.message ?? ""}\n'
      'responseData=${(responseData ?? {}).toString()}\n'
      'requestOptions=${(map ?? {}).toString()}\n'
      'errorType=${error.type ?? ''}\n'
      'extra=${(extra ?? {}).toString()}\n'
      'url=$decrypt16tPath \n');
  Sentry.captureMessage(
    'httpCode=$code\n'
    'errorMessage=${error.message ?? ""}\n'
    'responseData=${(responseData ?? {}).toString()}\n'
    'requestOptions=${(map ?? {}).toString()}\n'
    'errorType=${error.type ?? ''}\n'
    'extra=${(extra ?? {}).toString()}\n'
    'url=$decrypt16tPath \n',
  );
  Sentry.captureException(
    error,
    stackTrace: error.stackTrace,
  );
}

Sentry监控日志和AF埋点

// 注意: af和打日志,统一调这个函数
static void debugLog(Object? object) {
  if (!CGAppBaseConfig.productionEnvironment) {
    debugPrint('${object ?? ''}');
  }
  Sentry.addBreadcrumb(
      Breadcrumb(message: object.toString(), level: SentryLevel.info));
}

Sentry监控home键和返回键

  • 物理返回键:main_physics_back
/// 物理返回键
late final main_physics_back = 'main_physics_back'.create;

Future<bool> onWillPop() async {
  BurialPointEvent.homePage.main_physics_back.send();
    return true;
  }
}
  • 按home键去到桌面:main_back_desktop
  • 从桌面回到app中:main_from_desktop_enter
class XXXpage with WidgetsBindingObserver {
 
  @override
  void initState() {
    super.initState();

    // 监听前后台切换
    WidgetsBinding.instance.addObserver(this);
  }
  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    // 应用从后台,回到前台
    if (state == AppLifecycleState.resumed) {
      LoggerUtils.d("App => Foreground");
      Sentry.addBreadcrumb(Breadcrumb(
          message: 'main_from_desktop_enter', level: SentryLevel.info));
    } else if (state == AppLifecycleState.paused) {
      // 应用进入后台
      LoggerUtils.d("App => Background");
      Sentry.addBreadcrumb(
          Breadcrumb(message: 'main_back_desktop', level: SentryLevel.info));
    }
  }

  @override
  void dispose() {
    // 注销观察者
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容