Skip to content

Commit e3c09d8

Browse files
authored
Feat/dart default integrations (#187)
1 parent 1214d94 commit e3c09d8

22 files changed

+480
-282
lines changed

CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44

55
- Fix: StackTrace frames with 'package' uri.scheme are inApp by default #185
66
- Enhancement: add loadContextsIntegration tests
7-
- StackTrace factory : package are inApp by default
87
- Fix: missing app's stack traces for Flutter errors
8+
- Enhancement: add isolateErrorIntegration and runZonedGuardedIntegration to default integrations in sentry-dart
9+
10+
### Breaking changes
11+
12+
- `Sentry.init` and `SentryFlutter.init` have an optional callback argument which runs the host app after Sentry initialization.
913
- add loadContextsIntegration tests
1014
- Ref: add missing docs and move sentry web plugin to the inner src folder
1115
- Ref: Remove deprecated classes (Flutter Plugin for Android) and cleaning up #186

dart/README.md

+31-14
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,42 @@ import 'dart:async';
4040
import 'package:sentry/sentry.dart';
4141
4242
Future<void> main() async {
43-
await Sentry.init((options) {
44-
options.dsn = 'https://[email protected]/add-your-dsn-here';
45-
});
46-
47-
try {
48-
aMethodThatMightFail();
49-
} catch (exception, stackTrace) {
50-
await Sentry.captureException(
51-
exception,
52-
stackTrace: stackTrace,
53-
);
54-
}
43+
await Sentry.init(
44+
(options) {
45+
options.dsn = 'https://[email protected]/add-your-dsn-here';
46+
},
47+
initApp, // Init your App.
48+
);
5549
}
5650
57-
void aMethodThatMightFail() {
58-
throw null;
51+
void initApp() {
52+
// your app code
5953
}
6054
```
6155

56+
Or, if you want to run your app in your own error zone [runZonedGuarded] :
57+
58+
```dart
59+
import 'dart:async';
60+
import 'package:sentry/sentry.dart';
61+
62+
Future<void> main() async {
63+
await Sentry.init(
64+
(options) {
65+
options.dsn = 'https://[email protected]/add-your-dsn-here';
66+
},
67+
);
68+
69+
// Init your App.
70+
initApp();
71+
}
72+
73+
void initApp() {
74+
// your app code
75+
}
76+
77+
```
78+
6279
#### Resources
6380

6481
* [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/dart/)

dart/example/README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ The example in this directory throws an error and sends it to Sentry.io. Use it
44
as a source of example code, or to smoke-test your Sentry.io configuration.
55

66
To use the example, create a Sentry.io account and get a DSN for your project.
7-
Then run the following command, replacing "{DSN}" with the one you got from
8-
Sentry.io:
7+
In the `main.dart` file, replace the `dsn` value with the one you got from Sentry.io.
8+
Then run the following command :
99

1010
```
11-
dart example/main.dart {DSN}
11+
dart example/main.dart
1212
```

dart/example/main.dart

+8-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ Future<void> main() async {
1717
SentryEvent processTagEvent(SentryEvent event, Object hint) =>
1818
event..tags.addAll({'page-locale': 'en-us'});
1919

20-
await Sentry.init((options) => options
21-
..dsn = dsn
22-
..addEventProcessor(processTagEvent));
20+
await Sentry.init(
21+
(options) => options
22+
..dsn = dsn
23+
..addEventProcessor(processTagEvent),
24+
runApp,
25+
);
2326

2427
Sentry.addBreadcrumb(
2528
Breadcrumb(
@@ -48,7 +51,9 @@ Future<void> main() async {
4851
..setTag('build', '579')
4952
..setExtra('company-name', 'Dart Inc');
5053
});
54+
}
5155

56+
void runApp() async {
5257
print('\nReporting a complete event example: ');
5358

5459
// Sends a full Sentry event payload to show the different parts of the UI.

dart/example_web/web/main.dart

+20-16
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,15 @@ const dsn =
1111
'https://[email protected]/5428562';
1212

1313
Future<void> main() async {
14-
querySelector('#output').text = 'Your Dart app is running.';
15-
16-
querySelector('#btEvent')
17-
.onClick
18-
.listen((event) => captureCompleteExampleEvent());
19-
querySelector('#btMessage').onClick.listen((event) => captureMessage());
20-
querySelector('#btException').onClick.listen((event) => captureException());
21-
22-
await initSentry();
23-
}
24-
25-
Future<void> initSentry() async {
2614
SentryEvent processTagEvent(SentryEvent event, Object hint) =>
2715
event..tags.addAll({'page-locale': 'en-us'});
2816

29-
await Sentry.init((options) => options
30-
..dsn = dsn
31-
..addEventProcessor(processTagEvent));
17+
await Sentry.init(
18+
(options) => options
19+
..dsn = dsn
20+
..addEventProcessor(processTagEvent),
21+
runApp,
22+
);
3223

3324
Sentry.addBreadcrumb(
3425
Breadcrumb(
@@ -59,6 +50,18 @@ Future<void> initSentry() async {
5950
});
6051
}
6152

53+
void runApp() {
54+
print('runApp');
55+
56+
querySelector('#output').text = 'Your Dart app is running.';
57+
58+
querySelector('#btEvent')
59+
.onClick
60+
.listen((event) => captureCompleteExampleEvent());
61+
querySelector('#btMessage').onClick.listen((event) => captureMessage());
62+
querySelector('#btException').onClick.listen((event) => captureException());
63+
}
64+
6265
void captureMessage() async {
6366
print('Capturing Message : ');
6467
final sentryId = await Sentry.captureMessage(
@@ -81,7 +84,8 @@ void captureException() async {
8184
print(stackTrace);
8285
final sentryId = await Sentry.captureException(
8386
error,
84-
stackTrace: stackTrace,
87+
stackTrace: stackTrace
88+
.toString() /* on safari (14.0) passing a stacktrace object fails */,
8589
);
8690

8791
print('Capture exception : SentryId: ${sentryId}');

dart/lib/sentry.dart

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
// found in the LICENSE file.
44

55
/// A pure Dart client for Sentry.io crash reporting.
6+
export 'src/default_integrations.dart';
7+
export 'src/hub.dart';
8+
export 'src/noop_isolate_error_integration.dart'
9+
if (dart.library.io) 'src/isolate_error_integration.dart';
610
export 'src/protocol.dart';
711
export 'src/scope.dart';
812
export 'src/sentry.dart';
913
export 'src/sentry_client.dart';
10-
export 'src/hub.dart';
1114
export 'src/sentry_options.dart';
12-
export 'src/transport/transport.dart';
1315
// useful for integrations
1416
export 'src/throwable_mechanism.dart';
17+
export 'src/transport/transport.dart';
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'dart:async';
2+
3+
import 'hub.dart';
4+
import 'protocol.dart';
5+
import 'sentry.dart';
6+
import 'sentry_options.dart';
7+
import 'throwable_mechanism.dart';
8+
9+
/// integration that capture errors on the runZonedGuarded error handler
10+
Integration runZonedGuardedIntegration(
11+
AppRunner appRunner,
12+
) {
13+
void integration(Hub hub, SentryOptions options) {
14+
runZonedGuarded(() async {
15+
await appRunner();
16+
}, (exception, stackTrace) async {
17+
// runZonedGuarded doesn't crash the App.
18+
const mechanism = Mechanism(type: 'runZonedGuarded', handled: true);
19+
final throwableMechanism = ThrowableMechanism(mechanism, exception);
20+
21+
final event = SentryEvent(
22+
throwable: throwableMechanism,
23+
level: SentryLevel.fatal,
24+
);
25+
26+
await hub.captureEvent(event, stackTrace: stackTrace);
27+
});
28+
29+
options.sdk.addIntegration('runZonedGuardedIntegration');
30+
}
31+
32+
return integration;
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'dart:isolate';
2+
3+
import 'hub.dart';
4+
import 'protocol.dart';
5+
import 'sentry_options.dart';
6+
import 'throwable_mechanism.dart';
7+
8+
/// integration that capture errors on the current Isolate Error handler
9+
/// which is the main thread.
10+
void isolateErrorIntegration(Hub hub, SentryOptions options) {
11+
final receivePort = _createPort(hub, options);
12+
13+
Isolate.current.addErrorListener(receivePort.sendPort);
14+
15+
options.sdk.addIntegration('isolateErrorIntegration');
16+
}
17+
18+
RawReceivePort _createPort(Hub hub, SentryOptions options) {
19+
return RawReceivePort(
20+
(dynamic error) async {
21+
await handleIsolateError(hub, options, error);
22+
},
23+
);
24+
}
25+
26+
/// Parse and raise an event out of the Isolate error.
27+
/// Visible for testing.
28+
Future<void> handleIsolateError(
29+
Hub hub,
30+
SentryOptions options,
31+
dynamic error,
32+
) async {
33+
options.logger(SentryLevel.debug, 'Capture from IsolateError $error');
34+
35+
// https://api.dartlang.org/stable/2.7.0/dart-isolate/Isolate/addErrorListener.html
36+
// error is a list of 2 elements
37+
if (error is List<dynamic> && error.length == 2) {
38+
final dynamic throwable = error.first;
39+
final dynamic stackTrace = error.last;
40+
41+
// Isolate errors don't crash the App.
42+
const mechanism = Mechanism(type: 'isolateError', handled: true);
43+
final throwableMechanism = ThrowableMechanism(mechanism, throwable);
44+
final event = SentryEvent(
45+
throwable: throwableMechanism,
46+
level: SentryLevel.fatal,
47+
);
48+
49+
await hub.captureEvent(event, stackTrace: stackTrace);
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import 'hub.dart';
2+
import 'sentry_options.dart';
3+
4+
// noop web integration : isolate doesnt' work in browser
5+
void isolateErrorIntegration(Hub hub, SentryOptions options) {}

dart/lib/src/sentry.dart

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import 'dart:async';
22

3+
import 'default_integrations.dart';
34
import 'hub.dart';
45
import 'hub_adapter.dart';
56
import 'noop_hub.dart';
7+
import 'noop_isolate_error_integration.dart'
8+
if (dart.library.io) 'isolate_error_integration.dart';
69
import 'protocol.dart';
710
import 'sentry_client.dart';
811
import 'sentry_options.dart';
12+
import 'utils.dart';
913

1014
/// Configuration options callback
1115
typedef OptionsConfiguration = FutureOr<void> Function(SentryOptions);
1216

17+
/// Runs a callback inside of the `runZonedGuarded` method, useful for running your `runApp(MyApp())`
18+
typedef AppRunner = FutureOr<void> Function();
19+
1320
/// Sentry SDK main entry point
1421
class Sentry {
1522
static Hub _hub = NoOpHub();
@@ -20,11 +27,20 @@ class Sentry {
2027
static Hub get currentHub => _hub;
2128

2229
/// Initializes the SDK
23-
static Future<void> init(OptionsConfiguration optionsConfiguration) async {
30+
/// passing a [AppRunner] callback allows to run the app within its own error zone (`runZonedGuarded`)
31+
/// https://api.dart.dev/stable/2.10.4/dart-async/runZonedGuarded.html
32+
static Future<void> init(
33+
OptionsConfiguration optionsConfiguration, [
34+
AppRunner appRunner,
35+
List<Integration> initialIntegrations,
36+
]) async {
2437
if (optionsConfiguration == null) {
2538
throw ArgumentError('OptionsConfiguration is required.');
2639
}
40+
2741
final options = SentryOptions();
42+
await _initDefaultValues(options, appRunner, initialIntegrations);
43+
2844
await optionsConfiguration(options);
2945

3046
if (options == null) {
@@ -34,6 +50,47 @@ class Sentry {
3450
await _init(options);
3551
}
3652

53+
static Future<void> _initDefaultValues(
54+
SentryOptions options,
55+
AppRunner appRunner,
56+
List<Integration> initialIntegrations,
57+
) async {
58+
// if no environment is set, we set 'production' by default, but if we know it's
59+
// a non-release build, or the SENTRY_ENVIRONMENT is set, we read from it.
60+
if (const bool.hasEnvironment('SENTRY_ENVIRONMENT') || !isReleaseMode) {
61+
options.environment = const String.fromEnvironment(
62+
'SENTRY_ENVIRONMENT',
63+
defaultValue: 'debug',
64+
);
65+
}
66+
67+
// if the SENTRY_DSN is set, we read from it.
68+
options.dsn = const bool.hasEnvironment('SENTRY_DSN')
69+
? const String.fromEnvironment('SENTRY_DSN')
70+
: options.dsn;
71+
72+
// we need to execute integrations in a specific order sometimes,
73+
// and this initialIntegrations list makes it possible to inject and add
74+
// integrations before the runZonedGuardedIntegration gets added
75+
if (initialIntegrations != null && initialIntegrations.isNotEmpty) {
76+
initialIntegrations
77+
.forEach((integration) => options.addIntegration(integration));
78+
}
79+
80+
// Throws when running on the browser
81+
if (!isWeb) {
82+
// catch any errors that may occur within the entry function, main()
83+
// in the ‘root zone’ where all Dart programs start
84+
options.addIntegration(isolateErrorIntegration);
85+
}
86+
87+
// finally the runZonedGuarded, catch any errors in Dart code running
88+
// ‘outside’ the Flutter framework
89+
if (appRunner != null) {
90+
options.addIntegration(runZonedGuardedIntegration(appRunner));
91+
}
92+
}
93+
3794
/// Initializes the SDK
3895
static Future<void> _init(SentryOptions options) async {
3996
if (isEnabled) {

dart/lib/src/sentry_stack_trace_factory.dart

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class SentryStackTraceFactory {
2020
List<SentryStackFrame> getStackFrames(dynamic stackTrace) {
2121
if (stackTrace == null) return null;
2222

23+
// TODO : fix : in release mode on Safari passing a stacktrace object fails, but works if it's passed as String
2324
final chain = (stackTrace is StackTrace)
2425
? Chain.forTrace(stackTrace)
2526
: (stackTrace is String)

dart/lib/src/utils.dart

+4
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,7 @@ String formatDateAsIso8601WithMillisPrecision(DateTime date) {
2020

2121
/// helper to detect a browser context
2222
const isWeb = identical(0, 0.0);
23+
24+
/// helper to detect if app is in release mode
25+
const isReleaseMode =
26+
bool.fromEnvironment('dart.vm.product', defaultValue: false);

0 commit comments

Comments
 (0)