Skip to content

Commit d2f62d6

Browse files
denrasemarandaneto
andauthored
Remove duplicated breadcrumbs when syncing with iOS/macOS (#1283)
Co-authored-by: Manoel Aranda Neto <[email protected]>
1 parent 2d3b03d commit d2f62d6

File tree

5 files changed

+91
-28
lines changed

5 files changed

+91
-28
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#6131)
1313
- [diff](https://github.com/getsentry/sentry-java/compare/6.13.0...6.13.1)
1414

15+
### Fixes
16+
17+
- Remove duplicated breadcrumbs when syncing with iOS/macOS ([#1283](https://github.com/getsentry/sentry-dart/pull/1283))
18+
1519
## 6.20.1
1620

1721
### Fixes

flutter/ios/Classes/SentryFlutterPluginApple.swift

+27
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,28 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
5353

5454
}
5555

56+
private lazy var iso8601Formatter: DateFormatter = {
57+
let formatter = DateFormatter()
58+
formatter.locale = Locale(identifier: "en_US_POSIX")
59+
formatter.timeZone = TimeZone(abbreviation: "UTC")
60+
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
61+
return formatter
62+
}()
63+
64+
private lazy var iso8601FormatterWithMillisecondPrecision: DateFormatter = {
65+
let formatter = DateFormatter()
66+
formatter.locale = Locale(identifier: "en_US_POSIX")
67+
formatter.timeZone = TimeZone(abbreviation: "UTC")
68+
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
69+
return formatter
70+
}()
71+
72+
// Replace with `NSDate+SentryExtras` when available.
73+
private func dateFrom(iso8601String: String) -> Date? {
74+
return iso8601FormatterWithMillisecondPrecision.date(from: iso8601String)
75+
?? iso8601Formatter.date(from: iso8601String) // Parse date with low precision formatter for backward compatible
76+
}
77+
5678
// swiftlint:disable:next cyclomatic_complexity
5779
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
5880
switch call.method as String {
@@ -581,6 +603,11 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
581603
breadcrumbInstance.data = data
582604
}
583605

606+
if let timestampValue = breadcrumb["timestamp"] as? String,
607+
let timestamp = dateFrom(iso8601String: timestampValue) {
608+
breadcrumbInstance.timestamp = timestamp
609+
}
610+
584611
SentrySDK.addBreadcrumb(crumb: breadcrumbInstance)
585612

586613
result("")

flutter/lib/src/integrations/load_contexts_integration.dart

+10-12
Original file line numberDiff line numberDiff line change
@@ -137,21 +137,19 @@ class _LoadContextsIntegrationEventProcessor extends EventProcessor {
137137
}
138138

139139
final breadcrumbsList = infos['breadcrumbs'] as List?;
140-
if (breadcrumbsList != null && breadcrumbsList.isNotEmpty) {
141-
final breadcrumbs = event.breadcrumbs ?? [];
142-
final newBreadcrumbs =
140+
if (breadcrumbsList != null &&
141+
breadcrumbsList.isNotEmpty &&
142+
_options.enableScopeSync) {
143+
final breadcrumbsJson =
143144
List<Map<dynamic, dynamic>>.from(breadcrumbsList);
145+
final breadcrumbs = <Breadcrumb>[];
144146

145-
for (final breadcrumb in newBreadcrumbs) {
146-
final newBreadcrumb = Map<String, dynamic>.from(breadcrumb);
147-
final crumb = Breadcrumb.fromJson(newBreadcrumb);
148-
breadcrumbs.add(crumb);
147+
for (final breadcrumbJson in breadcrumbsJson) {
148+
final breadcrumb = Breadcrumb.fromJson(
149+
Map<String, dynamic>.from(breadcrumbJson),
150+
);
151+
breadcrumbs.add(breadcrumb);
149152
}
150-
151-
breadcrumbs.sort((a, b) {
152-
return a.timestamp.compareTo(b.timestamp);
153-
});
154-
155153
event = event.copyWith(breadcrumbs: breadcrumbs);
156154
}
157155

flutter/test/integrations/load_contexts_integration_test.dart

+50
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import 'package:flutter/services.dart';
44
import 'package:flutter_test/flutter_test.dart';
5+
import 'package:mockito/mockito.dart';
56
import 'package:package_info_plus/package_info_plus.dart';
67
import 'package:sentry_flutter/sentry_flutter.dart';
78
import 'package:sentry_flutter/src/integrations/load_contexts_integration.dart';
@@ -36,12 +37,61 @@ void main() {
3637
fixture.options.sdk.integrations.contains('loadContextsIntegration'),
3738
true);
3839
});
40+
41+
test('take breadcrumbs from native if scope sync is enabled', () async {
42+
fixture.options.enableScopeSync = true;
43+
44+
final eventBreadcrumb = Breadcrumb(message: 'event');
45+
var event = SentryEvent(breadcrumbs: [eventBreadcrumb]);
46+
47+
final nativeBreadcrumb = Breadcrumb(message: 'native');
48+
Map<String, dynamic> loadContexts = {
49+
'breadcrumbs': [nativeBreadcrumb.toJson()]
50+
};
51+
52+
final future = Future.value(loadContexts);
53+
when(fixture.methodChannel.invokeMethod<dynamic>('loadContexts'))
54+
.thenAnswer((_) => future);
55+
_channel.setMockMethodCallHandler((MethodCall methodCall) async {});
56+
57+
final integration = LoadContextsIntegration(fixture.methodChannel);
58+
integration.call(fixture.hub, fixture.options);
59+
event = (await fixture.options.eventProcessors.first.apply(event))!;
60+
61+
expect(event.breadcrumbs!.length, 1);
62+
expect(event.breadcrumbs!.first.message, 'native');
63+
});
64+
65+
test('take breadcrumbs from event if scope sync is disabled', () async {
66+
fixture.options.enableScopeSync = false;
67+
68+
final eventBreadcrumb = Breadcrumb(message: 'event');
69+
var event = SentryEvent(breadcrumbs: [eventBreadcrumb]);
70+
71+
final nativeBreadcrumb = Breadcrumb(message: 'native');
72+
Map<String, dynamic> loadContexts = {
73+
'breadcrumbs': [nativeBreadcrumb.toJson()]
74+
};
75+
76+
final future = Future.value(loadContexts);
77+
when(fixture.methodChannel.invokeMethod<dynamic>('loadContexts'))
78+
.thenAnswer((_) => future);
79+
_channel.setMockMethodCallHandler((MethodCall methodCall) async {});
80+
81+
final integration = LoadContextsIntegration(fixture.methodChannel);
82+
integration.call(fixture.hub, fixture.options);
83+
event = (await fixture.options.eventProcessors.first.apply(event))!;
84+
85+
expect(event.breadcrumbs!.length, 1);
86+
expect(event.breadcrumbs!.first.message, 'event');
87+
});
3988
});
4089
}
4190

4291
class Fixture {
4392
final hub = MockHub();
4493
final options = SentryFlutterOptions(dsn: fakeDsn);
94+
final methodChannel = MockMethodChannel();
4595

4696
LoadReleaseIntegration getIntegration({PackageLoader? loader}) {
4797
return LoadReleaseIntegration(loader ?? loadRelease);

flutter/test/integrations/load_contexts_integrations_test.dart

-16
Original file line numberDiff line numberDiff line change
@@ -405,22 +405,6 @@ void main() {
405405

406406
expect(event?.level, SentryLevel.fatal);
407407
});
408-
409-
test('should merge in breadcrumbs sorted by timestamp', () async {
410-
final integration = fixture.getSut();
411-
integration(fixture.hub, fixture.options);
412-
413-
final breadcrumb = Breadcrumb(
414-
message: 'flutter-crumb',
415-
timestamp: DateTime.fromMillisecondsSinceEpoch(1),
416-
);
417-
final e = getEvent(breadcrumbs: [breadcrumb]);
418-
final event = await fixture.options.eventProcessors.first.apply(e);
419-
420-
expect(event?.breadcrumbs?.length, 2);
421-
expect(event?.breadcrumbs?[0].message, 'native-crumb');
422-
expect(event?.breadcrumbs?[1].message, 'flutter-crumb');
423-
});
424408
}
425409

426410
class Fixture {

0 commit comments

Comments
 (0)