Skip to content

Commit b728df4

Browse files
denrasemarandaneto
andauthored
Feat: Screenshot Attachment (#1088)
Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com> Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com>
1 parent 3a69405 commit b728df4

24 files changed

+565
-20
lines changed

.github/workflows/flutter.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ jobs:
8181
if: runner.os == 'Linux'
8282
run: |
8383
cd flutter
84-
flutter test --platform chrome --test-randomize-ordering-seed=random
84+
flutter test --platform chrome --test-randomize-ordering-seed=random --exclude-tags canvasKit
85+
flutter test --platform chrome --test-randomize-ordering-seed=random --tags canvasKit --web-renderer canvaskit
8586
8687
- name: Test VM with coverage
8788
if: runner.os != 'macOS'

.github/workflows/web-example-ghpages.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
with:
1919
workingDir: flutter/example
2020
customArgs: --source-maps
21+
webRenderer: canvaskit
2122

2223
- name: Upload source maps
2324
run: |

CHANGELOG.md

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

33
## Unreleased
44

5+
### Features
6+
7+
- Feat: Screenshot Attachment ([#1088](https://github.com/getsentry/sentry-dart/pull/1088))
8+
59
### Fixes
610

711
- Merging of integrations and packages ([#1111](https://github.com/getsentry/sentry-dart/pull/1111))

dart/lib/sentry_private.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// attachments
2+
// ignore: invalid_export_of_internal_element
3+
export 'src/sentry_client_attachment_processor.dart';

dart/lib/src/sentry_attachment/sentry_attachment.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ class SentryAttachment {
8383
addToTransactions: addToTransactions,
8484
);
8585

86+
SentryAttachment.fromScreenshotData(Uint8List bytes)
87+
: this.fromUint8List(bytes, 'screenshot.png',
88+
contentType: 'image/png',
89+
attachmentType: SentryAttachment.typeAttachmentDefault);
90+
8691
/// Attachment type.
8792
/// Should be one of types given in [AttachmentType].
8893
final String attachmentType;

dart/lib/src/sentry_client.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import 'sentry_envelope.dart';
1919
import 'client_reports/client_report_recorder.dart';
2020
import 'client_reports/discard_reason.dart';
2121
import 'transport/data_category.dart';
22+
import 'sentry_client_attachment_processor.dart';
2223

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

3839
SentryStackTraceFactory get _stackTraceFactory => _options.stackTraceFactory;
3940

41+
SentryClientAttachmentProcessor get _clientAttachmentProcessor =>
42+
_options.clientAttachmentProcessor;
43+
4044
/// Instantiates a client using [SentryOptions]
4145
factory SentryClient(SentryOptions options) {
4246
if (options.sendClientReports) {
@@ -130,12 +134,15 @@ class SentryClient {
130134
preparedEvent = _eventWithRemovedBreadcrumbsIfHandled(preparedEvent);
131135
}
132136

137+
final attachments = await _clientAttachmentProcessor.processAttachments(
138+
scope?.attachments ?? [], preparedEvent);
139+
133140
final envelope = SentryEnvelope.fromEvent(
134141
preparedEvent,
135142
_options.sdk,
136143
dsn: _options.dsn,
137144
traceContext: scope?.span?.traceContext(),
138-
attachments: scope?.attachments,
145+
attachments: attachments.isNotEmpty ? attachments : null,
139146
);
140147

141148
final id = await captureEnvelope(envelope);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'dart:async';
2+
3+
import 'package:meta/meta.dart';
4+
5+
import './sentry_attachment/sentry_attachment.dart';
6+
import './protocol/sentry_event.dart';
7+
8+
@internal
9+
class SentryClientAttachmentProcessor {
10+
Future<List<SentryAttachment>> processAttachments(
11+
List<SentryAttachment> attachments, SentryEvent event) async {
12+
return attachments;
13+
}
14+
}

dart/lib/src/sentry_options.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:meta/meta.dart';
55
import 'package:http/http.dart';
66

77
import '../sentry.dart';
8+
import '../sentry_private.dart';
89
import 'client_reports/client_report_recorder.dart';
910
import 'client_reports/noop_client_report_recorder.dart';
1011
import 'sentry_exception_factory.dart';
@@ -354,6 +355,10 @@ class SentryOptions {
354355
@internal
355356
late SentryStackTraceFactory stackTraceFactory =
356357
SentryStackTraceFactory(this);
358+
359+
@internal
360+
late SentryClientAttachmentProcessor clientAttachmentProcessor =
361+
SentryClientAttachmentProcessor();
357362
}
358363

359364
/// This function is called with an SDK specific event object and can return a modified event

dart/test/mocks.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22

33
import 'package:sentry/sentry.dart';
4+
import 'package:sentry/sentry_private.dart';
45
import 'package:sentry/src/transport/rate_limiter.dart';
56

67
final fakeDsn = 'https://abc@def.ingest.sentry.io/1234567';
@@ -160,3 +161,23 @@ class MockRateLimiter implements RateLimiter {
160161
this.errorCode = errorCode;
161162
}
162163
}
164+
165+
enum MockAttachmentProcessorMode { filter, add }
166+
167+
/// Filtering out all attachments.
168+
class MockAttachmentProcessor implements SentryClientAttachmentProcessor {
169+
MockAttachmentProcessorMode mode;
170+
171+
MockAttachmentProcessor(this.mode);
172+
173+
@override
174+
Future<List<SentryAttachment>> processAttachments(
175+
List<SentryAttachment> attachments, SentryEvent event) async {
176+
switch (mode) {
177+
case MockAttachmentProcessorMode.filter:
178+
return <SentryAttachment>[];
179+
case MockAttachmentProcessorMode.add:
180+
return <SentryAttachment>[SentryAttachment.fromIntList([], "added")];
181+
}
182+
}
183+
}

dart/test/sentry_attachment_test.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,15 @@ void main() {
159159

160160
expect(attachment.addToTransactions, true);
161161
});
162+
163+
test('fromScreenshotData', () async {
164+
final attachment =
165+
SentryAttachment.fromScreenshotData(Uint8List.fromList([0, 0, 0, 0]));
166+
expect(attachment.attachmentType, SentryAttachment.typeAttachmentDefault);
167+
expect(attachment.contentType, 'image/png');
168+
expect(attachment.filename, 'screenshot.png');
169+
expect(attachment.addToTransactions, false);
170+
});
162171
});
163172
}
164173

dart/test/sentry_client_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22
import 'dart:convert';
33
import 'dart:typed_data';
44

5+
import 'package:collection/collection.dart';
56
import 'package:sentry/sentry.dart';
67
import 'package:sentry/src/client_reports/client_report.dart';
78
import 'package:sentry/src/client_reports/discard_reason.dart';
@@ -1043,6 +1044,45 @@ void main() {
10431044
});
10441045
});
10451046

1047+
group('SentryClientAttachmentProcessor', () {
1048+
late Fixture fixture;
1049+
1050+
setUp(() {
1051+
fixture = Fixture();
1052+
});
1053+
1054+
test('processor filtering out attachments', () async {
1055+
fixture.options.clientAttachmentProcessor =
1056+
MockAttachmentProcessor(MockAttachmentProcessorMode.filter);
1057+
final scope = Scope(fixture.options);
1058+
scope.addAttachment(SentryAttachment.fromIntList([], "scope-attachment"));
1059+
final sut = fixture.getSut();
1060+
1061+
final event = SentryEvent();
1062+
await sut.captureEvent(event, scope: scope);
1063+
1064+
final capturedEnvelope = (fixture.transport).envelopes.first;
1065+
final attachmentItem = capturedEnvelope.items.firstWhereOrNull(
1066+
(element) => element.header.type == SentryItemType.attachment);
1067+
expect(attachmentItem, null);
1068+
});
1069+
1070+
test('processor adding attachments', () async {
1071+
fixture.options.clientAttachmentProcessor =
1072+
MockAttachmentProcessor(MockAttachmentProcessorMode.add);
1073+
final scope = Scope(fixture.options);
1074+
final sut = fixture.getSut();
1075+
1076+
final event = SentryEvent();
1077+
await sut.captureEvent(event, scope: scope);
1078+
1079+
final capturedEnvelope = (fixture.transport).envelopes.first;
1080+
final attachmentItem = capturedEnvelope.items.firstWhereOrNull(
1081+
(element) => element.header.type == SentryItemType.attachment);
1082+
expect(attachmentItem != null, true);
1083+
});
1084+
});
1085+
10461086
group('ClientReportRecorder', () {
10471087
late Fixture fixture;
10481088

flutter/example/lib/main.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,19 @@ Future<void> main() async {
3131
options.attachThreads = true;
3232
options.enableWindowMetricBreadcrumbs = true;
3333
options.addIntegration(LoggingIntegration());
34+
options.attachScreenshot = true;
3435
// We can enable Sentry debug logging during development. This is likely
3536
// going to log too much for your app, but can be useful when figuring out
3637
// configuration issues, e.g. finding out why your events are not uploaded.
3738
options.debug = true;
3839
},
3940
// Init your App.
4041
appRunner: () => runApp(
41-
DefaultAssetBundle(
42-
bundle: SentryAssetBundle(enableStructuredDataTracing: true),
43-
child: MyApp(),
42+
SentryScreenshotWidget(
43+
child: DefaultAssetBundle(
44+
bundle: SentryAssetBundle(enableStructuredDataTracing: true),
45+
child: MyApp(),
46+
),
4447
),
4548
),
4649
);

flutter/lib/sentry_flutter.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export 'src/sentry_flutter_options.dart';
88
export 'src/flutter_sentry_attachment.dart';
99
export 'src/sentry_asset_bundle.dart';
1010
export 'src/integrations/on_error_integration.dart';
11+
export 'src/screenshot/sentry_screenshot_widget.dart';

flutter/lib/src/event_processor/flutter_enricher_event_processor.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
66
import 'package:sentry/sentry.dart';
77

88
import '../binding_utils.dart';
9-
import '../renderer/renderer.dart';
109
import '../sentry_flutter_options.dart';
1110

1211
typedef WidgetBindingGetter = WidgetsBinding? Function();
@@ -155,7 +154,7 @@ class FlutterEnricherEventProcessor extends EventProcessor {
155154
// Also always fails in tests.
156155
// See https://github.com/flutter/flutter/issues/83919
157156
// 'window_is_visible': _window.viewConfiguration.visible,
158-
'renderer': getRendererAsString()
157+
'renderer': _options.rendererWrapper.getRendererAsString()
159158
};
160159
}
161160

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'dart:async';
2+
3+
import 'package:sentry/sentry.dart';
4+
import 'package:sentry/sentry_private.dart';
5+
import '../screenshot/screenshot_attachment_processor.dart';
6+
import '../sentry_flutter_options.dart';
7+
8+
/// Adds [ScreenshotAttachmentProcessor] to options if [attachScreenshot] is true
9+
class ScreenshotIntegration implements Integration<SentryFlutterOptions> {
10+
SentryFlutterOptions? _options;
11+
12+
@override
13+
FutureOr<void> call(Hub hub, SentryFlutterOptions options) {
14+
if (options.attachScreenshot) {
15+
// ignore: invalid_use_of_internal_member
16+
options.clientAttachmentProcessor =
17+
ScreenshotAttachmentProcessor(options);
18+
_options = options;
19+
20+
options.sdk.addIntegration('screenshotIntegration');
21+
}
22+
}
23+
24+
@override
25+
FutureOr<void> close() {
26+
// ignore: invalid_use_of_internal_member
27+
_options?.clientAttachmentProcessor = SentryClientAttachmentProcessor();
28+
}
29+
}

flutter/lib/src/renderer/renderer.dart

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
1+
import 'package:meta/meta.dart';
2+
13
import 'unknown_renderer.dart'
24
if (dart.library.html) 'html_renderer.dart'
35
if (dart.library.io) 'io_renderer.dart' as implementation;
46

5-
FlutterRenderer getRenderer() => implementation.getRenderer();
7+
@internal
8+
class RendererWrapper {
9+
FlutterRenderer getRenderer() {
10+
return implementation.getRenderer();
11+
}
612

7-
String getRendererAsString() {
8-
switch (getRenderer()) {
9-
case FlutterRenderer.skia:
10-
return 'Skia';
11-
case FlutterRenderer.canvasKit:
12-
return 'CanvasKit';
13-
case FlutterRenderer.html:
14-
return 'HTML';
15-
case FlutterRenderer.unknown:
16-
return 'Unknown';
13+
String getRendererAsString() {
14+
switch (getRenderer()) {
15+
case FlutterRenderer.skia:
16+
return 'Skia';
17+
case FlutterRenderer.canvasKit:
18+
return 'CanvasKit';
19+
case FlutterRenderer.html:
20+
return 'HTML';
21+
case FlutterRenderer.unknown:
22+
return 'Unknown';
23+
}
1724
}
1825
}
1926

0 commit comments

Comments
 (0)