Skip to content

Commit

Permalink
Feat: Screenshot Attachment (#1088)
Browse files Browse the repository at this point in the history
Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com>
Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 9, 2022
1 parent 3a69405 commit b728df4
Show file tree
Hide file tree
Showing 24 changed files with 565 additions and 20 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/flutter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ jobs:
if: runner.os == 'Linux'
run: |
cd flutter
flutter test --platform chrome --test-randomize-ordering-seed=random
flutter test --platform chrome --test-randomize-ordering-seed=random --exclude-tags canvasKit
flutter test --platform chrome --test-randomize-ordering-seed=random --tags canvasKit --web-renderer canvaskit
- name: Test VM with coverage
if: runner.os != 'macOS'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/web-example-ghpages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
with:
workingDir: flutter/example
customArgs: --source-maps
webRenderer: canvaskit

- name: Upload source maps
run: |
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Feat: Screenshot Attachment ([#1088](https://github.com/getsentry/sentry-dart/pull/1088))

### Fixes

- Merging of integrations and packages ([#1111](https://github.com/getsentry/sentry-dart/pull/1111))
Expand Down
3 changes: 3 additions & 0 deletions dart/lib/sentry_private.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// attachments
// ignore: invalid_export_of_internal_element
export 'src/sentry_client_attachment_processor.dart';
5 changes: 5 additions & 0 deletions dart/lib/src/sentry_attachment/sentry_attachment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ class SentryAttachment {
addToTransactions: addToTransactions,
);

SentryAttachment.fromScreenshotData(Uint8List bytes)
: this.fromUint8List(bytes, 'screenshot.png',
contentType: 'image/png',
attachmentType: SentryAttachment.typeAttachmentDefault);

/// Attachment type.
/// Should be one of types given in [AttachmentType].
final String attachmentType;
Expand Down
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
14 changes: 14 additions & 0 deletions dart/lib/src/sentry_client_attachment_processor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'dart:async';

import 'package:meta/meta.dart';

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

@internal
class SentryClientAttachmentProcessor {
Future<List<SentryAttachment>> processAttachments(
List<SentryAttachment> attachments, SentryEvent event) async {
return attachments;
}
}
5 changes: 5 additions & 0 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:meta/meta.dart';
import 'package:http/http.dart';

import '../sentry.dart';
import '../sentry_private.dart';
import 'client_reports/client_report_recorder.dart';
import 'client_reports/noop_client_report_recorder.dart';
import 'sentry_exception_factory.dart';
Expand Down Expand Up @@ -354,6 +355,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
21 changes: 21 additions & 0 deletions dart/test/mocks.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:sentry/sentry.dart';
import 'package:sentry/sentry_private.dart';
import 'package:sentry/src/transport/rate_limiter.dart';

final fakeDsn = 'https://abc@def.ingest.sentry.io/1234567';
Expand Down Expand Up @@ -160,3 +161,23 @@ class MockRateLimiter implements RateLimiter {
this.errorCode = errorCode;
}
}

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")];
}
}
}
9 changes: 9 additions & 0 deletions dart/test/sentry_attachment_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ void main() {

expect(attachment.addToTransactions, true);
});

test('fromScreenshotData', () async {
final attachment =
SentryAttachment.fromScreenshotData(Uint8List.fromList([0, 0, 0, 0]));
expect(attachment.attachmentType, SentryAttachment.typeAttachmentDefault);
expect(attachment.contentType, 'image/png');
expect(attachment.filename, 'screenshot.png');
expect(attachment.addToTransactions, false);
});
});
}

Expand Down
40 changes: 40 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
9 changes: 6 additions & 3 deletions flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,19 @@ Future<void> main() async {
options.attachThreads = true;
options.enableWindowMetricBreadcrumbs = true;
options.addIntegration(LoggingIntegration());
options.attachScreenshot = true;
// We can enable Sentry debug logging during development. This is likely
// going to log too much for your app, but can be useful when figuring out
// configuration issues, e.g. finding out why your events are not uploaded.
options.debug = true;
},
// Init your App.
appRunner: () => runApp(
DefaultAssetBundle(
bundle: SentryAssetBundle(enableStructuredDataTracing: true),
child: MyApp(),
SentryScreenshotWidget(
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/screenshot/sentry_screenshot_widget.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
import 'package:sentry/sentry.dart';

import '../binding_utils.dart';
import '../renderer/renderer.dart';
import '../sentry_flutter_options.dart';

typedef WidgetBindingGetter = WidgetsBinding? Function();
Expand Down Expand Up @@ -155,7 +154,7 @@ class FlutterEnricherEventProcessor extends EventProcessor {
// Also always fails in tests.
// See https://github.com/flutter/flutter/issues/83919
// 'window_is_visible': _window.viewConfiguration.visible,
'renderer': getRendererAsString()
'renderer': _options.rendererWrapper.getRendererAsString()
};
}

Expand Down
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:sentry/sentry.dart';
import 'package:sentry/sentry_private.dart';
import '../screenshot/screenshot_attachment_processor.dart';
import '../sentry_flutter_options.dart';

/// Adds [ScreenshotAttachmentProcessor] to options if [attachScreenshot] is true
class ScreenshotIntegration implements Integration<SentryFlutterOptions> {
SentryFlutterOptions? _options;

@override
FutureOr<void> call(Hub hub, SentryFlutterOptions options) {
if (options.attachScreenshot) {
// ignore: invalid_use_of_internal_member
options.clientAttachmentProcessor =
ScreenshotAttachmentProcessor(options);
_options = options;

options.sdk.addIntegration('screenshotIntegration');
}
}

@override
FutureOr<void> close() {
// ignore: invalid_use_of_internal_member
_options?.clientAttachmentProcessor = SentryClientAttachmentProcessor();
}
}
29 changes: 18 additions & 11 deletions flutter/lib/src/renderer/renderer.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import 'package:meta/meta.dart';

import 'unknown_renderer.dart'
if (dart.library.html) 'html_renderer.dart'
if (dart.library.io) 'io_renderer.dart' as implementation;

FlutterRenderer getRenderer() => implementation.getRenderer();
@internal
class RendererWrapper {
FlutterRenderer getRenderer() {
return implementation.getRenderer();
}

String getRendererAsString() {
switch (getRenderer()) {
case FlutterRenderer.skia:
return 'Skia';
case FlutterRenderer.canvasKit:
return 'CanvasKit';
case FlutterRenderer.html:
return 'HTML';
case FlutterRenderer.unknown:
return 'Unknown';
String getRendererAsString() {
switch (getRenderer()) {
case FlutterRenderer.skia:
return 'Skia';
case FlutterRenderer.canvasKit:
return 'CanvasKit';
case FlutterRenderer.html:
return 'HTML';
case FlutterRenderer.unknown:
return 'Unknown';
}
}
}

Expand Down
Loading

0 comments on commit b728df4

Please sign in to comment.