Skip to content

Commit fe6fcc8

Browse files
authored
Feat: Sync Scope to Native (#858)
1 parent 1a418c5 commit fe6fcc8

28 files changed

+1169
-101
lines changed

.github/workflows/flutter.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ jobs:
208208
steps:
209209
- uses: actions/checkout@v3
210210
# 1.20.0
211+
# To recreate baseline run: detekt -i flutter/android,flutter/example/android -b flutter/config/detekt-bl.xml -cb
211212
- uses: natiginfo/action-detekt-all@74990bda6bfc47977e1e06aae9f47f320e7587ce
212213
with:
213-
args: -i flutter --baseline flutter/config/detekt-bl.xml
214+
args: -i flutter/android,flutter/example/android --baseline flutter/config/detekt-bl.xml

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* Fix: Fix `SentryAssetBundle` on Flutter >= 3.1 (#877)
77
* Feat: Add Android thread to platform stacktraces (#853)
88
* Fix: Rename auto initialize property (#857)
9+
* Feat: Sync Scope to Native (#858)
10+
* Bump: Sentry-Android to 6.0.0-beta.4 (#871)
911
* Bump: Sentry-Android to 6.0.0 (#879)
1012

1113
## 6.6.0-alpha.2

dart/example/bin/example.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ Future<void> runApp() async {
4040
);
4141

4242
Sentry.configureScope((scope) {
43+
scope.setUser(SentryUser(
44+
id: '800',
45+
username: 'first-user',
46+
47+
// ipAddress: '127.0.0.1', sendDefaultPii feature is enabled
48+
extras: <String, String>{'first-sign-in': '2020-01-01'},
49+
));
4350
scope
44-
..user = SentryUser(
45-
id: '800',
46-
username: 'first-user',
47-
48-
// ipAddress: '127.0.0.1', sendDefaultPii feature is enabled
49-
extras: <String, String>{'first-sign-in': '2020-01-01'},
50-
)
5151
// ..fingerprint = ['example-dart'], fingerprint forces events to group together
5252
..transaction = '/example/app'
5353
..level = SentryLevel.warning

dart/example_web/web/main.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,21 @@ void runApp() {
4040

4141
Sentry.configureScope((scope) {
4242
scope
43-
..user = SentryUser(
44-
id: '800',
45-
username: 'first-user',
46-
47-
// ipAddress: '127.0.0.1',
48-
extras: <String, String>{'first-sign-in': '2020-01-01'},
49-
)
5043
// ..fingerprint = ['example-dart']
5144
..transaction = '/example/app'
5245
..level = SentryLevel.warning
5346
..setTag('build', '579')
5447
..setExtra('company-name', 'Dart Inc');
48+
49+
scope.setUser(
50+
SentryUser(
51+
id: '800',
52+
username: 'first-user',
53+
54+
// ipAddress: '127.0.0.1',
55+
extras: <String, String>{'first-sign-in': '2020-01-01'},
56+
),
57+
);
5558
});
5659

5760
querySelector('#btEvent')

dart/lib/sentry.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export 'src/noop_isolate_error_integration.dart'
1212
if (dart.library.io) 'src/isolate_error_integration.dart';
1313
export 'src/protocol.dart';
1414
export 'src/scope.dart';
15+
export 'src/scope_observer.dart';
1516
export 'src/sentry.dart';
1617
export 'src/sentry_envelope.dart';
1718
export 'src/sentry_envelope_item.dart';

dart/lib/src/scope.dart

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import 'dart:async';
12
import 'dart:collection';
23

3-
import 'sentry_attachment/sentry_attachment.dart';
44
import 'event_processor.dart';
55
import 'protocol.dart';
6+
import 'scope_observer.dart';
7+
import 'sentry_attachment/sentry_attachment.dart';
68
import 'sentry_options.dart';
9+
import 'sentry_span_interface.dart';
710
import 'sentry_tracer.dart';
8-
import 'tracing.dart';
911

1012
/// Scope data to be sent with the event
1113
class Scope {
@@ -46,8 +48,17 @@ class Scope {
4648
}
4749
}
4850

49-
/// Information about the current user.
50-
SentryUser? user;
51+
SentryUser? _user;
52+
53+
/// Get the current user.
54+
SentryUser? get user => _user;
55+
56+
/// Set the current user.
57+
Future<void> setUser(SentryUser? user) async {
58+
_user = user;
59+
await _callScopeObservers(
60+
(scopeObserver) async => await scopeObserver.setUser(user));
61+
}
5162

5263
List<String> _fingerprint = [];
5364

@@ -93,15 +104,21 @@ class Scope {
93104
Map<String, dynamic> get contexts => Map.unmodifiable(_contexts);
94105

95106
/// add an entry to the Scope's contexts
96-
void setContexts(String key, dynamic value) {
107+
Future<void> setContexts(String key, dynamic value) async {
97108
_contexts[key] = (value is num || value is bool || value is String)
98109
? {'value': value}
99110
: value;
111+
112+
await _callScopeObservers(
113+
(scopeObserver) async => await scopeObserver.setContexts(key, value));
100114
}
101115

102116
/// Removes a value from the Scope's contexts
103-
void removeContexts(String key) {
117+
Future<void> removeContexts(String key) async {
104118
_contexts.remove(key);
119+
120+
await _callScopeObservers(
121+
(scopeObserver) async => await scopeObserver.removeContexts(key));
105122
}
106123

107124
/// Scope's event processor list
@@ -114,14 +131,17 @@ class Scope {
114131

115132
final SentryOptions _options;
116133

117-
final List<SentryAttachment> _attachements = [];
134+
final List<SentryAttachment> _attachments = [];
135+
136+
List<SentryAttachment> get attachments => List.unmodifiable(_attachments);
118137

119-
List<SentryAttachment> get attachements => List.unmodifiable(_attachements);
138+
@Deprecated('Use attachments instead')
139+
List<SentryAttachment> get attachements => attachments;
120140

121141
Scope(this._options);
122142

123143
/// Adds a breadcrumb to the breadcrumbs queue
124-
void addBreadcrumb(Breadcrumb breadcrumb, {dynamic hint}) {
144+
Future<void> addBreadcrumb(Breadcrumb breadcrumb, {dynamic hint}) async {
125145
// bail out if maxBreadcrumbs is zero
126146
if (_options.maxBreadcrumbs == 0) {
127147
return;
@@ -151,19 +171,25 @@ class Scope {
151171
}
152172

153173
_breadcrumbs.add(breadcrumb);
174+
175+
await _callScopeObservers(
176+
(scopeObserver) async => await scopeObserver.addBreadcrumb(breadcrumb));
154177
}
155178

156179
void addAttachment(SentryAttachment attachment) {
157-
_attachements.add(attachment);
180+
_attachments.add(attachment);
158181
}
159182

160183
void clearAttachments() {
161-
_attachements.clear();
184+
_attachments.clear();
162185
}
163186

164187
/// Clear all the breadcrumbs
165-
void clearBreadcrumbs() {
188+
Future<void> clearBreadcrumbs() async {
166189
_breadcrumbs.clear();
190+
191+
await _callScopeObservers(
192+
(scopeObserver) async => await scopeObserver.clearBreadcrumbs());
167193
}
168194

169195
/// Adds an event processor
@@ -172,36 +198,46 @@ class Scope {
172198
}
173199

174200
/// Resets the Scope to its default state
175-
void clear() {
176-
clearBreadcrumbs();
201+
Future<void> clear() async {
202+
await clearBreadcrumbs();
177203
clearAttachments();
178204
level = null;
179205
_span = null;
180206
_transaction = null;
181-
user = null;
207+
await setUser(null);
182208
_fingerprint = [];
183209
_tags.clear();
184210
_extra.clear();
185211
_eventProcessors.clear();
186212
}
187213

188214
/// Sets a tag to the Scope
189-
void setTag(String key, String value) {
215+
Future<void> setTag(String key, String value) async {
190216
_tags[key] = value;
217+
await _callScopeObservers(
218+
(scopeObserver) async => await scopeObserver.setTag(key, value));
191219
}
192220

193221
/// Removes a tag from the Scope
194-
void removeTag(String key) {
222+
Future<void> removeTag(String key) async {
195223
_tags.remove(key);
224+
await _callScopeObservers(
225+
(scopeObserver) async => await scopeObserver.removeTag(key));
196226
}
197227

198228
/// Sets an extra to the Scope
199-
void setExtra(String key, dynamic value) {
229+
Future<void> setExtra(String key, dynamic value) async {
200230
_extra[key] = value;
231+
await _callScopeObservers(
232+
(scopeObserver) async => await scopeObserver.setExtra(key, value));
201233
}
202234

203235
/// Removes an extra from the Scope
204-
void removeExtra(String key) => _extra.remove(key);
236+
Future<void> removeExtra(String key) async {
237+
_extra.remove(key);
238+
await _callScopeObservers(
239+
(scopeObserver) async => await scopeObserver.removeExtra(key));
240+
}
205241

206242
Future<SentryEvent?> applyToEvent(
207243
SentryEvent event, {
@@ -327,11 +363,12 @@ class Scope {
327363
Scope clone() {
328364
final clone = Scope(_options)
329365
..level = level
330-
..user = user
331366
..fingerprint = List.from(fingerprint)
332367
.._transaction = _transaction
333368
.._span = _span;
334369

370+
clone.setUser(user);
371+
335372
for (final tag in _tags.keys) {
336373
clone.setTag(tag, _tags[tag]!);
337374
}
@@ -354,10 +391,19 @@ class Scope {
354391
}
355392
});
356393

357-
for (final attachment in _attachements) {
394+
for (final attachment in _attachments) {
358395
clone.addAttachment(attachment);
359396
}
360397

361398
return clone;
362399
}
400+
401+
Future<void> _callScopeObservers(
402+
Future<void> Function(ScopeObserver) action) async {
403+
if (_options.enableScopeSync) {
404+
for (final scopeObserver in _options.scopeObservers) {
405+
await action(scopeObserver);
406+
}
407+
}
408+
}
363409
}

dart/lib/src/scope_observer.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import 'dart:async';
2+
3+
import 'protocol/breadcrumb.dart';
4+
import 'protocol/sentry_user.dart';
5+
6+
abstract class ScopeObserver {
7+
Future<void> setContexts(String key, dynamic value);
8+
Future<void> removeContexts(String key);
9+
Future<void> setUser(SentryUser? user);
10+
Future<void> addBreadcrumb(Breadcrumb breadcrumb);
11+
Future<void> clearBreadcrumbs();
12+
Future<void> setExtra(String key, dynamic value);
13+
Future<void> removeExtra(String key);
14+
Future<void> setTag(String key, String value);
15+
Future<void> removeTag(String key);
16+
}

dart/lib/src/sentry_client.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,24 @@ class SentryClient {
115115
return _sentryId;
116116
}
117117
}
118+
119+
if (_options.platformChecker.platform.isAndroid &&
120+
_options.enableScopeSync) {
121+
/*
122+
We do this to avoid duplicate breadcrumbs on Android as sentry-android applies the breadcrumbs
123+
from the native scope onto every envelope sent through it. This scope will contain the breadcrumbs
124+
sent through the scope sync feature. This causes duplicate breadcrumbs.
125+
We then remove the breadcrumbs in all cases but if it is handled == false,
126+
this is a signal that the app would crash and android would lose the breadcrumbs by the time the app is restarted to read
127+
the envelope.
128+
*/
129+
preparedEvent = _eventWithRemovedBreadcrumbsIfHandled(preparedEvent);
130+
}
131+
118132
final envelope = SentryEnvelope.fromEvent(
119133
preparedEvent,
120134
_options.sdk,
121-
attachments: scope?.attachements,
135+
attachments: scope?.attachments,
122136
);
123137

124138
final id = await captureEnvelope(envelope);
@@ -297,7 +311,7 @@ class SentryClient {
297311

298312
final id = await captureEnvelope(
299313
SentryEnvelope.fromTransaction(preparedTransaction, _options.sdk,
300-
attachments: scope?.attachements
314+
attachments: scope?.attachments
301315
.where((element) => element.addToTransactions)
302316
.toList()),
303317
);
@@ -363,6 +377,19 @@ class SentryClient {
363377
_options.recorder.recordLostEvent(reason, category);
364378
}
365379

380+
SentryEvent _eventWithRemovedBreadcrumbsIfHandled(SentryEvent event) {
381+
final exceptions = event.exceptions ?? [];
382+
final handled = exceptions.isNotEmpty
383+
? exceptions.first.mechanism?.handled == true
384+
: false;
385+
386+
if (handled) {
387+
return event.copyWith(breadcrumbs: []);
388+
} else {
389+
return event;
390+
}
391+
}
392+
366393
Future<SentryId?> _attachClientReportsAndSend(SentryEnvelope envelope) {
367394
final clientReport = _options.recorder.flush();
368395
envelope.addClientReport(clientReport);

dart/lib/src/sentry_options.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import 'transport/noop_transport.dart';
1616
import 'utils.dart';
1717
import 'version.dart';
1818

19-
// TODO: Scope observers, enableScopeSync
2019
// TODO: shutdownTimeout, flushTimeoutMillis
2120
// https://api.dart.dev/stable/2.10.2/dart-io/HttpClient/close.html doesn't have a timeout param, we'd need to implement manually
2221

@@ -269,6 +268,17 @@ class SentryOptions {
269268
/// Send statistics to sentry when the client drops events.
270269
bool sendClientReports = true;
271270

271+
/// If enabled, [scopeObservers] will be called when mutating scope.
272+
bool enableScopeSync = true;
273+
274+
final List<ScopeObserver> _scopeObservers = [];
275+
276+
List<ScopeObserver> get scopeObservers => _scopeObservers;
277+
278+
void addScopeObserver(ScopeObserver scopeObserver) {
279+
_scopeObservers.add(scopeObserver);
280+
}
281+
272282
@internal
273283
late ClientReportRecorder recorder = NoOpClientReportRecorder();
274284

0 commit comments

Comments
 (0)