TF-4268 Improve SentryConfig and SentryManager: sampling, frames tracking, non-blocking capture
This commit is contained in:
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,20 +21,49 @@ 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) => _setUpSentryOptions(options, config),
|
||||
// Sentry will execute this runner if initialization succeeds.
|
||||
appRunner: appRunner,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
// In case of any error during init, return false so main() can fallback.
|
||||
logWarning('[SentryInitializer] Init failed: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -43,11 +73,6 @@ class SentryInitializer {
|
||||
|
||||
// Assign the callback to process events before sending them to Sentry
|
||||
options.beforeSend = _beforeSendHandler;
|
||||
},
|
||||
appRunner: appRunner,
|
||||
);
|
||||
|
||||
return config.isAvailable;
|
||||
}
|
||||
|
||||
/// Handler executed before sending an event to Sentry
|
||||
|
||||
@@ -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 {
|
||||
try {
|
||||
if (_isSentryAvailable) {
|
||||
log('[SentryManager] Already initialized, skipping');
|
||||
log('[SentryManager] Already initialized.');
|
||||
await appRunner();
|
||||
return;
|
||||
}
|
||||
_isSentryAvailable = await SentryInitializer.init(appRunner);
|
||||
log('[SentryManager] Sentry initialized: $_isSentryAvailable');
|
||||
|
||||
try {
|
||||
_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;
|
||||
|
||||
// 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),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
scope.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: message ?? exception.toString(),
|
||||
data: extras,
|
||||
level: SentryLevel.error,
|
||||
),
|
||||
);
|
||||
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. Metadata also goes into breadcrumbs.
|
||||
Future<void> captureMessage(
|
||||
/// Capture a text message.
|
||||
void captureMessage(
|
||||
String message, {
|
||||
SentryLevel level = SentryLevel.info,
|
||||
Map<String, dynamic>? extras,
|
||||
}) async {
|
||||
}) {
|
||||
if (!_isSentryAvailable) return;
|
||||
|
||||
unawaited(_captureMessageInternal(message, level, extras));
|
||||
}
|
||||
|
||||
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.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: message,
|
||||
data: extras,
|
||||
level: level,
|
||||
),
|
||||
);
|
||||
},
|
||||
withScope: (scope) => scope.setContexts('extras', extras),
|
||||
);
|
||||
} else {
|
||||
await Sentry.captureMessage(message, level: level);
|
||||
}
|
||||
} catch (e) {
|
||||
logWarning('[SentryManager] Capture message failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setUser(SentryUser user) async {
|
||||
/// 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');
|
||||
|
||||
Reference in New Issue
Block a user