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/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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user