Skip to content

Commit

Permalink
Support custom Sentry.runZoneGuarded zone creation (#2088)
Browse files Browse the repository at this point in the history
  • Loading branch information
buenaflor authored Dec 9, 2024
1 parent 32352da commit b6bb5b4
Show file tree
Hide file tree
Showing 11 changed files with 397 additions and 203 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Changelog

## Unreleased

### Features

- Support custom `Sentry.runZoneGuarded` zone creation ([#2088](https://github.com/getsentry/sentry-dart/pull/2088))
- Sentry will not create a custom zone anymore if it is started within a custom one.
- This fixes Zone miss-match errors when trying to initialize WidgetsBinding before Sentry on Flutter Web
- `Sentry.runZonedGuarded` creates a zone and also captures exceptions & breadcrumbs automatically.
```dart
Sentry.runZonedGuarded(() {
WidgetsBinding.ensureInitialized();
// Errors before init will not be handled by Sentry
SentryFlutter.init(
(options) {
...
},
appRunner: () => runApp(MyApp()),
);
} (error, stackTrace) {
// Automatically sends errors to Sentry, no need to do any
// captureException calls on your part.
// On top of that, you can do your own custom stuff in this callback.
});
```

## 8.11.0-beta.2

### Features
Expand Down Expand Up @@ -50,6 +77,7 @@
```
- Replace deprecated `BeforeScreenshotCallback` with new `BeforeCaptureCallback`.


### Fixes

- Catch errors thrown during `handleBeginFrame` and `handleDrawFrame` ([#2446](https://github.com/getsentry/sentry-dart/pull/2446))
Expand Down
10 changes: 7 additions & 3 deletions dart/lib/src/platform_checker.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import 'dart:async';
import 'platform/platform.dart';

/// Helper to check in which enviroment the library is running.
/// The envirment checks (release/debug/profile) are mutually exclusive.
/// Helper to check in which environment the library is running.
/// The environment checks (release/debug/profile) are mutually exclusive.
class PlatformChecker {
static const _jsUtil = 'dart.library.js_util';

PlatformChecker({
this.platform = instance,
bool? isWeb,
}) : isWeb = isWeb ?? _isWebWithWasmSupport();
bool? isRootZone,
}) : isWeb = isWeb ?? _isWebWithWasmSupport(),
isRootZone = isRootZone ?? Zone.current == Zone.root;

/// Check if running in release/production environment
bool isReleaseMode() {
Expand All @@ -26,6 +29,7 @@ class PlatformChecker {
}

final bool isWeb;
final bool isRootZone;

String get compileMode {
return isReleaseMode()
Expand Down
113 changes: 9 additions & 104 deletions dart/lib/src/run_zoned_guarded_integration.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import 'dart:async';

import 'package:meta/meta.dart';

import 'hub.dart';
import 'integration.dart';
import 'protocol.dart';
import 'sentry_options.dart';
import 'throwable_mechanism.dart';
import 'sentry_run_zoned_guarded.dart';
import '../sentry.dart';

/// Called inside of `runZonedGuarded`
typedef RunZonedGuardedRunner = Future<void> Function();
Expand All @@ -26,107 +21,17 @@ class RunZonedGuardedIntegration extends Integration<SentryOptions> {
final RunZonedGuardedRunner _runner;
final RunZonedGuardedOnError? _onError;

/// Needed to check if we somehow caused a `print()` recursion
bool _isPrinting = false;

@visibleForTesting
Future<void> captureError(
Hub hub,
SentryOptions options,
Object exception,
StackTrace stackTrace,
) async {
options.logger(
SentryLevel.error,
'Uncaught zone error',
logger: 'sentry.runZonedGuarded',
exception: exception,
stackTrace: stackTrace,
);

// runZonedGuarded doesn't crash the app, but is not handled by the user.
final mechanism = Mechanism(type: 'runZonedGuarded', handled: false);
final throwableMechanism = ThrowableMechanism(mechanism, exception);

final event = SentryEvent(
throwable: throwableMechanism,
level: options.markAutomaticallyCollectedErrorsAsFatal
? SentryLevel.fatal
: SentryLevel.error,
timestamp: hub.options.clock(),
);

// marks the span status if none to `internal_error` in case there's an
// unhandled error
hub.configureScope(
(scope) => scope.span?.status ??= const SpanStatus.internalError(),
);

await hub.captureEvent(event, stackTrace: stackTrace);
}

@override
Future<void> call(Hub hub, SentryOptions options) {
final completer = Completer<void>();

runZonedGuarded(
() async {
try {
await _runner();
} finally {
completer.complete();
}
},
(exception, stackTrace) async {
await captureError(hub, options, exception, stackTrace);
final onError = _onError;
if (onError != null) {
await onError(exception, stackTrace);
}
},
zoneSpecification: ZoneSpecification(
print: (self, parent, zone, line) {
if (!options.enablePrintBreadcrumbs || !hub.isEnabled) {
// early bail out, in order to better guard against the recursion
// as described below.
parent.print(zone, line);
return;
}

if (_isPrinting) {
// We somehow landed in a recursion.
// This happens for example if:
// - hub.addBreadcrumb() called print() itself
// - This happens for example if hub.isEnabled == false and
// options.logger == dartLogger
//
// Anyway, in order to not cause a stack overflow due to recursion
// we drop any further print() call while adding a breadcrumb.
parent.print(
zone,
'Recursion during print() call. '
'Abort adding print() call as Breadcrumb.',
);
return;
}

_isPrinting = true;

try {
hub.addBreadcrumb(
Breadcrumb.console(
message: line,
level: SentryLevel.debug,
),
);

parent.print(zone, line);
} finally {
_isPrinting = false;
}
},
),
);
SentryRunZonedGuarded.sentryRunZonedGuarded(hub, () async {
try {
await _runner();
} finally {
completer.complete();
}
}, _onError);

options.sdk.addIntegration('runZonedGuardedIntegration');

Expand Down
42 changes: 42 additions & 0 deletions dart/lib/src/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'noop_isolate_error_integration.dart'
import 'protocol.dart';
import 'sentry_client.dart';
import 'sentry_options.dart';
import 'sentry_run_zoned_guarded.dart';
import 'sentry_user_feedback.dart';
import 'tracing.dart';
import 'sentry_attachment/sentry_attachment.dart';
Expand Down Expand Up @@ -365,4 +366,45 @@ class Sentry {

@internal
static Hub get currentHub => _hub;

/// Creates a new error handling zone with Sentry integration using [runZonedGuarded].
///
/// This method provides automatic error reporting and breadcrumb tracking while
/// allowing you to define a custom error handling zone. It wraps Dart's native
/// [runZonedGuarded] function with Sentry-specific functionality.
///
/// This function automatically records calls to `print()` as Breadcrumbs and
/// can be configured using [SentryOptions.enablePrintBreadcrumbs].
///
/// ```dart
/// Sentry.runZonedGuarded(() {
/// WidgetsBinding.ensureInitialized();
///
/// // Errors before init will not be handled by Sentry
///
/// SentryFlutter.init(
/// (options) {
/// ...
/// },
/// appRunner: () => runApp(MyApp()),
/// );
/// } (error, stackTrace) {
/// // Automatically sends errors to Sentry, no need to do any
/// // captureException calls on your part.
/// // On top of that, you can do your own custom stuff in this callback.
/// });
/// ```
static runZonedGuarded<R>(
R Function() body,
void Function(Object error, StackTrace stack)? onError, {
Map<Object?, Object?>? zoneValues,
ZoneSpecification? zoneSpecification,
}) =>
SentryRunZonedGuarded.sentryRunZonedGuarded(
_hub,
body,
onError,
zoneValues: zoneValues,
zoneSpecification: zoneSpecification,
);
}
118 changes: 118 additions & 0 deletions dart/lib/src/sentry_run_zoned_guarded.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import 'dart:async';

import 'package:meta/meta.dart';

import '../sentry.dart';

@internal
class SentryRunZonedGuarded {
/// Needed to check if we somehow caused a `print()` recursion
static var _isPrinting = false;

static R? sentryRunZonedGuarded<R>(
Hub hub,
R Function() body,
void Function(Object error, StackTrace stack)? onError, {
Map<Object?, Object?>? zoneValues,
ZoneSpecification? zoneSpecification,
}) {
final sentryOnError = (exception, stackTrace) async {
final options = hub.options;
await _captureError(hub, options, exception, stackTrace);

if (onError != null) {
onError(exception, stackTrace);
}
};

final userPrint = zoneSpecification?.print;

final sentryZoneSpecification = ZoneSpecification.from(
zoneSpecification ?? ZoneSpecification(),
print: (self, parent, zone, line) async {
final options = hub.options;

if (userPrint != null) {
userPrint(self, parent, zone, line);
}

if (!options.enablePrintBreadcrumbs || !hub.isEnabled) {
// early bail out, in order to better guard against the recursion
// as described below.
parent.print(zone, line);
return;
}
if (_isPrinting) {
// We somehow landed in a recursion.
// This happens for example if:
// - hub.addBreadcrumb() called print() itself
// - This happens for example if hub.isEnabled == false and
// options.logger == dartLogger
//
// Anyway, in order to not cause a stack overflow due to recursion
// we drop any further print() call while adding a breadcrumb.
parent.print(
zone,
'Recursion during print() call.'
'Abort adding print() call as Breadcrumb.',
);
return;
}

try {
_isPrinting = true;
await hub.addBreadcrumb(
Breadcrumb.console(
message: line,
level: SentryLevel.debug,
),
);
parent.print(zone, line);
} finally {
_isPrinting = false;
}
},
);
return runZonedGuarded(
body,
sentryOnError,
zoneValues: zoneValues,
zoneSpecification: sentryZoneSpecification,
);
}

static Future<void> _captureError(
Hub hub,
SentryOptions options,
Object exception,
StackTrace stackTrace,
) async {
options.logger(
SentryLevel.error,
'Uncaught zone error',
logger: 'sentry.runZonedGuarded',
exception: exception,
stackTrace: stackTrace,
);

// runZonedGuarded doesn't crash the app, but is not handled by the user.
final mechanism = Mechanism(type: 'runZonedGuarded', handled: false);
final throwableMechanism = ThrowableMechanism(mechanism, exception);

final event = SentryEvent(
throwable: throwableMechanism,
level: options.markAutomaticallyCollectedErrorsAsFatal
? SentryLevel.fatal
: SentryLevel.error,
timestamp: hub.options.clock(),
);

// marks the span status if none to `internal_error` in case there's an
// unhandled error
hub.configureScope(
(scope) => scope.span?.status ??= const SpanStatus.internalError(),
);

await hub.captureEvent(event, stackTrace: stackTrace);
}
}
Loading

0 comments on commit b6bb5b4

Please sign in to comment.