From 25fc2250fcb946fb3a06766738affaaeb0a6bdf7 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:27:10 +0100 Subject: [PATCH] feat: improve device info parsing on dart & non-mobile Flutter (#2441) * feat: improve device info parsing on dart & non-mobile Flutter * chore: update changelog * fix tests on Linux --- CHANGELOG.md | 1 + .../enricher/io_enricher_event_processor.dart | 76 +++- .../src/protocol/sentry_operating_system.dart | 16 + .../enricher/io_enricher_test.dart | 347 +++++++++++------- 4 files changed, 297 insertions(+), 143 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb1adf24e..f216ff084f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Linux native error & obfuscation support ([#2431](https://github.com/getsentry/sentry-dart/pull/2431)) +- Improve Device context on plain Dart and Flutter desktop apps ([#2441](https://github.com/getsentry/sentry-dart/pull/2441)) ## 8.11.0-beta.1 diff --git a/dart/lib/src/event_processor/enricher/io_enricher_event_processor.dart b/dart/lib/src/event_processor/enricher/io_enricher_event_processor.dart index 0bf4c218d5..30268e9c2e 100644 --- a/dart/lib/src/event_processor/enricher/io_enricher_event_processor.dart +++ b/dart/lib/src/event_processor/enricher/io_enricher_event_processor.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:meta/meta.dart'; + import '../../../sentry.dart'; import 'enricher_event_processor.dart'; import 'io_platform_memory.dart'; @@ -16,6 +18,8 @@ class IoEnricherEventProcessor implements EnricherEventProcessor { final SentryOptions _options; late final String _dartVersion = _extractDartVersion(Platform.version); + late final SentryOperatingSystem _os = extractOperatingSystem( + Platform.operatingSystem, Platform.operatingSystemVersion); /// Extracts the semantic version and channel from the full version string. /// @@ -137,9 +141,46 @@ class IoEnricherEventProcessor implements EnricherEventProcessor { } SentryOperatingSystem _getOperatingSystem(SentryOperatingSystem? os) { - return (os ?? SentryOperatingSystem()).copyWith( - name: os?.name ?? Platform.operatingSystem, - version: os?.version ?? Platform.operatingSystemVersion, + if (os == null) { + return _os.clone(); + } else { + return _os.mergeWith(os); + } + } + + @internal + SentryOperatingSystem extractOperatingSystem( + String name, String rawDescription) { + RegExpMatch? match; + switch (name) { + case 'android': + match = _androidOsRegexp.firstMatch(rawDescription); + name = 'Android'; + break; + case 'ios': + name = 'iOS'; + match = _appleOsRegexp.firstMatch(rawDescription); + break; + case 'macos': + name = 'macOS'; + match = _appleOsRegexp.firstMatch(rawDescription); + break; + case 'linux': + name = 'Linux'; + match = _linuxOsRegexp.firstMatch(rawDescription); + break; + case 'windows': + name = 'Windows'; + match = _windowsOsRegexp.firstMatch(rawDescription); + break; + } + + return SentryOperatingSystem( + name: name, + rawDescription: rawDescription, + version: match?.namedGroupOrNull('version'), + build: match?.namedGroupOrNull('build'), + kernelVersion: match?.namedGroupOrNull('kernelVersion'), ); } @@ -150,3 +191,32 @@ class IoEnricherEventProcessor implements EnricherEventProcessor { ); } } + +// LYA-L29 10.1.0.289(C432E7R1P5) +// TE1A.220922.010 +final _androidOsRegexp = RegExp('^(?.*)\$', caseSensitive: false); + +// Linux 5.11.0-1018-gcp #20~20.04.2-Ubuntu SMP Fri Sep 3 01:01:37 UTC 2021 +final _linuxOsRegexp = RegExp( + '(?[a-z0-9+.\\-]+) (?#.*)\$', + caseSensitive: false); + +// Version 14.5 (Build 18E182) +final _appleOsRegexp = RegExp( + '(?[a-z0-9+.\\-]+)( \\(Build (?[a-z0-9+.\\-]+))\\)?\$', + caseSensitive: false); + +// "Windows 10 Pro" 10.0 (Build 19043) +final _windowsOsRegexp = RegExp( + ' (?[a-z0-9+.\\-]+)( \\(Build (?[a-z0-9+.\\-]+))\\)?\$', + caseSensitive: false); + +extension on RegExpMatch { + String? namedGroupOrNull(String name) { + if (groupNames.contains(name)) { + return namedGroup(name); + } else { + return null; + } + } +} diff --git a/dart/lib/src/protocol/sentry_operating_system.dart b/dart/lib/src/protocol/sentry_operating_system.dart index 8854a4d87f..dfe219d416 100644 --- a/dart/lib/src/protocol/sentry_operating_system.dart +++ b/dart/lib/src/protocol/sentry_operating_system.dart @@ -110,4 +110,20 @@ class SentryOperatingSystem { theme: theme ?? this.theme, unknown: unknown, ); + + SentryOperatingSystem mergeWith(SentryOperatingSystem other) => + SentryOperatingSystem( + name: other.name ?? name, + version: other.version ?? version, + build: other.build ?? build, + kernelVersion: other.kernelVersion ?? kernelVersion, + rooted: other.rooted ?? rooted, + rawDescription: other.rawDescription ?? rawDescription, + theme: other.theme ?? theme, + unknown: other.unknown == null + ? unknown + : unknown == null + ? null + : {...unknown!, ...other.unknown!}, + ); } diff --git a/dart/test/event_processor/enricher/io_enricher_test.dart b/dart/test/event_processor/enricher/io_enricher_test.dart index a48c8dfed5..a5d40e8b66 100644 --- a/dart/test/event_processor/enricher/io_enricher_test.dart +++ b/dart/test/event_processor/enricher/io_enricher_test.dart @@ -12,172 +12,239 @@ import '../../mocks/mock_platform_checker.dart'; import '../../test_utils.dart'; void main() { - group('io_enricher', () { - late Fixture fixture; + late Fixture fixture; - setUp(() { - fixture = Fixture(); - }); + setUp(() { + fixture = Fixture(); + }); - test('adds dart runtime', () { - final enricher = fixture.getSut(); - final event = enricher.apply(SentryEvent(), Hint()); - - expect(event?.contexts.runtimes, isNotEmpty); - final dartRuntime = event?.contexts.runtimes - .firstWhere((element) => element.name == 'Dart'); - expect(dartRuntime?.name, 'Dart'); - expect(dartRuntime?.rawDescription, isNotNull); - expect(dartRuntime!.version.toString(), isNot(Platform.version)); - expect(Platform.version, contains(dartRuntime.version.toString())); - }); + test('adds dart runtime', () { + final enricher = fixture.getSut(); + final event = enricher.apply(SentryEvent(), Hint()); + + expect(event?.contexts.runtimes, isNotEmpty); + final dartRuntime = event?.contexts.runtimes + .firstWhere((element) => element.name == 'Dart'); + expect(dartRuntime?.name, 'Dart'); + expect(dartRuntime?.rawDescription, isNotNull); + expect(dartRuntime!.version.toString(), isNot(Platform.version)); + expect(Platform.version, contains(dartRuntime.version.toString())); + }); - test('does add to existing runtimes', () { - final runtime = SentryRuntime(name: 'foo', version: 'bar'); - var event = SentryEvent(contexts: Contexts(runtimes: [runtime])); - final enricher = fixture.getSut(); + test('does add to existing runtimes', () { + final runtime = SentryRuntime(name: 'foo', version: 'bar'); + var event = SentryEvent(contexts: Contexts(runtimes: [runtime])); + final enricher = fixture.getSut(); - event = enricher.apply(event, Hint())!; + event = enricher.apply(event, Hint())!; - expect(event.contexts.runtimes.contains(runtime), true); - // second runtime is Dart runtime - expect(event.contexts.runtimes.length, 2); - }); + expect(event.contexts.runtimes.contains(runtime), true); + // second runtime is Dart runtime + expect(event.contexts.runtimes.length, 2); + }); - test( - 'does not add device, os and culture if native integration is available', - () { - final enricher = fixture.getSut(hasNativeIntegration: true); - final event = enricher.apply(SentryEvent(), Hint()); + test('does not add device, os and culture if native integration is available', + () { + final enricher = fixture.getSut(hasNativeIntegration: true); + final event = enricher.apply(SentryEvent(), Hint()); - expect(event?.contexts.device, isNull); - expect(event?.contexts.operatingSystem, isNull); - expect(event?.contexts.culture, isNull); - }); + expect(event?.contexts.device, isNull); + expect(event?.contexts.operatingSystem, isNull); + expect(event?.contexts.culture, isNull); + }); - test('adds device, os and culture if no native integration is available', - () { - final enricher = fixture.getSut(hasNativeIntegration: false); - final event = enricher.apply(SentryEvent(), Hint()); + test('adds device, os and culture if no native integration is available', () { + final enricher = fixture.getSut(hasNativeIntegration: false); + final event = enricher.apply(SentryEvent(), Hint()); - expect(event?.contexts.device, isNotNull); - expect(event?.contexts.operatingSystem, isNotNull); - expect(event?.contexts.culture, isNotNull); - }); + expect(event?.contexts.device, isNotNull); + expect(event?.contexts.operatingSystem, isNotNull); + expect(event?.contexts.culture, isNotNull); + }); - test('device has name', () { - final enricher = fixture.getSut(); - final event = enricher.apply(SentryEvent(), Hint()); + test('device has name', () { + final enricher = fixture.getSut(); + final event = enricher.apply(SentryEvent(), Hint()); - expect(event?.contexts.device?.name, isNotNull); - }); + expect(event?.contexts.device?.name, isNotNull); + }); - test('culture has locale and timezone', () { - final enricher = fixture.getSut(); - final event = enricher.apply(SentryEvent(), Hint()); + test('culture has locale and timezone', () { + final enricher = fixture.getSut(); + final event = enricher.apply(SentryEvent(), Hint()); - expect(event?.contexts.culture?.locale, isNotNull); - expect(event?.contexts.culture?.timezone, isNotNull); - }); + expect(event?.contexts.culture?.locale, isNotNull); + expect(event?.contexts.culture?.timezone, isNotNull); + }); - test('os has name and version', () { - final enricher = fixture.getSut(); - final event = enricher.apply(SentryEvent(), Hint()); + test('os has name and version', () { + final enricher = fixture.getSut(); + final event = enricher.apply(SentryEvent(), Hint()); - expect(event?.contexts.operatingSystem?.name, isNotNull); + expect(event?.contexts.operatingSystem?.name, isNotNull); + if (Platform.isLinux) { + expect(event?.contexts.operatingSystem?.kernelVersion, isNotNull); + } else { expect(event?.contexts.operatingSystem?.version, isNotNull); + } + }); + + group('os info parsing', () { + // See docs from [Platform.operatingSystemVersion]: + /// A string representing the version of the operating system or platform. + /// + /// The format of this string will vary by operating system, platform and + /// version and is not suitable for parsing. For example: + /// "Linux 5.11.0-1018-gcp #20~20.04.2-Ubuntu SMP Fri Sep 3 01:01:37 UTC 2021" + /// "Version 14.5 (Build 18E182)" + /// '"Windows 10 Pro" 10.0 (Build 19043)' + + Map parse(String name, String description) => + fixture.getSut().extractOperatingSystem(name, description).toJson(); + + test('android', () { + expect(parse('android', 'LYA-L29 10.1.0.289(C432E7R1P5)'), { + 'raw_description': 'LYA-L29 10.1.0.289(C432E7R1P5)', + 'name': 'Android', + 'build': 'LYA-L29 10.1.0.289(C432E7R1P5)', + }); + expect(parse('android', 'TE1A.220922.010'), { + 'raw_description': 'TE1A.220922.010', + 'name': 'Android', + 'build': 'TE1A.220922.010', + }); }); - test('adds Dart context with PII', () { - final enricher = fixture.getSut(includePii: true); - final event = enricher.apply(SentryEvent(), Hint()); - - final dartContext = event?.contexts['dart_context']; - expect(dartContext, isNotNull); - // Getting the executable sometimes throws - //expect(dartContext['executable'], isNotNull); - expect(dartContext['resolved_executable'], isNotNull); - expect(dartContext['script'], isNotNull); - // package_config and executable_arguments are optional + test('linux', () { + expect( + parse('linux', + 'Linux 5.11.0-1018-gcp #20~20.04.2-Ubuntu SMP Fri Sep 3 01:01:37 UTC 2021'), + { + 'raw_description': + 'Linux 5.11.0-1018-gcp #20~20.04.2-Ubuntu SMP Fri Sep 3 01:01:37 UTC 2021', + 'name': 'Linux', + 'kernel_version': '5.11.0-1018-gcp', + 'build': '#20~20.04.2-Ubuntu SMP Fri Sep 3 01:01:37 UTC 2021', + }); }); - test('adds Dart context without PII', () { - final enricher = fixture.getSut(includePii: false); - final event = enricher.apply(SentryEvent(), Hint()); - - final dartContext = event?.contexts['dart_context']; - expect(dartContext, isNotNull); - expect(dartContext['compile_mode'], isNotNull); - expect(dartContext['executable'], isNull); - expect(dartContext['resolved_executable'], isNull); - expect(dartContext['script'], isNull); - // package_config and executable_arguments are optional - // and Platform is not mockable + test('ios', () { + expect(parse('ios', 'Version 14.5 (Build 18E182)'), { + 'raw_description': 'Version 14.5 (Build 18E182)', + 'name': 'iOS', + 'version': '14.5', + 'build': '18E182', + }); }); - test('does not override event', () { - final fakeEvent = SentryEvent( - contexts: Contexts( - device: SentryDevice( - name: 'device_name', - ), - operatingSystem: SentryOperatingSystem( - name: 'sentry_os', - version: 'best version', - ), - culture: SentryCulture( - locale: 'de', - timezone: 'timezone', - ), - ), - ); + test('macos', () { + expect(parse('macos', 'Version 14.5 (Build 18E182)'), { + 'raw_description': 'Version 14.5 (Build 18E182)', + 'name': 'macOS', + 'version': '14.5', + 'build': '18E182', + }); + }); - final enricher = fixture.getSut( - includePii: true, - hasNativeIntegration: false, - ); + test('windows', () { + expect(parse('windows', '"Windows 10 Pro" 10.0 (Build 19043)'), { + 'raw_description': '"Windows 10 Pro" 10.0 (Build 19043)', + 'name': 'Windows', + 'version': '10.0', + 'build': '19043', + }); + }); + }); - final event = enricher.apply(fakeEvent, Hint()); + test('adds Dart context with PII', () { + final enricher = fixture.getSut(includePii: true); + final event = enricher.apply(SentryEvent(), Hint()); + + final dartContext = event?.contexts['dart_context']; + expect(dartContext, isNotNull); + // Getting the executable sometimes throws + //expect(dartContext['executable'], isNotNull); + expect(dartContext['resolved_executable'], isNotNull); + expect(dartContext['script'], isNotNull); + // package_config and executable_arguments are optional + }); - // contexts.device - expect( - event?.contexts.device?.name, - fakeEvent.contexts.device?.name, - ); - // contexts.culture - expect( - event?.contexts.culture?.locale, - fakeEvent.contexts.culture?.locale, - ); - expect( - event?.contexts.culture?.timezone, - fakeEvent.contexts.culture?.timezone, - ); - // contexts.operatingSystem - expect( - event?.contexts.operatingSystem?.name, - fakeEvent.contexts.operatingSystem?.name, - ); - expect( - event?.contexts.operatingSystem?.version, - fakeEvent.contexts.operatingSystem?.version, - ); - }); + test('adds Dart context without PII', () { + final enricher = fixture.getSut(includePii: false); + final event = enricher.apply(SentryEvent(), Hint()); + + final dartContext = event?.contexts['dart_context']; + expect(dartContext, isNotNull); + expect(dartContext['compile_mode'], isNotNull); + expect(dartContext['executable'], isNull); + expect(dartContext['resolved_executable'], isNull); + expect(dartContext['script'], isNull); + // package_config and executable_arguments are optional + // and Platform is not mockable + }); - test('$IoEnricherEventProcessor gets added on init', () async { - final options = defaultTestOptions(); - await Sentry.init( - (options) { - options.dsn = fakeDsn; - }, - options: options, - ); - await Sentry.close(); - - final ioEnricherCount = - options.eventProcessors.whereType().length; - expect(ioEnricherCount, 1); - }); + test('does not override event', () { + final fakeEvent = SentryEvent( + contexts: Contexts( + device: SentryDevice( + name: 'device_name', + ), + operatingSystem: SentryOperatingSystem( + name: 'sentry_os', + version: 'best version', + ), + culture: SentryCulture( + locale: 'de', + timezone: 'timezone', + ), + ), + ); + + final enricher = fixture.getSut( + includePii: true, + hasNativeIntegration: false, + ); + + final event = enricher.apply(fakeEvent, Hint()); + + // contexts.device + expect( + event?.contexts.device?.name, + fakeEvent.contexts.device?.name, + ); + // contexts.culture + expect( + event?.contexts.culture?.locale, + fakeEvent.contexts.culture?.locale, + ); + expect( + event?.contexts.culture?.timezone, + fakeEvent.contexts.culture?.timezone, + ); + // contexts.operatingSystem + expect( + event?.contexts.operatingSystem?.name, + fakeEvent.contexts.operatingSystem?.name, + ); + expect( + event?.contexts.operatingSystem?.version, + fakeEvent.contexts.operatingSystem?.version, + ); + }); + + test('$IoEnricherEventProcessor gets added on init', () async { + final options = defaultTestOptions(); + await Sentry.init( + (options) { + options.dsn = fakeDsn; + }, + options: options, + ); + await Sentry.close(); + + final ioEnricherCount = + options.eventProcessors.whereType().length; + expect(ioEnricherCount, 1); }); }