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 all 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
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 {
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
denrase marked this conversation as resolved.
Show resolved Hide resolved
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()
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
};
}

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> {
denrase marked this conversation as resolved.
Show resolved Hide resolved
SentryFlutterOptions? _options;

@override
FutureOr<void> call(Hub hub, SentryFlutterOptions options) {
denrase marked this conversation as resolved.
Show resolved Hide resolved
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