Skip to content

Support custom Sentry.runZoneGuarded zone creation #2088

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d248b9c
update
buenaflor Jun 6, 2024
34ea139
update
buenaflor Jun 6, 2024
f6fa1ac
Merge branch 'main' into fix/runzoneguarded
buenaflor Jun 24, 2024
e6f647a
Merge branch 'main' into fix/runzoneguarded
denrase Nov 20, 2024
3f0f7d9
remove sample code
denrase Nov 20, 2024
4b77b41
provide sentry wrapped runZonedGuarded
denrase Nov 20, 2024
b8b75a7
Merge branch 'main' into fix/runzoneguarded
denrase Nov 26, 2024
ce1f67c
Refactorings, add missing tests, add documentation
denrase Nov 26, 2024
c622fb5
Merge branch 'main' into fix/runzoneguarded
denrase Nov 26, 2024
5a4b37e
update docs
denrase Nov 26, 2024
d7eb8c3
Update changelog entry
denrase Nov 26, 2024
8fcc89c
format
denrase Nov 26, 2024
ff275ca
format
denrase Nov 26, 2024
1ef5eb2
add check to sentry flutter tests
denrase Nov 26, 2024
846639e
replace flaky debounce tests with completers
denrase Nov 26, 2024
20261fb
Merge branch 'main' into fix/runzoneguarded
buenaflor Nov 28, 2024
30d7800
Merge branch 'main' into fix/runzoneguarded
denrase Dec 2, 2024
f9e4beb
warn user if sentry runZonedGuarded should be used
denrase Dec 2, 2024
cb66907
updated comments
denrase Dec 2, 2024
4e550cc
update changelog
denrase Dec 2, 2024
321b584
remove redundant imports
denrase Dec 2, 2024
6634f0d
fix mock
denrase Dec 2, 2024
2ef2edf
fix docs
denrase Dec 2, 2024
13d245e
Merge branch 'main' into fix/runzoneguarded
denrase Dec 3, 2024
8d4d527
remove flaky warning
denrase Dec 3, 2024
90a88c9
only test for throw
denrase Dec 3, 2024
705a12e
fix import and changelog
denrase Dec 3, 2024
4b1e6c7
fix import
denrase Dec 3, 2024
c530deb
close sdk between tests
denrase Dec 3, 2024
2822764
format
denrase Dec 3, 2024
cc0cedd
run sentry close in test
denrase Dec 3, 2024
3da310e
format
denrase Dec 3, 2024
15f8d73
Merge branch 'main' into fix/runzoneguarded
buenaflor Dec 5, 2024
d23edf9
update test group name
denrase Dec 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading