Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add debounce to capturing screenshots #2368

Merged
merged 33 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
77cce7c
Add debounce to `ScreenshotWidget`
denrase Oct 22, 2024
f540ecb
add changelog entry
denrase Oct 22, 2024
e5f628a
provide clock in tests
denrase Oct 22, 2024
235de7b
remove unused import
denrase Oct 22, 2024
09c7f91
update clock at correct line
denrase Oct 22, 2024
7345987
Merge branch 'main' into feat/screenshot-debounce
denrase Oct 22, 2024
ba0b8e7
update cl
denrase Oct 22, 2024
3d489d9
use now from clock
denrase Oct 22, 2024
c088da8
Merge branch 'main' into feat/screenshot-debounce
denrase Nov 12, 2024
3a9d843
refactor debouncer to be similar to sentry-java impl
denrase Nov 12, 2024
e710098
use debouncer in screenshot_event_processor
denrase Nov 12, 2024
161f182
Change beforeScreenshot to beforeCapture callback
denrase Nov 12, 2024
d6b74f2
update changelog entry
denrase Nov 12, 2024
570b1aa
format
denrase Nov 12, 2024
fbea7d7
fix analyze issue
denrase Nov 12, 2024
ad5a9ce
feedback
denrase Nov 13, 2024
57087b1
recert timer impl for widgets observer
denrase Nov 13, 2024
1f5f85c
keep before screenshot callback
denrase Nov 13, 2024
f2950b9
Merge branch 'main' into feat/screenshot-debounce
denrase Nov 13, 2024
cfafff0
update cl
denrase Nov 13, 2024
eea29fc
update error msg
denrase Nov 13, 2024
006839e
refactor
denrase Nov 13, 2024
2e43ad3
add warning
denrase Nov 13, 2024
e96bb8d
add ignores for analyzer
denrase Nov 13, 2024
5a9bcac
Merge branch 'main' into feat/screenshot-debounce
denrase Nov 18, 2024
f4c724d
rename callback
denrase Nov 18, 2024
9fce5c0
Merge branch 'main' into feat/screenshot-debounce
denrase Nov 18, 2024
b157a35
rename to beforeScreenshotCapture
denrase Nov 18, 2024
28b0857
Merge branch 'main' into feat/screenshot-debounce
denrase Nov 25, 2024
7ce37e4
cleanup
denrase Nov 25, 2024
cd64cc1
fix cl
denrase Nov 25, 2024
d245569
move to features in cl
denrase Nov 25, 2024
4d5f535
add sample to changelog
denrase Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
- SDK creates a synthetic trace using `StackTrace.current`
- Internal SDK frames are removed to reduce noise
- Original stack traces (when provided) are left unchanged

- Add debounce to `ScreenshotWidget` ([#2368](https://github.com/getsentry/sentry-dart/pull/2368))
denrase marked this conversation as resolved.
Show resolved Hide resolved
- Replace deprecated `BeforeScreenshotCallback` with new `BeforeCaptureCallback`.

### Features

- Improve frame tracking accuracy ([#2372](https://github.com/getsentry/sentry-dart/pull/2372))
Expand Down
62 changes: 46 additions & 16 deletions flutter/lib/src/event_processor/screenshot_event_processor.dart
denrase marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@ import '../sentry_flutter_options.dart';
import '../renderer/renderer.dart';
import 'package:flutter/widgets.dart' as widget;

import '../utils/debouncer.dart';

class ScreenshotEventProcessor implements EventProcessor {
final SentryFlutterOptions _options;
final Debouncer _debouncer;

ScreenshotEventProcessor(this._options);
ScreenshotEventProcessor(this._options)
: _debouncer = Debouncer(
// ignore: invalid_use_of_internal_member
_options.clock,
waitTimeMs: 2000,
);

@override
Future<SentryEvent?> apply(SentryEvent event, Hint hint) async {
Expand All @@ -32,29 +40,51 @@ class ScreenshotEventProcessor implements EventProcessor {
return event; // No need to attach screenshot of feedback form.
}

// skip capturing in case of debouncing (=too many frequent capture requests)
// the BeforeCaptureCallback may overrules the debouncing decision
final shouldDebounce = _debouncer.shouldDebounce();

// ignore: deprecated_member_use_from_same_package
final beforeScreenshot = _options.beforeScreenshot;
if (beforeScreenshot != null) {
try {
final result = beforeScreenshot(event, hint: hint);
bool takeScreenshot;
final beforeCapture = _options.beforeScreenshotCapture;

try {
FutureOr<bool>? result;

if (beforeCapture != null) {
result = beforeCapture(event, hint, shouldDebounce);
} else if (beforeScreenshot != null) {
result = beforeScreenshot(event, hint: hint);
}

bool takeScreenshot = true;

if (result != null) {
if (result is Future<bool>) {
takeScreenshot = await result;
} else {
takeScreenshot = result;
}
if (!takeScreenshot) {
return event;
}
} catch (exception, stackTrace) {
} else if (shouldDebounce) {
_options.logger(
SentryLevel.error,
'The beforeScreenshot callback threw an exception',
exception: exception,
stackTrace: stackTrace,
SentryLevel.debug,
'Skipping screenshot capture due to debouncing (too many captures within ${_debouncer.waitTimeMs}ms)',
);
if (_options.automatedTestMode) {
rethrow;
}
takeScreenshot = false;
}

if (!takeScreenshot) {
return event;
}
} catch (exception, stackTrace) {
_options.logger(
SentryLevel.error,
'The beforeCapture/beforeScreenshot callback threw an exception',
exception: exception,
stackTrace: stackTrace,
);
if (_options.automatedTestMode) {
rethrow;
}
}

Expand Down
43 changes: 36 additions & 7 deletions flutter/lib/src/sentry_flutter_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,14 @@ class SentryFlutterOptions extends SentryOptions {
/// Only attach a screenshot when the app is resumed.
bool attachScreenshotOnlyWhenResumed = false;

@Deprecated(
'Will be removed in a future version. Use [beforeScreenshotCapture] instead')
BeforeScreenshotCallback? beforeScreenshot;

/// Sets a callback which is executed before capturing screenshots. Only
/// relevant if `attachScreenshot` is set to true. When false is returned
/// from the function, no screenshot will be attached.
BeforeScreenshotCallback? beforeScreenshot;
BeforeCaptureCallback? beforeScreenshotCapture;
denrase marked this conversation as resolved.
Show resolved Hide resolved

/// Enable or disable automatic breadcrumbs for User interactions Using [Listener]
///
Expand Down Expand Up @@ -387,9 +391,34 @@ class _SentryFlutterExperimentalOptions {
final replay = SentryReplayOptions();
}

/// Callback being executed in [ScreenshotEventProcessor], deciding if a
/// screenshot should be recorded and attached.
typedef BeforeScreenshotCallback = FutureOr<bool> Function(
SentryEvent event, {
Hint? hint,
});
@Deprecated(
'Will be removed in a future version. Use [BeforeCaptureCallback] instead')
typedef BeforeScreenshotCallback = FutureOr<bool> Function(SentryEvent event,
{Hint? hint});

/// A callback which can be used to suppress capturing of screenshots.
/// It's called in [ScreenshotEventProcessor] if screenshots are enabled.
/// This gives more fine-grained control over when capturing should be performed,
/// e.g., only capture screenshots for fatal events or override any debouncing for important events.
///
/// Since capturing can be resource-intensive, the debounce parameter should be respected if possible.
///
/// Example:
/// ```dart
/// if (debounce) {
/// return false;
/// } else {
/// // check event and hint
/// }
/// ```
///
/// [event] is the event to be checked.
/// [hint] provides additional hints.
/// [debounce] indicates if capturing is marked for being debounced.
///
/// Returns `true` if capturing should be performed, otherwise `false`.
typedef BeforeCaptureCallback = FutureOr<bool> Function(
SentryEvent event,
Hint hint,
bool debounce,
);
26 changes: 15 additions & 11 deletions flutter/lib/src/utils/debouncer.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import 'package:sentry/sentry.dart';

@internal
class Debouncer {
final int milliseconds;
Timer? _timer;
final ClockProvider clockProvider;
final int waitTimeMs;
DateTime? _lastExecutionTime;

Debouncer({required this.milliseconds});
Debouncer(this.clockProvider, {this.waitTimeMs = 2000});

void run(VoidCallback action) {
_timer?.cancel();
_timer = Timer(Duration(milliseconds: milliseconds), action);
}
bool shouldDebounce() {
final currentTime = clockProvider();
final lastExecutionTime = _lastExecutionTime;
_lastExecutionTime = currentTime;

if (lastExecutionTime != null &&
currentTime.difference(lastExecutionTime).inMilliseconds < waitTimeMs) {
return true;
}

void dispose() {
_timer?.cancel();
return false;
}
}
20 changes: 20 additions & 0 deletions flutter/lib/src/utils/timer_debouncer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';

@internal
class TimerDebouncer {
final int milliseconds;
Timer? _timer;

TimerDebouncer({required this.milliseconds});

void run(VoidCallback action) {
_timer?.cancel();
_timer = Timer(Duration(milliseconds: milliseconds), action);
}

void dispose() {
_timer?.cancel();

Check warning on line 18 in flutter/lib/src/utils/timer_debouncer.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/utils/timer_debouncer.dart#L17-L18

Added lines #L17 - L18 were not covered by tests
}
}
9 changes: 4 additions & 5 deletions flutter/lib/src/widgets_binding_observer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'utils/timer_debouncer.dart';
import '../sentry_flutter.dart';
import 'utils/debouncer.dart';

/// This is a `WidgetsBindingObserver` which can observe some events of a
/// Flutter application.
Expand All @@ -25,7 +25,8 @@ class SentryWidgetsBindingObserver with WidgetsBindingObserver {
required SentryFlutterOptions options,
}) : _hub = hub ?? HubAdapter(),
_options = options,
_screenSizeStreamController = StreamController(sync: true) {
_screenSizeStreamController = StreamController(sync: true),
_didChangeMetricsDebouncer = TimerDebouncer(milliseconds: 100) {
if (_options.enableWindowMetricBreadcrumbs) {
_screenSizeStreamController.stream
.map(
Expand All @@ -47,12 +48,11 @@ class SentryWidgetsBindingObserver with WidgetsBindingObserver {

final Hub _hub;
final SentryFlutterOptions _options;
final TimerDebouncer _didChangeMetricsDebouncer;

// ignore: deprecated_member_use
final StreamController<SingletonFlutterWindow?> _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.
Expand Down Expand Up @@ -91,7 +91,6 @@ class SentryWidgetsBindingObserver with WidgetsBindingObserver {
if (!_options.enableWindowMetricBreadcrumbs) {
return;
}

_didChangeMetricsDebouncer.run(() {
// ignore: deprecated_member_use
final window = _options.bindingUtils.instance?.window;
Expand Down
Loading
Loading