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

Feat: Screenshot Attachment #1088

Merged
merged 60 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
ab120a9
add SentryClientAttachmentProcessor to sentry dart
denrase Oct 24, 2022
12a0789
process attachments in sentry client
denrase Oct 24, 2022
490f68e
add missing import
denrase Oct 24, 2022
34b6b1f
add sentry widget and screenshot integration + processor + attachment
denrase Oct 24, 2022
e6e2afe
export sentry_widget
denrase Oct 24, 2022
6865a56
format
denrase Oct 24, 2022
4d5b3c0
fix runtime issues
denrase Oct 24, 2022
e72e124
fix documentation
denrase Oct 24, 2022
f35689f
test new behaviour in client
denrase Oct 25, 2022
33aa55c
test screenshot integration
denrase Oct 25, 2022
4a4b0df
test properties
denrase Oct 25, 2022
0c43605
create screenshot in processor
denrase Oct 25, 2022
e477d14
add changelog entry
denrase Oct 25, 2022
a0a094a
add opt-in to options for screenshot feature
denrase Oct 25, 2022
14b157f
add comment
denrase Oct 25, 2022
ce8eb33
move property to flutter options
denrase Oct 25, 2022
9c9d5f0
enable screenshots in example app
denrase Oct 25, 2022
693b5ce
Merge branch 'main' into feat/screenshot_atachment
denrase Oct 25, 2022
860ff84
rename widget
denrase Oct 25, 2022
8eb3400
Merge branch 'main' into feat/screenshot_atachment
denrase Oct 25, 2022
278be0d
add features section
denrase Oct 25, 2022
760a472
run format
denrase Oct 25, 2022
fca116e
mark attahcment processor as internal
denrase Oct 25, 2022
6efd03f
Merge branch 'main' into feat/screenshot_atachment
denrase Nov 7, 2022
1065bb9
Merge branch 'main' into feat/screenshot_atachment
marandaneto Nov 7, 2022
7e1acd0
Merge branch 'feat/screenshot_atachment' of github.com:getsentry/sent…
marandaneto Nov 7, 2022
50915e1
fix attachment.addToTransactions expectation
denrase Nov 7, 2022
4f3f384
Merge branch 'feat/screenshot_atachment' of github.com:getsentry/sent…
denrase Nov 7, 2022
e8d5e4a
move mock to mocks.dart
denrase Nov 7, 2022
6f59058
change naming of global key
denrase Nov 7, 2022
5a1f1ec
return just attachments if completer fails for some reason
denrase Nov 7, 2022
27bf210
no need to create new list
denrase Nov 7, 2022
3bc46a7
create sentry_private export file
marandaneto Nov 7, 2022
97bf0d9
use sut param
denrase Nov 7, 2022
3d681cc
Merge branch 'feat/screenshot_atachment' of github.com:getsentry/sent…
denrase Nov 7, 2022
47024c9
Merge branch 'main' into feat/screenshot_atachment
denrase Nov 7, 2022
d0045a1
also install screenshot integration on window, linux & fuchsia
denrase Nov 7, 2022
fe0b533
only install screenshots integration with skia and canvaskit renderer
denrase Nov 7, 2022
0018e50
test screenshot creation for renderers
denrase Nov 7, 2022
e70240e
local var for readability
denrase Nov 7, 2022
f8f60c9
remove unused import
denrase Nov 7, 2022
24bc74f
align test setup/dependencies
denrase Nov 7, 2022
0e7a5f4
move screenshot integration to own grooup
denrase Nov 7, 2022
2ab5321
only run screenshot attachment test on vm
denrase Nov 8, 2022
670c2e2
run tests on vm
denrase Nov 8, 2022
bf1c212
update comment regarding rendering
denrase Nov 8, 2022
675da74
remove cp comment
denrase Nov 8, 2022
63c8976
when run in web, only run this test with canvasKit renderer
denrase Nov 8, 2022
07ce1e1
remove screenshot integration from native expectations
denrase Nov 8, 2022
c6cc51c
remove attach screenshot param
denrase Nov 8, 2022
05c3680
check image resolutiona and byte count
denrase Nov 8, 2022
0c943a5
log if renderer is not supported
denrase Nov 8, 2022
e640a40
dont use addPostFrameCallback
denrase Nov 8, 2022
518fe65
Merge branch 'main' into feat/screenshot_atachment
denrase Nov 8, 2022
ad03524
remove unused dependency
denrase Nov 8, 2022
43f977b
remove unused imports
denrase Nov 8, 2022
be87aa7
Merge branch 'main' into feat/screenshot_atachment
marandaneto Nov 9, 2022
16dede7
fixes
marandaneto Nov 9, 2022
f0392b4
add comments
marandaneto Nov 9, 2022
1b68c67
Merge branch 'main' into feat/screenshot_atachment
marandaneto Nov 9, 2022
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
2 changes: 2 additions & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ export 'src/sentry_user_feedback.dart';
export 'src/utils/tracing_utils.dart';
// tracing
export 'src/tracing.dart';
// attachments
export 'src/sentry_client_attachment_processor.dart';
9 changes: 8 additions & 1 deletion dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'sentry_envelope.dart';
import 'client_reports/client_report_recorder.dart';
import 'client_reports/discard_reason.dart';
import 'transport/data_category.dart';
import 'sentry_client_attachment_processor.dart';

/// Default value for [User.ipAddress]. It gets set when an event does not have
/// a user and IP address. Only applies if [SentryOptions.sendDefaultPii] is set
Expand All @@ -37,6 +38,9 @@ class SentryClient {

SentryStackTraceFactory get _stackTraceFactory => _options.stackTraceFactory;

SentryClientAttachmentProcessor get _clientAttachmentProcessor =>
_options.clientAttachmentProcessor;

/// Instantiates a client using [SentryOptions]
factory SentryClient(SentryOptions options) {
if (options.sendClientReports) {
Expand Down Expand Up @@ -130,12 +134,15 @@ class SentryClient {
preparedEvent = _eventWithRemovedBreadcrumbsIfHandled(preparedEvent);
}

final attachments = await _clientAttachmentProcessor.processAttachments(
scope?.attachments ?? [], preparedEvent);

final envelope = SentryEnvelope.fromEvent(
preparedEvent,
_options.sdk,
dsn: _options.dsn,
traceContext: scope?.span?.traceContext(),
attachments: scope?.attachments,
attachments: attachments.isNotEmpty ? attachments : null,
);

final id = await captureEnvelope(envelope);
Expand Down
11 changes: 11 additions & 0 deletions dart/lib/src/sentry_client_attachment_processor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'dart:async';

import './sentry_attachment/sentry_attachment.dart';
import './protocol/sentry_event.dart';

class SentryClientAttachmentProcessor {
Future<List<SentryAttachment>> processAttachments(
List<SentryAttachment> attachments, SentryEvent event) async {
return attachments;
}
}
4 changes: 4 additions & 0 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,10 @@ class SentryOptions {
@internal
late SentryStackTraceFactory stackTraceFactory =
SentryStackTraceFactory(this);

@internal
late SentryClientAttachmentProcessor clientAttachmentProcessor =
SentryClientAttachmentProcessor();
}

/// This function is called with an SDK specific event object and can return a modified event
Expand Down
60 changes: 60 additions & 0 deletions dart/test/sentry_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:collection/collection.dart';
import 'package:sentry/sentry.dart';
import 'package:sentry/src/client_reports/client_report.dart';
import 'package:sentry/src/client_reports/discard_reason.dart';
Expand Down Expand Up @@ -1043,6 +1044,45 @@ void main() {
});
});

group('SentryClientAttachmentProcessor', () {
late Fixture fixture;

setUp(() {
fixture = Fixture();
});

test('processor filtering out attachments', () async {
fixture.options.clientAttachmentProcessor =
MockAttachmentProcessor(MockAttachmentProcessorMode.filter);
final scope = Scope(fixture.options);
scope.addAttachment(SentryAttachment.fromIntList([], "scope-attachment"));
final sut = fixture.getSut();

final event = SentryEvent();
await sut.captureEvent(event, scope: scope);

final capturedEnvelope = (fixture.transport).envelopes.first;
final attachmentItem = capturedEnvelope.items.firstWhereOrNull(
(element) => element.header.type == SentryItemType.attachment);
expect(attachmentItem, null);
});

test('processor adding attachments', () async {
fixture.options.clientAttachmentProcessor =
MockAttachmentProcessor(MockAttachmentProcessorMode.add);
final scope = Scope(fixture.options);
final sut = fixture.getSut();

final event = SentryEvent();
await sut.captureEvent(event, scope: scope);

final capturedEnvelope = (fixture.transport).envelopes.first;
final attachmentItem = capturedEnvelope.items.firstWhereOrNull(
(element) => element.header.type == SentryItemType.attachment);
expect(attachmentItem != null, true);
});
});

group('ClientReportRecorder', () {
late Fixture fixture;

Expand Down Expand Up @@ -1341,3 +1381,23 @@ class Fixture {
loggedException = exception;
}
}

enum MockAttachmentProcessorMode { filter, add }

/// Filtering out all attachments.
class MockAttachmentProcessor implements SentryClientAttachmentProcessor {
MockAttachmentProcessorMode mode;

MockAttachmentProcessor(this.mode);

@override
Future<List<SentryAttachment>> processAttachments(
List<SentryAttachment> attachments, SentryEvent event) async {
switch (mode) {
case MockAttachmentProcessorMode.filter:
return <SentryAttachment>[];
case MockAttachmentProcessorMode.add:
return <SentryAttachment>[SentryAttachment.fromIntList([], "added")];
}
}
}
8 changes: 5 additions & 3 deletions flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ Future<void> main() async {
},
// Init your App.
appRunner: () => runApp(
DefaultAssetBundle(
bundle: SentryAssetBundle(enableStructuredDataTracing: true),
child: MyApp(),
SentryWidget(
child: DefaultAssetBundle(
bundle: SentryAssetBundle(enableStructuredDataTracing: true),
child: MyApp(),
),
),
),
);
Expand Down
1 change: 1 addition & 0 deletions flutter/lib/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export 'src/sentry_flutter_options.dart';
export 'src/flutter_sentry_attachment.dart';
export 'src/sentry_asset_bundle.dart';
export 'src/integrations/on_error_integration.dart';
export 'src/sentry_widget.dart';
29 changes: 29 additions & 0 deletions flutter/lib/src/integrations/screenshot_integration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'dart:async';

import 'package:flutter/scheduler.dart';
import 'package:sentry/sentry.dart';
import '../screenshot/screenshot_attachment_processor.dart';
import '../sentry_flutter_options.dart';

class ScreenshotIntegration implements Integration<SentryFlutterOptions> {
SentryFlutterOptions? _options;

@override
FutureOr<void> call(Hub hub, SentryFlutterOptions options) {
// ignore: invalid_use_of_internal_member
options.clientAttachmentProcessor = ScreenshotAttachmentProcessor(() {
try {
/// Flutter >= 2.12 throws if SchedulerBinding.instance isn't initialized.
return SchedulerBinding.instance;
} catch (_) {}
return null;
}, options);
_options = options;
}

@override
FutureOr<void> close() {
// ignore: invalid_use_of_internal_member
_options?.clientAttachmentProcessor = SentryClientAttachmentProcessor();
}
}
63 changes: 63 additions & 0 deletions flutter/lib/src/screenshot/screenshot_attachment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui show ImageByteFormat;

import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:sentry/sentry.dart';

import '../sentry_widget.dart';

class ScreenshotAttachment implements SentryAttachment {
final SchedulerBinding _schedulerBinding;
final SentryOptions _options;

ScreenshotAttachment(this._schedulerBinding, this._options);

@override
String attachmentType = SentryAttachment.typeAttachmentDefault;

@override
String? contentType = 'image/png';

@override
String filename = 'screenshot.png';

@override
bool addToTransactions = true;

@override
FutureOr<Uint8List> get bytes async {
final _completer = Completer<Uint8List?>();
// We add an post frame callback because we aren't able to take a screenshot
// if there's currently a draw in process.
_schedulerBinding.addPostFrameCallback((timeStamp) async {
final screenshot = await _createScreenshot();
_completer.complete(screenshot);
});
return await _completer.future ?? Uint8List.fromList([]);
}

Future<Uint8List?> _createScreenshot() async {
try {
final renderObject =
sentryWidgetGlobalKey.currentContext?.findRenderObject();

if (renderObject is RenderRepaintBoundary) {
final image = await renderObject.toImage(pixelRatio: 1);
// At the time of writing there's no other image format available which
// Sentry understands.
final bytes = await image.toByteData(format: ui.ImageByteFormat.png);
return bytes?.buffer.asUint8List();
}
} catch (exception, stackTrace) {
_options.logger(
SentryLevel.error,
'Could not create screenshot.',
exception: exception,
stackTrace: stackTrace,
);
}
return null;
}
}
36 changes: 36 additions & 0 deletions flutter/lib/src/screenshot/screenshot_attachment_processor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'dart:async';

import 'package:sentry/sentry.dart';
import '../integrations/native_app_start_integration.dart';
import '../sentry_widget.dart';
import 'screenshot_attachment.dart';

class ScreenshotAttachmentProcessor implements SentryClientAttachmentProcessor {
final SchedulerBindingProvider _schedulerBindingProvider;
final SentryOptions _options;

ScreenshotAttachmentProcessor(this._schedulerBindingProvider, this._options);

/// This is true when the SentryWidget is in the view hierarchy
bool get _attachScreenshot => sentryWidgetGlobalKey.currentContext != null;

@override
Future<List<SentryAttachment>> processAttachments(
List<SentryAttachment> attachments, SentryEvent event) async {
if (event.exceptions == null &&
event.throwable == null &&
_attachScreenshot) {
return attachments;
}
final schedulerBinding = _schedulerBindingProvider();
if (schedulerBinding != null) {
final attachmentsWithScreenshot = <SentryAttachment>[];
attachmentsWithScreenshot.addAll(attachments);
attachmentsWithScreenshot
.add(ScreenshotAttachment(schedulerBinding, _options));
return attachmentsWithScreenshot;
} else {
return attachments;
}
}
}
2 changes: 2 additions & 0 deletions flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:meta/meta.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../sentry_flutter.dart';
import 'event_processor/android_platform_exception_event_processor.dart';
import 'integrations/screenshot_integration.dart';
import 'native_scope_observer.dart';
import 'sentry_native.dart';
import 'sentry_native_channel.dart';
Expand Down Expand Up @@ -139,6 +140,7 @@ mixin SentryFlutter {
!platformChecker.isWeb &&
(platform.isAndroid || platform.isIOS || platform.isMacOS)) {
integrations.add(LoadImageListIntegration(channel));
integrations.add(ScreenshotIntegration());
}

integrations.add(DebugPrintIntegration());
Expand Down
38 changes: 38 additions & 0 deletions flutter/lib/src/sentry_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';

/// Key which is used to identify the [RepaintBoundary]
final sentryWidgetGlobalKey = GlobalKey(debugLabel: 'sentry_widget');

/// You can add screenshots of [child] to crash reports by adding this widget.
/// Ideally you are adding it around your app widget like in the following
/// example.
/// ```dart
/// runApp(SentryWidget(child: App()));
/// ```
///
/// Remarks:
/// - Depending on the place where it's used, you might have a transparent
/// background.
/// - Platform Views currently can't be captured.
/// - It only works on Flutters Canvas Kit Web renderer. For more information
/// see https://flutter.dev/docs/development/tools/web-renderers
/// - You can only have one [SentryWidget] widget in your widget tree at all
/// times.
class SentryWidget extends StatefulWidget {
const SentryWidget({Key? key, required this.child}) : super(key: key);

final Widget child;

@override
_SentryWidgetState createState() => _SentryWidgetState();
}

class _SentryWidgetState extends State<SentryWidget> {
@override
Widget build(BuildContext context) {
return RepaintBoundary(
key: sentryWidgetGlobalKey,
child: widget.child,
);
}
}
47 changes: 47 additions & 0 deletions flutter/test/integrations/screenshot_integration_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_flutter/src/integrations/screenshot_integration.dart';
import 'package:sentry_flutter/src/integrations/widgets_binding_integration.dart';
import 'package:sentry_flutter/src/screenshot/screenshot_attachment_processor.dart';

import '../mocks.mocks.dart';

/// Tests that require `WidgetsFlutterBinding.ensureInitialized();` not
/// being called at all.
void main() {
late Fixture fixture;

setUp(() {
fixture = Fixture();
});

test('screenshotIntegration creates screenshot processor', () async {
final integration = ScreenshotIntegration();

await integration(fixture.hub, fixture.options);

expect(
// ignore: invalid_use_of_internal_member
fixture.options.clientAttachmentProcessor
is ScreenshotAttachmentProcessor,
true);
});

test('screenshotIntegration close resets processor', () async {
final integration = ScreenshotIntegration();

await integration(fixture.hub, fixture.options);
await integration.close();

expect(
// ignore: invalid_use_of_internal_member
fixture.options.clientAttachmentProcessor
is ScreenshotAttachmentProcessor,
false);
});
}

class Fixture {
final hub = MockHub();
final options = SentryFlutterOptions();
}