TF-4268 Improve SentryConfig and SentryManager: sampling, frames tracking, non-blocking capture

This commit is contained in:
dab246
2026-04-07 16:09:55 +07:00
committed by Dat H. Pham
parent 2a666056c2
commit c9ec1c2820
4 changed files with 163 additions and 83 deletions
+6 -12
View File
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:core/utils/build_utils.dart'; import 'package:core/utils/build_utils.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:core/utils/sentry/sentry_manager.dart'; import 'package:core/utils/sentry/sentry_manager.dart';
@@ -82,17 +80,13 @@ void _internalLog(
if (shouldSentry) { if (shouldSentry) {
if (level == Level.trace) { if (level == Level.trace) {
unawaited( SentryManager.instance.captureMessage(rawMessage, extras: extras);
SentryManager.instance.captureMessage(rawMessage, extras: extras),
);
} else { } else {
unawaited( SentryManager.instance.captureException(
SentryManager.instance.captureException( exception ?? rawMessage,
exception ?? rawMessage, stackTrace: stackTrace,
stackTrace: stackTrace, message: rawMessage,
message: rawMessage, extras: extras,
extras: extras,
),
); );
} }
} }
+15 -5
View File
@@ -1,6 +1,5 @@
import 'package:core/utils/application_manager.dart'; import 'package:core/utils/application_manager.dart';
import 'package:core/utils/build_utils.dart'; import 'package:core/utils/build_utils.dart';
import 'package:core/utils/config/env_loader.dart';
import 'package:core/utils/app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
@@ -15,7 +14,7 @@ class SentryConfig {
// Current app release version // Current app release version
final String release; final String release;
// // Performance monitoring: Set tracesSampleRate to 1.0 to capture 100% of transactions for tracing // Performance monitoring: Set tracesSampleRate to 1.0 to capture 100% of transactions for tracing
final double tracesSampleRate; final double tracesSampleRate;
// Optional profiling // Optional profiling
@@ -38,23 +37,34 @@ class SentryConfig {
// This must match the `--dist` parameter used when uploading source maps to Sentry. // This must match the `--dist` parameter used when uploading source maps to Sentry.
final String? dist; final String? dist;
// Release Health: The sampling rate for sessions (0.0 to 1.0). Defines the percentage of sessions to send.
final double sessionSampleRate;
// Error tracking: The sampling rate for errors (0.0 to 1.0). If set to 0.1, only 10% of errors are sent.
final double onErrorSampleRate;
// Performance: Tracks UI rendering performance (slow and frozen frames).
final bool enableFramesTracking;
SentryConfig({ SentryConfig({
required this.dsn, required this.dsn,
required this.environment, required this.environment,
required this.release, required this.release,
this.tracesSampleRate = 1.0, this.tracesSampleRate = 1.0,
this.profilesSampleRate = 1.0, this.profilesSampleRate = 1.0,
this.sessionSampleRate = 1.0,
this.onErrorSampleRate = 1.0,
this.enableLogs = true, this.enableLogs = true,
this.enableFramesTracking = true,
this.isDebug = BuildUtils.isDebugMode, this.isDebug = BuildUtils.isDebugMode,
this.attachScreenshot = false, this.attachScreenshot = false,
this.isAvailable = false, this.isAvailable = false,
this.dist, this.dist,
}); });
/// Load configuration from an env file. /// Loads configuration from loaded environment variables.
static Future<SentryConfig?> load() async { static Future<SentryConfig?> load() async {
await EnvLoader.loadConfigFromEnv(); // Note: Ensure EnvLoader.loadEnvFile() is called in main.dart before this.
final sentryAvailable = dotenv.get('SENTRY_ENABLED', fallback: 'false'); final sentryAvailable = dotenv.get('SENTRY_ENABLED', fallback: 'false');
final isAvailable = sentryAvailable == 'true'; final isAvailable = sentryAvailable == 'true';
+48 -23
View File
@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/sentry/sentry_config.dart'; import 'package:core/utils/sentry/sentry_config.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
@@ -20,34 +21,58 @@ class SentryInitializer {
'token', 'token',
]; ];
static Future<bool> init(FutureOr<void> Function() appRunner) async { /// Initializes Sentry.
final config = await SentryConfig.load(); /// Returns [true] if Sentry started successfully and handled the app execution.
/// Returns [false] if Sentry is disabled or failed to start.
static Future<bool> init({
FutureOr<void> Function()? appRunner,
SentryConfig? sentryConfig,
}) async {
try {
final config = sentryConfig ?? await SentryConfig.load();
if (config == null) return false; // Return false if config is missing or Sentry is disabled via Env.
if (config == null || !config.isAvailable) {
return false;
}
await SentryFlutter.init( await SentryFlutter.init(
(options) { (options) => _setUpSentryOptions(options, config),
options.dsn = config.dsn; // Sentry will execute this runner if initialization succeeds.
options.environment = config.environment; appRunner: appRunner,
options.release = config.release; );
options.dist = config.dist;
options.tracesSampleRate = config.tracesSampleRate;
options.profilesSampleRate = config.profilesSampleRate;
options.enableLogs = config.enableLogs;
options.debug = config.isDebug;
options.attachScreenshot = config.attachScreenshot;
options.maxRequestBodySize = MaxRequestBodySize.small;
// Automatically enable breadcrumbs that are appropriate for the current platform return true;
options.enableBreadcrumbTrackingForCurrentPlatform(); } catch (e) {
// In case of any error during init, return false so main() can fallback.
logWarning('[SentryInitializer] Init failed: $e');
return false;
}
}
// Assign the callback to process events before sending them to Sentry static FutureOr<void> _setUpSentryOptions(
options.beforeSend = _beforeSendHandler; SentryFlutterOptions options,
}, SentryConfig config,
appRunner: appRunner, ) async {
); options.dsn = config.dsn;
options.environment = config.environment;
options.release = config.release;
options.dist = config.dist;
options.tracesSampleRate = config.tracesSampleRate;
options.profilesSampleRate = config.profilesSampleRate;
options.replay.sessionSampleRate = config.sessionSampleRate;
options.replay.onErrorSampleRate = config.onErrorSampleRate;
options.enableLogs = config.enableLogs;
options.enableFramesTracking = config.enableFramesTracking;
options.debug = config.isDebug;
options.attachScreenshot = config.attachScreenshot;
options.maxRequestBodySize = MaxRequestBodySize.small;
return config.isAvailable; // Automatically enable breadcrumbs that are appropriate for the current platform
options.enableBreadcrumbTrackingForCurrentPlatform();
// Assign the callback to process events before sending them to Sentry
options.beforeSend = _beforeSendHandler;
} }
/// Handler executed before sending an event to Sentry /// Handler executed before sending an event to Sentry
+94 -43
View File
@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:core/utils/app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:core/utils/sentry/sentry_config.dart';
import 'package:core/utils/sentry/sentry_initializer.dart'; import 'package:core/utils/sentry/sentry_initializer.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
@@ -14,22 +15,25 @@ class SentryManager {
bool get isSentryAvailable => _isSentryAvailable; bool get isSentryAvailable => _isSentryAvailable;
/// Initialize Sentry. App still runs if init fails. /// Initialize Sentry.
Future<void> initialize({ Future<void> initialize({
required FutureOr<void> Function() appRunner, required FutureOr<void> Function() appRunner,
required FutureOr<void> Function() fallBackRunner, required FutureOr<void> Function() fallBackRunner,
}) async { }) async {
if (_isSentryAvailable) {
log('[SentryManager] Already initialized.');
await appRunner();
return;
}
try { try {
if (_isSentryAvailable) { _isSentryAvailable = await SentryInitializer.init(appRunner: appRunner);
log('[SentryManager] Already initialized, skipping');
await appRunner();
return;
}
_isSentryAvailable = await SentryInitializer.init(appRunner);
log('[SentryManager] Sentry initialized: $_isSentryAvailable');
if (!_isSentryAvailable) { if (!_isSentryAvailable) {
logWarning('[SentryManager] Sentry failed to init, running fallback.');
await fallBackRunner(); await fallBackRunner();
} else {
log('[SentryManager] Sentry active.');
} }
} catch (e) { } catch (e) {
logWarning('[SentryManager] Init failed. Exception $e'); logWarning('[SentryManager] Init failed. Exception $e');
@@ -37,69 +41,116 @@ class SentryManager {
} }
} }
/// Capture an exception. Metadata is attached as breadcrumbs. Future<void> initializeWithSentryConfig(SentryConfig sentryConfig) async {
Future<void> captureException( if (_isSentryAvailable) return;
try {
_isSentryAvailable = await SentryInitializer.init(
sentryConfig: sentryConfig,
);
if (_isSentryAvailable) {
log('[SentryManager] Sentry active.');
}
} catch (e) {
logWarning('[SentryManager] Init exception: $e');
}
}
/// Capture an exception.
void captureException(
dynamic exception, { dynamic exception, {
StackTrace? stackTrace, StackTrace? stackTrace,
String? message, String? message,
Map<String, dynamic>? extras, Map<String, dynamic>? extras,
}) async { }) {
if (!_isSentryAvailable) return; if (!_isSentryAvailable) return;
await Sentry.captureException( // Use unawaited to prevent linter warnings about unawaited futures.
exception, // We do not want the UI to pause while Sentry writes the crash report.
stackTrace: stackTrace, unawaited(
withScope: (scope) { _captureExceptionInternal(exception, stackTrace, message, extras),
scope.addBreadcrumb(
Breadcrumb(
message: message ?? exception.toString(),
data: extras,
level: SentryLevel.error,
),
);
},
); );
} }
/// Capture a text message. Metadata also goes into breadcrumbs. Future<void> _captureExceptionInternal(
Future<void> captureMessage( dynamic exception,
StackTrace? stackTrace,
String? message,
Map<String, dynamic>? extras,
) async {
try {
final hasExtras = extras != null && extras.isNotEmpty;
final hasMessage = message != null && message.isNotEmpty;
if (hasExtras || hasMessage) {
await Sentry.captureException(
exception,
stackTrace: stackTrace,
withScope: (scope) {
if (hasExtras) scope.setContexts('extras', extras);
if (hasMessage) scope.setTag('message', message);
},
);
} else {
await Sentry.captureException(
exception,
stackTrace: stackTrace,
);
}
} catch (e) {
logWarning('[SentryManager] Capture exception failed: $e');
}
}
/// Capture a text message.
void captureMessage(
String message, { String message, {
SentryLevel level = SentryLevel.info, SentryLevel level = SentryLevel.info,
Map<String, dynamic>? extras, Map<String, dynamic>? extras,
}) async { }) {
if (!_isSentryAvailable) return; if (!_isSentryAvailable) return;
await Sentry.captureMessage( unawaited(_captureMessageInternal(message, level, extras));
message,
level: level,
withScope: (scope) {
scope.addBreadcrumb(
Breadcrumb(
message: message,
data: extras,
level: level,
),
);
},
);
} }
Future<void> setUser(SentryUser user) async { Future<void> _captureMessageInternal(
String message,
SentryLevel level,
Map<String, dynamic>? extras,
) async {
try {
if (extras != null && extras.isNotEmpty) {
await Sentry.captureMessage(
message,
level: level,
withScope: (scope) => scope.setContexts('extras', extras),
);
} else {
await Sentry.captureMessage(message, level: level);
}
} catch (e) {
logWarning('[SentryManager] Capture message failed: $e');
}
}
/// Sets the user context.
void setUser(SentryUser user) {
if (!_isSentryAvailable) return; if (!_isSentryAvailable) return;
try { try {
await Sentry.configureScope((scope) => scope.setUser(user)); Sentry.configureScope((scope) => scope.setUser(user));
log('[SentryManager] User set: ${user.email}'); log('[SentryManager] User set: ${user.email}');
} catch (e) { } catch (e) {
logWarning('[SentryManager] Set user failed. Exception: $e'); logWarning('[SentryManager] Set user failed. Exception: $e');
} }
} }
Future<void> clearUser() async { /// Clears the user context.
void clearUser() {
if (!_isSentryAvailable) return; if (!_isSentryAvailable) return;
try { try {
await Sentry.configureScope((scope) => scope.setUser(null)); Sentry.configureScope((scope) => scope.setUser(null));
log('[SentryManager] User cleared'); log('[SentryManager] User cleared');
} catch (e) { } catch (e) {
logWarning('[SentryManager] Clear user failed. Exception: $e'); logWarning('[SentryManager] Clear user failed. Exception: $e');