From b8bd099b88ce1ef5b4d6f08d4d5e4d07f535daff Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 26 Aug 2024 11:17:24 +0200 Subject: [PATCH 01/16] add fvm to gitignore for simpler switch between flutter versions --- flutter/.gitignore | 4 ++++ 1 file changed, 4 insertions(+) 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 From f4169191056c80efe7d54c42e2d88e2e7e9da36d Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 26 Aug 2024 11:19:34 +0200 Subject: [PATCH 02/16] removed deprecated window.devicePixelRatio from screenshot_event_processor --- .../lib/src/event_processor/screenshot_event_processor.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 8981afe7b1..3528f0c65c 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -88,8 +88,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) { From caf888c88ef337112f2765ad93916cbdba1c7b99 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 27 Aug 2024 11:07:53 +0200 Subject: [PATCH 03/16] migrate from SingleFlutterWindows to FlutterView --- dart/example_web/web/event.dart | 11 +- dart/example_web_legacy/web/event.dart | 11 +- .../html_enricher_event_processor.dart | 25 ++- .../web_enricher_event_processor.dart | 24 ++- dart/lib/src/protocol/sentry_device.dart | 154 ++++++++++++------ dart/test/contexts_test.dart | 20 ++- .../enricher/web_enricher_test.dart | 31 ++-- dart/test/mocks.dart | 11 +- dart/test/protocol/device_test.dart | 56 +++++-- dart/test/protocol/sentry_device_test.dart | 75 ++++++--- dart/test/sentry_envelope_item_test.dart | 9 +- flutter/example/lib/main.dart | 7 +- flutter/example/lib/main_test.dart | 143 ++++++++++++++++ flutter/example/lib/multi_view_app.dart | 76 +++++++++ flutter/example/web/flutter_bootstrap.js | 15 ++ flutter/example/web/index.html | 5 + flutter/example/web/index_backup.html | 51 ++++++ .../flutter_enricher_event_processor.dart | 80 +++++---- flutter/lib/src/widgets_binding_observer.dart | 70 +++++--- flutter/pubspec.yaml | 1 + ...flutter_enricher_event_processor_test.dart | 29 ++-- .../test/widgets_binding_observer_test.dart | 11 +- 22 files changed, 706 insertions(+), 209 deletions(-) create mode 100644 flutter/example/lib/main_test.dart create mode 100644 flutter/example/lib/multi_view_app.dart create mode 100644 flutter/example/web/flutter_bootstrap.js create mode 100644 flutter/example/web/index_backup.html 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..fa5e325999 100644 --- a/dart/test/protocol/sentry_device_test.dart +++ b/dart/test/protocol/sentry_device_test.dart @@ -14,11 +14,18 @@ void main() { modelId: 'testModelId', arch: 'testArch', batteryLevel: 23.0, - orientation: SentryOrientation.landscape, manufacturer: 'testOEM', brand: 'testBrand', - screenDensity: 99.1, - screenDpi: 100, + views: [ + SentryView( + 0, + orientation: SentryOrientation.landscape, + screenDensity: 99.1, + screenDpi: 100, + screenHeightPixels: 100, + screenWidthPixels: 100, + ) + ], online: false, charging: true, lowMemory: false, @@ -42,8 +49,6 @@ void main() { supportsAudio: true, supportsLocationService: true, supportsVibration: true, - screenHeightPixels: 100, - screenWidthPixels: 100, unknown: testUnknown, ); @@ -54,11 +59,18 @@ 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, + 'screen_height_pixels': 100, + 'screen_width_pixels': 100, + } + ], 'online': false, 'charging': true, 'low_memory': false, @@ -82,8 +94,6 @@ void main() { 'supports_audio': true, 'supports_location_service': true, 'supports_vibration': true, - 'screen_height_pixels': 100, - 'screen_width_pixels': 100, }; sentryDeviceJson.addAll(testUnknown); @@ -91,6 +101,14 @@ void main() { test('toJson', () { 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, @@ -160,8 +178,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, ); }); @@ -178,11 +207,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 +241,6 @@ void main() { supportsAudio: false, supportsLocationService: false, supportsVibration: false, - screenHeightPixels: 2, - screenWidthPixels: 2, ); expect('name1', copy.name); @@ -216,11 +249,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 +279,6 @@ void main() { expect(false, copy.supportsAudio); expect(false, copy.supportsLocationService); expect(false, copy.supportsVibration); - expect(2, copy.screenHeightPixels); - expect(2, copy.screenWidthPixels); }); }); } 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/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 82bcd0b3c8..2af2ae39d0 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 @@ -44,11 +45,13 @@ final GlobalKey navigatorKey = GlobalKey(); Future main() async { await setupSentry( - () => runApp( + () => runWidget( SentryWidget( child: DefaultAssetBundle( bundle: SentryAssetBundle(), - child: const MyApp(), + child: MultiViewApp( + viewBuilder: (BuildContext context) => const MyApp(), + ), ), ), ), diff --git a/flutter/example/lib/main_test.dart b/flutter/example/lib/main_test.dart new file mode 100644 index 0000000000..6b0ed7c420 --- /dev/null +++ b/flutter/example/lib/main_test.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_logging/sentry_logging.dart'; + +import 'multi_view_app.dart'; + +const String exampleDsn = + 'https://e85b375ffb9f43cf8bdf9787768149e0@o447951.ingest.sentry.io/5428562'; + +/// This is an exampleUrl that will be used to demonstrate how http requests are captured. +const String exampleUrl = 'https://jsonplaceholder.typicode.com/todos/'; + +var _isIntegrationTest = false; + +final GlobalKey navigatorKey = GlobalKey(); + +Future main() async { + await setupSentry( + () => runWidget( + SentryWidget( + child: DefaultAssetBundle( + bundle: SentryAssetBundle(), + child: MultiViewApp( + viewBuilder: (BuildContext context) => const MyApp(), + ), + ), + ), + ), + exampleDsn, + ); +} + +Future setupSentry( + AppRunner appRunner, + String dsn, { + bool isIntegrationTest = false, + BeforeSendCallback? beforeSendCallback, +}) async { + await SentryFlutter.init( + (options) { + options.dsn = exampleDsn; + options.tracesSampleRate = 1.0; + options.profilesSampleRate = 1.0; + options.reportPackages = false; + options.addInAppInclude('sentry_flutter_multiview_example'); + options.considerInAppFramesByDefault = false; + options.attachThreads = true; + options.enableWindowMetricBreadcrumbs = true; + options.addIntegration(LoggingIntegration(minEventLevel: Level.INFO)); + options.sendDefaultPii = true; + options.reportSilentFlutterErrors = true; + options.attachScreenshot = true; + options.screenshotQuality = SentryScreenshotQuality.low; + options.attachViewHierarchy = true; + // We can enable Sentry debug logging during development. This is likely + // going to log too much for your app, but can be useful when figuring out + // configuration issues, e.g. finding out why your events are not uploaded. + options.debug = true; + options.spotlight = Spotlight(enabled: true); + options.enableTimeToFullDisplayTracing = true; + options.enableMetrics = true; + + options.maxRequestBodySize = MaxRequestBodySize.always; + options.maxResponseBodySize = MaxResponseBodySize.always; + options.navigatorKey = navigatorKey; + + _isIntegrationTest = isIntegrationTest; + if (_isIntegrationTest) { + options.dist = '1'; + options.environment = 'integration'; + options.beforeSend = beforeSendCallback; + } + }, + // Init your App. + appRunner: appRunner, + ); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: MyHomePage( + title: 'Flutter Demo Home Page ${View.of(context).viewId}'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), // This trailing comma makes auto-formatting nicer for build methods. + ); + } +} 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/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..64b0902cb8 100644 --- a/flutter/example/web/index.html +++ b/flutter/example/web/index.html @@ -35,6 +35,10 @@ +
+
+ +
@@ -46,6 +50,7 @@ } + diff --git a/flutter/example/web/index_backup.html b/flutter/example/web/index_backup.html new file mode 100644 index 0000000000..406b34e2c3 --- /dev/null +++ b/flutter/example/web/index_backup.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + sentry_flutter_example + + + + + + + + + + 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..5fd53d589c 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,63 @@ 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) { + var currentViews = currentDevice.views + .where((view) => view.viewId == platformView.viewId) + .toList(); + + SentryView updatedView; + if (currentViews.isNotEmpty) { + updatedView = currentViews.single.copyWith( + screenWidthPixels: platformView.physicalSize.width.toInt(), + screenHeightPixels: platformView.physicalSize.height.toInt(), + screenDensity: platformView.devicePixelRatio.toDouble(), + ); + } else { + updatedView = SentryView( + platformView.viewId, + screenWidthPixels: platformView.physicalSize.width.toInt(), + screenHeightPixels: platformView.physicalSize.height.toInt(), + screenDensity: platformView.devicePixelRatio.toDouble(), + ); + } + updatedViews.add(updatedView); + } + + return currentDevice.copyWith( + views: updatedViews, ); } 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/widgets_binding_observer.dart b/flutter/lib/src/widgets_binding_observer.dart index 7c0db1842f..3221967a57 100644 --- a/flutter/lib/src/widgets_binding_observer.dart +++ b/flutter/lib/src/widgets_binding_observer.dart @@ -29,27 +29,41 @@ 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 +107,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 +133,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 +158,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/pubspec.yaml b/flutter/pubspec.yaml index d71721230d..c2e26ad4ef 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: meta: ^1.3.0 ffi: ^2.0.0 + web: any dev_dependencies: build_runner: ^2.4.2 flutter_test: 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); }); From 80bbe0e4346585905c0182684a31e7b837b84124 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 2 Sep 2024 15:35:46 +0200 Subject: [PATCH 04/16] update feedback package --- flutter/example/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/example/pubspec.yaml b/flutter/example/pubspec.yaml index dae5c41ece..3b52f475be 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` From eea494e19b918322edaa21fbed204a0797c8808d Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 2 Sep 2024 15:37:16 +0200 Subject: [PATCH 05/16] allow list of navigatorKeys in sentryOptions --- .../event_processor/flutter_enricher_event_processor.dart | 5 +++-- flutter/lib/src/sentry_flutter_options.dart | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) 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 5fd53d589c..a89d972411 100644 --- a/flutter/lib/src/event_processor/flutter_enricher_event_processor.dart +++ b/flutter/lib/src/event_processor/flutter_enricher_event_processor.dart @@ -103,10 +103,11 @@ class FlutterEnricherEventProcessor implements EventProcessor { return _packages; } - SentryCulture _getCulture(SentryCulture? culture) { + SentryCulture _getCulture(SentryCulture? culture, {int? viewId}) { final windowLanguageTag = _widgetsBinding?.platformDispatcher.locale.toLanguageTag(); - final screenLocale = _retrieveWidgetLocale(_options.navigatorKey); + final screenLocale = + _retrieveWidgetLocale(_options.navigatorKeys[viewId ?? 0]); final languageTag = screenLocale?.toLanguageTag() ?? windowLanguageTag; // Future enhancement: diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 347da9ada3..5e2790ec2e 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -326,7 +326,8 @@ class SentryFlutterOptions extends SentryOptions { } /// The [navigatorKey] is used to add information of the currently used locale to the contexts. - GlobalKey? navigatorKey; + /// Keep the same order as with flutter views + List> navigatorKeys = []; } /// Callback being executed in [ScreenshotEventProcessor], deciding if a From 98bc647bc685a998e75b11bf1e453b847f1f441d Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 2 Sep 2024 15:38:39 +0200 Subject: [PATCH 06/16] adapt example app to handle multiple navigatorKeys --- flutter/example/lib/main.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 14a1ea6710..61f05f52bf 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -41,7 +41,10 @@ const String exampleUrl = 'https://jsonplaceholder.typicode.com/todos/'; const _channel = MethodChannel('example.flutter.sentry.io'); var _isIntegrationTest = false; -final GlobalKey navigatorKey = GlobalKey(); +final GlobalKey navigatorKey1 = GlobalKey(); +final GlobalKey navigatorKey2 = GlobalKey(); + +final navigatorKeys = [navigatorKey1, navigatorKey2]; Future main() async { await setupSentry( @@ -91,7 +94,7 @@ Future setupSentry( options.maxRequestBodySize = MaxRequestBodySize.always; options.maxResponseBodySize = MaxResponseBodySize.always; - options.navigatorKey = navigatorKey; + options.navigatorKeys = navigatorKeys; _isIntegrationTest = isIntegrationTest; if (_isIntegrationTest) { @@ -120,7 +123,7 @@ class _MyAppState extends State { create: (_) => ThemeProvider(), child: Builder( builder: (context) => MaterialApp( - navigatorKey: navigatorKey, + navigatorKey: navigatorKeys[View.of(context).viewId - 1], navigatorObservers: [ SentryNavigatorObserver(), ], From 14da1a820af40fb241ccc6f60be6ac53b5884c63 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 3 Sep 2024 16:02:47 +0200 Subject: [PATCH 07/16] add multiple globalKeys for sentry widget and sentry screenshot widget --- flutter/example/lib/main.dart | 26 +++++++++++++++---- .../screenshot/sentry_screenshot_widget.dart | 13 +++++----- flutter/lib/src/sentry_widget.dart | 20 +++++++++----- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 546593c208..e8386071d0 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -46,14 +46,30 @@ final GlobalKey navigatorKey2 = GlobalKey(); final navigatorKeys = [navigatorKey1, navigatorKey2]; +final sentryWidgetKey1 = GlobalKey(debugLabel: 'sentry_widget_1'); +final sentryWidgetKey2 = GlobalKey(debugLabel: 'sentry_widget_2'); +final sentryWidgetKeys = [sentryWidgetKey1, sentryWidgetKey2]; + +final sentryScreenshotWidgetKey1 = + GlobalKey(debugLabel: 'sentry_screenshot_widget_1'); +final sentryScreenshotWidgetKey2 = + GlobalKey(debugLabel: 'sentry_screenshot_widget_2'); +final sentryScreenshotWidgetKeys = [ + sentryScreenshotWidgetKey1, + sentryScreenshotWidgetKey2 +]; + Future main() async { await setupSentry( () => runWidget( - SentryWidget( - child: DefaultAssetBundle( - bundle: SentryAssetBundle(), - child: MultiViewApp( - viewBuilder: (BuildContext context) => const MyApp(), + MultiViewApp( + viewBuilder: (BuildContext context) => SentryWidget( + sentryWidgetGlobalKey: sentryWidgetKeys[View.of(context).viewId - 1], + sentryScreenshotWidgetGlobalKey: + sentryScreenshotWidgetKeys[View.of(context).viewId - 1], + child: DefaultAssetBundle( + bundle: SentryAssetBundle(), + child: const MyApp(), ), ), ), diff --git a/flutter/lib/src/screenshot/sentry_screenshot_widget.dart b/flutter/lib/src/screenshot/sentry_screenshot_widget.dart index 6eafb935a5..7c3b93f16e 100644 --- a/flutter/lib/src/screenshot/sentry_screenshot_widget.dart +++ b/flutter/lib/src/screenshot/sentry_screenshot_widget.dart @@ -1,11 +1,6 @@ import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; -/// Key which is used to identify the [RepaintBoundary] -@internal -final sentryScreenshotWidgetGlobalKey = - GlobalKey(debugLabel: 'sentry_screenshot_widget'); - /// You can add screenshots of [child] to crash reports by adding this widget. /// Ideally you are adding it around your app widget like in the following /// example. @@ -23,8 +18,12 @@ final sentryScreenshotWidgetGlobalKey = /// times. class SentryScreenshotWidget extends StatefulWidget { final Widget child; + final GlobalKey> sentryScreenshotWidgetGlobalKey; - const SentryScreenshotWidget({super.key, required this.child}); + const SentryScreenshotWidget( + {super.key, + required this.child, + required this.sentryScreenshotWidgetGlobalKey}); @override _SentryScreenshotWidgetState createState() => _SentryScreenshotWidgetState(); @@ -34,7 +33,7 @@ class _SentryScreenshotWidgetState extends State { @override Widget build(BuildContext context) { return RepaintBoundary( - key: sentryScreenshotWidgetGlobalKey, + key: widget.sentryScreenshotWidgetGlobalKey, child: widget.child, ); } diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index 40709a5465..7a1e1520f2 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -2,16 +2,19 @@ import 'package:flutter/cupertino.dart'; import 'package:meta/meta.dart'; import '../sentry_flutter.dart'; -/// Key which is used to identify the [SentryWidget] -@internal -final sentryWidgetGlobalKey = GlobalKey(debugLabel: 'sentry_widget'); - /// This widget serves as a wrapper to include Sentry widgets such /// as [SentryScreenshotWidget] and [SentryUserInteractionWidget]. class SentryWidget extends StatefulWidget { final Widget child; + final GlobalKey> sentryWidgetGlobalKey; + final GlobalKey> sentryScreenshotWidgetGlobalKey; - const SentryWidget({super.key, required this.child}); + const SentryWidget({ + super.key, + required this.child, + required this.sentryWidgetGlobalKey, + required this.sentryScreenshotWidgetGlobalKey, + }); @override _SentryWidgetState createState() => _SentryWidgetState(); @@ -21,10 +24,13 @@ class _SentryWidgetState extends State { @override Widget build(BuildContext context) { Widget content = widget.child; - content = SentryScreenshotWidget(child: content); + content = SentryScreenshotWidget( + sentryScreenshotWidgetGlobalKey: widget.sentryScreenshotWidgetGlobalKey, + child: content, + ); content = SentryUserInteractionWidget(child: content); return Container( - key: sentryWidgetGlobalKey, + key: widget.sentryWidgetGlobalKey, child: content, ); } From 91b7161787faca5fc5207a2c1cda4a840e4a77a8 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 4 Sep 2024 15:02:29 +0200 Subject: [PATCH 08/16] revert back to sentryWidgetGlobalKey and remove sentryWidget and navigatorKey from example app --- flutter/example/lib/main.dart | 35 ++++--------------- .../flutter_enricher_event_processor.dart | 5 ++- .../screenshot_event_processor.dart | 13 +++---- flutter/lib/src/replay/recorder.dart | 2 +- flutter/lib/src/sentry_flutter_options.dart | 2 +- flutter/lib/src/sentry_widget.dart | 4 +++ 6 files changed, 19 insertions(+), 42 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index e8386071d0..42d85065d3 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -41,36 +41,13 @@ const String exampleUrl = 'https://jsonplaceholder.typicode.com/todos/'; const _channel = MethodChannel('example.flutter.sentry.io'); var _isIntegrationTest = false; -final GlobalKey navigatorKey1 = GlobalKey(); -final GlobalKey navigatorKey2 = GlobalKey(); - -final navigatorKeys = [navigatorKey1, navigatorKey2]; - -final sentryWidgetKey1 = GlobalKey(debugLabel: 'sentry_widget_1'); -final sentryWidgetKey2 = GlobalKey(debugLabel: 'sentry_widget_2'); -final sentryWidgetKeys = [sentryWidgetKey1, sentryWidgetKey2]; - -final sentryScreenshotWidgetKey1 = - GlobalKey(debugLabel: 'sentry_screenshot_widget_1'); -final sentryScreenshotWidgetKey2 = - GlobalKey(debugLabel: 'sentry_screenshot_widget_2'); -final sentryScreenshotWidgetKeys = [ - sentryScreenshotWidgetKey1, - sentryScreenshotWidgetKey2 -]; - Future main() async { await setupSentry( () => runWidget( MultiViewApp( - viewBuilder: (BuildContext context) => SentryWidget( - sentryWidgetGlobalKey: sentryWidgetKeys[View.of(context).viewId - 1], - sentryScreenshotWidgetGlobalKey: - sentryScreenshotWidgetKeys[View.of(context).viewId - 1], - child: DefaultAssetBundle( - bundle: SentryAssetBundle(), - child: const MyApp(), - ), + viewBuilder: (BuildContext context) => DefaultAssetBundle( + bundle: SentryAssetBundle(), + child: const MyApp(), ), ), ), @@ -110,7 +87,7 @@ Future setupSentry( options.maxRequestBodySize = MaxRequestBodySize.always; options.maxResponseBodySize = MaxResponseBodySize.always; - options.navigatorKeys = navigatorKeys; + // options.navigatorKey = navigatorKeys; options.experimental.replay.sessionSampleRate = 1.0; options.experimental.replay.errorSampleRate = 1.0; @@ -142,7 +119,6 @@ class _MyAppState extends State { create: (_) => ThemeProvider(), child: Builder( builder: (context) => MaterialApp( - navigatorKey: navigatorKeys[View.of(context).viewId - 1], navigatorObservers: [ SentryNavigatorObserver(), ], @@ -196,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/lib/src/event_processor/flutter_enricher_event_processor.dart b/flutter/lib/src/event_processor/flutter_enricher_event_processor.dart index a89d972411..5fd53d589c 100644 --- a/flutter/lib/src/event_processor/flutter_enricher_event_processor.dart +++ b/flutter/lib/src/event_processor/flutter_enricher_event_processor.dart @@ -103,11 +103,10 @@ class FlutterEnricherEventProcessor implements EventProcessor { return _packages; } - SentryCulture _getCulture(SentryCulture? culture, {int? viewId}) { + SentryCulture _getCulture(SentryCulture? culture) { final windowLanguageTag = _widgetsBinding?.platformDispatcher.locale.toLanguageTag(); - final screenLocale = - _retrieveWidgetLocale(_options.navigatorKeys[viewId ?? 0]); + final screenLocale = _retrieveWidgetLocale(_options.navigatorKey); final languageTag = screenLocale?.toLanguageTag() ?? windowLanguageTag; // Future enhancement: diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 3528f0c65c..81a2235b54 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; @@ -17,7 +15,7 @@ class ScreenshotEventProcessor implements EventProcessor { /// This is true when the SentryWidget is in the view hierarchy bool get _hasSentryScreenshotWidget => - sentryScreenshotWidgetGlobalKey.currentContext != null; + sentryWidgetGlobalKey.currentContext != null; @override Future apply(SentryEvent event, Hint hint) async { @@ -86,11 +84,10 @@ class ScreenshotEventProcessor implements EventProcessor { Future _createScreenshot() async { try { final renderObject = - sentryScreenshotWidgetGlobalKey.currentContext?.findRenderObject(); + sentryWidgetGlobalKey.currentContext?.findRenderObject(); if (renderObject is RenderRepaintBoundary) { - final pixelRatio = - widget.View.of(sentryScreenshotWidgetGlobalKey.currentContext!) - .devicePixelRatio; + final pixelRatio = widget.View.of(sentryWidgetGlobalKey.currentContext!) + .devicePixelRatio; var imageResult = _getImage(renderObject, pixelRatio); Image image; if (imageResult is Future) { diff --git a/flutter/lib/src/replay/recorder.dart b/flutter/lib/src/replay/recorder.dart index a1f4ea1a0b..3e5a7c327f 100644 --- a/flutter/lib/src/replay/recorder.dart +++ b/flutter/lib/src/replay/recorder.dart @@ -31,7 +31,7 @@ class ScreenshotRecorder { } Future capture(ScreenshotRecorderCallback callback) async { - final context = sentryScreenshotWidgetGlobalKey.currentContext; + final context = sentryWidgetGlobalKey.currentContext; final renderObject = context?.findRenderObject() as RenderRepaintBoundary?; if (context == null || renderObject == null) { if (!warningLogged) { diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 23bd76ffdb..e8ca692b81 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -345,7 +345,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 - List> navigatorKeys = []; + GlobalKey? navigatorKey; @meta.internal FileSystem fileSystem = LocalFileSystem(); diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index 7a1e1520f2..97ab41c40e 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -2,6 +2,10 @@ import 'package:flutter/cupertino.dart'; import 'package:meta/meta.dart'; import '../sentry_flutter.dart'; +/// Key which is used to identify the [SentryWidget] +@internal +final sentryWidgetGlobalKey = GlobalKey(debugLabel: 'sentry_widget'); + /// This widget serves as a wrapper to include Sentry widgets such /// as [SentryScreenshotWidget] and [SentryUserInteractionWidget]. class SentryWidget extends StatefulWidget { From 7f9b83f366e7edb4a7a49d345c20209e4f2f3cf3 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 4 Sep 2024 15:08:22 +0200 Subject: [PATCH 09/16] fix some errors with non existing variables. --- .../src/screenshot/sentry_screenshot_widget.dart | 13 +++++++------ flutter/lib/src/sentry_widget.dart | 12 ++---------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/flutter/lib/src/screenshot/sentry_screenshot_widget.dart b/flutter/lib/src/screenshot/sentry_screenshot_widget.dart index 7c3b93f16e..6eafb935a5 100644 --- a/flutter/lib/src/screenshot/sentry_screenshot_widget.dart +++ b/flutter/lib/src/screenshot/sentry_screenshot_widget.dart @@ -1,6 +1,11 @@ import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; +/// Key which is used to identify the [RepaintBoundary] +@internal +final sentryScreenshotWidgetGlobalKey = + GlobalKey(debugLabel: 'sentry_screenshot_widget'); + /// You can add screenshots of [child] to crash reports by adding this widget. /// Ideally you are adding it around your app widget like in the following /// example. @@ -18,12 +23,8 @@ import 'package:meta/meta.dart'; /// times. class SentryScreenshotWidget extends StatefulWidget { final Widget child; - final GlobalKey> sentryScreenshotWidgetGlobalKey; - const SentryScreenshotWidget( - {super.key, - required this.child, - required this.sentryScreenshotWidgetGlobalKey}); + const SentryScreenshotWidget({super.key, required this.child}); @override _SentryScreenshotWidgetState createState() => _SentryScreenshotWidgetState(); @@ -33,7 +34,7 @@ class _SentryScreenshotWidgetState extends State { @override Widget build(BuildContext context) { return RepaintBoundary( - key: widget.sentryScreenshotWidgetGlobalKey, + key: sentryScreenshotWidgetGlobalKey, child: widget.child, ); } diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index 97ab41c40e..821d34c3cf 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -10,15 +10,8 @@ final sentryWidgetGlobalKey = GlobalKey(debugLabel: 'sentry_widget'); /// as [SentryScreenshotWidget] and [SentryUserInteractionWidget]. class SentryWidget extends StatefulWidget { final Widget child; - final GlobalKey> sentryWidgetGlobalKey; - final GlobalKey> sentryScreenshotWidgetGlobalKey; - const SentryWidget({ - super.key, - required this.child, - required this.sentryWidgetGlobalKey, - required this.sentryScreenshotWidgetGlobalKey, - }); + const SentryWidget({super.key, required this.child}); @override _SentryWidgetState createState() => _SentryWidgetState(); @@ -29,12 +22,11 @@ class _SentryWidgetState extends State { Widget build(BuildContext context) { Widget content = widget.child; content = SentryScreenshotWidget( - sentryScreenshotWidgetGlobalKey: widget.sentryScreenshotWidgetGlobalKey, child: content, ); content = SentryUserInteractionWidget(child: content); return Container( - key: widget.sentryWidgetGlobalKey, + key: sentryWidgetGlobalKey, child: content, ); } From de86dd01ff78ac1e329414d3e4e2221dd84621d9 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Fri, 6 Sep 2024 11:03:12 +0200 Subject: [PATCH 10/16] fix unitTests --- .../flutter_enricher_event_processor.dart | 27 ++++++++++++++----- .../screenshot_event_processor.dart | 9 ++++--- flutter/lib/src/replay/recorder.dart | 2 +- 3 files changed, 26 insertions(+), 12 deletions(-) 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 5fd53d589c..8766a5f298 100644 --- a/flutter/lib/src/event_processor/flutter_enricher_event_processor.dart +++ b/flutter/lib/src/event_processor/flutter_enricher_event_processor.dart @@ -185,16 +185,21 @@ class FlutterEnricherEventProcessor implements EventProcessor { final List updatedViews = []; for (var platformView in platformDispatcher.views) { - var currentViews = currentDevice.views + final currentDeviceView = device?.views .where((view) => view.viewId == platformView.viewId) - .toList(); + .first; SentryView updatedView; - if (currentViews.isNotEmpty) { - updatedView = currentViews.single.copyWith( - screenWidthPixels: platformView.physicalSize.width.toInt(), - screenHeightPixels: platformView.physicalSize.height.toInt(), - screenDensity: platformView.devicePixelRatio.toDouble(), + 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( @@ -202,6 +207,8 @@ class FlutterEnricherEventProcessor implements EventProcessor { 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); @@ -212,6 +219,12 @@ class FlutterEnricherEventProcessor implements EventProcessor { ); } + SentryOrientation _getSentryOrientation(double width, double height) { + return width > height + ? SentryOrientation.landscape + : SentryOrientation.portrait; + } + SentryOperatingSystem _getOperatingSystem(SentryOperatingSystem? os) { return (os ?? SentryOperatingSystem()).copyWith( theme: os?.theme ?? diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 81a2235b54..780e78c52a 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -15,7 +15,7 @@ class ScreenshotEventProcessor implements EventProcessor { /// This is true when the SentryWidget is in the view hierarchy bool get _hasSentryScreenshotWidget => - sentryWidgetGlobalKey.currentContext != null; + sentryScreenshotWidgetGlobalKey.currentContext != null; @override Future apply(SentryEvent event, Hint hint) async { @@ -84,10 +84,11 @@ class ScreenshotEventProcessor implements EventProcessor { Future _createScreenshot() async { try { final renderObject = - sentryWidgetGlobalKey.currentContext?.findRenderObject(); + sentryScreenshotWidgetGlobalKey.currentContext?.findRenderObject(); if (renderObject is RenderRepaintBoundary) { - final pixelRatio = widget.View.of(sentryWidgetGlobalKey.currentContext!) - .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/replay/recorder.dart b/flutter/lib/src/replay/recorder.dart index 3e5a7c327f..a1f4ea1a0b 100644 --- a/flutter/lib/src/replay/recorder.dart +++ b/flutter/lib/src/replay/recorder.dart @@ -31,7 +31,7 @@ class ScreenshotRecorder { } Future capture(ScreenshotRecorderCallback callback) async { - final context = sentryWidgetGlobalKey.currentContext; + final context = sentryScreenshotWidgetGlobalKey.currentContext; final renderObject = context?.findRenderObject() as RenderRepaintBoundary?; if (context == null || renderObject == null) { if (!warningLogged) { From 1b757534ea98c7fe79ea19e4674b9a69819bfd9a Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 9 Sep 2024 11:11:57 +0200 Subject: [PATCH 11/16] remove `web: any` dependency --- flutter/pubspec.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ba508ccf4b..ad36c1252f 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -29,7 +29,6 @@ dependencies: ffi: ^2.0.0 file: '>=6.1.4' - web: any dev_dependencies: build_runner: ^2.4.2 flutter_test: From 00bff23b0a05144c625f9e10fa88809a269d5550 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 9 Sep 2024 12:11:25 +0200 Subject: [PATCH 12/16] fix dio mock for sentryDevice --- dio/test/mocks.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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, From aaa812b5fa8e68d8c90928f86eaae5d9cf2e4201 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 9 Sep 2024 14:11:30 +0200 Subject: [PATCH 13/16] fix sentryDevice dart tests --- dart/test/protocol/sentry_device_test.dart | 237 ++++++++++++--------- 1 file changed, 140 insertions(+), 97 deletions(-) diff --git a/dart/test/protocol/sentry_device_test.dart b/dart/test/protocol/sentry_device_test.dart index fa5e325999..a4d6722658 100644 --- a/dart/test/protocol/sentry_device_test.dart +++ b/dart/test/protocol/sentry_device_test.dart @@ -5,101 +5,12 @@ 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, - 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, - ); - - final sentryDeviceJson = { - '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, - }; - 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]), @@ -116,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, @@ -126,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, @@ -173,8 +117,9 @@ void main() { }); group('copyWith', () { + final fixture = Fixture(); test('copyWith keeps unchanged', () { - final data = sentryDevice; + final data = fixture.getSentryDeviceObject(); final copy = data.copyWith(); @@ -196,7 +141,7 @@ void main() { }); test('copyWith takes new values', () { - final data = sentryDevice; + final data = fixture.getSentryDeviceObject(); final bootTime = DateTime.now(); @@ -282,3 +227,101 @@ void main() { }); }); } + +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, + ); +} From 221dadd56b3dc0ddef3dba31a8dd1fc9076d130d Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 9 Sep 2024 14:23:36 +0200 Subject: [PATCH 14/16] fix sentryDevice in dart example app --- dart/example/bin/event_example.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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, From cb75ff1d8f623aa9bd7c3249bc5e62fa704cd464 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 9 Sep 2024 14:46:39 +0200 Subject: [PATCH 15/16] fix null check error --- flutter/lib/src/widgets_binding_observer.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/src/widgets_binding_observer.dart b/flutter/lib/src/widgets_binding_observer.dart index 3221967a57..72188fd75d 100644 --- a/flutter/lib/src/widgets_binding_observer.dart +++ b/flutter/lib/src/widgets_binding_observer.dart @@ -55,7 +55,8 @@ class SentryWidgetsBindingObserver with WidgetsBindingObserver { .listen(_onScreenSizeChanged); final views = - _options.bindingUtils.instance!.platformDispatcher.views.toList(); + _options.bindingUtils.instance?.platformDispatcher.views.toList() ?? + []; _screenSizeStreamController.add(views); } } From 1d82e562992cc1100fb5727ba26055128e499599 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 10 Sep 2024 18:05:34 +0200 Subject: [PATCH 16/16] remove unused files and remove unnecessary elements in index.html --- flutter/example/lib/main_test.dart | 143 -------------------------- flutter/example/web/index.html | 11 -- flutter/example/web/index_backup.html | 51 --------- 3 files changed, 205 deletions(-) delete mode 100644 flutter/example/lib/main_test.dart delete mode 100644 flutter/example/web/index_backup.html diff --git a/flutter/example/lib/main_test.dart b/flutter/example/lib/main_test.dart deleted file mode 100644 index 6b0ed7c420..0000000000 --- a/flutter/example/lib/main_test.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_logging/sentry_logging.dart'; - -import 'multi_view_app.dart'; - -const String exampleDsn = - 'https://e85b375ffb9f43cf8bdf9787768149e0@o447951.ingest.sentry.io/5428562'; - -/// This is an exampleUrl that will be used to demonstrate how http requests are captured. -const String exampleUrl = 'https://jsonplaceholder.typicode.com/todos/'; - -var _isIntegrationTest = false; - -final GlobalKey navigatorKey = GlobalKey(); - -Future main() async { - await setupSentry( - () => runWidget( - SentryWidget( - child: DefaultAssetBundle( - bundle: SentryAssetBundle(), - child: MultiViewApp( - viewBuilder: (BuildContext context) => const MyApp(), - ), - ), - ), - ), - exampleDsn, - ); -} - -Future setupSentry( - AppRunner appRunner, - String dsn, { - bool isIntegrationTest = false, - BeforeSendCallback? beforeSendCallback, -}) async { - await SentryFlutter.init( - (options) { - options.dsn = exampleDsn; - options.tracesSampleRate = 1.0; - options.profilesSampleRate = 1.0; - options.reportPackages = false; - options.addInAppInclude('sentry_flutter_multiview_example'); - options.considerInAppFramesByDefault = false; - options.attachThreads = true; - options.enableWindowMetricBreadcrumbs = true; - options.addIntegration(LoggingIntegration(minEventLevel: Level.INFO)); - options.sendDefaultPii = true; - options.reportSilentFlutterErrors = true; - options.attachScreenshot = true; - options.screenshotQuality = SentryScreenshotQuality.low; - options.attachViewHierarchy = true; - // We can enable Sentry debug logging during development. This is likely - // going to log too much for your app, but can be useful when figuring out - // configuration issues, e.g. finding out why your events are not uploaded. - options.debug = true; - options.spotlight = Spotlight(enabled: true); - options.enableTimeToFullDisplayTracing = true; - options.enableMetrics = true; - - options.maxRequestBodySize = MaxRequestBodySize.always; - options.maxResponseBodySize = MaxResponseBodySize.always; - options.navigatorKey = navigatorKey; - - _isIntegrationTest = isIntegrationTest; - if (_isIntegrationTest) { - options.dist = '1'; - options.environment = 'integration'; - options.beforeSend = beforeSendCallback; - } - }, - // Init your App. - appRunner: appRunner, - ); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: MyHomePage( - title: 'Flutter Demo Home Page ${View.of(context).viewId}'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(widget.title), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. - ); - } -} diff --git a/flutter/example/web/index.html b/flutter/example/web/index.html index 64b0902cb8..1c40323e6f 100644 --- a/flutter/example/web/index.html +++ b/flutter/example/web/index.html @@ -39,17 +39,6 @@
- - - diff --git a/flutter/example/web/index_backup.html b/flutter/example/web/index_backup.html deleted file mode 100644 index 406b34e2c3..0000000000 --- a/flutter/example/web/index_backup.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - sentry_flutter_example - - - - - - - - - -