Skip to content

Commit

Permalink
Add beforeCapture for View Hierarchy (#2523)
Browse files Browse the repository at this point in the history
  • Loading branch information
denrase authored Jan 2, 2025
1 parent f735167 commit e442847
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 22 deletions.
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
65 changes: 58 additions & 7 deletions flutter/lib/src/view_hierarchy/view_hierarchy_event_processor.dart
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

0 comments on commit e442847

Please sign in to comment.