Skip to content

Commit 7954fb3

Browse files
authored
Add screenshot to SentryFeedbackWidget (#2369)
1 parent 0479083 commit 7954fb3

File tree

9 files changed

+152
-14
lines changed

9 files changed

+152
-14
lines changed

CHANGELOG.md

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

33
## Unreleased
44

5+
### Features
6+
7+
- Add screenshot to `SentryFeedbackWidget` ([#2369](https://github.com/getsentry/sentry-dart/pull/2369))
8+
- Use `SentryFlutter.captureScreenshot` to create a screenshot attachment
9+
- Call `SentryFeedbackWidget` with this attachment to add it to the user feedback
10+
11+
```dart
12+
final id = await Sentry.captureMessage('UserFeedback');
13+
final screenshot = await SentryFlutter.captureScreenshot();
14+
15+
Navigator.push(
16+
context,
17+
MaterialPageRoute(
18+
builder: (context) => SentryFeedbackWidget(
19+
associatedEventId: id,
20+
screenshot: screenshot,
21+
),
22+
fullscreenDialog: true,
23+
),
24+
);
25+
```
26+
527
### Enhancements
628

729
- Cache parsed DSN ([#2365](https://github.com/getsentry/sentry-dart/pull/2365))

flutter/example/lib/main.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,12 +502,14 @@ class MainScaffold extends StatelessWidget {
502502
TooltipButton(
503503
onPressed: () async {
504504
final id = await Sentry.captureMessage('UserFeedback');
505+
final screenshot = await SentryFlutter.captureScreenshot();
506+
505507
if (!context.mounted) return;
506508
Navigator.push(
507509
context,
508510
MaterialPageRoute(
509-
builder: (context) =>
510-
SentryFeedbackWidget(associatedEventId: id),
511+
builder: (context) => SentryFeedbackWidget(
512+
associatedEventId: id, screenshot: screenshot),
511513
fullscreenDialog: true,
512514
),
513515
);

flutter/lib/src/event_processor/screenshot_event_processor.dart

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import 'dart:math';
33
import 'dart:typed_data';
44
import 'dart:ui';
55

6+
import 'package:flutter/rendering.dart';
7+
import 'package:meta/meta.dart';
68
import 'package:sentry/sentry.dart';
79
import '../screenshot/sentry_screenshot_widget.dart';
810
import '../sentry_flutter_options.dart';
9-
import 'package:flutter/rendering.dart';
1011
import '../renderer/renderer.dart';
1112
import 'package:flutter/widgets.dart' as widget;
1213

@@ -15,10 +16,6 @@ class ScreenshotEventProcessor implements EventProcessor {
1516

1617
ScreenshotEventProcessor(this._options);
1718

18-
/// This is true when the SentryWidget is in the view hierarchy
19-
bool get _hasSentryScreenshotWidget =>
20-
sentryScreenshotWidgetGlobalKey.currentContext != null;
21-
2219
@override
2320
Future<SentryEvent?> apply(SentryEvent event, Hint hint) async {
2421
if (event is SentryTransaction) {
@@ -27,9 +24,14 @@ class ScreenshotEventProcessor implements EventProcessor {
2724

2825
if (event.exceptions == null &&
2926
event.throwable == null &&
30-
_hasSentryScreenshotWidget) {
27+
SentryScreenshotWidget.isMounted) {
3128
return event;
3229
}
30+
31+
if (event.type == 'feedback') {
32+
return event; // No need to attach screenshot of feedback form.
33+
}
34+
3335
final beforeScreenshot = _options.beforeScreenshot;
3436
if (beforeScreenshot != null) {
3537
try {
@@ -75,14 +77,15 @@ class ScreenshotEventProcessor implements EventProcessor {
7577
return event;
7678
}
7779

78-
final bytes = await _createScreenshot();
80+
final bytes = await createScreenshot();
7981
if (bytes != null) {
8082
hint.screenshot = SentryAttachment.fromScreenshotData(bytes);
8183
}
8284
return event;
8385
}
8486

85-
Future<Uint8List?> _createScreenshot() async {
87+
@internal
88+
Future<Uint8List?> createScreenshot() async {
8689
try {
8790
final renderObject =
8891
sentryScreenshotWidgetGlobalKey.currentContext?.findRenderObject();

flutter/lib/src/feedback/sentry_feedback_widget.dart

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class SentryFeedbackWidget extends StatefulWidget {
2121
this.isRequiredLabel = '(required)',
2222
this.isNameRequired = false,
2323
this.isEmailRequired = false,
24+
this.screenshot,
2425
}) : assert(associatedEventId != const SentryId.empty()),
2526
_hub = hub ?? HubAdapter();
2627

@@ -45,6 +46,8 @@ class SentryFeedbackWidget extends StatefulWidget {
4546
final bool isNameRequired;
4647
final bool isEmailRequired;
4748

49+
final SentryAttachment? screenshot;
50+
4851
@override
4952
_SentryFeedbackWidgetState createState() => _SentryFeedbackWidgetState();
5053
}
@@ -197,7 +200,12 @@ class _SentryFeedbackWidgetState extends State<SentryFeedbackWidget> {
197200
name: _nameController.text,
198201
associatedEventId: widget.associatedEventId,
199202
);
200-
await _captureFeedback(feedback);
203+
Hint? hint;
204+
final screenshot = widget.screenshot;
205+
if (screenshot != null) {
206+
hint = Hint.withScreenshot(screenshot);
207+
}
208+
await _captureFeedback(feedback, hint);
201209

202210
bool mounted;
203211
try {
@@ -246,7 +254,7 @@ class _SentryFeedbackWidgetState extends State<SentryFeedbackWidget> {
246254
return null;
247255
}
248256

249-
Future<SentryId> _captureFeedback(SentryFeedback feedback) {
250-
return widget._hub.captureFeedback(feedback);
257+
Future<SentryId> _captureFeedback(SentryFeedback feedback, Hint? hint) {
258+
return widget._hub.captureFeedback(feedback, hint: hint);
251259
}
252260
}

flutter/lib/src/screenshot/sentry_screenshot_widget.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ class SentryScreenshotWidget extends StatefulWidget {
2828

2929
@override
3030
_SentryScreenshotWidgetState createState() => _SentryScreenshotWidgetState();
31+
32+
/// This is true when the [SentryScreenshotWidget] is in the widget tree.
33+
static bool get isMounted =>
34+
sentryScreenshotWidgetGlobalKey.currentContext != null;
3135
}
3236

3337
class _SentryScreenshotWidgetState extends State<SentryScreenshotWidget> {

flutter/lib/src/sentry_flutter.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'event_processor/android_platform_exception_event_processor.dart';
99
import 'event_processor/flutter_enricher_event_processor.dart';
1010
import 'event_processor/flutter_exception_event_processor.dart';
1111
import 'event_processor/platform_exception_event_processor.dart';
12+
import 'event_processor/screenshot_event_processor.dart';
1213
import 'event_processor/url_filter/url_filter_event_processor.dart';
1314
import 'event_processor/widget_event_processor.dart';
1415
import 'file_system_transport.dart';
@@ -284,6 +285,36 @@ mixin SentryFlutter {
284285
}
285286
}
286287

288+
/// Uses [SentryScreenshotWidget] to capture the current screen as a
289+
/// [SentryAttachment].
290+
static Future<SentryAttachment?> captureScreenshot() async {
291+
// ignore: invalid_use_of_internal_member
292+
final options = Sentry.currentHub.options;
293+
if (!SentryScreenshotWidget.isMounted) {
294+
options.logger(
295+
SentryLevel.debug,
296+
'SentryScreenshotWidget could not be found in the widget tree.',
297+
);
298+
return null;
299+
}
300+
final processors =
301+
options.eventProcessors.whereType<ScreenshotEventProcessor>();
302+
if (processors.isEmpty) {
303+
options.logger(
304+
SentryLevel.debug,
305+
'ScreenshotEventProcessor could not be found.',
306+
);
307+
return null;
308+
}
309+
final processor = processors.first;
310+
final bytes = await processor.createScreenshot();
311+
if (bytes != null) {
312+
return SentryAttachment.fromScreenshotData(bytes);
313+
} else {
314+
return null;
315+
}
316+
}
317+
287318
@internal
288319
static SentryNativeBinding? get native => _native;
289320

flutter/test/event_processor/screenshot_event_processor_test.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,36 @@ void main() {
9696
added: true, isWeb: false, expectedMaxWidthOrHeight: widthOrHeight);
9797
});
9898

99+
testWidgets('does not add screenshot for feedback events', (tester) async {
100+
await tester.runAsync(() async {
101+
final sut = fixture.getSut(null, false);
102+
103+
await tester.pumpWidget(
104+
SentryScreenshotWidget(
105+
child: Text('Catching Pokémon is a snap!',
106+
textDirection: TextDirection.ltr),
107+
),
108+
);
109+
110+
final feedback = SentryFeedback(
111+
message: 'message',
112+
contactEmail: '[email protected]',
113+
name: 'Joe Dirt',
114+
associatedEventId: null,
115+
);
116+
final feedbackEvent = SentryEvent(
117+
type: 'feedback',
118+
contexts: Contexts(feedback: feedback),
119+
level: SentryLevel.info,
120+
);
121+
122+
final hint = Hint();
123+
await sut.apply(feedbackEvent, hint);
124+
125+
expect(hint.screenshot, isNull);
126+
});
127+
});
128+
99129
group('beforeScreenshot', () {
100130
testWidgets('does add screenshot if beforeScreenshot returns true',
101131
(tester) async {

flutter/test/feedback/sentry_feedback_widget_test.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,44 @@ void main() {
104104
fixture = Fixture();
105105
});
106106

107+
testWidgets('does add screenshot attachment to hint', (tester) async {
108+
final screenshot = SentryAttachment.fromIntList([0, 0, 0, 0], 'test.png');
109+
110+
await fixture.pumpFeedbackWidget(
111+
tester,
112+
(hub) => SentryFeedbackWidget(
113+
hub: hub,
114+
screenshot: screenshot,
115+
),
116+
);
117+
118+
when(fixture.hub.captureFeedback(
119+
any,
120+
hint: anyNamed('hint'),
121+
withScope: anyNamed('withScope'),
122+
)).thenAnswer(
123+
(_) async => SentryId.fromId('1988bb1b6f0d4c509e232f0cb9aaeaea'));
124+
125+
await tester.enterText(
126+
find.byKey(ValueKey('sentry_feedback_name_textfield')),
127+
"fixture-name");
128+
await tester.enterText(
129+
find.byKey(ValueKey('sentry_feedback_email_textfield')),
130+
"fixture-email");
131+
await tester.enterText(
132+
find.byKey(ValueKey('sentry_feedback_message_textfield')),
133+
"fixture-message");
134+
await tester.tap(find.text('Send Bug Report'));
135+
await tester.pumpAndSettle();
136+
137+
verify(fixture.hub.captureFeedback(
138+
any,
139+
hint: argThat(predicate<Hint>((hint) => hint.screenshot == screenshot),
140+
named: 'hint'),
141+
withScope: anyNamed('withScope'),
142+
)).called(1);
143+
});
144+
107145
testWidgets('does call hub captureFeedback on submit', (tester) async {
108146
await fixture.pumpFeedbackWidget(
109147
tester,

min_version_test/lib/main.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ class _MyHomePageState extends State<MyHomePage> {
144144
Text(
145145
'$_counter',
146146
// ignore: deprecated_member_use
147-
style: Theme.of(context).textTheme.headline4,
147+
style: Theme.of(context).textTheme.headlineMedium,
148148
),
149149
],
150150
),

0 commit comments

Comments
 (0)