diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5117d226..672ed7fce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), ``` +### Improvements + +- Debouncing of SentryWidgetsBindingObserver.didChangeMetrics with delay of 100ms. ([#2232](https://github.com/getsentry/sentry-dart/pull/2232)) + ### Dependencies - Bump Android SDK from v7.13.0 to v7.14.0 ([#2228](https://github.com/getsentry/sentry-dart/pull/2228)) diff --git a/flutter/lib/src/utils/debouncer.dart b/flutter/lib/src/utils/debouncer.dart new file mode 100644 index 0000000000..b714b41b4e --- /dev/null +++ b/flutter/lib/src/utils/debouncer.dart @@ -0,0 +1,20 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:meta/meta.dart'; + +@internal +class Debouncer { + final int milliseconds; + Timer? _timer; + + Debouncer({required this.milliseconds}); + + void run(VoidCallback action) { + _timer?.cancel(); + _timer = Timer(Duration(milliseconds: milliseconds), action); + } + + void dispose() { + _timer?.cancel(); + } +} diff --git a/flutter/lib/src/widgets_binding_observer.dart b/flutter/lib/src/widgets_binding_observer.dart index b199b7f8a5..7c0db1842f 100644 --- a/flutter/lib/src/widgets_binding_observer.dart +++ b/flutter/lib/src/widgets_binding_observer.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../sentry_flutter.dart'; +import 'utils/debouncer.dart'; /// This is a `WidgetsBindingObserver` which can observe some events of a /// Flutter application. @@ -50,6 +51,8 @@ class SentryWidgetsBindingObserver with WidgetsBindingObserver { // ignore: deprecated_member_use final StreamController _screenSizeStreamController; + final _didChangeMetricsDebouncer = Debouncer(milliseconds: 100); + /// This method records lifecycle events. /// It tries to mimic the behavior of ActivityBreadcrumbsIntegration of Sentry /// Android for lifecycle events. @@ -88,9 +91,12 @@ class SentryWidgetsBindingObserver with WidgetsBindingObserver { if (!_options.enableWindowMetricBreadcrumbs) { return; } - // ignore: deprecated_member_use - final window = _options.bindingUtils.instance?.window; - _screenSizeStreamController.add(window); + + _didChangeMetricsDebouncer.run(() { + // ignore: deprecated_member_use + final window = _options.bindingUtils.instance?.window; + _screenSizeStreamController.add(window); + }); } void _onScreenSizeChanged(Map data) { diff --git a/flutter/test/widgets_binding_observer_test.dart b/flutter/test/widgets_binding_observer_test.dart index 8e3bff9c25..86973ba91c 100644 --- a/flutter/test/widgets_binding_observer_test.dart +++ b/flutter/test/widgets_binding_observer_test.dart @@ -193,6 +193,9 @@ void main() { // ignore: deprecated_member_use window.physicalSizeTestValue = Size(newWidth, newHeight); + // waiting for debouncing with 100ms added https://github.com/getsentry/sentry-dart/issues/400 + await tester.pump(Duration(milliseconds: 150)); + final breadcrumb = verify(hub.addBreadcrumb(captureAny)).captured.single as Breadcrumb; @@ -230,6 +233,9 @@ void main() { // ignore: deprecated_member_use window.devicePixelRatioTestValue = newPixelRatio; + // waiting for debouncing with 100ms added https://github.com/getsentry/sentry-dart/issues/400 + await tester.pump(Duration(milliseconds: 150)); + final breadcrumb = verify(hub.addBreadcrumb(captureAny)).captured.single as Breadcrumb; @@ -265,6 +271,9 @@ void main() { // ignore: deprecated_member_use window.viewInsetsTestValue = WindowPadding.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)); instance.removeObserver(observer); @@ -286,6 +295,9 @@ void main() { window.onMetricsChanged!(); + // waiting for debouncing with 100ms added https://github.com/getsentry/sentry-dart/issues/400 + await tester.pump(Duration(milliseconds: 150)); + verifyNever(hub.addBreadcrumb(captureAny)); instance.removeObserver(observer); @@ -400,5 +412,70 @@ void main() { instance.removeObserver(observer); }); + + testWidgets('debouncing didChangeMetrics with 100ms delay', + (WidgetTester tester) async { + final hub = MockHub(); + + final observer = SentryWidgetsBindingObserver( + hub: hub, + options: flutterTrackingEnabledOptions, + ); + final instance = tester.binding; + instance.addObserver(observer); + + // ignore: deprecated_member_use + final window = instance.window; + + // ignore: deprecated_member_use + window.physicalSizeTestValue = window.physicalSize; + + const newPixelRatio = 1.7; + // ignore: deprecated_member_use + window.devicePixelRatioTestValue = newPixelRatio; + + verifyNever(hub.addBreadcrumb(captureAny)); + + // waiting for debouncing with 100ms added https://github.com/getsentry/sentry-dart/issues/400 + await tester.pump(Duration(milliseconds: 150)); + + verify(hub.addBreadcrumb(captureAny)); + + instance.removeObserver(observer); + }); + + testWidgets('debouncing: didChangeMetrics is called only once in 100ms', + (WidgetTester tester) async { + final hub = MockHub(); + + final observer = SentryWidgetsBindingObserver( + hub: hub, + options: flutterTrackingEnabledOptions, + ); + final instance = tester.binding; + instance.addObserver(observer); + + // ignore: deprecated_member_use + final window = instance.window; + + // ignore: deprecated_member_use + window.physicalSizeTestValue = window.physicalSize; + + // ignore: deprecated_member_use + window.devicePixelRatioTestValue = 2.1; + // ignore: deprecated_member_use + window.devicePixelRatioTestValue = 2.2; + // ignore: deprecated_member_use + window.devicePixelRatioTestValue = 2.3; + + verifyNever(hub.addBreadcrumb(captureAny)); + + // waiting for debouncing with 100ms added https://github.com/getsentry/sentry-dart/issues/400 + await tester.pump(Duration(milliseconds: 150)); + + verify(hub.addBreadcrumb(captureAny)).called(1); + + instance.removeObserver(observer); + }); }); }