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

Fix: Add missing iOS contexts #761

Merged
merged 17 commits into from
Apr 27, 2022
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased:

* Fix: Add missing iOS contexts (#761)

## 6.5.1

- Update event contexts (#838)
Expand Down
6 changes: 5 additions & 1 deletion dart/lib/src/protocol/sentry_user.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,16 @@ class SentryUser {

/// Deserializes a [SentryUser] from JSON [Map].
factory SentryUser.fromJson(Map<String, dynamic> json) {
var extras = json['extras'];
if (extras != null) {
extras = Map<String, dynamic>.from(extras as Map);
}
return SentryUser(
id: json['id'],
username: json['username'],
email: json['email'],
ipAddress: json['ip_address'],
extras: json['extras'],
extras: extras,
);
}

Expand Down
25 changes: 25 additions & 0 deletions flutter/ios/Classes/SentryFlutterPluginApple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,31 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {

var infos = ["contexts": context]

if let tags = serializedScope["tags"] as? [String: String] {
infos["tags"] = tags
}
if let extra = serializedScope["extra"] as? [String: Any] {
infos["extra"] = extra
}
if let user = serializedScope["user"] as? [String: Any] {
infos["user"] = user
}
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
if let dist = serializedScope["dist"] as? String {
infos["dist"] = dist
}
if let environment = serializedScope["environment"] as? String {
infos["environment"] = environment
}
if let fingerprint = serializedScope["fingerprint"] as? [String] {
infos["fingerprint"] = fingerprint
}
if let level = serializedScope["level"] as? String {
infos["level"] = level
}
if let breadcrumbs = serializedScope["breadcrumbs"] as? [[String: Any]] {
infos["breadcrumbs"] = breadcrumbs
}

if let user = serializedScope["user"] as? [String: Any] {
infos["user"] = user
} else {
Expand Down
93 changes: 84 additions & 9 deletions flutter/lib/src/default_integrations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,10 @@ class _LoadContextsIntegrationEventProcessor extends EventProcessor {
final infos = Map<String, dynamic>.from(
await (_channel.invokeMethod('loadContexts')),
);
if (infos['contexts'] != null) {
final contextsMap = infos['contexts'] as Map?;
if (contextsMap != null && contextsMap.isNotEmpty) {
final contexts = Contexts.fromJson(
Map<String, dynamic>.from(infos['contexts'] as Map),
Map<String, dynamic>.from(contextsMap),
);
final eventContexts = event.contexts.clone();

Expand All @@ -195,14 +196,87 @@ class _LoadContextsIntegrationEventProcessor extends EventProcessor {
event = event.copyWith(contexts: eventContexts);
}

final userMap = infos['user'];
if (event.user == null && userMap != null) {
final user = Map<String, dynamic>.from(userMap as Map);
final tagsMap = infos['tags'] as Map?;
if (tagsMap != null && tagsMap.isNotEmpty) {
final tags = event.tags ?? {};
final newTags = Map<String, String>.from(tagsMap);

for (final tag in newTags.entries) {
if (!tags.containsKey(tag.key)) {
tags[tag.key] = tag.value;
}
}
event = event.copyWith(tags: tags);
}

final extraMap = infos['extra'] as Map?;
if (extraMap != null && extraMap.isNotEmpty) {
final extras = event.extra ?? {};
final newExtras = Map<String, dynamic>.from(extraMap);

for (final extra in newExtras.entries) {
if (!extras.containsKey(extra.key)) {
extras[extra.key] = extra.value;
}
}
event = event.copyWith(extra: extras);
}

final userMap = infos['user'] as Map?;
if (event.user == null && userMap != null && userMap.isNotEmpty) {
final user = Map<String, dynamic>.from(userMap);
event = event.copyWith(user: SentryUser.fromJson(user));
}

if (infos['integrations'] != null) {
final integrations = List<String>.from(infos['integrations'] as List);
final distString = infos['dist'] as String?;
if (event.dist == null && distString != null) {
denrase marked this conversation as resolved.
Show resolved Hide resolved
event = event.copyWith(dist: distString);
}

final environmentString = infos['environment'] as String?;
if (event.environment == null && environmentString != null) {
event = event.copyWith(environment: environmentString);
}

final fingerprintList = infos['fingerprint'] as List?;
if (fingerprintList != null && fingerprintList.isNotEmpty) {
final eventFingerprints = event.fingerprint ?? [];
final newFingerprint = List<String>.from(fingerprintList);

for (final fingerprint in newFingerprint) {
if (!eventFingerprints.contains(fingerprint)) {
eventFingerprints.add(fingerprint);
}
}
event = event.copyWith(fingerprint: eventFingerprints);
}

final levelString = infos['level'] as String?;
if (event.level == null && levelString != null) {
event = event.copyWith(level: SentryLevel.fromName(levelString));
}

final breadcrumbsList = infos['breadcrumbs'] as List?;
if (breadcrumbsList != null && breadcrumbsList.isNotEmpty) {
final breadcrumbs = event.breadcrumbs ?? [];
final newBreadcrumbs = List<Map>.from(breadcrumbsList);

for (final breadcrumb in newBreadcrumbs) {
final newBreadcrumb = Map<String, dynamic>.from(breadcrumb);
final crumb = Breadcrumb.fromJson(newBreadcrumb);
breadcrumbs.add(crumb);
}

breadcrumbs.sort((a, b) {
return a.timestamp.compareTo(b.timestamp);
});

event = event.copyWith(breadcrumbs: breadcrumbs);
}

final integrationsList = infos['integrations'] as List?;
if (integrationsList != null && integrationsList.isNotEmpty) {
final integrations = List<String>.from(integrationsList);
final sdk = event.sdk ?? _options.sdk;

for (final integration in integrations) {
Expand All @@ -214,8 +288,9 @@ class _LoadContextsIntegrationEventProcessor extends EventProcessor {
event = event.copyWith(sdk: sdk);
}

if (infos['package'] != null) {
final package = Map<String, String>.from(infos['package'] as Map);
final packageMap = infos['package'] as Map?;
if (packageMap != null && packageMap.isNotEmpty) {
final package = Map<String, String>.from(packageMap);
final sdk = event.sdk ?? _options.sdk;

final name = package['sdk_name'];
Expand Down
171 changes: 170 additions & 1 deletion flutter/test/load_contexts_integrations_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ void main() {
SentryEvent getEvent(
{SdkVersion? sdk,
Map<String, String>? tags,
Map<String, dynamic>? extra,
SentryUser? user,
String? dist,
String? environment,
List<String>? fingerprint,
SentryLevel? level,
List<Breadcrumb>? breadcrumbs,
List<String> integrations = const ['EventIntegration'],
List<SentryPackage> packages = const [
SentryPackage('event-package', '2.0')
Expand All @@ -39,6 +46,13 @@ void main() {
packages: packages,
),
tags: tags,
extra: extra,
user: user,
dist: dist,
environment: environment,
fingerprint: fingerprint,
level: level,
breadcrumbs: breadcrumbs,
);
}

Expand Down Expand Up @@ -265,6 +279,143 @@ void main() {
expect(event?.tags?.containsKey('event.environment'), false);
},
);

test('should merge in tags from native without overriding flutter keys',
() async {
final integration = fixture.getSut();
integration(fixture.hub, fixture.options);

final e = getEvent(tags: {'key': 'flutter', 'key-a': 'flutter'});
final event = await fixture.options.eventProcessors.first.apply(e);

expect(event?.tags?['key'], 'flutter');
expect(event?.tags?['key-a'], 'flutter');
expect(event?.tags?['key-b'], 'native');
});

test('should merge in extra from native without overriding flutter keys',
() async {
final integration = fixture.getSut();
integration(fixture.hub, fixture.options);

final e = getEvent(extra: {'key': 'flutter', 'key-a': 'flutter'});
final event = await fixture.options.eventProcessors.first.apply(e);

expect(event?.extra?['key'], 'flutter');
expect(event?.extra?['key-a'], 'flutter');
expect(event?.extra?['key-b'], 'native');
});

test('should set user from native', () async {
final integration = fixture.getSut();
integration(fixture.hub, fixture.options);

final e = getEvent();
final event = await fixture.options.eventProcessors.first.apply(e);

expect(event?.user?.id, '196E065A-AAF7-409A-9A6C-A81F40274CB9');
expect(event?.user?.username, 'fixture-username');
expect(event?.user?.email, 'fixture-email');
expect(event?.user?.ipAddress, 'fixture-ip_address');
expect(event?.user?.extras?['key'], 'value');
});

test('should not override user with native', () async {
final integration = fixture.getSut();
integration(fixture.hub, fixture.options);

final e = getEvent(user: SentryUser(id: 'abc'));
final event = await fixture.options.eventProcessors.first.apply(e);

expect(event?.user?.id, 'abc');
});

test('should set dist from native', () async {
final integration = fixture.getSut();
integration(fixture.hub, fixture.options);

final e = getEvent();
final event = await fixture.options.eventProcessors.first.apply(e);

expect(event?.dist, 'fixture-dist');
});

test('should not override dist with native', () async {
final integration = fixture.getSut();
integration(fixture.hub, fixture.options);

final e = getEvent(dist: 'abc');
final event = await fixture.options.eventProcessors.first.apply(e);

expect(event?.dist, 'abc');
});

test('should set environment from native', () async {
final integration = fixture.getSut();
integration(fixture.hub, fixture.options);

final e = getEvent();
final event = await fixture.options.eventProcessors.first.apply(e);

expect(event?.environment, 'fixture-environment');
});

test('should not override environment with native', () async {
final integration = fixture.getSut();
integration(fixture.hub, fixture.options);

final e = getEvent(environment: 'abc');
final event = await fixture.options.eventProcessors.first.apply(e);

expect(event?.environment, 'abc');
});

test('should merge in fingerprint from native without duplicating entries',
() async {
final integration = fixture.getSut();
integration(fixture.hub, fixture.options);

final e = getEvent(fingerprint: ['fingerprint-a', 'fingerprint-b']);
final event = await fixture.options.eventProcessors.first.apply(e);

expect(event?.fingerprint, ['fingerprint-a', 'fingerprint-b']);
});

test('should set level from native', () async {
final integration = fixture.getSut();
integration(fixture.hub, fixture.options);

final e = getEvent();
final event = await fixture.options.eventProcessors.first.apply(e);

expect(event?.level, SentryLevel.error);
});

test('should not override level with native', () async {
final integration = fixture.getSut();
integration(fixture.hub, fixture.options);

final e = getEvent(level: SentryLevel.fatal);
final event = await fixture.options.eventProcessors.first.apply(e);

expect(event?.level, SentryLevel.fatal);
});

test('should merge in breadcrumbs sorted by timestamp', () async {
final integration = fixture.getSut();
integration(fixture.hub, fixture.options);

final breadcrumb = Breadcrumb(
message: 'flutter-crumb',
timestamp: DateTime.fromMillisecondsSinceEpoch(1),
);
final e = getEvent(breadcrumbs: [breadcrumb]);
final event = await fixture.options.eventProcessors.first.apply(e);

expect(event?.breadcrumbs?.length, 2);
expect(event?.breadcrumbs?[0].message, 'native-crumb');
expect(event?.breadcrumbs?[1].message, 'flutter-crumb');
});
}

class Fixture {
Expand All @@ -288,7 +439,25 @@ class Fixture {
'runtime': {'name': 'RT1'},
'theme': 'material',
},
'user': {'id': '196E065A-AAF7-409A-9A6C-A81F40274CB9'}
'user': {
'id': '196E065A-AAF7-409A-9A6C-A81F40274CB9',
'username': 'fixture-username',
'email': 'fixture-email',
'ip_address': 'fixture-ip_address',
'extras': {'key': 'value'},
},
'tags': {'key-a': 'native', 'key-b': 'native'},
'extra': {'key-a': 'native', 'key-b': 'native'},
'dist': 'fixture-dist',
'environment': 'fixture-environment',
'fingerprint': ['fingerprint-a'],
'level': 'error',
'breadcrumbs': [
<String, dynamic>{
'timestamp': '1970-01-01T00:00:00.000Z',
'message': 'native-crumb',
}
]
}}) {
channel.setMockMethodCallHandler((MethodCall methodCall) async {
called = true;
Expand Down