Skip to content
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

Support custom Sentry.runZoneGuarded zone creation #2088

Merged
merged 34 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@
});
```
- Replace deprecated `BeforeScreenshotCallback` with new `BeforeCaptureCallback`.
- 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.
denrase marked this conversation as resolved.
Show resolved Hide resolved
- `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.
});
```

### Fixes

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.dart';
import 'sentry_run_zoned_guarded.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
44 changes: 44 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,47 @@ class Sentry {

@internal
static Hub get currentHub => _hub;

/// With [runZonedGuarded] you can create a custom zone, and still let Sentry
/// report errors and breadcrumbs automatically.
///
/// It takes the same parameters as the dart function.
///
/// Please be aware that any errors in the zone which occur before the [init]
/// call cannot be handled by Sentry.
denrase marked this conversation as resolved.
Show resolved Hide resolved
///
/// ```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.
/// });
/// ```
///
/// This function also records calls to `print()` as Breadcrumbs.
/// This can be configured with [SentryOptions.enablePrintBreadcrumbs]
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