diff --git a/dart/example/bin/event_example.dart b/dart/example/bin/event_example.dart index e7ba4c4dd6..c28547cc25 100644 --- a/dart/example/bin/event_example.dart +++ b/dart/example/bin/event_example.dart @@ -55,11 +55,16 @@ final event = SentryEvent( modelId: 'LRX22G', arch: 'armeabi-v7a', batteryLevel: 99, - orientation: SentryOrientation.landscape, manufacturer: 'samsung', brand: 'samsung', - screenDensity: 2.1, - screenDpi: 320, + views: [ + SentryView( + 0, + screenDensity: 2.1, + screenDpi: 320, + orientation: SentryOrientation.landscape, + ) + ], online: true, charging: true, lowMemory: true, diff --git a/dart/example_web/web/event.dart b/dart/example_web/web/event.dart index 6e3e8b3e0b..40be7ee01f 100644 --- a/dart/example_web/web/event.dart +++ b/dart/example_web/web/event.dart @@ -54,11 +54,16 @@ final event = SentryEvent( modelId: 'LRX22G', arch: 'armeabi-v7a', batteryLevel: 99, - orientation: SentryOrientation.landscape, manufacturer: 'samsung', brand: 'samsung', - screenDensity: 2.1, - screenDpi: 320, + views: [ + SentryView( + 0, + orientation: SentryOrientation.landscape, + screenDensity: 2.1, + screenDpi: 320, + ) + ], online: true, charging: true, lowMemory: true, diff --git a/dart/example_web_legacy/web/event.dart b/dart/example_web_legacy/web/event.dart index 6e3e8b3e0b..40be7ee01f 100644 --- a/dart/example_web_legacy/web/event.dart +++ b/dart/example_web_legacy/web/event.dart @@ -54,11 +54,16 @@ final event = SentryEvent( modelId: 'LRX22G', arch: 'armeabi-v7a', batteryLevel: 99, - orientation: SentryOrientation.landscape, manufacturer: 'samsung', brand: 'samsung', - screenDensity: 2.1, - screenDpi: 320, + views: [ + SentryView( + 0, + orientation: SentryOrientation.landscape, + screenDensity: 2.1, + screenDpi: 320, + ) + ], online: true, charging: true, lowMemory: true, diff --git a/dart/lib/src/event_processor/enricher/html_enricher_event_processor.dart b/dart/lib/src/event_processor/enricher/html_enricher_event_processor.dart index e51cff4b71..9d1137d1fa 100644 --- a/dart/lib/src/event_processor/enricher/html_enricher_event_processor.dart +++ b/dart/lib/src/event_processor/enricher/html_enricher_event_processor.dart @@ -55,16 +55,25 @@ class WebEnricherEventProcessor implements EnricherEventProcessor { } SentryDevice _getDevice(SentryDevice? device) { - return (device ?? SentryDevice()).copyWith( + final currentDevice = device ?? SentryDevice(); + final currentView = currentDevice.views.isNotEmpty + ? currentDevice.views.first + : SentryView(0); + return currentDevice.copyWith( online: device?.online ?? _window.navigator.onLine, memorySize: device?.memorySize ?? _getMemorySize(), - orientation: device?.orientation ?? _getScreenOrientation(), - screenHeightPixels: device?.screenHeightPixels ?? - _window.screen?.available.height.toInt(), - screenWidthPixels: - device?.screenWidthPixels ?? _window.screen?.available.width.toInt(), - screenDensity: - device?.screenDensity ?? _window.devicePixelRatio.toDouble(), + views: [ + currentView.copyWith( + orientation: + device?.views.first.orientation ?? _getScreenOrientation(), + screenHeightPixels: device?.views.first.screenHeightPixels ?? + _window.screen?.available.height.toInt(), + screenWidthPixels: device?.views.first.screenWidthPixels ?? + _window.screen?.available.width.toInt(), + screenDensity: device?.views.first.screenDensity ?? + _window.devicePixelRatio.toDouble(), + ) + ], ); } diff --git a/dart/lib/src/event_processor/enricher/web_enricher_event_processor.dart b/dart/lib/src/event_processor/enricher/web_enricher_event_processor.dart index 27bb6b99db..9fe0a7253d 100644 --- a/dart/lib/src/event_processor/enricher/web_enricher_event_processor.dart +++ b/dart/lib/src/event_processor/enricher/web_enricher_event_processor.dart @@ -57,15 +57,25 @@ class WebEnricherEventProcessor implements EnricherEventProcessor { } SentryDevice _getDevice(SentryDevice? device) { - return (device ?? SentryDevice()).copyWith( + final currentDevice = device ?? SentryDevice(); + final currentView = currentDevice.views.isNotEmpty + ? currentDevice.views.first + : SentryView(0); + return currentDevice.copyWith( online: device?.online ?? _window.navigator.onLine, memorySize: device?.memorySize ?? _getMemorySize(), - orientation: device?.orientation ?? _getScreenOrientation(), - screenHeightPixels: - device?.screenHeightPixels ?? _window.screen.availHeight, - screenWidthPixels: device?.screenWidthPixels ?? _window.screen.availWidth, - screenDensity: - device?.screenDensity ?? _window.devicePixelRatio.toDouble(), + views: [ + currentView.copyWith( + orientation: + device?.views.first.orientation ?? _getScreenOrientation(), + screenHeightPixels: device?.views.first.screenHeightPixels ?? + _window.screen.availHeight, + screenWidthPixels: device?.views.first.screenWidthPixels ?? + _window.screen.availWidth, + screenDensity: device?.views.first.screenDensity ?? + _window.devicePixelRatio.toDouble(), + ) + ], ); } diff --git a/dart/lib/src/protocol/sentry_device.dart b/dart/lib/src/protocol/sentry_device.dart index 1bc5c89b78..55f2b7470e 100644 --- a/dart/lib/src/protocol/sentry_device.dart +++ b/dart/lib/src/protocol/sentry_device.dart @@ -5,6 +5,98 @@ import 'access_aware_map.dart'; /// If a device is on portrait or landscape mode enum SentryOrientation { portrait, landscape } +class SentryView { + /// The id of this view. For single view application it is always 1. + final int viewId; + + /// Defines the orientation of a device. + final SentryOrientation? orientation; + + /// The screen height in pixels. (e.g.: `600`, `1080`). + final int? screenHeightPixels; + + /// The screen width in pixels. (e.g.: `800`, `1920`). + final int? screenWidthPixels; + + /// A floating point denoting the screen density. + final double? screenDensity; + + /// A decimal value reflecting the DPI (dots-per-inch) density. + final int? screenDpi; + + @internal + final Map? unknown; + + SentryView( + this.viewId, { + this.orientation, + this.screenHeightPixels, + this.screenWidthPixels, + this.screenDensity, + this.screenDpi, + this.unknown, + }); + + SentryView clone() => SentryView( + viewId, + orientation: orientation, + screenHeightPixels: screenHeightPixels, + screenWidthPixels: screenWidthPixels, + screenDensity: screenDensity, + screenDpi: screenDpi, + unknown: unknown, + ); + + SentryView copyWith({ + int? viewId, + SentryOrientation? orientation, + int? screenHeightPixels, + int? screenWidthPixels, + double? screenDensity, + int? screenDpi, + }) => + SentryView( + viewId ?? this.viewId, + orientation: orientation ?? this.orientation, + screenHeightPixels: screenHeightPixels ?? this.screenHeightPixels, + screenWidthPixels: screenWidthPixels ?? this.screenWidthPixels, + screenDensity: screenDensity ?? this.screenDensity, + screenDpi: screenDpi ?? this.screenDpi, + unknown: unknown, + ); + + /// Produces a [Map] that can be serialized to JSON. + Map toJson() { + return { + ...?unknown, + 'view_id': viewId, + if (orientation != null) 'orientation': orientation!.name, + if (screenWidthPixels != null) 'screen_width_pixels': screenWidthPixels, + if (screenHeightPixels != null) + 'screen_height_pixels': screenHeightPixels, + if (screenDensity != null) 'screen_density': screenDensity, + if (screenDpi != null) 'screen_dpi': screenDpi, + }; + } + + factory SentryView.fromJson(Map data) { + final json = AccessAwareMap(data); + return SentryView( + json['view_id'], + orientation: json['orientation'] == 'portrait' + ? SentryOrientation.portrait + : json['orientation'] == 'landscape' + ? SentryOrientation.landscape + : null, + screenHeightPixels: json['screen_height_pixels']?.toInt(), + screenWidthPixels: json['screen_width_pixels']?.toInt(), + screenDensity: json['screen_density'], + screenDpi: json['screen_dpi'], + unknown: json.notAccessed(), + ); + } +} + /// This describes the device that caused the event. @immutable class SentryDevice { @@ -17,13 +109,9 @@ class SentryDevice { this.modelId, this.arch, this.batteryLevel, - this.orientation, this.manufacturer, this.brand, - this.screenHeightPixels, - this.screenWidthPixels, - this.screenDensity, - this.screenDpi, + this.views = const [], this.online, this.charging, this.lowMemory, @@ -75,26 +163,14 @@ class SentryDevice { /// defining the battery level (in the range 0-100). final double? batteryLevel; - /// Defines the orientation of a device. - final SentryOrientation? orientation; - /// The manufacturer of the device. final String? manufacturer; /// The brand of the device. final String? brand; - /// The screen height in pixels. (e.g.: `600`, `1080`). - final int? screenHeightPixels; - - /// The screen width in pixels. (e.g.: `800`, `1920`). - final int? screenWidthPixels; - - /// A floating point denoting the screen density. - final double? screenDensity; - - /// A decimal value reflecting the DPI (dots-per-inch) density. - final int? screenDpi; + /// The collection of views, which are rendered and shown to the user + final List views; /// Whether the device was online or not. final bool? online; @@ -188,17 +264,13 @@ class SentryDevice { batteryLevel: (json['battery_level'] is num ? json['battery_level'] as num : null) ?.toDouble(), - orientation: json['orientation'] == 'portrait' - ? SentryOrientation.portrait - : json['orientation'] == 'landscape' - ? SentryOrientation.landscape - : null, manufacturer: json['manufacturer'], brand: json['brand'], - screenHeightPixels: json['screen_height_pixels']?.toInt(), - screenWidthPixels: json['screen_width_pixels']?.toInt(), - screenDensity: json['screen_density'], - screenDpi: json['screen_dpi'], + views: json['views'] == null + ? [] + : (json['views'] as List) + .map((view) => SentryView.fromJson(view)) + .toList(), online: json['online'], charging: json['charging'], lowMemory: json['low_memory'], @@ -238,14 +310,10 @@ class SentryDevice { if (modelId != null) 'model_id': modelId, if (arch != null) 'arch': arch, if (batteryLevel != null) 'battery_level': batteryLevel, - if (orientation != null) 'orientation': orientation!.name, if (manufacturer != null) 'manufacturer': manufacturer, if (brand != null) 'brand': brand, - if (screenWidthPixels != null) 'screen_width_pixels': screenWidthPixels, - if (screenHeightPixels != null) - 'screen_height_pixels': screenHeightPixels, - if (screenDensity != null) 'screen_density': screenDensity, - if (screenDpi != null) 'screen_dpi': screenDpi, + if (views.isNotEmpty) + 'views': views.map((view) => view.toJson()).toList(), if (online != null) 'online': online, if (charging != null) 'charging': charging, if (lowMemory != null) 'low_memory': lowMemory, @@ -284,13 +352,9 @@ class SentryDevice { modelId: modelId, arch: arch, batteryLevel: batteryLevel, - orientation: orientation, manufacturer: manufacturer, brand: brand, - screenHeightPixels: screenHeightPixels, - screenWidthPixels: screenWidthPixels, - screenDensity: screenDensity, - screenDpi: screenDpi, + views: views, online: online, charging: charging, lowMemory: lowMemory, @@ -324,13 +388,9 @@ class SentryDevice { String? modelId, String? arch, double? batteryLevel, - SentryOrientation? orientation, String? manufacturer, String? brand, - int? screenHeightPixels, - int? screenWidthPixels, - double? screenDensity, - int? screenDpi, + List? views, bool? online, bool? charging, bool? lowMemory, @@ -362,13 +422,9 @@ class SentryDevice { modelId: modelId ?? this.modelId, arch: arch ?? this.arch, batteryLevel: batteryLevel ?? this.batteryLevel, - orientation: orientation ?? this.orientation, manufacturer: manufacturer ?? this.manufacturer, brand: brand ?? this.brand, - screenHeightPixels: screenHeightPixels ?? this.screenHeightPixels, - screenWidthPixels: screenWidthPixels ?? this.screenWidthPixels, - screenDensity: screenDensity ?? this.screenDensity, - screenDpi: screenDpi ?? this.screenDpi, + views: views ?? this.views, online: online ?? this.online, charging: charging ?? this.charging, lowMemory: lowMemory ?? this.lowMemory, diff --git a/dart/test/contexts_test.dart b/dart/test/contexts_test.dart index 31cc55b7cd..7f1b6289b4 100644 --- a/dart/test/contexts_test.dart +++ b/dart/test/contexts_test.dart @@ -19,11 +19,14 @@ void main() { modelId: 'testModelId', arch: 'testArch', batteryLevel: 23, - orientation: SentryOrientation.landscape, manufacturer: 'testOEM', brand: 'testBrand', - screenDensity: 99.1, - screenDpi: 100, + views: [ + SentryView(0, + orientation: SentryOrientation.landscape, + screenDensity: 99.1, + screenDpi: 100), + ], online: false, charging: true, lowMemory: false, @@ -66,11 +69,16 @@ void main() { 'model_id': 'testModelId', 'arch': 'testArch', 'battery_level': 23.0, - 'orientation': 'landscape', 'manufacturer': 'testOEM', 'brand': 'testBrand', - 'screen_density': 99.1, - 'screen_dpi': 100, + 'views': [ + { + 'view_id': 0, + 'orientation': 'landscape', + 'screen_density': 99.1, + 'screen_dpi': 100, + }, + ], 'online': false, 'charging': true, 'low_memory': false, diff --git a/dart/test/event_processor/enricher/web_enricher_test.dart b/dart/test/event_processor/enricher/web_enricher_test.dart index d2590a09fa..1cbaac845d 100644 --- a/dart/test/event_processor/enricher/web_enricher_test.dart +++ b/dart/test/event_processor/enricher/web_enricher_test.dart @@ -112,7 +112,7 @@ void main() { var enricher = fixture.getSut(); final event = enricher.apply(SentryEvent(), Hint()); - expect(event?.contexts.device?.screenDensity, isNotNull); + expect(event?.contexts.device!.views.first.screenDensity, isNotNull); }); test('culture has timezone', () { @@ -128,10 +128,15 @@ void main() { device: SentryDevice( online: false, memorySize: 200, - orientation: SentryOrientation.landscape, - screenHeightPixels: 1080, - screenWidthPixels: 1920, - screenDensity: 2, + views: [ + SentryView( + 0, + orientation: SentryOrientation.landscape, + screenHeightPixels: 1080, + screenWidthPixels: 1920, + screenDensity: 2, + ), + ], ), operatingSystem: SentryOperatingSystem( name: 'sentry_os', @@ -156,20 +161,20 @@ void main() { fakeEvent.contexts.device?.memorySize, ); expect( - event?.contexts.device?.orientation, - fakeEvent.contexts.device?.orientation, + event?.contexts.device?.views.first.orientation, + fakeEvent.contexts.device?.views.first.orientation, ); expect( - event?.contexts.device?.screenHeightPixels, - fakeEvent.contexts.device?.screenHeightPixels, + event?.contexts.device?.views.first.screenHeightPixels, + fakeEvent.contexts.device?.views.first.screenHeightPixels, ); expect( - event?.contexts.device?.screenWidthPixels, - fakeEvent.contexts.device?.screenWidthPixels, + event?.contexts.device?.views.first.screenWidthPixels, + fakeEvent.contexts.device?.views.first.screenWidthPixels, ); expect( - event?.contexts.device?.screenDensity, - fakeEvent.contexts.device?.screenDensity, + event?.contexts.device?.views.first.screenDensity, + fakeEvent.contexts.device?.views.first.screenDensity, ); // contexts.culture expect( diff --git a/dart/test/mocks.dart b/dart/test/mocks.dart index 679b9f73cd..593cc8915f 100644 --- a/dart/test/mocks.dart +++ b/dart/test/mocks.dart @@ -77,11 +77,16 @@ final fakeEvent = SentryEvent( modelId: 'LRX22G', arch: 'armeabi-v7a', batteryLevel: 99, - orientation: SentryOrientation.landscape, manufacturer: 'samsung', brand: 'samsung', - screenDensity: 2.1, - screenDpi: 320, + views: [ + SentryView( + 0, + orientation: SentryOrientation.landscape, + screenDensity: 2.1, + screenDpi: 320, + ) + ], online: true, charging: true, lowMemory: true, diff --git a/dart/test/protocol/device_test.dart b/dart/test/protocol/device_test.dart index 8e21729000..3294ccff9d 100644 --- a/dart/test/protocol/device_test.dart +++ b/dart/test/protocol/device_test.dart @@ -8,8 +8,19 @@ void main() { final copy = data.copyWith(); + final dataJson = data.toJson(); + final copyJson = copy.toJson(); + + expect( + MapEquality().equals(dataJson['views'][0], copyJson['views'][0]), + true, + ); + + dataJson.remove('views'); + copyJson.remove('views'); + expect( - MapEquality().equals(data.toJson(), copy.toJson()), + MapEquality().equals(dataJson, copyJson), true, ); }); @@ -26,13 +37,19 @@ void main() { modelId: 'modelId1', arch: 'arch1', batteryLevel: 2, - orientation: SentryOrientation.portrait, manufacturer: 'manufacturer1', brand: 'brand1', - screenHeightPixels: 900, - screenWidthPixels: 700, - screenDensity: 99.2, - screenDpi: 99, + views: data.views + .map( + (view) => view.copyWith( + orientation: SentryOrientation.portrait, + screenHeightPixels: 900, + screenWidthPixels: 700, + screenDensity: 99.2, + screenDpi: 99, + ), + ) + .toList(), online: true, charging: false, lowMemory: true, @@ -53,13 +70,17 @@ void main() { expect('modelId1', copy.modelId); expect('arch1', copy.arch); expect(2, copy.batteryLevel); - expect(SentryOrientation.portrait, copy.orientation); expect('manufacturer1', copy.manufacturer); expect('brand1', copy.brand); - expect(900, copy.screenHeightPixels); - expect(700, copy.screenWidthPixels); - expect(99.2, copy.screenDensity); - expect(99, copy.screenDpi); + + expect(1, copy.views.length); + expect(0, copy.views.first.viewId); + expect(SentryOrientation.portrait, copy.views.first.orientation); + expect(900, copy.views.first.screenHeightPixels); + expect(700, copy.views.first.screenWidthPixels); + expect(99.2, copy.views.first.screenDensity); + expect(99, copy.views.first.screenDpi); + expect(true, copy.online); expect(false, copy.charging); expect(true, copy.lowMemory); @@ -82,13 +103,16 @@ SentryDevice _generate({DateTime? testBootTime}) => SentryDevice( modelId: 'modelId', arch: 'arch', batteryLevel: 1, - orientation: SentryOrientation.landscape, manufacturer: 'manufacturer', brand: 'brand', - screenHeightPixels: 600, - screenWidthPixels: 800, - screenDensity: 99.1, - screenDpi: 100, + views: [ + SentryView(0, + orientation: SentryOrientation.landscape, + screenHeightPixels: 600, + screenWidthPixels: 800, + screenDensity: 99.1, + screenDpi: 100) + ], online: false, charging: true, lowMemory: false, diff --git a/dart/test/protocol/sentry_device_test.dart b/dart/test/protocol/sentry_device_test.dart index 7f405e09bf..a4d6722658 100644 --- a/dart/test/protocol/sentry_device_test.dart +++ b/dart/test/protocol/sentry_device_test.dart @@ -5,91 +5,20 @@ import 'package:test/test.dart'; import '../mocks.dart'; void main() { - final testBootTime = DateTime.fromMicrosecondsSinceEpoch(0); - - final sentryDevice = SentryDevice( - name: 'testDevice', - family: 'testFamily', - model: 'testModel', - modelId: 'testModelId', - arch: 'testArch', - batteryLevel: 23.0, - orientation: SentryOrientation.landscape, - manufacturer: 'testOEM', - brand: 'testBrand', - screenDensity: 99.1, - screenDpi: 100, - online: false, - charging: true, - lowMemory: false, - simulator: true, - memorySize: 1234567, - freeMemory: 12345, - usableMemory: 9876, - storageSize: 1234567, - freeStorage: 1234567, - externalStorageSize: 98765, - externalFreeStorage: 98765, - bootTime: testBootTime, - batteryStatus: 'Unknown', - cpuDescription: 'M1 Pro Max Ultra', - deviceType: 'Flutter Device', - deviceUniqueIdentifier: 'uuid', - processorCount: 4, - processorFrequency: 1.2, - supportsAccelerometer: true, - supportsGyroscope: true, - supportsAudio: true, - supportsLocationService: true, - supportsVibration: true, - screenHeightPixels: 100, - screenWidthPixels: 100, - unknown: testUnknown, - ); - - final sentryDeviceJson = { - 'name': 'testDevice', - 'family': 'testFamily', - 'model': 'testModel', - 'model_id': 'testModelId', - 'arch': 'testArch', - 'battery_level': 23.0, - 'orientation': 'landscape', - 'manufacturer': 'testOEM', - 'brand': 'testBrand', - 'screen_density': 99.1, - 'screen_dpi': 100, - 'online': false, - 'charging': true, - 'low_memory': false, - 'simulator': true, - 'memory_size': 1234567, - 'free_memory': 12345, - 'usable_memory': 9876, - 'storage_size': 1234567, - 'free_storage': 1234567, - 'external_storage_size': 98765, - 'external_free_storage': 98765, - 'boot_time': testBootTime.toIso8601String(), - 'battery_status': 'Unknown', - 'cpu_description': 'M1 Pro Max Ultra', - 'device_type': 'Flutter Device', - 'device_unique_identifier': 'uuid', - 'processor_count': 4, - 'processor_frequency': 1.2, - 'supports_accelerometer': true, - 'supports_gyroscope': true, - 'supports_audio': true, - 'supports_location_service': true, - 'supports_vibration': true, - 'screen_height_pixels': 100, - 'screen_width_pixels': 100, - }; - sentryDeviceJson.addAll(testUnknown); - group('json', () { + final fixture = Fixture(); + test('toJson', () { - final json = sentryDevice.toJson(); + final json = fixture.getSentryDeviceObject().toJson(); + final sentryDeviceJson = fixture.getSentryDeviceJson(); + + expect( + MapEquality().equals(sentryDeviceJson['views'][0], json['views'][0]), + true, + ); + + sentryDeviceJson.remove('views'); + json.remove('views'); expect( MapEquality().equals(sentryDeviceJson, json), @@ -98,9 +27,19 @@ void main() { }); test('fromJson', () { + final sentryDeviceJson = fixture.getSentryDeviceJson(); + final sentryDevice = SentryDevice.fromJson(sentryDeviceJson); final json = sentryDevice.toJson(); + expect( + MapEquality().equals(sentryDeviceJson['views'][0], json['views'][0]), + true, + ); + + sentryDeviceJson.remove('views'); + json.remove('views'); + expect( MapEquality().equals(sentryDeviceJson, json), true, @@ -108,12 +47,35 @@ void main() { }); test('fromJson double screen_height_pixels and screen_width_pixels', () { - sentryDeviceJson['screen_height_pixels'] = 100.0; - sentryDeviceJson['screen_width_pixels'] = 100.0; + final sentryDeviceJson = fixture.getSentryDeviceJson(); + sentryDeviceJson['views'][0]['screen_height_pixels'] = 100.0; + sentryDeviceJson['views'][0]['screen_width_pixels'] = 100.0; final sentryDevice = SentryDevice.fromJson(sentryDeviceJson); final json = sentryDevice.toJson(); + final data = sentryDevice; + + final copy = data.copyWith(); + + final dataJson = data.toJson(); + final copyJson = copy.toJson(); + + expect( + MapEquality().equals(dataJson['views'][0], copyJson['views'][0]), + true, + ); + + dataJson.remove('views'); + copyJson.remove('views'); + sentryDeviceJson.remove('views'); + json.remove('views'); + + expect( + MapEquality().equals(dataJson, copyJson), + true, + ); + expect( MapEquality().equals(sentryDeviceJson, json), true, @@ -155,19 +117,31 @@ void main() { }); group('copyWith', () { + final fixture = Fixture(); test('copyWith keeps unchanged', () { - final data = sentryDevice; + final data = fixture.getSentryDeviceObject(); final copy = data.copyWith(); + final dataJson = data.toJson(); + final copyJson = copy.toJson(); + expect( - MapEquality().equals(data.toJson(), copy.toJson()), + MapEquality().equals(dataJson['views'][0], copyJson['views'][0]), + true, + ); + + dataJson.remove('views'); + copyJson.remove('views'); + + expect( + MapEquality().equals(dataJson, copyJson), true, ); }); test('copyWith takes new values', () { - final data = sentryDevice; + final data = fixture.getSentryDeviceObject(); final bootTime = DateTime.now(); @@ -178,11 +152,17 @@ void main() { modelId: 'modelId1', arch: 'arch1', batteryLevel: 2, - orientation: SentryOrientation.portrait, manufacturer: 'manufacturer1', brand: 'brand1', - screenDensity: 99.2, - screenDpi: 99, + views: [ + data.views.first.copyWith( + orientation: SentryOrientation.portrait, + screenDensity: 99.2, + screenDpi: 99, + screenHeightPixels: 2, + screenWidthPixels: 2, + ) + ], online: true, charging: false, lowMemory: true, @@ -206,8 +186,6 @@ void main() { supportsAudio: false, supportsLocationService: false, supportsVibration: false, - screenHeightPixels: 2, - screenWidthPixels: 2, ); expect('name1', copy.name); @@ -216,11 +194,13 @@ void main() { expect('modelId1', copy.modelId); expect('arch1', copy.arch); expect(2, copy.batteryLevel); - expect(SentryOrientation.portrait, copy.orientation); expect('manufacturer1', copy.manufacturer); expect('brand1', copy.brand); - expect(99.2, copy.screenDensity); - expect(99, copy.screenDpi); + expect(SentryOrientation.portrait, copy.views.first.orientation); + expect(99.2, copy.views.first.screenDensity); + expect(99, copy.views.first.screenDpi); + expect(2, copy.views.first.screenHeightPixels); + expect(2, copy.views.first.screenWidthPixels); expect(true, copy.online); expect(false, copy.charging); expect(true, copy.lowMemory); @@ -244,8 +224,104 @@ void main() { expect(false, copy.supportsAudio); expect(false, copy.supportsLocationService); expect(false, copy.supportsVibration); - expect(2, copy.screenHeightPixels); - expect(2, copy.screenWidthPixels); }); }); } + +class Fixture { + late final testBootTime = DateTime.fromMicrosecondsSinceEpoch(0); + + Map getSentryDeviceJson() { + var json = { + 'name': 'testDevice', + 'family': 'testFamily', + 'model': 'testModel', + 'model_id': 'testModelId', + 'arch': 'testArch', + 'battery_level': 23.0, + 'manufacturer': 'testOEM', + 'brand': 'testBrand', + 'views': [ + { + 'view_id': 0, + 'orientation': 'landscape', + 'screen_density': 99.1, + 'screen_dpi': 100, + 'screen_height_pixels': 100, + 'screen_width_pixels': 100, + }, + ], + 'online': false, + 'charging': true, + 'low_memory': false, + 'simulator': true, + 'memory_size': 1234567, + 'free_memory': 12345, + 'usable_memory': 9876, + 'storage_size': 1234567, + 'free_storage': 1234567, + 'external_storage_size': 98765, + 'external_free_storage': 98765, + 'boot_time': testBootTime.toIso8601String(), + 'battery_status': 'Unknown', + 'cpu_description': 'M1 Pro Max Ultra', + 'device_type': 'Flutter Device', + 'device_unique_identifier': 'uuid', + 'processor_count': 4, + 'processor_frequency': 1.2, + 'supports_accelerometer': true, + 'supports_gyroscope': true, + 'supports_audio': true, + 'supports_location_service': true, + 'supports_vibration': true, + }; + + json.addAll(testUnknown); + return json; + } + + SentryDevice getSentryDeviceObject() => SentryDevice( + name: 'testDevice', + family: 'testFamily', + model: 'testModel', + modelId: 'testModelId', + arch: 'testArch', + batteryLevel: 23.0, + manufacturer: 'testOEM', + brand: 'testBrand', + views: [ + SentryView( + 0, + orientation: SentryOrientation.landscape, + screenDensity: 99.1, + screenDpi: 100, + screenHeightPixels: 100, + screenWidthPixels: 100, + ) + ], + online: false, + charging: true, + lowMemory: false, + simulator: true, + memorySize: 1234567, + freeMemory: 12345, + usableMemory: 9876, + storageSize: 1234567, + freeStorage: 1234567, + externalStorageSize: 98765, + externalFreeStorage: 98765, + bootTime: testBootTime, + batteryStatus: 'Unknown', + cpuDescription: 'M1 Pro Max Ultra', + deviceType: 'Flutter Device', + deviceUniqueIdentifier: 'uuid', + processorCount: 4, + processorFrequency: 1.2, + supportsAccelerometer: true, + supportsGyroscope: true, + supportsAudio: true, + supportsLocationService: true, + supportsVibration: true, + unknown: testUnknown, + ); +} diff --git a/dart/test/sentry_envelope_item_test.dart b/dart/test/sentry_envelope_item_test.dart index c5a205c945..10b395eaf4 100644 --- a/dart/test/sentry_envelope_item_test.dart +++ b/dart/test/sentry_envelope_item_test.dart @@ -66,9 +66,12 @@ void main() { ); final tracer = SentryTracer(context, MockHub()); final tr = SentryTransaction(tracer); - tr.contexts.device = SentryDevice( - orientation: SentryOrientation.landscape, - ); + tr.contexts.device = SentryDevice(views: [ + SentryView( + 0, + orientation: SentryOrientation.landscape, + ) + ]); final sut = SentryEnvelopeItem.fromTransaction(tr); diff --git a/dio/test/mocks.dart b/dio/test/mocks.dart index 30e53c5772..129c0ce7f5 100644 --- a/dio/test/mocks.dart +++ b/dio/test/mocks.dart @@ -74,11 +74,16 @@ final fakeEvent = SentryEvent( modelId: 'LRX22G', arch: 'armeabi-v7a', batteryLevel: 99, - orientation: SentryOrientation.landscape, manufacturer: 'samsung', brand: 'samsung', - screenDensity: 2.1, - screenDpi: 320, + views: [ + SentryView( + 0, + orientation: SentryOrientation.landscape, + screenDensity: 2.1, + screenDpi: 320, + ), + ], online: true, charging: true, lowMemory: true, diff --git a/flutter/.gitignore b/flutter/.gitignore index 068bf84155..34d67f8cab 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -11,3 +11,7 @@ build/ .vscode/launch.json cocoa_bindings_temp + +# ignore FVM - Flutter Version Management(https://fvm.app/) +.fvm +.fvmrc diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index dd870eb587..8a7f8e3119 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -28,6 +28,7 @@ import 'auto_close_screen.dart'; import 'drift/connection/connection.dart'; import 'drift/database.dart'; import 'isar/user.dart'; +import 'multi_view_app.dart'; import 'user_feedback_dialog.dart'; // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io @@ -40,13 +41,11 @@ const String exampleUrl = 'https://jsonplaceholder.typicode.com/todos/'; const _channel = MethodChannel('example.flutter.sentry.io'); var _isIntegrationTest = false; -final GlobalKey navigatorKey = GlobalKey(); - Future main() async { await setupSentry( - () => runApp( - SentryWidget( - child: DefaultAssetBundle( + () => runWidget( + MultiViewApp( + viewBuilder: (BuildContext context) => DefaultAssetBundle( bundle: SentryAssetBundle(), child: const MyApp(), ), @@ -88,7 +87,7 @@ Future setupSentry( options.maxRequestBodySize = MaxRequestBodySize.always; options.maxResponseBodySize = MaxResponseBodySize.always; - options.navigatorKey = navigatorKey; + // options.navigatorKey = navigatorKeys; options.experimental.replay.sessionSampleRate = 1.0; options.experimental.replay.onErrorSampleRate = 1.0; @@ -120,7 +119,6 @@ class _MyAppState extends State { create: (_) => ThemeProvider(), child: Builder( builder: (context) => MaterialApp( - navigatorKey: navigatorKey, navigatorObservers: [ SentryNavigatorObserver(), ], @@ -174,7 +172,8 @@ class MainScaffold extends StatelessWidget { } return Scaffold( appBar: AppBar( - title: const Text('Sentry Flutter Example'), + title: + Text('Sentry Flutter Example (ViewId:${View.of(context).viewId})'), actions: [ IconButton( onPressed: () { diff --git a/flutter/example/lib/multi_view_app.dart b/flutter/example/lib/multi_view_app.dart new file mode 100644 index 0000000000..a1271c492d --- /dev/null +++ b/flutter/example/lib/multi_view_app.dart @@ -0,0 +1,76 @@ +// multi_view_app.dart + +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' show FlutterView; +import 'package:flutter/widgets.dart'; + +/// Calls [viewBuilder] for every view added to the app to obtain the widget to +/// render into that view. The current view can be looked up with [View.of]. +class MultiViewApp extends StatefulWidget { + const MultiViewApp({super.key, required this.viewBuilder}); + + final WidgetBuilder viewBuilder; + + @override + State createState() => _MultiViewAppState(); +} + +class _MultiViewAppState extends State + with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _updateViews(); + } + + @override + void didUpdateWidget(MultiViewApp oldWidget) { + super.didUpdateWidget(oldWidget); + // Need to re-evaluate the viewBuilder callback for all views. + _views.clear(); + _updateViews(); + } + + @override + void didChangeMetrics() { + _updateViews(); + } + + Map _views = {}; + + void _updateViews() { + final Map newViews = {}; + for (final FlutterView view + in WidgetsBinding.instance.platformDispatcher.views) { + final Widget viewWidget = _views[view.viewId] ?? _createViewWidget(view); + newViews[view.viewId] = viewWidget; + } + setState(() { + _views = newViews; + }); + } + + Widget _createViewWidget(FlutterView view) { + return View( + view: view, + child: Builder( + builder: widget.viewBuilder, + ), + ); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ViewCollection(views: _views.values.toList(growable: false)); + } +} diff --git a/flutter/example/pubspec.yaml b/flutter/example/pubspec.yaml index 398b34568b..ad17709928 100644 --- a/flutter/example/pubspec.yaml +++ b/flutter/example/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: sentry_drift: sentry_isar: universal_platform: ^1.0.0 - feedback: ^2.0.0 + feedback: ^3.1.0 provider: ^6.0.0 dio: any # This gets constrained by `sentry_dio` sqflite: any # This gets constrained by `sentry_sqflite` diff --git a/flutter/example/web/flutter_bootstrap.js b/flutter/example/web/flutter_bootstrap.js new file mode 100644 index 0000000000..88e5dea495 --- /dev/null +++ b/flutter/example/web/flutter_bootstrap.js @@ -0,0 +1,15 @@ +// flutter_bootstrap.js +{{flutter_js}} +{{flutter_build_config}} + +_flutter.loader.load({ + onEntrypointLoaded: async function onEntrypointLoaded(engineInitializer) { + let engine = await engineInitializer.initializeEngine({ + multiViewEnabled: true, // Enables embedded mode. + }); + let app = await engine.runApp(); + // Make this `app` object available to your JS app. + app.addView({hostElement: document.querySelector("#left")}); + app.addView({hostElement: document.querySelector("#right")}); + } +}); diff --git a/flutter/example/web/index.html b/flutter/example/web/index.html index 406b34e2c3..1c40323e6f 100644 --- a/flutter/example/web/index.html +++ b/flutter/example/web/index.html @@ -35,17 +35,11 @@ - - - +
+
+ +
+ diff --git a/flutter/lib/src/event_processor/flutter_enricher_event_processor.dart b/flutter/lib/src/event_processor/flutter_enricher_event_processor.dart index 0ea1b731d6..8766a5f298 100644 --- a/flutter/lib/src/event_processor/flutter_enricher_event_processor.dart +++ b/flutter/lib/src/event_processor/flutter_enricher_event_processor.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -26,8 +25,6 @@ class FlutterEnricherEventProcessor implements EventProcessor { // Thus we call it on demand after all the initialization happened. WidgetsBinding? get _widgetsBinding => _options.bindingUtils.instance; - // ignore: deprecated_member_use - SingletonFlutterWindow? get _window => _widgetsBinding?.window; Map _packages = {}; @override @@ -107,7 +104,8 @@ class FlutterEnricherEventProcessor implements EventProcessor { } SentryCulture _getCulture(SentryCulture? culture) { - final windowLanguageTag = _window?.locale.toLanguageTag(); + final windowLanguageTag = + _widgetsBinding?.platformDispatcher.locale.toLanguageTag(); final screenLocale = _retrieveWidgetLocale(_options.navigatorKey); final languageTag = screenLocale?.toLanguageTag() ?? windowLanguageTag; @@ -115,7 +113,8 @@ class FlutterEnricherEventProcessor implements EventProcessor { // _window?.locales return (culture ?? SentryCulture()).copyWith( - is24HourFormat: culture?.is24HourFormat ?? _window?.alwaysUse24HourFormat, + is24HourFormat: culture?.is24HourFormat ?? + _widgetsBinding?.platformDispatcher.alwaysUse24HourFormat, locale: culture?.locale ?? languageTag, timezone: culture?.timezone ?? DateTime.now().timeZoneName, ); @@ -125,8 +124,10 @@ class FlutterEnricherEventProcessor implements EventProcessor { final currentLifecycle = _widgetsBinding?.lifecycleState; final debugPlatformOverride = debugDefaultTargetPlatformOverride; final tempDebugBrightnessOverride = debugBrightnessOverride; - final initialLifecycleState = _window?.initialLifecycleState; - final defaultRouteName = _window?.defaultRouteName; + final initialLifecycleState = + _widgetsBinding?.platformDispatcher.initialLifecycleState; + final defaultRouteName = + _widgetsBinding?.platformDispatcher.defaultRouteName; // A FlutterEngine has no renderViewElement if it was started or is // accessed from an isolate different to the main isolate. @@ -158,44 +159,76 @@ class FlutterEnricherEventProcessor implements EventProcessor { } Map _getAccessibilityContext() { - final window = _window; - if (window == null) { + final platformDispatcher = _widgetsBinding?.platformDispatcher; + if (platformDispatcher == null) { return {}; } return { 'accessible_navigation': - window.accessibilityFeatures.accessibleNavigation, - 'bold_text': window.accessibilityFeatures.boldText, - 'disable_animations': window.accessibilityFeatures.disableAnimations, - 'high_contrast': window.accessibilityFeatures.highContrast, - 'invert_colors': window.accessibilityFeatures.invertColors, - 'reduce_motion': window.accessibilityFeatures.reduceMotion, + platformDispatcher.accessibilityFeatures.accessibleNavigation, + 'bold_text': platformDispatcher.accessibilityFeatures.boldText, + 'disable_animations': + platformDispatcher.accessibilityFeatures.disableAnimations, + 'high_contrast': platformDispatcher.accessibilityFeatures.highContrast, + 'invert_colors': platformDispatcher.accessibilityFeatures.invertColors, + 'reduce_motion': platformDispatcher.accessibilityFeatures.reduceMotion, }; } SentryDevice? _getDevice(SentryDevice? device) { - final window = _window; - if (window == null) { + final platformDispatcher = _widgetsBinding?.platformDispatcher; + if (platformDispatcher == null) { return device; } - final orientation = window.physicalSize.width > window.physicalSize.height - ? SentryOrientation.landscape - : SentryOrientation.portrait; - return (device ?? SentryDevice()).copyWith( - orientation: device?.orientation ?? orientation, - screenHeightPixels: - device?.screenHeightPixels ?? window.physicalSize.height.toInt(), - screenWidthPixels: - device?.screenWidthPixels ?? window.physicalSize.width.toInt(), - screenDensity: device?.screenDensity ?? window.devicePixelRatio, + final currentDevice = device ?? SentryDevice(); + final List updatedViews = []; + + for (var platformView in platformDispatcher.views) { + final currentDeviceView = device?.views + .where((view) => view.viewId == platformView.viewId) + .first; + + SentryView updatedView; + if (currentDeviceView != null) { + updatedView = currentDeviceView.copyWith( + screenWidthPixels: currentDeviceView.screenWidthPixels ?? + platformView.physicalSize.width.toInt(), + screenHeightPixels: currentDeviceView.screenHeightPixels ?? + platformView.physicalSize.height.toInt(), + screenDensity: currentDeviceView.screenDensity ?? + platformView.devicePixelRatio.toDouble(), + orientation: _getSentryOrientation(platformView.physicalSize.width, + platformView.physicalSize.height), + ); + } else { + updatedView = SentryView( + platformView.viewId, + screenWidthPixels: platformView.physicalSize.width.toInt(), + screenHeightPixels: platformView.physicalSize.height.toInt(), + screenDensity: platformView.devicePixelRatio.toDouble(), + orientation: _getSentryOrientation(platformView.physicalSize.width, + platformView.physicalSize.height), + ); + } + updatedViews.add(updatedView); + } + + return currentDevice.copyWith( + views: updatedViews, ); } + SentryOrientation _getSentryOrientation(double width, double height) { + return width > height + ? SentryOrientation.landscape + : SentryOrientation.portrait; + } + SentryOperatingSystem _getOperatingSystem(SentryOperatingSystem? os) { return (os ?? SentryOperatingSystem()).copyWith( - // ignore: deprecated_member_use - theme: os?.theme ?? describeEnum(window.platformBrightness), + theme: os?.theme ?? + _widgetsBinding?.platformDispatcher.platformBrightness.name, ); } diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 8981afe7b1..780e78c52a 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -3,9 +3,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'dart:ui'; -import 'package:sentry/sentry.dart'; -import '../screenshot/sentry_screenshot_widget.dart'; -import '../sentry_flutter_options.dart'; +import '../../sentry_flutter.dart'; import 'package:flutter/rendering.dart'; import '../renderer/renderer.dart'; import 'package:flutter/widgets.dart' as widget; @@ -88,8 +86,9 @@ class ScreenshotEventProcessor implements EventProcessor { final renderObject = sentryScreenshotWidgetGlobalKey.currentContext?.findRenderObject(); if (renderObject is RenderRepaintBoundary) { - // ignore: deprecated_member_use - final pixelRatio = window.devicePixelRatio; + final pixelRatio = + widget.View.of(sentryScreenshotWidgetGlobalKey.currentContext!) + .devicePixelRatio; var imageResult = _getImage(renderObject, pixelRatio); Image image; if (imageResult is Future) { diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 18b9d8c919..c24c2bc7c3 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -344,6 +344,7 @@ class SentryFlutterOptions extends SentryOptions { } /// The [navigatorKey] is used to add information of the currently used locale to the contexts. + /// Keep the same order as with flutter views GlobalKey? navigatorKey; @meta.internal diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index 40709a5465..821d34c3cf 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -21,7 +21,9 @@ class _SentryWidgetState extends State { @override Widget build(BuildContext context) { Widget content = widget.child; - content = SentryScreenshotWidget(child: content); + content = SentryScreenshotWidget( + child: content, + ); content = SentryUserInteractionWidget(child: content); return Container( key: sentryWidgetGlobalKey, diff --git a/flutter/lib/src/widgets_binding_observer.dart b/flutter/lib/src/widgets_binding_observer.dart index 7c0db1842f..72188fd75d 100644 --- a/flutter/lib/src/widgets_binding_observer.dart +++ b/flutter/lib/src/widgets_binding_observer.dart @@ -29,27 +29,42 @@ class SentryWidgetsBindingObserver with WidgetsBindingObserver { if (_options.enableWindowMetricBreadcrumbs) { _screenSizeStreamController.stream .map( - (window) => { - 'new_pixel_ratio': window?.devicePixelRatio, - 'new_height': window?.physicalSize.height, - 'new_width': window?.physicalSize.width, - }, + (views) => views + .map( + (view) => { + 'view_id': view?.viewId, + 'new_pixel_ratio': view?.devicePixelRatio, + 'new_height': view?.physicalSize.height, + 'new_width': view?.physicalSize.width, + }, + ) + .toList(), ) - .distinct(mapEquals) + .distinct((prev, next) { + if (prev.length != next.length) return false; + + for (int i = 0; i < prev.length; i++) { + // Check each map for equality + if (!mapEquals(prev[i], next[i])) { + return false; + } + } + return true; + }) .skip(1) // Skip initial event added below in constructor .listen(_onScreenSizeChanged); - // ignore: deprecated_member_use - final window = _options.bindingUtils.instance?.window; - _screenSizeStreamController.add(window); + final views = + _options.bindingUtils.instance?.platformDispatcher.views.toList() ?? + []; + _screenSizeStreamController.add(views); } } final Hub _hub; final SentryFlutterOptions _options; - // ignore: deprecated_member_use - final StreamController _screenSizeStreamController; + final StreamController> _screenSizeStreamController; final _didChangeMetricsDebouncer = Debouncer(milliseconds: 100); @@ -93,21 +108,23 @@ class SentryWidgetsBindingObserver with WidgetsBindingObserver { } _didChangeMetricsDebouncer.run(() { - // ignore: deprecated_member_use - final window = _options.bindingUtils.instance?.window; - _screenSizeStreamController.add(window); + final views = + _options.bindingUtils.instance!.platformDispatcher.views.toList(); + _screenSizeStreamController.add(views); }); } - void _onScreenSizeChanged(Map data) { - _hub.addBreadcrumb(Breadcrumb( - message: 'Screen size changed', - category: 'device.screen', - type: 'navigation', - data: data, - // ignore: invalid_use_of_internal_member - timestamp: _options.clock(), - )); + void _onScreenSizeChanged(List> views) { + for (var view in views) { + _hub.addBreadcrumb(Breadcrumb( + message: 'Screen size changed', + category: 'device.screen', + type: 'navigation', + data: view, + // ignore: invalid_use_of_internal_member + timestamp: _options.clock(), + )); + } } /// See also: @@ -117,9 +134,9 @@ class SentryWidgetsBindingObserver with WidgetsBindingObserver { if (!_options.enableBrightnessChangeBreadcrumbs) { return; } + final brightness = - // ignore: deprecated_member_use - _options.bindingUtils.instance?.window.platformBrightness; + _options.bindingUtils.instance?.platformDispatcher.platformBrightness; final brightnessDescription = brightness == Brightness.dark ? 'dark' : 'light'; @@ -142,9 +159,9 @@ class SentryWidgetsBindingObserver with WidgetsBindingObserver { if (!_options.enableTextScaleChangeBreadcrumbs) { return; } + final newTextScaleFactor = - // ignore: deprecated_member_use - _options.bindingUtils.instance?.window.textScaleFactor; + _options.bindingUtils.instance?.platformDispatcher.textScaleFactor; _hub.addBreadcrumb(Breadcrumb( message: 'Text scale factor changed to $newTextScaleFactor.', diff --git a/flutter/test/event_processor/flutter_enricher_event_processor_test.dart b/flutter/test/event_processor/flutter_enricher_event_processor_test.dart index 892e7ff517..f4b20304fd 100644 --- a/flutter/test/event_processor/flutter_enricher_event_processor_test.dart +++ b/flutter/test/event_processor/flutter_enricher_event_processor_test.dart @@ -325,10 +325,15 @@ void main() { final fakeEvent = SentryEvent( contexts: Contexts( device: SentryDevice( - orientation: SentryOrientation.landscape, - screenHeightPixels: 1080, - screenWidthPixels: 1920, - screenDensity: 2, + views: [ + SentryView( + 0, + orientation: SentryOrientation.landscape, + screenHeightPixels: 1080, + screenWidthPixels: 1920, + screenDensity: 2, + ) + ], ), operatingSystem: SentryOperatingSystem( theme: 'dark', @@ -345,20 +350,20 @@ void main() { // contexts.device expect( - event?.contexts.device?.orientation, - fakeEvent.contexts.device?.orientation, + event?.contexts.device?.views.first.orientation, + fakeEvent.contexts.device?.views.first.orientation, ); expect( - event?.contexts.device?.screenHeightPixels, - fakeEvent.contexts.device?.screenHeightPixels, + event?.contexts.device?.views.first.screenHeightPixels, + fakeEvent.contexts.device?.views.first.screenHeightPixels, ); expect( - event?.contexts.device?.screenWidthPixels, - fakeEvent.contexts.device?.screenWidthPixels, + event?.contexts.device?.views.first.screenWidthPixels, + fakeEvent.contexts.device?.views.first.screenWidthPixels, ); expect( - event?.contexts.device?.screenDensity, - fakeEvent.contexts.device?.screenDensity, + event?.contexts.device?.views.first.screenDensity, + fakeEvent.contexts.device?.views.first.screenDensity, ); expect( event?.contexts.operatingSystem?.theme, diff --git a/flutter/test/widgets_binding_observer_test.dart b/flutter/test/widgets_binding_observer_test.dart index 86973ba91c..49ee3b710b 100644 --- a/flutter/test/widgets_binding_observer_test.dart +++ b/flutter/test/widgets_binding_observer_test.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -204,6 +202,7 @@ void main() { expect(breadcrumb.type, 'navigation'); expect(breadcrumb.level, SentryLevel.info); expect(breadcrumb.data, { + 'view_id': 0, // ignore: deprecated_member_use 'new_pixel_ratio': window.devicePixelRatio, 'new_height': newHeight, @@ -244,6 +243,7 @@ void main() { expect(breadcrumb.type, 'navigation'); expect(breadcrumb.level, SentryLevel.info); expect(breadcrumb.data, { + 'view_id': 0, 'new_pixel_ratio': newPixelRatio, // ignore: deprecated_member_use 'new_height': window.physicalSize.height, @@ -265,16 +265,15 @@ void main() { final instance = tester.binding; instance.addObserver(observer); - // ignore: deprecated_member_use - final window = instance.window; + final view = tester.view; - // ignore: deprecated_member_use - window.viewInsetsTestValue = WindowPadding.zero; + view.padding = FakeViewPadding.zero; // waiting for debouncing with 100ms added https://github.com/getsentry/sentry-dart/issues/400 await tester.pump(Duration(milliseconds: 150)); verifyNever(hub.addBreadcrumb(captureAny)); + // verify(hub.addBreadcrumb(captureAny)).called(1); instance.removeObserver(observer); });