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/platform_info.dart';
import 'package:core/utils/sentry/sentry_manager.dart';
@@ -82,17 +80,13 @@ void _internalLog(
if (shouldSentry) {
if (level == Level.trace) {
unawaited(
SentryManager.instance.captureMessage(rawMessage, extras: extras),
);
SentryManager.instance.captureMessage(rawMessage, extras: extras);
} else {
unawaited(
SentryManager.instance.captureException(
exception ?? rawMessage,
stackTrace: stackTrace,
message: rawMessage,
extras: extras,
),
SentryManager.instance.captureException(
exception ?? rawMessage,
stackTrace: stackTrace,
message: rawMessage,
extras: extras,
);
}
}
+15 -5
View File
@@ -1,6 +1,5 @@
import 'package:core/utils/application_manager.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:flutter_dotenv/flutter_dotenv.dart';
@@ -15,7 +14,7 @@ class SentryConfig {
// Current app release version
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;
// Optional profiling
@@ -38,23 +37,34 @@ class SentryConfig {
// This must match the `--dist` parameter used when uploading source maps to Sentry.
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({
required this.dsn,
required this.environment,
required this.release,
this.tracesSampleRate = 1.0,
this.profilesSampleRate = 1.0,
this.sessionSampleRate = 1.0,
this.onErrorSampleRate = 1.0,
this.enableLogs = true,
this.enableFramesTracking = true,
this.isDebug = BuildUtils.isDebugMode,
this.attachScreenshot = false,
this.isAvailable = false,
this.dist,
});
/// Load configuration from an env file.
/// Loads configuration from loaded environment variables.
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 isAvailable = sentryAvailable == 'true';
+48 -23
View File
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/sentry/sentry_config.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
@@ -20,34 +21,58 @@ class SentryInitializer {
'token',
];
static Future<bool> init(FutureOr<void> Function() appRunner) async {
final config = await SentryConfig.load();
/// Initializes Sentry.
/// 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(
(options) {
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.enableLogs = config.enableLogs;
options.debug = config.isDebug;
options.attachScreenshot = config.attachScreenshot;
options.maxRequestBodySize = MaxRequestBodySize.small;
await SentryFlutter.init(
(options) => _setUpSentryOptions(options, config),
// Sentry will execute this runner if initialization succeeds.
appRunner: appRunner,
);
// Automatically enable breadcrumbs that are appropriate for the current platform
options.enableBreadcrumbTrackingForCurrentPlatform();
return true;
} 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
options.beforeSend = _beforeSendHandler;
},
appRunner: appRunner,
);
static FutureOr<void> _setUpSentryOptions(
SentryFlutterOptions options,
SentryConfig config,
) 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
+94 -43
View File
@@ -1,6 +1,7 @@
import 'dart:async';
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:sentry_flutter/sentry_flutter.dart';
@@ -14,22 +15,25 @@ class SentryManager {
bool get isSentryAvailable => _isSentryAvailable;
/// Initialize Sentry. App still runs if init fails.
/// Initialize Sentry.
Future<void> initialize({
required FutureOr<void> Function() appRunner,
required FutureOr<void> Function() fallBackRunner,
}) async {
if (_isSentryAvailable) {
log('[SentryManager] Already initialized.');
await appRunner();
return;
}
try {
if (_isSentryAvailable) {
log('[SentryManager] Already initialized, skipping');
await appRunner();
return;
}
_isSentryAvailable = await SentryInitializer.init(appRunner);
log('[SentryManager] Sentry initialized: $_isSentryAvailable');
_isSentryAvailable = await SentryInitializer.init(appRunner: appRunner);
if (!_isSentryAvailable) {
logWarning('[SentryManager] Sentry failed to init, running fallback.');
await fallBackRunner();
} else {
log('[SentryManager] Sentry active.');
}
} catch (e) {
logWarning('[SentryManager] Init failed. Exception $e');
@@ -37,69 +41,116 @@ class SentryManager {
}
}
/// Capture an exception. Metadata is attached as breadcrumbs.
Future<void> captureException(
Future<void> initializeWithSentryConfig(SentryConfig sentryConfig) async {
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, {
StackTrace? stackTrace,
String? message,
Map<String, dynamic>? extras,
}) async {
}) {
if (!_isSentryAvailable) return;
await Sentry.captureException(
exception,
stackTrace: stackTrace,
withScope: (scope) {
scope.addBreadcrumb(
Breadcrumb(
message: message ?? exception.toString(),
data: extras,
level: SentryLevel.error,
),
);
},
// Use unawaited to prevent linter warnings about unawaited futures.
// We do not want the UI to pause while Sentry writes the crash report.
unawaited(
_captureExceptionInternal(exception, stackTrace, message, extras),
);
}
/// Capture a text message. Metadata also goes into breadcrumbs.
Future<void> captureMessage(
Future<void> _captureExceptionInternal(
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, {
SentryLevel level = SentryLevel.info,
Map<String, dynamic>? extras,
}) async {
}) {
if (!_isSentryAvailable) return;
await Sentry.captureMessage(
message,
level: level,
withScope: (scope) {
scope.addBreadcrumb(
Breadcrumb(
message: message,
data: extras,
level: level,
),
);
},
);
unawaited(_captureMessageInternal(message, level, extras));
}
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;
try {
await Sentry.configureScope((scope) => scope.setUser(user));
Sentry.configureScope((scope) => scope.setUser(user));
log('[SentryManager] User set: ${user.email}');
} catch (e) {
logWarning('[SentryManager] Set user failed. Exception: $e');
}
}
Future<void> clearUser() async {
/// Clears the user context.
void clearUser() {
if (!_isSentryAvailable) return;
try {
await Sentry.configureScope((scope) => scope.setUser(null));
Sentry.configureScope((scope) => scope.setUser(null));
log('[SentryManager] User cleared');
} catch (e) {
logWarning('[SentryManager] Clear user failed. Exception: $e');