Skip to content

Commit e442847

Browse files
authored
Add beforeCapture for View Hierarchy (#2523)
1 parent f735167 commit e442847

File tree

7 files changed

+289
-22
lines changed

7 files changed

+289
-22
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Add `beforeCapture` for View Hierarchy ([#2523](https://github.com/getsentry/sentry-dart/pull/2523))
8+
- View hierarchy calls are now debounced for 2 seconds.
9+
510
### Enhancements
611

712
- Replay: improve performance of screenshot data to native recorder ([#2530](https://github.com/getsentry/sentry-dart/pull/2530))

flutter/lib/src/event_processor/screenshot_event_processor.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class ScreenshotEventProcessor implements EventProcessor {
2929
_debouncer = Debouncer(
3030
// ignore: invalid_use_of_internal_member
3131
_options.clock,
32-
waitTimeMs: 2000,
32+
waitTime: Duration(milliseconds: 2000),
3333
);
3434
}
3535

@@ -50,7 +50,7 @@ class ScreenshotEventProcessor implements EventProcessor {
5050
}
5151

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

5656
// ignore: deprecated_member_use_from_same_package
@@ -77,7 +77,7 @@ class ScreenshotEventProcessor implements EventProcessor {
7777
} else if (shouldDebounce) {
7878
_options.logger(
7979
SentryLevel.debug,
80-
'Skipping screenshot capture due to debouncing (too many captures within ${_debouncer.waitTimeMs}ms)',
80+
'Skipping screenshot capture due to debouncing (too many captures within ${_debouncer.waitTime.inMilliseconds}ms)',
8181
);
8282
takeScreenshot = false;
8383
}
@@ -88,7 +88,7 @@ class ScreenshotEventProcessor implements EventProcessor {
8888
} catch (exception, stackTrace) {
8989
_options.logger(
9090
SentryLevel.error,
91-
'The beforeCapture/beforeScreenshot callback threw an exception',
91+
'The beforeCaptureScreenshot/beforeScreenshot callback threw an exception',
9292
exception: exception,
9393
stackTrace: stackTrace,
9494
);

flutter/lib/src/sentry_flutter_options.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,17 @@ class SentryFlutterOptions extends SentryOptions {
241241

242242
/// Enables the View Hierarchy feature.
243243
///
244-
/// Renders an ASCII represention of the entire view hierarchy of the
244+
/// Renders an ASCII representation of the entire view hierarchy of the
245245
/// application when an error happens and includes it as an attachment.
246246
@meta.experimental
247247
bool attachViewHierarchy = false;
248248

249+
/// Sets a callback which is executed before capturing view hierarchy. Only
250+
/// relevant if `attachViewHierarchy` is set to true. When false is returned
251+
/// from the function, no view hierarchy will be attached.
252+
@meta.experimental
253+
BeforeCaptureCallback? beforeCaptureViewHierarchy;
254+
249255
/// Enables collection of view hierarchy element identifiers.
250256
///
251257
/// Identifiers are extracted from widget keys.

flutter/lib/src/utils/debouncer.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@ import 'package:sentry/sentry.dart';
44
@internal
55
class Debouncer {
66
final ClockProvider clockProvider;
7-
final int waitTimeMs;
7+
final Duration waitTime;
88
DateTime? _lastExecutionTime;
99

10-
Debouncer(this.clockProvider, {this.waitTimeMs = 2000});
10+
Debouncer(this.clockProvider,
11+
{this.waitTime = const Duration(milliseconds: 2000)});
1112

1213
bool shouldDebounce() {
1314
final currentTime = clockProvider();
1415
final lastExecutionTime = _lastExecutionTime;
1516
_lastExecutionTime = currentTime;
1617

1718
if (lastExecutionTime != null &&
18-
currentTime.difference(lastExecutionTime).inMilliseconds < waitTimeMs) {
19+
currentTime.difference(lastExecutionTime) < waitTime) {
1920
return true;
2021
}
2122

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1+
import 'dart:async';
2+
13
import '../../sentry_flutter.dart';
4+
import '../utils/debouncer.dart';
25
import 'sentry_tree_walker.dart';
36

47
/// A [EventProcessor] that renders an ASCII representation of the entire view
58
/// hierarchy of the application when an error happens and includes it as an
69
/// attachment to the [Hint].
710
class SentryViewHierarchyEventProcessor implements EventProcessor {
8-
SentryViewHierarchyEventProcessor(this._options);
9-
1011
final SentryFlutterOptions _options;
12+
late final Debouncer _debouncer;
13+
14+
SentryViewHierarchyEventProcessor(this._options) {
15+
_debouncer = Debouncer(
16+
// ignore: invalid_use_of_internal_member
17+
_options.clock,
18+
waitTime: Duration(milliseconds: 2000),
19+
);
20+
}
1121

1222
@override
13-
SentryEvent? apply(SentryEvent event, Hint hint) {
23+
Future<SentryEvent?> apply(SentryEvent event, Hint hint) async {
1424
if (event is SentryTransaction) {
1525
return event;
1626
}
@@ -23,15 +33,56 @@ class SentryViewHierarchyEventProcessor implements EventProcessor {
2333
if (instance == null) {
2434
return event;
2535
}
26-
final sentryViewHierarchy = walkWidgetTree(instance, _options);
2736

37+
// skip capturing in case of debouncing (=too many frequent capture requests)
38+
// the BeforeCaptureCallback may overrule the debouncing decision
39+
final shouldDebounce = _debouncer.shouldDebounce();
40+
41+
try {
42+
final beforeCapture = _options.beforeCaptureViewHierarchy;
43+
FutureOr<bool>? result;
44+
45+
if (beforeCapture != null) {
46+
result = beforeCapture(event, hint, shouldDebounce);
47+
}
48+
49+
bool captureViewHierarchy = true;
50+
51+
if (result != null) {
52+
if (result is Future<bool>) {
53+
captureViewHierarchy = await result;
54+
} else {
55+
captureViewHierarchy = result;
56+
}
57+
} else if (shouldDebounce) {
58+
_options.logger(
59+
SentryLevel.debug,
60+
'Skipping view hierarchy capture due to debouncing (too many captures within ${_debouncer.waitTime.inMilliseconds}ms)',
61+
);
62+
captureViewHierarchy = false;
63+
}
64+
65+
if (!captureViewHierarchy) {
66+
return event;
67+
}
68+
} catch (exception, stackTrace) {
69+
_options.logger(
70+
SentryLevel.error,
71+
'The beforeCaptureViewHierarchy callback threw an exception',
72+
exception: exception,
73+
stackTrace: stackTrace,
74+
);
75+
if (_options.automatedTestMode) {
76+
rethrow;
77+
}
78+
}
79+
80+
final sentryViewHierarchy = walkWidgetTree(instance, _options);
2881
if (sentryViewHierarchy == null) {
2982
return event;
3083
}
31-
32-
final viewHierarchy =
84+
hint.viewHierarchy =
3385
SentryAttachment.fromViewHierarchy(sentryViewHierarchy);
34-
hint.viewHierarchy = viewHierarchy;
3586
return event;
3687
}
3788
}

flutter/test/utils/debouncer_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,6 @@ class Fixture {
4848
DateTime mockClock() => DateTime.fromMillisecondsSinceEpoch(currentTimeMs);
4949

5050
Debouncer getSut({int waitTimeMs = 3000}) {
51-
return Debouncer(mockClock, waitTimeMs: waitTimeMs);
51+
return Debouncer(mockClock, waitTime: Duration(milliseconds: waitTimeMs));
5252
}
5353
}

0 commit comments

Comments
 (0)