Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

### Features

- Add `beforeCapture` for View Hierarchy ([#2523](https://github.com/getsentry/sentry-dart/pull/2523))
- View hierarchy calls are now debounced for 2 seconds.

### Enhancements

- Replay: improve performance of screenshot data to native recorder ([#2530](https://github.com/getsentry/sentry-dart/pull/2530))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ScreenshotEventProcessor implements EventProcessor {
_debouncer = Debouncer(
// ignore: invalid_use_of_internal_member
_options.clock,
waitTimeMs: 2000,
waitTime: Duration(milliseconds: 2000),
);
}

Expand All @@ -50,7 +50,7 @@ class ScreenshotEventProcessor implements EventProcessor {
}

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

// ignore: deprecated_member_use_from_same_package
Expand All @@ -77,7 +77,7 @@ class ScreenshotEventProcessor implements EventProcessor {
} else if (shouldDebounce) {
_options.logger(
SentryLevel.debug,
'Skipping screenshot capture due to debouncing (too many captures within ${_debouncer.waitTimeMs}ms)',
'Skipping screenshot capture due to debouncing (too many captures within ${_debouncer.waitTime.inMilliseconds}ms)',
);
takeScreenshot = false;
}
Expand All @@ -88,7 +88,7 @@ class ScreenshotEventProcessor implements EventProcessor {
} catch (exception, stackTrace) {
_options.logger(
SentryLevel.error,
'The beforeCapture/beforeScreenshot callback threw an exception',
'The beforeCaptureScreenshot/beforeScreenshot callback threw an exception',
exception: exception,
stackTrace: stackTrace,
);
Expand Down
8 changes: 7 additions & 1 deletion flutter/lib/src/sentry_flutter_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,17 @@ class SentryFlutterOptions extends SentryOptions {

/// Enables the View Hierarchy feature.
///
/// Renders an ASCII represention of the entire view hierarchy of the
/// Renders an ASCII representation of the entire view hierarchy of the
/// application when an error happens and includes it as an attachment.
@meta.experimental
bool attachViewHierarchy = false;

/// Sets a callback which is executed before capturing view hierarchy. Only
/// relevant if `attachViewHierarchy` is set to true. When false is returned
/// from the function, no view hierarchy will be attached.
@meta.experimental
BeforeCaptureCallback? beforeCaptureViewHierarchy;

/// Enables collection of view hierarchy element identifiers.
///
/// Identifiers are extracted from widget keys.
Expand Down
7 changes: 4 additions & 3 deletions flutter/lib/src/utils/debouncer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import 'package:sentry/sentry.dart';
@internal
class Debouncer {
final ClockProvider clockProvider;
final int waitTimeMs;
final Duration waitTime;
DateTime? _lastExecutionTime;

Debouncer(this.clockProvider, {this.waitTimeMs = 2000});
Debouncer(this.clockProvider,
{this.waitTime = const Duration(milliseconds: 2000)});

bool shouldDebounce() {
final currentTime = clockProvider();
final lastExecutionTime = _lastExecutionTime;
_lastExecutionTime = currentTime;

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import 'dart:async';

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

/// A [EventProcessor] that renders an ASCII representation of the entire view
/// hierarchy of the application when an error happens and includes it as an
/// attachment to the [Hint].
class SentryViewHierarchyEventProcessor implements EventProcessor {
SentryViewHierarchyEventProcessor(this._options);

final SentryFlutterOptions _options;
late final Debouncer _debouncer;

SentryViewHierarchyEventProcessor(this._options) {
_debouncer = Debouncer(
// ignore: invalid_use_of_internal_member
_options.clock,
waitTime: Duration(milliseconds: 2000),
);
}

@override
SentryEvent? apply(SentryEvent event, Hint hint) {
Future<SentryEvent?> apply(SentryEvent event, Hint hint) async {
if (event is SentryTransaction) {
return event;
}
Expand All @@ -23,15 +33,56 @@ class SentryViewHierarchyEventProcessor implements EventProcessor {
if (instance == null) {
return event;
}
final sentryViewHierarchy = walkWidgetTree(instance, _options);

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

try {
final beforeCapture = _options.beforeCaptureViewHierarchy;
FutureOr<bool>? result;

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

bool captureViewHierarchy = true;

if (result != null) {
if (result is Future<bool>) {
captureViewHierarchy = await result;
} else {
captureViewHierarchy = result;
}
} else if (shouldDebounce) {
_options.logger(
SentryLevel.debug,
'Skipping view hierarchy capture due to debouncing (too many captures within ${_debouncer.waitTime.inMilliseconds}ms)',
);
captureViewHierarchy = false;
}

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

final sentryViewHierarchy = walkWidgetTree(instance, _options);
if (sentryViewHierarchy == null) {
return event;
}

final viewHierarchy =
hint.viewHierarchy =
SentryAttachment.fromViewHierarchy(sentryViewHierarchy);
hint.viewHierarchy = viewHierarchy;
return event;
}
}
2 changes: 1 addition & 1 deletion flutter/test/utils/debouncer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ class Fixture {
DateTime mockClock() => DateTime.fromMillisecondsSinceEpoch(currentTimeMs);

Debouncer getSut({int waitTimeMs = 3000}) {
return Debouncer(mockClock, waitTimeMs: waitTimeMs);
return Debouncer(mockClock, waitTime: Duration(milliseconds: waitTimeMs));
}
}
Loading
Loading