From 3d3d635124df96816c5682e2b525f4f775529453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sun, 20 Jun 2021 16:59:09 +0200 Subject: [PATCH 01/34] Attachment support --- CHANGELOG.md | 2 + dart/lib/attachment.dart | 64 +++++++++++++++++++ dart/lib/sentry.dart | 1 + dart/lib/src/scope.dart | 14 +++- .../_io_scope_extensions.dart | 38 +++++++++++ .../_web_scope_extensions.dart | 7 ++ .../scope_extensions/scope_extensions.dart | 51 +++++++++++++++ dart/lib/src/sentry_client.dart | 11 +++- dart/lib/src/sentry_envelope.dart | 17 ++++- dart/lib/src/sentry_envelope_item.dart | 32 +++++++--- dart/lib/src/sentry_envelope_item_header.dart | 28 ++++---- flutter/example/lib/main.dart | 13 ++++ 12 files changed, 252 insertions(+), 26 deletions(-) create mode 100644 dart/lib/attachment.dart create mode 100644 dart/lib/src/scope_extensions/_io_scope_extensions.dart create mode 100644 dart/lib/src/scope_extensions/_web_scope_extensions.dart create mode 100644 dart/lib/src/scope_extensions/scope_extensions.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 70fc7d470c..c18cf6d8ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ * Feat: Add Culture Context (#491) * Feat: Add DeduplicationEventProcessor (#498) * Feat: Capture failed requests as event (#473) +* Feat: `beforeSend` callback accepts async code (#494) +* Feat: Add support for attachments (#494) ## Breaking Changes: diff --git a/dart/lib/attachment.dart b/dart/lib/attachment.dart new file mode 100644 index 0000000000..717bc9b3d5 --- /dev/null +++ b/dart/lib/attachment.dart @@ -0,0 +1,64 @@ +import 'dart:typed_data'; + +// https://develop.sentry.dev/sdk/envelopes/#attachment +class Attachment { + Attachment({ + required this.content, + required this.fileName, + AttachmentType? type, + String? mimeType, + }) : mimeType = mimeType ?? 'application/octet-stream', + type = type ?? AttachmentType.attachment; + + /// Attachment type. + final AttachmentType type; + + /// Attachment content. + final Uint8List content; + + /// Attachment file name. + final String fileName; + + /// Attachment content type. + final String mimeType; +} + +/// Attachment type. +enum AttachmentType { + /// Standard attachment without special meaning. + attachment, + + /// Minidump file that creates an error event and is symbolicated. + /// The file should start with the MDMP magic bytes. + minidump, + + /// Apple crash report file that creates an error event and is symbolicated. + appleCrashReport, + + /// XML file containing UE4 crash meta data. + /// During event ingestion, event contexts and extra fields are extracted from + /// this file. + unrealContext, + + /// Plain-text log file obtained from UE4 crashes. + /// During event ingestion, the last logs are extracted into event + /// breadcrumbs. + unrealLogs +} + +extension AttachmentTypeX on AttachmentType { + String toSentryIdentifier() { + switch (this) { + case AttachmentType.attachment: + return 'event.attachment'; + case AttachmentType.minidump: + return 'event.minidump'; + case AttachmentType.appleCrashReport: + return 'event.applecrashreport'; + case AttachmentType.unrealContext: + return 'unreal.context'; + case AttachmentType.unrealLogs: + return 'unreal.logs'; + } + } +} diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index d358cea613..bc01773e28 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -22,3 +22,4 @@ export 'src/transport/transport.dart'; export 'src/integration.dart'; export 'src/event_processor.dart'; export 'src/http_client/sentry_http_client.dart'; +export 'src/scope_extensions/scope_extensions.dart'; diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index bd78b14921..6e26d8af86 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -1,5 +1,5 @@ import 'dart:collection'; - +import '../attachment.dart'; import 'event_processor.dart'; import 'protocol.dart'; import 'sentry_options.dart'; @@ -81,6 +81,10 @@ class Scope { final SentryOptions _options; + final List _attachements = []; + + List get attachements => UnmodifiableListView(_attachements); + Scope(this._options); /// Adds a breadcrumb to the breadcrumbs queue @@ -116,6 +120,10 @@ class Scope { _breadcrumbs.add(breadcrumb); } + void addAttachment(Attachment attachment) async { + _attachements.add(attachment); + } + /// Clear all the breadcrumbs void clearBreadcrumbs() { _breadcrumbs.clear(); @@ -285,6 +293,10 @@ class Scope { } }); + for (final attachment in _attachements) { + clone._attachements.add(attachment); + } + return clone; } } diff --git a/dart/lib/src/scope_extensions/_io_scope_extensions.dart b/dart/lib/src/scope_extensions/_io_scope_extensions.dart new file mode 100644 index 0000000000..40c7ed5826 --- /dev/null +++ b/dart/lib/src/scope_extensions/_io_scope_extensions.dart @@ -0,0 +1,38 @@ +import 'dart:io'; + +import '../../attachment.dart'; +import '../scope.dart'; + +extension IoScopeExtensions on Scope { + Future addAttachementPath( + String path, { + AttachmentType? type, + String? mimeType, + }) async { + final file = File(path); + + addAttachment( + Attachment( + type: type, + content: await file.readAsBytes(), + fileName: file.uri.pathSegments.last, + mimeType: mimeType, + ), + ); + } + + Future addAttachementFile( + File file, { + AttachmentType? type, + String? mimeType, + }) async { + addAttachment( + Attachment( + type: type, + content: await file.readAsBytes(), + fileName: file.uri.pathSegments.last, + mimeType: mimeType, + ), + ); + } +} diff --git a/dart/lib/src/scope_extensions/_web_scope_extensions.dart b/dart/lib/src/scope_extensions/_web_scope_extensions.dart new file mode 100644 index 0000000000..abc51e5117 --- /dev/null +++ b/dart/lib/src/scope_extensions/_web_scope_extensions.dart @@ -0,0 +1,7 @@ +import '../scope.dart'; + +extension IoScopeExtensions on Scope { + Future addAttachementPath(String path) async { + // no op + } +} diff --git a/dart/lib/src/scope_extensions/scope_extensions.dart b/dart/lib/src/scope_extensions/scope_extensions.dart new file mode 100644 index 0000000000..e78de25e5e --- /dev/null +++ b/dart/lib/src/scope_extensions/scope_extensions.dart @@ -0,0 +1,51 @@ +export '_io_scope_extensions.dart' + if (dart.library.html) '_web_scope_extensions.dart'; + +import 'dart:typed_data'; + +import '../../attachment.dart'; +import '../scope.dart'; + +extension ScopeExtensions on Scope { + void addAttachmentBytes( + Uint8List bytes, + String fileName, { + String? mimeType, + AttachmentType? type, + }) { + addAttachment(Attachment( + type: type, + content: bytes, + fileName: fileName, + mimeType: mimeType, + )); + } + + void addAttachmentIntList( + List bytes, + String fileName, { + String? mimeType, + AttachmentType? type, + }) { + addAttachment(Attachment( + type: type, + content: Uint8List.fromList(bytes), + fileName: fileName, + mimeType: mimeType, + )); + } + + void addAttachmentByteData( + ByteData bytes, + String fileName, { + String? mimeType, + AttachmentType? type, + }) { + addAttachment(Attachment( + type: type, + content: bytes.buffer.asUint8List(), + fileName: fileName, + mimeType: mimeType, + )); + } +} diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 2ca151d0e1..834d8a633d 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -108,8 +108,13 @@ class SentryClient { return _sentryId; } } - final envelope = SentryEnvelope.fromEvent(preparedEvent, _options.sdk); - return await _options.transport.send(envelope); + final envelope = SentryEnvelope.fromEvent( + preparedEvent, + _options.sdk, + attachments: scope?.attachements, + ); + + return captureEnvelope(envelope); } SentryEvent _prepareEvent(SentryEvent event, {dynamic stackTrace}) { @@ -201,7 +206,7 @@ class SentryClient { } /// Reports the [envelope] to Sentry.io. - Future captureEnvelope(SentryEnvelope envelope) { + Future captureEnvelope(SentryEnvelope envelope) { return _options.transport.send(envelope); } diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index 18a1835231..b8cc16aca5 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import '../attachment.dart'; import 'sentry_envelope_header.dart'; import 'sentry_envelope_item.dart'; import 'protocol/sentry_event.dart'; @@ -16,9 +17,19 @@ class SentryEnvelope { final List items; /// Create an `SentryEnvelope` with containing one `SentryEnvelopeItem` which holds the `SentyEvent` data. - factory SentryEnvelope.fromEvent(SentryEvent event, SdkVersion sdkVersion) { - return SentryEnvelope(SentryEnvelopeHeader(event.eventId, sdkVersion), - [SentryEnvelopeItem.fromEvent(event)]); + factory SentryEnvelope.fromEvent( + SentryEvent event, + SdkVersion sdkVersion, { + List? attachments, + }) { + return SentryEnvelope( + SentryEnvelopeHeader(event.eventId, sdkVersion), + [ + SentryEnvelopeItem.fromEvent(event), + if (attachments != null) + ...attachments.map((e) => SentryEnvelopeItem.fromAttachment(e)) + ], + ); } /// Stream binary data representation of `Envelope` file encoded. diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index 5c00a4e1e6..be0d6ced24 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import '../attachment.dart'; import 'sentry_item_type.dart'; import 'protocol/sentry_event.dart'; import 'sentry_envelope_item_header.dart'; @@ -8,11 +9,16 @@ import 'sentry_envelope_item_header.dart'; class SentryEnvelopeItem { SentryEnvelopeItem(this.header, this.dataFactory); - /// Header with info about type and length of data in bytes. - final SentryEnvelopeItemHeader header; - - /// Create binary data representation of item data. - final Future> Function() dataFactory; + factory SentryEnvelopeItem.fromAttachment(Attachment attachment) { + final header = SentryEnvelopeItemHeader( + SentryItemType.attachment, + () async => attachment.content.lengthInBytes, + contentType: attachment.mimeType, + fileName: attachment.fileName, + attachmentType: attachment.type.toSentryIdentifier(), + ); + return SentryEnvelopeItem(header, () async => attachment.content); + } /// Create an `SentryEnvelopeItem` which holds the `SentyEvent` data. factory SentryEnvelopeItem.fromEvent(SentryEvent event) { @@ -26,11 +32,21 @@ class SentryEnvelopeItem { }; return SentryEnvelopeItem( - SentryEnvelopeItemHeader(SentryItemType.event, getLength, - contentType: 'application/json'), - cachedItem.getData); + SentryEnvelopeItemHeader( + SentryItemType.event, + getLength, + contentType: 'application/json', + ), + cachedItem.getData, + ); } + /// Header with info about type and length of data in bytes. + final SentryEnvelopeItemHeader header; + + /// Create binary data representation of item data. + final Future> Function() dataFactory; + /// Stream binary data of `Envelope` item. Stream> envelopeItemStream() async* { yield utf8.encode(jsonEncode(await header.toJson())); diff --git a/dart/lib/src/sentry_envelope_item_header.dart b/dart/lib/src/sentry_envelope_item_header.dart index 013a62d35b..db1abc9bbd 100644 --- a/dart/lib/src/sentry_envelope_item_header.dart +++ b/dart/lib/src/sentry_envelope_item_header.dart @@ -1,7 +1,12 @@ /// Header with item info about type and length of data in bytes. class SentryEnvelopeItemHeader { - SentryEnvelopeItemHeader(this.type, this.length, - {this.contentType, this.fileName}); + SentryEnvelopeItemHeader( + this.type, + this.length, { + this.contentType, + this.fileName, + this.attachmentType, + }); /// Type of encoded data. final String type; @@ -13,17 +18,18 @@ class SentryEnvelopeItemHeader { final String? fileName; + final String? attachmentType; + /// Item header encoded as JSON Future> toJson() async { - final json = {}; - if (contentType != null) { - json['content_type'] = contentType; - } - if (fileName != null) { - json['filename'] = fileName; - } - json['type'] = type; - json['length'] = await length(); + final json = { + if (contentType != null) 'content_type': contentType, + if (fileName != null) 'filename': fileName, + if (attachmentType != null) 'attachment_type': type, + 'type': type, + 'length': await length(), + }; + return json; } } diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 8fcbdc36f8..2b2180a3d8 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -128,6 +129,18 @@ class MainScaffold extends StatelessWidget { ); }, ), + RaisedButton( + child: const Text('Capture message with attachment'), + onPressed: () { + Sentry.captureMessage( + 'This message has an attachment', + withScope: (scope) { + final txt = 'Lorem Ipsum dolar sit amet'; + scope.addAttachmentIntList(utf8.encode(txt), 'foobar.txt'); + }, + ); + }, + ), if (UniversalPlatform.isIOS || UniversalPlatform.isMacOS) const CocoaExample(), if (UniversalPlatform.isAndroid) const AndroidExample(), From 9ecad73ae224a2ca75555a9d2eea7e9c7fcd6619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sun, 20 Jun 2021 20:07:29 +0200 Subject: [PATCH 02/34] fix pr id --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c18cf6d8ca..875d8f56e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ * Feat: Add DeduplicationEventProcessor (#498) * Feat: Capture failed requests as event (#473) * Feat: `beforeSend` callback accepts async code (#494) -* Feat: Add support for attachments (#494) +* Feat: Add support for attachments (#505) ## Breaking Changes: From 901f3b45c15bfd85f8bc18e9769ecd3c630ac89f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Mon, 21 Jun 2021 18:11:55 +0200 Subject: [PATCH 03/34] wip --- dart/lib/sentry.dart | 2 +- dart/lib/{ => src}/attachment.dart | 48 +++++++++++++++++ .../_io_attachment_extensions.dart | 40 +++++++++++++++ .../_web_attachment_extensions.dart | 3 ++ .../attachment_extensions.dart | 2 + dart/lib/src/scope.dart | 2 +- .../_io_scope_extensions.dart | 38 -------------- .../_web_scope_extensions.dart | 7 --- .../scope_extensions/scope_extensions.dart | 51 ------------------- dart/lib/src/sentry_envelope.dart | 2 +- dart/lib/src/sentry_envelope_item.dart | 2 +- flutter/example/lib/main.dart | 7 ++- flutter/lib/sentry_flutter.dart | 1 + flutter/lib/src/attachment_extensions.dart | 30 +++++++++++ 14 files changed, 134 insertions(+), 101 deletions(-) rename dart/lib/{ => src}/attachment.dart (61%) create mode 100644 dart/lib/src/attachment_extensions/_io_attachment_extensions.dart create mode 100644 dart/lib/src/attachment_extensions/_web_attachment_extensions.dart create mode 100644 dart/lib/src/attachment_extensions/attachment_extensions.dart delete mode 100644 dart/lib/src/scope_extensions/_io_scope_extensions.dart delete mode 100644 dart/lib/src/scope_extensions/_web_scope_extensions.dart delete mode 100644 dart/lib/src/scope_extensions/scope_extensions.dart create mode 100644 flutter/lib/src/attachment_extensions.dart diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index bc01773e28..50646e1055 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -22,4 +22,4 @@ export 'src/transport/transport.dart'; export 'src/integration.dart'; export 'src/event_processor.dart'; export 'src/http_client/sentry_http_client.dart'; -export 'src/scope_extensions/scope_extensions.dart'; +export 'src/attachment.dart'; diff --git a/dart/lib/attachment.dart b/dart/lib/src/attachment.dart similarity index 61% rename from dart/lib/attachment.dart rename to dart/lib/src/attachment.dart index 717bc9b3d5..7e95c21a6a 100644 --- a/dart/lib/attachment.dart +++ b/dart/lib/src/attachment.dart @@ -1,6 +1,9 @@ import 'dart:typed_data'; +export 'attachment_extensions/attachment_extensions.dart'; // https://develop.sentry.dev/sdk/envelopes/#attachment + +/// Arbitrary content which gets attached to an event. class Attachment { Attachment({ required this.content, @@ -10,6 +13,51 @@ class Attachment { }) : mimeType = mimeType ?? 'application/octet-stream', type = type ?? AttachmentType.attachment; + /// Creates an [Attachment] from a [Uint8List] + factory Attachment.fromUint8List( + Uint8List bytes, + String fileName, { + String? mimeType, + AttachmentType? type, + }) { + return Attachment( + type: type, + content: bytes, + fileName: fileName, + mimeType: mimeType, + ); + } + + /// Creates an [Attachment] from a [List] + factory Attachment.fromIntList( + List bytes, + String fileName, { + String? mimeType, + AttachmentType? type, + }) { + return Attachment( + type: type, + content: Uint8List.fromList(bytes), + fileName: fileName, + mimeType: mimeType, + ); + } + + /// Creates an [Attachment] from [ByteData] + factory Attachment.fromByteData( + ByteData bytes, + String fileName, { + String? mimeType, + AttachmentType? type, + }) { + return Attachment( + type: type, + content: bytes.buffer.asUint8List(), + fileName: fileName, + mimeType: mimeType, + ); + } + /// Attachment type. final AttachmentType type; diff --git a/dart/lib/src/attachment_extensions/_io_attachment_extensions.dart b/dart/lib/src/attachment_extensions/_io_attachment_extensions.dart new file mode 100644 index 0000000000..8a46d73c39 --- /dev/null +++ b/dart/lib/src/attachment_extensions/_io_attachment_extensions.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import '../attachment.dart'; +import '../scope.dart'; + +extension IoScopeExtensions on Scope { + /// Creates an attachment from a given path. + /// Only available on `dart:io` platforms. + /// Not available on web. + static Future fromPath( + String path, { + AttachmentType? type, + String? mimeType, + }) async { + final file = File(path); + + return Attachment( + type: type, + content: await file.readAsBytes(), + fileName: file.uri.pathSegments.last, + mimeType: mimeType, + ); + } + + /// Creates an attachment from a given [File]. + /// Only available on `dart:io` platforms. + /// Not available on web. + static Future fromFile( + File file, { + AttachmentType? type, + String? mimeType, + }) async { + return Attachment( + type: type, + content: await file.readAsBytes(), + fileName: file.uri.pathSegments.last, + mimeType: mimeType, + ); + } +} diff --git a/dart/lib/src/attachment_extensions/_web_attachment_extensions.dart b/dart/lib/src/attachment_extensions/_web_attachment_extensions.dart new file mode 100644 index 0000000000..6479520018 --- /dev/null +++ b/dart/lib/src/attachment_extensions/_web_attachment_extensions.dart @@ -0,0 +1,3 @@ +import '../attachment.dart'; + +extension WebAttachmentExtensions on Attachment {} diff --git a/dart/lib/src/attachment_extensions/attachment_extensions.dart b/dart/lib/src/attachment_extensions/attachment_extensions.dart new file mode 100644 index 0000000000..58b5d73b83 --- /dev/null +++ b/dart/lib/src/attachment_extensions/attachment_extensions.dart @@ -0,0 +1,2 @@ +export '_io_attachment_extensions.dart' + if (dart.library.html) '_web_scope_extensions.dart'; diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 6e26d8af86..aee2f011bd 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -1,5 +1,5 @@ import 'dart:collection'; -import '../attachment.dart'; +import 'attachment.dart'; import 'event_processor.dart'; import 'protocol.dart'; import 'sentry_options.dart'; diff --git a/dart/lib/src/scope_extensions/_io_scope_extensions.dart b/dart/lib/src/scope_extensions/_io_scope_extensions.dart deleted file mode 100644 index 40c7ed5826..0000000000 --- a/dart/lib/src/scope_extensions/_io_scope_extensions.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'dart:io'; - -import '../../attachment.dart'; -import '../scope.dart'; - -extension IoScopeExtensions on Scope { - Future addAttachementPath( - String path, { - AttachmentType? type, - String? mimeType, - }) async { - final file = File(path); - - addAttachment( - Attachment( - type: type, - content: await file.readAsBytes(), - fileName: file.uri.pathSegments.last, - mimeType: mimeType, - ), - ); - } - - Future addAttachementFile( - File file, { - AttachmentType? type, - String? mimeType, - }) async { - addAttachment( - Attachment( - type: type, - content: await file.readAsBytes(), - fileName: file.uri.pathSegments.last, - mimeType: mimeType, - ), - ); - } -} diff --git a/dart/lib/src/scope_extensions/_web_scope_extensions.dart b/dart/lib/src/scope_extensions/_web_scope_extensions.dart deleted file mode 100644 index abc51e5117..0000000000 --- a/dart/lib/src/scope_extensions/_web_scope_extensions.dart +++ /dev/null @@ -1,7 +0,0 @@ -import '../scope.dart'; - -extension IoScopeExtensions on Scope { - Future addAttachementPath(String path) async { - // no op - } -} diff --git a/dart/lib/src/scope_extensions/scope_extensions.dart b/dart/lib/src/scope_extensions/scope_extensions.dart deleted file mode 100644 index e78de25e5e..0000000000 --- a/dart/lib/src/scope_extensions/scope_extensions.dart +++ /dev/null @@ -1,51 +0,0 @@ -export '_io_scope_extensions.dart' - if (dart.library.html) '_web_scope_extensions.dart'; - -import 'dart:typed_data'; - -import '../../attachment.dart'; -import '../scope.dart'; - -extension ScopeExtensions on Scope { - void addAttachmentBytes( - Uint8List bytes, - String fileName, { - String? mimeType, - AttachmentType? type, - }) { - addAttachment(Attachment( - type: type, - content: bytes, - fileName: fileName, - mimeType: mimeType, - )); - } - - void addAttachmentIntList( - List bytes, - String fileName, { - String? mimeType, - AttachmentType? type, - }) { - addAttachment(Attachment( - type: type, - content: Uint8List.fromList(bytes), - fileName: fileName, - mimeType: mimeType, - )); - } - - void addAttachmentByteData( - ByteData bytes, - String fileName, { - String? mimeType, - AttachmentType? type, - }) { - addAttachment(Attachment( - type: type, - content: bytes.buffer.asUint8List(), - fileName: fileName, - mimeType: mimeType, - )); - } -} diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index b8cc16aca5..f287898f07 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import '../attachment.dart'; +import 'attachment.dart'; import 'sentry_envelope_header.dart'; import 'sentry_envelope_item.dart'; import 'protocol/sentry_event.dart'; diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index be0d6ced24..db9173b995 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import '../attachment.dart'; +import 'attachment.dart'; import 'sentry_item_type.dart'; import 'protocol/sentry_event.dart'; import 'sentry_envelope_item_header.dart'; diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 2b2180a3d8..0a2159b618 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -136,7 +136,12 @@ class MainScaffold extends StatelessWidget { 'This message has an attachment', withScope: (scope) { final txt = 'Lorem Ipsum dolar sit amet'; - scope.addAttachmentIntList(utf8.encode(txt), 'foobar.txt'); + scope.addAttachment( + Attachment.fromIntList( + txt.encode(txt), + 'foobar.txt', + ), + ); }, ); }, diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index 5fc6546099..7c3666a02f 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -5,3 +5,4 @@ export 'src/default_integrations.dart'; export 'src/navigation/sentry_navigator_observer.dart'; export 'src/sentry_flutter.dart'; export 'src/sentry_flutter_options.dart'; +export 'src/attachment_extensions.dart'; diff --git a/flutter/lib/src/attachment_extensions.dart b/flutter/lib/src/attachment_extensions.dart new file mode 100644 index 0000000000..9f78c026ca --- /dev/null +++ b/flutter/lib/src/attachment_extensions.dart @@ -0,0 +1,30 @@ +import 'package:flutter/services.dart'; +import 'package:sentry/sentry.dart'; + +extension AttachmentExtension on Attachment { + /// Creates an attachment from an asset out of a [AssetBundle]. + /// If no bundle is given, it's using the [rootBundle]. + /// Typically you want to use it like this: + /// ```dart + /// final attachment = Attachment.fromAsset( + /// 'assets/foo_bar.txt', + /// bundle: DefaultAssetBundle.of(context), + /// ); + /// ``` + static Future fromAsset( + String key, { + AssetBundle? bundle, + AttachmentType? type, + String? mimeType, + }) async { + final data = await (bundle ?? rootBundle).load(key); + final fileName = Uri.parse(key).pathSegments.last; + + return Attachment.fromByteData( + data, + fileName, + type: type, + mimeType: mimeType, + ); + } +} From f6db8f156222eeda0adcc9338d7eea0710e98b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Mon, 21 Jun 2021 20:17:38 +0200 Subject: [PATCH 04/34] More examples --- flutter/example/lib/main.dart | 42 ++++++++++++++++++++++++++++++----- flutter/example/pubspec.yaml | 1 + 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 0a2159b618..07a4ff4c17 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:universal_platform/universal_platform.dart'; +import 'package:feedback/feedback.dart' as feedback; // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io const String _exampleDsn = @@ -34,11 +35,13 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { - return MaterialApp( - navigatorObservers: [ - SentryNavigatorObserver(), - ], - home: const MainScaffold(), + return feedback.BetterFeedback( + child: MaterialApp( + navigatorObservers: [ + SentryNavigatorObserver(), + ], + home: const MainScaffold(), + ), ); } } @@ -138,14 +141,41 @@ class MainScaffold extends StatelessWidget { final txt = 'Lorem Ipsum dolar sit amet'; scope.addAttachment( Attachment.fromIntList( - txt.encode(txt), + utf8.encode(txt), 'foobar.txt', + mimeType: 'text/plain', ), ); }, ); }, ), + RaisedButton( + child: const Text('Capture message with image attachment'), + onPressed: () { + feedback.BetterFeedback.of(context) + .show((feedback.UserFeedback feedback) { + Sentry.captureMessage( + feedback.text, + withScope: (scope) { + final entries = feedback.extra?.entries; + if (entries != null) { + for (final extra in entries) { + scope.setExtra(extra.key, extra.value); + } + } + scope.addAttachment( + Attachment.fromUint8List( + feedback.screenshot, + 'feedback.png', + mimeType: 'image/png', + ), + ); + }, + ); + }); + }, + ), if (UniversalPlatform.isIOS || UniversalPlatform.isMacOS) const CocoaExample(), if (UniversalPlatform.isAndroid) const AndroidExample(), diff --git a/flutter/example/pubspec.yaml b/flutter/example/pubspec.yaml index 9f98b64f39..0f7bf84ad6 100644 --- a/flutter/example/pubspec.yaml +++ b/flutter/example/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: sdk: flutter sentry_flutter: any universal_platform: ^1.0.0-nullsafety + feedback: 2.0.0 dependency_overrides: sentry: From 6468814d63c582ea73f74da9d21a8df79f49242e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Tue, 22 Jun 2021 14:16:42 +0200 Subject: [PATCH 05/34] Update dart/lib/src/sentry_envelope_item_header.dart Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> --- dart/lib/src/sentry_envelope_item_header.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dart/lib/src/sentry_envelope_item_header.dart b/dart/lib/src/sentry_envelope_item_header.dart index db1abc9bbd..a69de56c81 100644 --- a/dart/lib/src/sentry_envelope_item_header.dart +++ b/dart/lib/src/sentry_envelope_item_header.dart @@ -25,7 +25,7 @@ class SentryEnvelopeItemHeader { final json = { if (contentType != null) 'content_type': contentType, if (fileName != null) 'filename': fileName, - if (attachmentType != null) 'attachment_type': type, + if (attachmentType != null) 'attachment_type': attachmentType, 'type': type, 'length': await length(), }; From 5e0cde0d1e328a161d559abaebe06de60dd86829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sat, 26 Jun 2021 17:45:55 +0200 Subject: [PATCH 06/34] WIP --- dart/lib/sentry.dart | 2 +- dart/lib/src/attachment.dart | 112 ------------------ .../_io_attachment_extensions.dart | 40 ------- .../_web_attachment_extensions.dart | 3 - dart/lib/src/scope.dart | 13 +- dart/lib/src/sentry_attachment.dart | 102 ++++++++++++++++ .../_io_attachment_extensions.dart | 42 +++++++ .../_web_attachment_extensions.dart | 3 + .../attachment_extensions.dart | 0 dart/lib/src/sentry_client.dart | 5 +- dart/lib/src/sentry_envelope.dart | 4 +- dart/lib/src/sentry_envelope_item.dart | 22 ++-- flutter/example/lib/main.dart | 8 +- flutter/lib/src/attachment_extensions.dart | 14 +-- 14 files changed, 187 insertions(+), 183 deletions(-) delete mode 100644 dart/lib/src/attachment.dart delete mode 100644 dart/lib/src/attachment_extensions/_io_attachment_extensions.dart delete mode 100644 dart/lib/src/attachment_extensions/_web_attachment_extensions.dart create mode 100644 dart/lib/src/sentry_attachment.dart create mode 100644 dart/lib/src/sentry_attachment_extensions/_io_attachment_extensions.dart create mode 100644 dart/lib/src/sentry_attachment_extensions/_web_attachment_extensions.dart rename dart/lib/src/{attachment_extensions => sentry_attachment_extensions}/attachment_extensions.dart (100%) diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 50646e1055..59abda21cd 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -22,4 +22,4 @@ export 'src/transport/transport.dart'; export 'src/integration.dart'; export 'src/event_processor.dart'; export 'src/http_client/sentry_http_client.dart'; -export 'src/attachment.dart'; +export 'src/sentry_attachment.dart'; diff --git a/dart/lib/src/attachment.dart b/dart/lib/src/attachment.dart deleted file mode 100644 index 7e95c21a6a..0000000000 --- a/dart/lib/src/attachment.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'dart:typed_data'; -export 'attachment_extensions/attachment_extensions.dart'; - -// https://develop.sentry.dev/sdk/envelopes/#attachment - -/// Arbitrary content which gets attached to an event. -class Attachment { - Attachment({ - required this.content, - required this.fileName, - AttachmentType? type, - String? mimeType, - }) : mimeType = mimeType ?? 'application/octet-stream', - type = type ?? AttachmentType.attachment; - - /// Creates an [Attachment] from a [Uint8List] - factory Attachment.fromUint8List( - Uint8List bytes, - String fileName, { - String? mimeType, - AttachmentType? type, - }) { - return Attachment( - type: type, - content: bytes, - fileName: fileName, - mimeType: mimeType, - ); - } - - /// Creates an [Attachment] from a [List] - factory Attachment.fromIntList( - List bytes, - String fileName, { - String? mimeType, - AttachmentType? type, - }) { - return Attachment( - type: type, - content: Uint8List.fromList(bytes), - fileName: fileName, - mimeType: mimeType, - ); - } - - /// Creates an [Attachment] from [ByteData] - factory Attachment.fromByteData( - ByteData bytes, - String fileName, { - String? mimeType, - AttachmentType? type, - }) { - return Attachment( - type: type, - content: bytes.buffer.asUint8List(), - fileName: fileName, - mimeType: mimeType, - ); - } - - /// Attachment type. - final AttachmentType type; - - /// Attachment content. - final Uint8List content; - - /// Attachment file name. - final String fileName; - - /// Attachment content type. - final String mimeType; -} - -/// Attachment type. -enum AttachmentType { - /// Standard attachment without special meaning. - attachment, - - /// Minidump file that creates an error event and is symbolicated. - /// The file should start with the MDMP magic bytes. - minidump, - - /// Apple crash report file that creates an error event and is symbolicated. - appleCrashReport, - - /// XML file containing UE4 crash meta data. - /// During event ingestion, event contexts and extra fields are extracted from - /// this file. - unrealContext, - - /// Plain-text log file obtained from UE4 crashes. - /// During event ingestion, the last logs are extracted into event - /// breadcrumbs. - unrealLogs -} - -extension AttachmentTypeX on AttachmentType { - String toSentryIdentifier() { - switch (this) { - case AttachmentType.attachment: - return 'event.attachment'; - case AttachmentType.minidump: - return 'event.minidump'; - case AttachmentType.appleCrashReport: - return 'event.applecrashreport'; - case AttachmentType.unrealContext: - return 'unreal.context'; - case AttachmentType.unrealLogs: - return 'unreal.logs'; - } - } -} diff --git a/dart/lib/src/attachment_extensions/_io_attachment_extensions.dart b/dart/lib/src/attachment_extensions/_io_attachment_extensions.dart deleted file mode 100644 index 8a46d73c39..0000000000 --- a/dart/lib/src/attachment_extensions/_io_attachment_extensions.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'dart:io'; - -import '../attachment.dart'; -import '../scope.dart'; - -extension IoScopeExtensions on Scope { - /// Creates an attachment from a given path. - /// Only available on `dart:io` platforms. - /// Not available on web. - static Future fromPath( - String path, { - AttachmentType? type, - String? mimeType, - }) async { - final file = File(path); - - return Attachment( - type: type, - content: await file.readAsBytes(), - fileName: file.uri.pathSegments.last, - mimeType: mimeType, - ); - } - - /// Creates an attachment from a given [File]. - /// Only available on `dart:io` platforms. - /// Not available on web. - static Future fromFile( - File file, { - AttachmentType? type, - String? mimeType, - }) async { - return Attachment( - type: type, - content: await file.readAsBytes(), - fileName: file.uri.pathSegments.last, - mimeType: mimeType, - ); - } -} diff --git a/dart/lib/src/attachment_extensions/_web_attachment_extensions.dart b/dart/lib/src/attachment_extensions/_web_attachment_extensions.dart deleted file mode 100644 index 6479520018..0000000000 --- a/dart/lib/src/attachment_extensions/_web_attachment_extensions.dart +++ /dev/null @@ -1,3 +0,0 @@ -import '../attachment.dart'; - -extension WebAttachmentExtensions on Attachment {} diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index aee2f011bd..bcff9506f5 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -1,5 +1,5 @@ import 'dart:collection'; -import 'attachment.dart'; +import 'sentry_attachment.dart'; import 'event_processor.dart'; import 'protocol.dart'; import 'sentry_options.dart'; @@ -81,9 +81,10 @@ class Scope { final SentryOptions _options; - final List _attachements = []; + final List _attachements = []; - List get attachements => UnmodifiableListView(_attachements); + List get attachements => + UnmodifiableListView(_attachements); Scope(this._options); @@ -120,10 +121,14 @@ class Scope { _breadcrumbs.add(breadcrumb); } - void addAttachment(Attachment attachment) async { + void addAttachment(SentryAttachment attachment) async { _attachements.add(attachment); } + void clearAttachments() { + _attachements.clear(); + } + /// Clear all the breadcrumbs void clearBreadcrumbs() { _breadcrumbs.clear(); diff --git a/dart/lib/src/sentry_attachment.dart b/dart/lib/src/sentry_attachment.dart new file mode 100644 index 0000000000..0a0e12139b --- /dev/null +++ b/dart/lib/src/sentry_attachment.dart @@ -0,0 +1,102 @@ +import 'dart:async'; +import 'dart:typed_data'; +export 'sentry_attachment_extensions/attachment_extensions.dart'; + +// https://develop.sentry.dev/sdk/envelopes/#attachment + +typedef ContentLoader = FutureOr Function(); + +/// Arbitrary content which gets attached to an event. +class SentryAttachment { + SentryAttachment({ + required ContentLoader loader, + required this.filename, + String? attachmentType, + this.contentType, + }) : _loader = loader, + attachmentType = attachmentType ?? AttachmentType.attachment; + + /// Creates an [SentryAttachment] from a [Uint8List] + factory SentryAttachment.fromUint8List( + Uint8List bytes, + String fileName, { + String? contentType, + String? attachmentType, + }) { + return SentryAttachment( + attachmentType: attachmentType, + loader: () => bytes, + filename: fileName, + contentType: contentType, + ); + } + + /// Creates an [SentryAttachment] from a [List] + factory SentryAttachment.fromIntList( + List bytes, + String fileName, { + String? contentType, + String? attachmentType, + }) { + return SentryAttachment( + attachmentType: attachmentType, + loader: () => Uint8List.fromList(bytes), + filename: fileName, + contentType: contentType, + ); + } + + /// Creates an [SentryAttachment] from [ByteData] + factory SentryAttachment.fromByteData( + ByteData bytes, + String fileName, { + String? contentType, + String? attachmentType, + }) { + return SentryAttachment( + attachmentType: attachmentType, + loader: () => bytes.buffer.asUint8List(), + filename: fileName, + contentType: contentType, + ); + } + + /// Attachment type. + /// Should be one of types given in [AttachmentType]. + final String attachmentType; + + /// Attachment content. + FutureOr get bytes => _loader(); + + final ContentLoader _loader; + + /// Attachment file name. + final String filename; + + /// Attachment content type. + /// Inferred by Sentry if it's not given. + final String? contentType; +} + +/// Attachment type. +class AttachmentType { + /// Standard attachment without special meaning. + static const String attachment = 'event.attachment'; + + /// Minidump file that creates an error event and is symbolicated. + /// The file should start with the `MDMP` magic bytes. + static const String minidump = 'event.minidump'; + + /// Apple crash report file that creates an error event and is symbolicated. + static const String appleCrashReport = 'event.applecrashreport'; + + /// XML file containing UE4 crash meta data. + /// During event ingestion, event contexts and extra fields are extracted from + /// this file. + static const String unrealContext = 'unreal.context'; + + /// Plain-text log file obtained from UE4 crashes. + /// During event ingestion, the last logs are extracted into event + /// breadcrumbs. + static const String unrealLogs = 'unreal.logs'; +} diff --git a/dart/lib/src/sentry_attachment_extensions/_io_attachment_extensions.dart b/dart/lib/src/sentry_attachment_extensions/_io_attachment_extensions.dart new file mode 100644 index 0000000000..292f0d1fd8 --- /dev/null +++ b/dart/lib/src/sentry_attachment_extensions/_io_attachment_extensions.dart @@ -0,0 +1,42 @@ +import 'dart:io'; + +import '../sentry_attachment.dart'; +import '../scope.dart'; + +extension IoAttachmentExtensions on Scope { + /// Creates an attachment from a given path. + /// Only available on `dart:io` platforms. + /// Not available on web. + static SentryAttachment fromPath( + String path, { + String? filename, + String? attachmentType, + String? contentType, + }) { + final file = File(path); + + return fromFile( + file, + attachmentType: attachmentType, + contentType: contentType, + filename: filename, + ); + } + + /// Creates an attachment from a given [File]. + /// Only available on `dart:io` platforms. + /// Not available on web. + static SentryAttachment fromFile( + File file, { + String? filename, + String? attachmentType, + String? contentType, + }) { + return SentryAttachment( + attachmentType: attachmentType, + loader: () => file.readAsBytes(), + filename: filename ?? file.uri.pathSegments.last, + contentType: contentType, + ); + } +} diff --git a/dart/lib/src/sentry_attachment_extensions/_web_attachment_extensions.dart b/dart/lib/src/sentry_attachment_extensions/_web_attachment_extensions.dart new file mode 100644 index 0000000000..38b44b4cf1 --- /dev/null +++ b/dart/lib/src/sentry_attachment_extensions/_web_attachment_extensions.dart @@ -0,0 +1,3 @@ +import '../sentry_attachment.dart'; + +extension WebAttachmentExtensions on SentryAttachment {} diff --git a/dart/lib/src/attachment_extensions/attachment_extensions.dart b/dart/lib/src/sentry_attachment_extensions/attachment_extensions.dart similarity index 100% rename from dart/lib/src/attachment_extensions/attachment_extensions.dart rename to dart/lib/src/sentry_attachment_extensions/attachment_extensions.dart diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 834d8a633d..398aeb5629 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -114,7 +114,8 @@ class SentryClient { attachments: scope?.attachements, ); - return captureEnvelope(envelope); + final id = await captureEnvelope(envelope); + return id!; } SentryEvent _prepareEvent(SentryEvent event, {dynamic stackTrace}) { @@ -206,7 +207,7 @@ class SentryClient { } /// Reports the [envelope] to Sentry.io. - Future captureEnvelope(SentryEnvelope envelope) { + Future captureEnvelope(SentryEnvelope envelope) { return _options.transport.send(envelope); } diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index f287898f07..5445f8e2f7 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'attachment.dart'; +import 'sentry_attachment.dart'; import 'sentry_envelope_header.dart'; import 'sentry_envelope_item.dart'; import 'protocol/sentry_event.dart'; @@ -20,7 +20,7 @@ class SentryEnvelope { factory SentryEnvelope.fromEvent( SentryEvent event, SdkVersion sdkVersion, { - List? attachments, + List? attachments, }) { return SentryEnvelope( SentryEnvelopeHeader(event.eventId, sdkVersion), diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index db9173b995..0825c7c28a 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'attachment.dart'; +import 'sentry_attachment.dart'; import 'sentry_item_type.dart'; import 'protocol/sentry_event.dart'; import 'sentry_envelope_item_header.dart'; @@ -9,18 +9,24 @@ import 'sentry_envelope_item_header.dart'; class SentryEnvelopeItem { SentryEnvelopeItem(this.header, this.dataFactory); - factory SentryEnvelopeItem.fromAttachment(Attachment attachment) { + factory SentryEnvelopeItem.fromAttachment(SentryAttachment attachment) { + final cachedItem = _CachedItem(() async { + return await attachment.bytes; + }); + + final getLength = () async => (await cachedItem.getData()).length; + final header = SentryEnvelopeItemHeader( SentryItemType.attachment, - () async => attachment.content.lengthInBytes, - contentType: attachment.mimeType, - fileName: attachment.fileName, - attachmentType: attachment.type.toSentryIdentifier(), + getLength, + contentType: attachment.contentType, + fileName: attachment.filename, + attachmentType: attachment.attachmentType, ); - return SentryEnvelopeItem(header, () async => attachment.content); + return SentryEnvelopeItem(header, cachedItem.getData); } - /// Create an `SentryEnvelopeItem` which holds the `SentyEvent` data. + /// Create an [SentryEnvelopeItem] which holds the [SentryEvent] data. factory SentryEnvelopeItem.fromEvent(SentryEvent event) { final cachedItem = _CachedItem(() async { final jsonEncoded = jsonEncode(event.toJson()); diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 07a4ff4c17..daf855ef0d 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -140,10 +140,10 @@ class MainScaffold extends StatelessWidget { withScope: (scope) { final txt = 'Lorem Ipsum dolar sit amet'; scope.addAttachment( - Attachment.fromIntList( + SentryAttachment.fromIntList( utf8.encode(txt), 'foobar.txt', - mimeType: 'text/plain', + contentType: 'text/plain', ), ); }, @@ -165,10 +165,10 @@ class MainScaffold extends StatelessWidget { } } scope.addAttachment( - Attachment.fromUint8List( + SentryAttachment.fromUint8List( feedback.screenshot, 'feedback.png', - mimeType: 'image/png', + contentType: 'image/png', ), ); }, diff --git a/flutter/lib/src/attachment_extensions.dart b/flutter/lib/src/attachment_extensions.dart index 9f78c026ca..fb92e1c8d2 100644 --- a/flutter/lib/src/attachment_extensions.dart +++ b/flutter/lib/src/attachment_extensions.dart @@ -1,7 +1,7 @@ import 'package:flutter/services.dart'; import 'package:sentry/sentry.dart'; -extension AttachmentExtension on Attachment { +extension AttachmentExtension on SentryAttachment { /// Creates an attachment from an asset out of a [AssetBundle]. /// If no bundle is given, it's using the [rootBundle]. /// Typically you want to use it like this: @@ -11,20 +11,20 @@ extension AttachmentExtension on Attachment { /// bundle: DefaultAssetBundle.of(context), /// ); /// ``` - static Future fromAsset( + static Future fromAsset( String key, { AssetBundle? bundle, - AttachmentType? type, - String? mimeType, + String? type, + String? contentType, }) async { final data = await (bundle ?? rootBundle).load(key); final fileName = Uri.parse(key).pathSegments.last; - return Attachment.fromByteData( + return SentryAttachment.fromByteData( data, fileName, - type: type, - mimeType: mimeType, + attachmentType: type, + contentType: contentType, ); } } From 6cca7013e42df3a7bbddccd23f47cfb7108e8a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sat, 26 Jun 2021 18:13:13 +0200 Subject: [PATCH 07/34] WIP --- dart/lib/sentry_io.dart | 2 ++ dart/lib/src/sentry_attachment.dart | 1 - .../_web_attachment_extensions.dart | 3 --- .../sentry_attachment_extensions/attachment_extensions.dart | 2 -- ...ttachment_extensions.dart => io_attachment_extensions.dart} | 0 5 files changed, 2 insertions(+), 6 deletions(-) create mode 100644 dart/lib/sentry_io.dart delete mode 100644 dart/lib/src/sentry_attachment_extensions/_web_attachment_extensions.dart delete mode 100644 dart/lib/src/sentry_attachment_extensions/attachment_extensions.dart rename dart/lib/src/sentry_attachment_extensions/{_io_attachment_extensions.dart => io_attachment_extensions.dart} (100%) diff --git a/dart/lib/sentry_io.dart b/dart/lib/sentry_io.dart new file mode 100644 index 0000000000..53b2620cf0 --- /dev/null +++ b/dart/lib/sentry_io.dart @@ -0,0 +1,2 @@ +export 'sentry.dart'; +export 'src/sentry_attachment_extensions/io_attachment_extensions.dart'; diff --git a/dart/lib/src/sentry_attachment.dart b/dart/lib/src/sentry_attachment.dart index 0a0e12139b..78b6704fc2 100644 --- a/dart/lib/src/sentry_attachment.dart +++ b/dart/lib/src/sentry_attachment.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:typed_data'; -export 'sentry_attachment_extensions/attachment_extensions.dart'; // https://develop.sentry.dev/sdk/envelopes/#attachment diff --git a/dart/lib/src/sentry_attachment_extensions/_web_attachment_extensions.dart b/dart/lib/src/sentry_attachment_extensions/_web_attachment_extensions.dart deleted file mode 100644 index 38b44b4cf1..0000000000 --- a/dart/lib/src/sentry_attachment_extensions/_web_attachment_extensions.dart +++ /dev/null @@ -1,3 +0,0 @@ -import '../sentry_attachment.dart'; - -extension WebAttachmentExtensions on SentryAttachment {} diff --git a/dart/lib/src/sentry_attachment_extensions/attachment_extensions.dart b/dart/lib/src/sentry_attachment_extensions/attachment_extensions.dart deleted file mode 100644 index 58b5d73b83..0000000000 --- a/dart/lib/src/sentry_attachment_extensions/attachment_extensions.dart +++ /dev/null @@ -1,2 +0,0 @@ -export '_io_attachment_extensions.dart' - if (dart.library.html) '_web_scope_extensions.dart'; diff --git a/dart/lib/src/sentry_attachment_extensions/_io_attachment_extensions.dart b/dart/lib/src/sentry_attachment_extensions/io_attachment_extensions.dart similarity index 100% rename from dart/lib/src/sentry_attachment_extensions/_io_attachment_extensions.dart rename to dart/lib/src/sentry_attachment_extensions/io_attachment_extensions.dart From 5445b83826a49747f02d0cd0acbf219973b42cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sun, 27 Jun 2021 10:34:15 +0200 Subject: [PATCH 08/34] more tests --- dart/lib/src/scope.dart | 1 + dart/lib/src/sentry_attachment.dart | 1 + dart/test/scope_test.dart | 40 +++++++++++++ dart/test/sentry_attachment_test.dart | 85 +++++++++++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 dart/test/sentry_attachment_test.dart diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index bcff9506f5..5c640be328 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -142,6 +142,7 @@ class Scope { /// Resets the Scope to its default state void clear() { clearBreadcrumbs(); + clearAttachments(); level = null; transaction = null; user = null; diff --git a/dart/lib/src/sentry_attachment.dart b/dart/lib/src/sentry_attachment.dart index 78b6704fc2..920a127ec8 100644 --- a/dart/lib/src/sentry_attachment.dart +++ b/dart/lib/src/sentry_attachment.dart @@ -65,6 +65,7 @@ class SentryAttachment { final String attachmentType; /// Attachment content. + /// Is loaded while sending this attachment. FutureOr get bytes => _loader(); final ContentLoader _loader; diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index 9a07456fb7..3176ebc715 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -154,6 +154,38 @@ void main() { expect(sut.breadcrumbs.length, 0); }); + test('adds $SentryAttachment', () { + final sut = fixture.getSut(); + + final attachment = SentryAttachment.fromIntList([0, 0, 0, 0], 'test.txt'); + sut.addAttachment(attachment); + + expect(sut.attachements.last, attachment); + expect(sut.attachements.length, 1); + }); + + test('clear() removes all $SentryAttachment', () { + final sut = fixture.getSut(); + + final attachment = SentryAttachment.fromIntList([0, 0, 0, 0], 'test.txt'); + sut.addAttachment(attachment); + expect(sut.attachements.length, 1); + sut.clear(); + + expect(sut.attachements.length, 0); + }); + + test('clearAttachments() removes all $SentryAttachment', () { + final sut = fixture.getSut(); + + final attachment = SentryAttachment.fromIntList([0, 0, 0, 0], 'test.txt'); + sut.addAttachment(attachment); + expect(sut.attachements.length, 1); + sut.clearAttachments(); + + expect(sut.attachements.length, 0); + }); + test('sets tag', () { final sut = fixture.getSut(); @@ -232,6 +264,13 @@ void main() { test('clones', () { final sut = fixture.getSut(); + + sut.addBreadcrumb(Breadcrumb( + message: 'test log', + timestamp: DateTime.utc(2019), + )); + sut.addAttachment(SentryAttachment.fromIntList([0, 0, 0, 0], 'test.txt')); + final clone = sut.clone(); expect(sut.user, clone.user); expect(sut.transaction, clone.transaction); @@ -239,6 +278,7 @@ void main() { expect(sut.tags, clone.tags); expect(sut.breadcrumbs, clone.breadcrumbs); expect(sut.contexts, clone.contexts); + expect(sut.attachements, clone.attachements); expect(ListEquality().equals(sut.fingerprint, clone.fingerprint), true); expect( ListEquality().equals(sut.eventProcessors, clone.eventProcessors), diff --git a/dart/test/sentry_attachment_test.dart b/dart/test/sentry_attachment_test.dart new file mode 100644 index 0000000000..9192c7fd69 --- /dev/null +++ b/dart/test/sentry_attachment_test.dart @@ -0,0 +1,85 @@ +import 'dart:typed_data'; + +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'mocks/mock_transport.dart'; + +void main() { + group('$SentryAttachment ctor', () { + test('default', () async { + final attachment = SentryAttachment( + loader: () => Uint8List.fromList([0, 0, 0, 0]), + filename: 'test.txt', + ); + expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.contentType, isNull); + expect(attachment.filename, 'test.txt'); + await expectLater(await attachment.bytes, [0, 0, 0, 0]); + }); + + test('fromIntList', () async { + final attachment = SentryAttachment.fromIntList([0, 0, 0, 0], 'test.txt'); + expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.contentType, isNull); + expect(attachment.filename, 'test.txt'); + await expectLater(await attachment.bytes, [0, 0, 0, 0]); + }); + + test('fromUint8List', () async { + final attachment = SentryAttachment.fromUint8List( + Uint8List.fromList([0, 0, 0, 0]), + 'test.txt', + ); + expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.contentType, isNull); + expect(attachment.filename, 'test.txt'); + await expectLater(await attachment.bytes, [0, 0, 0, 0]); + }); + + test('fromByteData', () async { + final attachment = SentryAttachment.fromByteData( + ByteData.sublistView(Uint8List.fromList([0, 0, 0, 0])), + 'test.txt', + ); + expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.contentType, isNull); + expect(attachment.filename, 'test.txt'); + await expectLater(await attachment.bytes, [0, 0, 0, 0]); + }); + }); + + group('$Scope $SentryAttachment tests', () { + test('Sending with attachments', () async { + final options = SentryOptions(dsn: fakeDsn); + final transport = MockTransport(); + options.transport = transport; + final hub = Hub(options); + await hub.captureEvent(SentryEvent(), withScope: (scope) { + scope.addAttachment( + SentryAttachment.fromIntList([0, 0, 0, 0], 'test.txt'), + ); + }); + expect(transport.envelopes.length, 1); + expect(transport.envelopes.first.items.length, 2); + final attachmentEnvelope = transport.envelopes.first.items[1]; + expect( + attachmentEnvelope.header.attachmentType, + AttachmentType.attachment, + ); + expect( + attachmentEnvelope.header.contentType, + isNull, + ); + expect( + attachmentEnvelope.header.fileName, + 'test.txt', + ); + await expectLater( + await attachmentEnvelope.header.length(), + 4, + ); + }); + }); +} From 32ee66b7fd1dd2a22633c2407b5f5938ee0571a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Mon, 28 Jun 2021 13:14:17 +0200 Subject: [PATCH 09/34] use list unmodifiable --- dart/lib/src/scope.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 5c640be328..e79f8285ba 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -83,8 +83,7 @@ class Scope { final List _attachements = []; - List get attachements => - UnmodifiableListView(_attachements); + List get attachements => List.unmodifiable(_attachements); Scope(this._options); From 664617e137d4d314a44db70aa46db3470617e9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Mon, 28 Jun 2021 18:30:37 +0200 Subject: [PATCH 10/34] transport can return null --- dart/lib/src/sentry_client.dart | 2 +- dart/lib/src/transport/http_transport.dart | 7 +++++-- dart/lib/src/transport/noop_transport.dart | 3 +-- dart/lib/src/transport/transport.dart | 2 +- flutter/lib/src/file_system_transport.dart | 6 +++--- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 398aeb5629..a901ebc8a4 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -115,7 +115,7 @@ class SentryClient { ); final id = await captureEnvelope(envelope); - return id!; + return id ?? SentryId.empty(); } SentryEvent _prepareEvent(SentryEvent event, {dynamic stackTrace}) { diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 3a93da51f1..7137308381 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -45,7 +45,7 @@ class HttpTransport implements Transport { } @override - Future send(SentryEnvelope envelope) async { + Future send(SentryEnvelope envelope) async { final filteredEnvelope = _rateLimiter.filter(envelope); if (filteredEnvelope == null) { return SentryId.empty(); @@ -77,7 +77,10 @@ class HttpTransport implements Transport { } final eventId = json.decode(response.body)['id']; - return eventId != null ? SentryId.fromId(eventId) : SentryId.empty(); + if (eventId == null) { + return null; + } + return SentryId.fromId(eventId); } Future _createStreamedRequest( diff --git a/dart/lib/src/transport/noop_transport.dart b/dart/lib/src/transport/noop_transport.dart index 705dfc3a9c..117a8756cd 100644 --- a/dart/lib/src/transport/noop_transport.dart +++ b/dart/lib/src/transport/noop_transport.dart @@ -7,6 +7,5 @@ import 'transport.dart'; class NoOpTransport implements Transport { @override - Future send(SentryEnvelope envelope) => - Future.value(SentryId.empty()); + Future send(SentryEnvelope envelope) => Future.value(null); } diff --git a/dart/lib/src/transport/transport.dart b/dart/lib/src/transport/transport.dart index 08de9e9322..f0a6a2c996 100644 --- a/dart/lib/src/transport/transport.dart +++ b/dart/lib/src/transport/transport.dart @@ -6,5 +6,5 @@ import '../protocol.dart'; /// A transport is in charge of sending the event/envelope either via http /// or caching in the disk. abstract class Transport { - Future send(SentryEnvelope envelope); + Future send(SentryEnvelope envelope); } diff --git a/flutter/lib/src/file_system_transport.dart b/flutter/lib/src/file_system_transport.dart index 55d6e29de3..9574607d6c 100644 --- a/flutter/lib/src/file_system_transport.dart +++ b/flutter/lib/src/file_system_transport.dart @@ -10,7 +10,7 @@ class FileSystemTransport implements Transport { final SentryOptions _options; @override - Future send(SentryEnvelope envelope) async { + Future send(SentryEnvelope envelope) async { final envelopeData = []; await envelope.envelopeStream().forEach(envelopeData.addAll); // https://flutter.dev/docs/development/platform-integration/platform-channels#codec @@ -24,9 +24,9 @@ class FileSystemTransport implements Transport { exception: exception, stackTrace: stackTrace, ); - return SentryId.empty(); + return null; } - return envelope.header.eventId ?? SentryId.empty(); + return envelope.header.eventId; } } From 75a7b604d4de9c4f961d54b6287fff1d99a00421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Mon, 28 Jun 2021 18:45:49 +0200 Subject: [PATCH 11/34] fix test --- dart/test/transport/http_transport_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart index 89e5613814..0597ecfcd2 100644 --- a/dart/test/transport/http_transport_test.dart +++ b/dart/test/transport/http_transport_test.dart @@ -92,7 +92,7 @@ void main() { SentryEnvelope.fromEvent(sentryEvent, fixture.options.sdk); final eventId = await sut.send(envelope); - expect(eventId, SentryId.empty()); + expect(eventId.toString(), SentryId.empty().toString()); expect(httpCalled, false); }); }); From 887adda1ed014892e7ca69409d272a801deb45fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Mon, 28 Jun 2021 19:46:51 +0200 Subject: [PATCH 12/34] wip --- dart/lib/sentry.dart | 2 +- dart/lib/sentry_io.dart | 2 +- dart/lib/src/scope.dart | 2 +- .../io_sentry_attachment.dart | 35 +++++++++++++ .../sentry_attachment.dart | 51 +++++++++---------- .../io_attachment_extensions.dart | 42 --------------- dart/lib/src/sentry_envelope.dart | 2 +- dart/lib/src/sentry_envelope_item.dart | 2 +- dart/test/sentry_attachment_test.dart | 2 +- dart/test/sentry_attachment_test_io.dart | 31 +++++++++++ dart/test_resources/testfile.txt | 1 + flutter/lib/sentry_flutter.dart | 2 +- ...ns.dart => flutter_sentry_attachment.dart} | 25 +++++---- .../test/flutter_sentry_attachment_test.dart | 1 + 14 files changed, 110 insertions(+), 90 deletions(-) create mode 100644 dart/lib/src/sentry_attachment/io_sentry_attachment.dart rename dart/lib/src/{ => sentry_attachment}/sentry_attachment.dart (74%) delete mode 100644 dart/lib/src/sentry_attachment_extensions/io_attachment_extensions.dart create mode 100644 dart/test/sentry_attachment_test_io.dart create mode 100644 dart/test_resources/testfile.txt rename flutter/lib/src/{attachment_extensions.dart => flutter_sentry_attachment.dart} (53%) create mode 100644 flutter/test/flutter_sentry_attachment_test.dart diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 59abda21cd..439bf21587 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -22,4 +22,4 @@ export 'src/transport/transport.dart'; export 'src/integration.dart'; export 'src/event_processor.dart'; export 'src/http_client/sentry_http_client.dart'; -export 'src/sentry_attachment.dart'; +export 'src/sentry_attachment/sentry_attachment.dart'; diff --git a/dart/lib/sentry_io.dart b/dart/lib/sentry_io.dart index 53b2620cf0..db73e953ba 100644 --- a/dart/lib/sentry_io.dart +++ b/dart/lib/sentry_io.dart @@ -1,2 +1,2 @@ export 'sentry.dart'; -export 'src/sentry_attachment_extensions/io_attachment_extensions.dart'; +export 'src/sentry_attachment/io_sentry_attachment.dart'; diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index e79f8285ba..73a7cbd96b 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -1,5 +1,5 @@ import 'dart:collection'; -import 'sentry_attachment.dart'; +import 'sentry_attachment/sentry_attachment.dart'; import 'event_processor.dart'; import 'protocol.dart'; import 'sentry_options.dart'; diff --git a/dart/lib/src/sentry_attachment/io_sentry_attachment.dart b/dart/lib/src/sentry_attachment/io_sentry_attachment.dart new file mode 100644 index 0000000000..a1e2585556 --- /dev/null +++ b/dart/lib/src/sentry_attachment/io_sentry_attachment.dart @@ -0,0 +1,35 @@ +import 'dart:io'; + +import 'sentry_attachment.dart'; + +class IoSentryAttachment extends SentryAttachment { + /// Creates an attachment from a given path. + /// Only available on `dart:io` platforms. + /// Not available on web. + IoSentryAttachment.fromPath( + String path, { + String? filename, + String? attachmentType, + String? contentType, + }) : this.fromFile( + File(path), + attachmentType: attachmentType, + contentType: contentType, + filename: filename, + ); + + /// Creates an attachment from a given [File]. + /// Only available on `dart:io` platforms. + /// Not available on web. + IoSentryAttachment.fromFile( + File file, { + String? filename, + String? attachmentType, + String? contentType, + }) : super.fromLoader( + loader: () => file.readAsBytes(), + filename: filename ?? file.uri.pathSegments.last, + attachmentType: attachmentType, + contentType: contentType, + ); +} diff --git a/dart/lib/src/sentry_attachment.dart b/dart/lib/src/sentry_attachment/sentry_attachment.dart similarity index 74% rename from dart/lib/src/sentry_attachment.dart rename to dart/lib/src/sentry_attachment/sentry_attachment.dart index 920a127ec8..89f82ed9b0 100644 --- a/dart/lib/src/sentry_attachment.dart +++ b/dart/lib/src/sentry_attachment/sentry_attachment.dart @@ -1,13 +1,14 @@ import 'dart:async'; import 'dart:typed_data'; +// https://develop.sentry.dev/sdk/features/#attachments // https://develop.sentry.dev/sdk/envelopes/#attachment typedef ContentLoader = FutureOr Function(); /// Arbitrary content which gets attached to an event. class SentryAttachment { - SentryAttachment({ + SentryAttachment.fromLoader({ required ContentLoader loader, required this.filename, String? attachmentType, @@ -16,49 +17,43 @@ class SentryAttachment { attachmentType = attachmentType ?? AttachmentType.attachment; /// Creates an [SentryAttachment] from a [Uint8List] - factory SentryAttachment.fromUint8List( + SentryAttachment.fromUint8List( Uint8List bytes, String fileName, { String? contentType, String? attachmentType, - }) { - return SentryAttachment( - attachmentType: attachmentType, - loader: () => bytes, - filename: fileName, - contentType: contentType, - ); - } + }) : this.fromLoader( + attachmentType: attachmentType, + loader: () => bytes, + filename: fileName, + contentType: contentType, + ); /// Creates an [SentryAttachment] from a [List] - factory SentryAttachment.fromIntList( + SentryAttachment.fromIntList( List bytes, String fileName, { String? contentType, String? attachmentType, - }) { - return SentryAttachment( - attachmentType: attachmentType, - loader: () => Uint8List.fromList(bytes), - filename: fileName, - contentType: contentType, - ); - } + }) : this.fromLoader( + attachmentType: attachmentType, + loader: () => Uint8List.fromList(bytes), + filename: fileName, + contentType: contentType, + ); /// Creates an [SentryAttachment] from [ByteData] - factory SentryAttachment.fromByteData( + SentryAttachment.fromByteData( ByteData bytes, String fileName, { String? contentType, String? attachmentType, - }) { - return SentryAttachment( - attachmentType: attachmentType, - loader: () => bytes.buffer.asUint8List(), - filename: fileName, - contentType: contentType, - ); - } + }) : this.fromLoader( + attachmentType: attachmentType, + loader: () => bytes.buffer.asUint8List(), + filename: fileName, + contentType: contentType, + ); /// Attachment type. /// Should be one of types given in [AttachmentType]. diff --git a/dart/lib/src/sentry_attachment_extensions/io_attachment_extensions.dart b/dart/lib/src/sentry_attachment_extensions/io_attachment_extensions.dart deleted file mode 100644 index 292f0d1fd8..0000000000 --- a/dart/lib/src/sentry_attachment_extensions/io_attachment_extensions.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'dart:io'; - -import '../sentry_attachment.dart'; -import '../scope.dart'; - -extension IoAttachmentExtensions on Scope { - /// Creates an attachment from a given path. - /// Only available on `dart:io` platforms. - /// Not available on web. - static SentryAttachment fromPath( - String path, { - String? filename, - String? attachmentType, - String? contentType, - }) { - final file = File(path); - - return fromFile( - file, - attachmentType: attachmentType, - contentType: contentType, - filename: filename, - ); - } - - /// Creates an attachment from a given [File]. - /// Only available on `dart:io` platforms. - /// Not available on web. - static SentryAttachment fromFile( - File file, { - String? filename, - String? attachmentType, - String? contentType, - }) { - return SentryAttachment( - attachmentType: attachmentType, - loader: () => file.readAsBytes(), - filename: filename ?? file.uri.pathSegments.last, - contentType: contentType, - ); - } -} diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index 5445f8e2f7..ce79f61c68 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'sentry_attachment.dart'; +import 'sentry_attachment/sentry_attachment.dart'; import 'sentry_envelope_header.dart'; import 'sentry_envelope_item.dart'; import 'protocol/sentry_event.dart'; diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index 0825c7c28a..9c9e5f021d 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'sentry_attachment.dart'; +import 'sentry_attachment/sentry_attachment.dart'; import 'sentry_item_type.dart'; import 'protocol/sentry_event.dart'; import 'sentry_envelope_item_header.dart'; diff --git a/dart/test/sentry_attachment_test.dart b/dart/test/sentry_attachment_test.dart index 9192c7fd69..ccf00da2f6 100644 --- a/dart/test/sentry_attachment_test.dart +++ b/dart/test/sentry_attachment_test.dart @@ -9,7 +9,7 @@ import 'mocks/mock_transport.dart'; void main() { group('$SentryAttachment ctor', () { test('default', () async { - final attachment = SentryAttachment( + final attachment = SentryAttachment.fromLoader( loader: () => Uint8List.fromList([0, 0, 0, 0]), filename: 'test.txt', ); diff --git a/dart/test/sentry_attachment_test_io.dart b/dart/test/sentry_attachment_test_io.dart new file mode 100644 index 0000000000..1ef161da43 --- /dev/null +++ b/dart/test/sentry_attachment_test_io.dart @@ -0,0 +1,31 @@ +@TestOn('vm') + +import 'dart:io'; + +import 'package:sentry/sentry_io.dart'; +import 'package:test/test.dart'; + +void main() { + group('$SentryAttachment ctor', () { + test('fromFile', () async { + final file = File('test_resources/testfile.txt'); + + final attachment = IoSentryAttachment.fromFile(file); + expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.contentType, isNull); + expect(attachment.filename, 'testfile.txt'); + await expectLater( + await attachment.bytes, [102, 111, 111, 32, 98, 97, 114]); + }); + + test('fromPath', () async { + final attachment = + IoSentryAttachment.fromPath('test_resources/testfile.txt'); + expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.contentType, isNull); + expect(attachment.filename, 'testfile.txt'); + await expectLater( + await attachment.bytes, [102, 111, 111, 32, 98, 97, 114]); + }); + }); +} diff --git a/dart/test_resources/testfile.txt b/dart/test_resources/testfile.txt new file mode 100644 index 0000000000..96c906756d --- /dev/null +++ b/dart/test_resources/testfile.txt @@ -0,0 +1 @@ +foo bar \ No newline at end of file diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index 7c3666a02f..9d4b8cc89f 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -5,4 +5,4 @@ export 'src/default_integrations.dart'; export 'src/navigation/sentry_navigator_observer.dart'; export 'src/sentry_flutter.dart'; export 'src/sentry_flutter_options.dart'; -export 'src/attachment_extensions.dart'; +export 'src/flutter_sentry_attachment.dart'; diff --git a/flutter/lib/src/attachment_extensions.dart b/flutter/lib/src/flutter_sentry_attachment.dart similarity index 53% rename from flutter/lib/src/attachment_extensions.dart rename to flutter/lib/src/flutter_sentry_attachment.dart index fb92e1c8d2..c644a942c2 100644 --- a/flutter/lib/src/attachment_extensions.dart +++ b/flutter/lib/src/flutter_sentry_attachment.dart @@ -1,7 +1,7 @@ import 'package:flutter/services.dart'; import 'package:sentry/sentry.dart'; -extension AttachmentExtension on SentryAttachment { +class FlutterSentryAttachment extends SentryAttachment { /// Creates an attachment from an asset out of a [AssetBundle]. /// If no bundle is given, it's using the [rootBundle]. /// Typically you want to use it like this: @@ -11,20 +11,19 @@ extension AttachmentExtension on SentryAttachment { /// bundle: DefaultAssetBundle.of(context), /// ); /// ``` - static Future fromAsset( + FlutterSentryAttachment.fromAsset( String key, { + String? filename, AssetBundle? bundle, String? type, String? contentType, - }) async { - final data = await (bundle ?? rootBundle).load(key); - final fileName = Uri.parse(key).pathSegments.last; - - return SentryAttachment.fromByteData( - data, - fileName, - attachmentType: type, - contentType: contentType, - ); - } + }) : super.fromLoader( + loader: () async { + final data = await (bundle ?? rootBundle).load(key); + return data.buffer.asUint8List(); + }, + filename: filename ?? Uri.parse(key).pathSegments.last, + attachmentType: type, + contentType: contentType, + ); } diff --git a/flutter/test/flutter_sentry_attachment_test.dart b/flutter/test/flutter_sentry_attachment_test.dart new file mode 100644 index 0000000000..ab73b3a234 --- /dev/null +++ b/flutter/test/flutter_sentry_attachment_test.dart @@ -0,0 +1 @@ +void main() {} From 0e415032b336847b40c73041762478ebcc121fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Mon, 28 Jun 2021 19:55:58 +0200 Subject: [PATCH 13/34] test --- .../test/flutter_sentry_attachment_test.dart | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/flutter/test/flutter_sentry_attachment_test.dart b/flutter/test/flutter_sentry_attachment_test.dart index ab73b3a234..26194c29ea 100644 --- a/flutter/test/flutter_sentry_attachment_test.dart +++ b/flutter/test/flutter_sentry_attachment_test.dart @@ -1 +1,33 @@ -void main() {} +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +void main() { + test('fromAsset', () async { + final attachment = FlutterSentryAttachment.fromAsset( + 'foobar.txt', + bundle: TestAssetBundle(), + ); + + expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.contentType, isNull); + expect(attachment.filename, 'foobar.txt'); + await expectLater(await attachment.bytes, [102, 111, 111, 32, 98, 97, 114]); + }); +} + +class TestAssetBundle extends CachingAssetBundle { + @override + Future load(String key) async { + if (key == 'foobar.txt') { + return ByteData.view( + Uint8List.fromList(utf8.encode('foo bar')).buffer, + ); + } + + return ByteData(0); + } +} From 661116b5a795320b323016f08225b2239aa5c89b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Mon, 28 Jun 2021 20:05:27 +0200 Subject: [PATCH 14/34] fix file system transport --- flutter/lib/src/file_system_transport.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/src/file_system_transport.dart b/flutter/lib/src/file_system_transport.dart index 9574607d6c..d155a93538 100644 --- a/flutter/lib/src/file_system_transport.dart +++ b/flutter/lib/src/file_system_transport.dart @@ -24,7 +24,7 @@ class FileSystemTransport implements Transport { exception: exception, stackTrace: stackTrace, ); - return null; + return SentryId.empty(); } return envelope.header.eventId; From 3a12ed7a8106efa4634200b8d1eab60eb2fbe745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Tue, 29 Jun 2021 08:06:33 +0200 Subject: [PATCH 15/34] skip attachment if it throws --- dart/lib/src/sentry_envelope.dart | 4 +- dart/lib/src/sentry_envelope_item.dart | 16 +- dart/test/sentry_client_test.dart | 5 +- dart/test/sentry_envelope_item_test.dart | 3 +- dart/test/sentry_envelope_test.dart | 11 +- dart/test/sentry_envelope_vm_test.dart | 251 +++++++++++++++++++++++ 6 files changed, 270 insertions(+), 20 deletions(-) diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index ce79f61c68..cdd455e9c5 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -38,9 +38,7 @@ class SentryEnvelope { final newLineData = utf8.encode('\n'); for (final item in items) { yield newLineData; - await for (final chunk in item.envelopeItemStream()) { - yield chunk; - } + yield await item.envelopeItemStream(); } } } diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index 9c9e5f021d..f33ac682c1 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -54,10 +54,18 @@ class SentryEnvelopeItem { final Future> Function() dataFactory; /// Stream binary data of `Envelope` item. - Stream> envelopeItemStream() async* { - yield utf8.encode(jsonEncode(await header.toJson())); - yield utf8.encode('\n'); - yield await dataFactory(); + Future> envelopeItemStream() async { + // Each item needs to be encoded as one unit. + // Otherwise the header alredy got yielded if the content throws + // an exception. + try { + final itemHeader = utf8.encode(jsonEncode(await header.toJson())); + final newLine = utf8.encode('\n'); + final data = await dataFactory(); + return [...itemHeader, ...newLine, ...data]; + } catch (e) { + return []; + } } } diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index aec2f74b8e..3e42cfea35 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -725,9 +725,8 @@ void main() { Future eventFromEnvelope(SentryEnvelope envelope) async { final envelopeItemData = []; - await envelope.items.first - .envelopeItemStream() - .forEach(envelopeItemData.addAll); + envelopeItemData.addAll(await envelope.items.first.envelopeItemStream()); + final envelopeItem = utf8.decode(envelopeItemData); final envelopeItemJson = jsonDecode(envelopeItem.split('\n').last); return SentryEvent.fromJson(envelopeItemJson as Map); diff --git a/dart/test/sentry_envelope_item_test.dart b/dart/test/sentry_envelope_item_test.dart index 3e000cf953..c367e57c9c 100644 --- a/dart/test/sentry_envelope_item_test.dart +++ b/dart/test/sentry_envelope_item_test.dart @@ -24,8 +24,7 @@ void main() { final headerJsonEncoded = jsonEncode(headerJson); final expected = utf8.encode('$headerJsonEncoded\n{fixture}'); - final actualItem = []; - await sut.envelopeItemStream().forEach(actualItem.addAll); + final actualItem = await sut.envelopeItemStream(); expect(actualItem, expected); }); diff --git a/dart/test/sentry_envelope_test.dart b/dart/test/sentry_envelope_test.dart index 624e4b0992..c153509ab6 100644 --- a/dart/test/sentry_envelope_test.dart +++ b/dart/test/sentry_envelope_test.dart @@ -31,8 +31,7 @@ void main() { final expectedHeaderJson = header.toJson(); final expectedHeaderJsonSerialized = jsonEncode(expectedHeaderJson); - final expectedItem = []; - await item.envelopeItemStream().forEach(expectedItem.addAll); + final expectedItem = await item.envelopeItemStream(); final expectedItemSerialized = utf8.decode(expectedItem); final expected = utf8.encode( @@ -60,13 +59,9 @@ void main() { expect(await sut.items[0].header.length(), await expectedEnvelopeItem.header.length()); - final actualItem = []; - await sut.items[0].envelopeItemStream().forEach(actualItem.addAll); + final actualItem = await sut.items[0].envelopeItemStream(); - final expectedItem = []; - await expectedEnvelopeItem - .envelopeItemStream() - .forEach(expectedItem.addAll); + final expectedItem = await expectedEnvelopeItem.envelopeItemStream(); expect(actualItem, expectedItem); }); diff --git a/dart/test/sentry_envelope_vm_test.dart b/dart/test/sentry_envelope_vm_test.dart index ffbdd14e08..497552a2f9 100644 --- a/dart/test/sentry_envelope_vm_test.dart +++ b/dart/test/sentry_envelope_vm_test.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:sentry/sentry.dart'; +import 'package:sentry/sentry_io.dart'; import 'package:sentry/src/sentry_envelope.dart'; import 'package:sentry/src/sentry_envelope_header.dart'; import 'package:sentry/src/sentry_envelope_item_header.dart'; @@ -42,5 +43,255 @@ void main() { expect(expectedEnvelopeData, envelopeData); }); + + test('skips attachment if path is invalid', () async { + final event = SentryEvent( + eventId: SentryId.empty(), + timestamp: DateTime(1970, 1, 1), + ); + final sdkVersion = SdkVersion(name: '', version: ''); + final attachment = + IoSentryAttachment.fromPath('this_path_does_not_exist.txt'); + + final envelope = SentryEnvelope.fromEvent(event, sdkVersion, + attachments: [attachment]); + + final data = + (await envelope.envelopeStream().toList()).reduce((a, b) => a + b); + + expect(data, envelopeBinaryData); + }); }); } + +final envelopeBinaryData = [ + 123, + 34, + 101, + 118, + 101, + 110, + 116, + 95, + 105, + 100, + 34, + 58, + 34, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 34, + 44, + 34, + 115, + 100, + 107, + 34, + 58, + 123, + 34, + 110, + 97, + 109, + 101, + 34, + 58, + 34, + 34, + 44, + 34, + 118, + 101, + 114, + 115, + 105, + 111, + 110, + 34, + 58, + 34, + 34, + 125, + 125, + 10, + 123, + 34, + 99, + 111, + 110, + 116, + 101, + 110, + 116, + 95, + 116, + 121, + 112, + 101, + 34, + 58, + 34, + 97, + 112, + 112, + 108, + 105, + 99, + 97, + 116, + 105, + 111, + 110, + 47, + 106, + 115, + 111, + 110, + 34, + 44, + 34, + 116, + 121, + 112, + 101, + 34, + 58, + 34, + 101, + 118, + 101, + 110, + 116, + 34, + 44, + 34, + 108, + 101, + 110, + 103, + 116, + 104, + 34, + 58, + 56, + 54, + 125, + 10, + 123, + 34, + 101, + 118, + 101, + 110, + 116, + 95, + 105, + 100, + 34, + 58, + 34, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 48, + 34, + 44, + 34, + 116, + 105, + 109, + 101, + 115, + 116, + 97, + 109, + 112, + 34, + 58, + 34, + 49, + 57, + 55, + 48, + 45, + 48, + 49, + 45, + 48, + 49, + 84, + 48, + 48, + 58, + 48, + 48, + 58, + 48, + 48, + 46, + 48, + 48, + 48, + 90, + 34, + 125, + 10 +]; From 9dae6ab55cf1c3b45b3a248703721e9c3f336990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Thu, 1 Jul 2021 08:38:21 +0200 Subject: [PATCH 16/34] fix sentry id --- dart/lib/src/protocol/sentry_id.dart | 30 ++++++++++++----- dart/test/sentry_id_test.dart | 35 ++++++++++++++++++++ dart/test/transport/http_transport_test.dart | 2 +- 3 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 dart/test/sentry_id_test.dart diff --git a/dart/lib/src/protocol/sentry_id.dart b/dart/lib/src/protocol/sentry_id.dart index d9291d42d0..500c8980a2 100644 --- a/dart/lib/src/protocol/sentry_id.dart +++ b/dart/lib/src/protocol/sentry_id.dart @@ -1,28 +1,40 @@ import 'package:meta/meta.dart'; import 'package:uuid/uuid.dart'; -/// Hexadecimal string representing a uuid4 value +/// Hexadecimal string representing a uuid4 value. +/// The length is exactly 32 +/// characters. Dashes are not allowed. Has to be lowercase. @immutable class SentryId { - static final SentryId _emptyId = - SentryId.fromId('00000000-0000-0000-0000-000000000000'); - /// The ID Sentry.io assigned to the submitted event for future reference. final String _id; static final Uuid _uuidGenerator = Uuid(); - SentryId._internal({String? id}) : _id = id ?? _uuidGenerator.v4(); + SentryId._internal({String? id}) + : _id = + id?.replaceAll('-', '') ?? _uuidGenerator.v4().replaceAll('-', ''); /// Generates a new SentryId - factory SentryId.newId() => SentryId._internal(); + SentryId.newId() : this._internal(); /// Generates a SentryId with the given UUID - factory SentryId.fromId(String id) => SentryId._internal(id: id); + SentryId.fromId(String id) : this._internal(id: id); /// SentryId with an empty UUID - factory SentryId.empty() => _emptyId; + const SentryId.empty() : _id = '00000000000000000000000000000000'; + + @override + String toString() => _id; + + @override + int get hashCode => _id.hashCode; @override - String toString() => _id.replaceAll('-', ''); + bool operator ==(o) { + if (o is SentryId) { + return o._id == _id; + } + return false; + } } diff --git a/dart/test/sentry_id_test.dart b/dart/test/sentry_id_test.dart new file mode 100644 index 0000000000..23878f7082 --- /dev/null +++ b/dart/test/sentry_id_test.dart @@ -0,0 +1,35 @@ +import 'package:sentry/src/protocol/sentry_id.dart'; +import 'package:test/test.dart'; + +void main() { + test('empty id', () { + expect(SentryId.empty().toString(), '00000000000000000000000000000000'); + }); + + test('empty id equals from empty id', () { + expect( + SentryId.empty(), + SentryId.fromId('00000000000000000000000000000000'), + ); + }); + + test('uuid format with dashes', () { + expect( + SentryId.fromId('00000000-0000-0000-0000-000000000000'), + SentryId.empty(), + ); + }); + + test('empty id equality', () { + expect(SentryId.empty(), SentryId.empty()); + }); + + test('id roundtrip', () { + final id = SentryId.newId(); + expect(id, SentryId.fromId(id.toString())); + }); + + test('newId should not be equal to newId', () { + expect(SentryId.newId() == SentryId.newId(), false); + }); +} diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart index 0597ecfcd2..89e5613814 100644 --- a/dart/test/transport/http_transport_test.dart +++ b/dart/test/transport/http_transport_test.dart @@ -92,7 +92,7 @@ void main() { SentryEnvelope.fromEvent(sentryEvent, fixture.options.sdk); final eventId = await sut.send(envelope); - expect(eventId.toString(), SentryId.empty().toString()); + expect(eventId, SentryId.empty()); expect(httpCalled, false); }); }); From 313d407486c7a5a2daad74581fadce27bc29dedc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Fri, 2 Jul 2021 19:19:48 +0200 Subject: [PATCH 17/34] WIP --- CHANGELOG.md | 3 +- dart/lib/src/scope.dart | 4 +- .../sentry_attachment/sentry_attachment.dart | 45 +++++++++---------- dart/lib/src/sentry_envelope.dart | 7 ++- dart/test/sentry_attachment_test.dart | 10 ++--- dart/test/sentry_attachment_test_io.dart | 4 +- dart/test/sentry_envelope_vm_test.dart | 10 +++-- 7 files changed, 43 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 875d8f56e3..149a454b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +* Feat: Add support for attachments (#505) + # 6.0.0-beta.1 * Feat: Browser detection (#502) @@ -8,7 +10,6 @@ * Feat: Add DeduplicationEventProcessor (#498) * Feat: Capture failed requests as event (#473) * Feat: `beforeSend` callback accepts async code (#494) -* Feat: Add support for attachments (#505) ## Breaking Changes: diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 73a7cbd96b..89b003df20 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -120,7 +120,7 @@ class Scope { _breadcrumbs.add(breadcrumb); } - void addAttachment(SentryAttachment attachment) async { + void addAttachment(SentryAttachment attachment) { _attachements.add(attachment); } @@ -299,7 +299,7 @@ class Scope { }); for (final attachment in _attachements) { - clone._attachements.add(attachment); + clone.addAttachment(attachment); } return clone; diff --git a/dart/lib/src/sentry_attachment/sentry_attachment.dart b/dart/lib/src/sentry_attachment/sentry_attachment.dart index 89f82ed9b0..33d609e097 100644 --- a/dart/lib/src/sentry_attachment/sentry_attachment.dart +++ b/dart/lib/src/sentry_attachment/sentry_attachment.dart @@ -8,13 +8,33 @@ typedef ContentLoader = FutureOr Function(); /// Arbitrary content which gets attached to an event. class SentryAttachment { + /// Standard attachment without special meaning. + static const String typeAttachmentDefault = 'event.attachment'; + + /// Minidump file that creates an error event and is symbolicated. + /// The file should start with the `MDMP` magic bytes. + static const String typeMinidump = 'event.minidump'; + + /// Apple crash report file that creates an error event and is symbolicated. + static const String typeAppleCrashReport = 'event.applecrashreport'; + + /// XML file containing UE4 crash meta data. + /// During event ingestion, event contexts and extra fields are extracted from + /// this file. + static const String typeUnrealContext = 'unreal.context'; + + /// Plain-text log file obtained from UE4 crashes. + /// During event ingestion, the last logs are extracted into event + /// breadcrumbs. + static const String typeUnrealLogs = 'unreal.logs'; + SentryAttachment.fromLoader({ required ContentLoader loader, required this.filename, String? attachmentType, this.contentType, }) : _loader = loader, - attachmentType = attachmentType ?? AttachmentType.attachment; + attachmentType = attachmentType ?? typeAttachmentDefault; /// Creates an [SentryAttachment] from a [Uint8List] SentryAttachment.fromUint8List( @@ -72,26 +92,3 @@ class SentryAttachment { /// Inferred by Sentry if it's not given. final String? contentType; } - -/// Attachment type. -class AttachmentType { - /// Standard attachment without special meaning. - static const String attachment = 'event.attachment'; - - /// Minidump file that creates an error event and is symbolicated. - /// The file should start with the `MDMP` magic bytes. - static const String minidump = 'event.minidump'; - - /// Apple crash report file that creates an error event and is symbolicated. - static const String appleCrashReport = 'event.applecrashreport'; - - /// XML file containing UE4 crash meta data. - /// During event ingestion, event contexts and extra fields are extracted from - /// this file. - static const String unrealContext = 'unreal.context'; - - /// Plain-text log file obtained from UE4 crashes. - /// During event ingestion, the last logs are extracted into event - /// breadcrumbs. - static const String unrealLogs = 'unreal.logs'; -} diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index cdd455e9c5..6de2f69a4b 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -37,8 +37,11 @@ class SentryEnvelope { yield utf8.encode(jsonEncode(header.toJson())); final newLineData = utf8.encode('\n'); for (final item in items) { - yield newLineData; - yield await item.envelopeItemStream(); + final itemStream = await item.envelopeItemStream(); + if (itemStream.isNotEmpty) { + yield newLineData; + yield itemStream; + } } } } diff --git a/dart/test/sentry_attachment_test.dart b/dart/test/sentry_attachment_test.dart index ccf00da2f6..b4d47bbfdd 100644 --- a/dart/test/sentry_attachment_test.dart +++ b/dart/test/sentry_attachment_test.dart @@ -13,7 +13,7 @@ void main() { loader: () => Uint8List.fromList([0, 0, 0, 0]), filename: 'test.txt', ); - expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.attachmentType, SentryAttachment.typeAttachmentDefault); expect(attachment.contentType, isNull); expect(attachment.filename, 'test.txt'); await expectLater(await attachment.bytes, [0, 0, 0, 0]); @@ -21,7 +21,7 @@ void main() { test('fromIntList', () async { final attachment = SentryAttachment.fromIntList([0, 0, 0, 0], 'test.txt'); - expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.attachmentType, SentryAttachment.typeAttachmentDefault); expect(attachment.contentType, isNull); expect(attachment.filename, 'test.txt'); await expectLater(await attachment.bytes, [0, 0, 0, 0]); @@ -32,7 +32,7 @@ void main() { Uint8List.fromList([0, 0, 0, 0]), 'test.txt', ); - expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.attachmentType, SentryAttachment.typeAttachmentDefault); expect(attachment.contentType, isNull); expect(attachment.filename, 'test.txt'); await expectLater(await attachment.bytes, [0, 0, 0, 0]); @@ -43,7 +43,7 @@ void main() { ByteData.sublistView(Uint8List.fromList([0, 0, 0, 0])), 'test.txt', ); - expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.attachmentType, SentryAttachment.typeAttachmentDefault); expect(attachment.contentType, isNull); expect(attachment.filename, 'test.txt'); await expectLater(await attachment.bytes, [0, 0, 0, 0]); @@ -66,7 +66,7 @@ void main() { final attachmentEnvelope = transport.envelopes.first.items[1]; expect( attachmentEnvelope.header.attachmentType, - AttachmentType.attachment, + SentryAttachment.typeAttachmentDefault, ); expect( attachmentEnvelope.header.contentType, diff --git a/dart/test/sentry_attachment_test_io.dart b/dart/test/sentry_attachment_test_io.dart index 1ef161da43..2752355a2b 100644 --- a/dart/test/sentry_attachment_test_io.dart +++ b/dart/test/sentry_attachment_test_io.dart @@ -11,7 +11,7 @@ void main() { final file = File('test_resources/testfile.txt'); final attachment = IoSentryAttachment.fromFile(file); - expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.attachmentType, SentryAttachment.typeAttachmentDefault); expect(attachment.contentType, isNull); expect(attachment.filename, 'testfile.txt'); await expectLater( @@ -21,7 +21,7 @@ void main() { test('fromPath', () async { final attachment = IoSentryAttachment.fromPath('test_resources/testfile.txt'); - expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.attachmentType, SentryAttachment.typeAttachmentDefault); expect(attachment.contentType, isNull); expect(attachment.filename, 'testfile.txt'); await expectLater( diff --git a/dart/test/sentry_envelope_vm_test.dart b/dart/test/sentry_envelope_vm_test.dart index 497552a2f9..fc0396dc5a 100644 --- a/dart/test/sentry_envelope_vm_test.dart +++ b/dart/test/sentry_envelope_vm_test.dart @@ -53,8 +53,11 @@ void main() { final attachment = IoSentryAttachment.fromPath('this_path_does_not_exist.txt'); - final envelope = SentryEnvelope.fromEvent(event, sdkVersion, - attachments: [attachment]); + final envelope = SentryEnvelope.fromEvent( + event, + sdkVersion, + attachments: [attachment], + ); final data = (await envelope.envelopeStream().toList()).reduce((a, b) => a + b); @@ -292,6 +295,5 @@ final envelopeBinaryData = [ 48, 90, 34, - 125, - 10 + 125 ]; From cd6aae62ed8b5b88d2fc7749c15d887f81d2eaab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Fri, 2 Jul 2021 20:20:40 +0200 Subject: [PATCH 18/34] rename test file to be identified as test --- ...try_attachment_test_io.dart => sentry_attachment_io_test.dart} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dart/test/{sentry_attachment_test_io.dart => sentry_attachment_io_test.dart} (100%) diff --git a/dart/test/sentry_attachment_test_io.dart b/dart/test/sentry_attachment_io_test.dart similarity index 100% rename from dart/test/sentry_attachment_test_io.dart rename to dart/test/sentry_attachment_io_test.dart From 20af83732c5872103142a61a2d6af8f02956868e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Fri, 2 Jul 2021 20:35:22 +0200 Subject: [PATCH 19/34] fix --- flutter/test/flutter_sentry_attachment_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/test/flutter_sentry_attachment_test.dart b/flutter/test/flutter_sentry_attachment_test.dart index 26194c29ea..621683e6bb 100644 --- a/flutter/test/flutter_sentry_attachment_test.dart +++ b/flutter/test/flutter_sentry_attachment_test.dart @@ -12,7 +12,7 @@ void main() { bundle: TestAssetBundle(), ); - expect(attachment.attachmentType, AttachmentType.attachment); + expect(attachment.attachmentType, SentryAttachment.typeAttachmentDefault); expect(attachment.contentType, isNull); expect(attachment.filename, 'foobar.txt'); await expectLater(await attachment.bytes, [102, 111, 111, 32, 98, 97, 114]); From 5727ef1837b3860cb0efca9aa5b7667a1a2e15e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Mon, 5 Jul 2021 08:17:49 +0200 Subject: [PATCH 20/34] test fixture --- dart/test/sentry_attachment_test.dart | 29 +++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/dart/test/sentry_attachment_test.dart b/dart/test/sentry_attachment_test.dart index b4d47bbfdd..96bf0b53da 100644 --- a/dart/test/sentry_attachment_test.dart +++ b/dart/test/sentry_attachment_test.dart @@ -51,19 +51,22 @@ void main() { }); group('$Scope $SentryAttachment tests', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + test('Sending with attachments', () async { - final options = SentryOptions(dsn: fakeDsn); - final transport = MockTransport(); - options.transport = transport; - final hub = Hub(options); - await hub.captureEvent(SentryEvent(), withScope: (scope) { + final sut = fixture.getSut(); + await sut.captureEvent(SentryEvent(), withScope: (scope) { scope.addAttachment( SentryAttachment.fromIntList([0, 0, 0, 0], 'test.txt'), ); }); - expect(transport.envelopes.length, 1); - expect(transport.envelopes.first.items.length, 2); - final attachmentEnvelope = transport.envelopes.first.items[1]; + expect(fixture.transport.envelopes.length, 1); + expect(fixture.transport.envelopes.first.items.length, 2); + final attachmentEnvelope = fixture.transport.envelopes.first.items[1]; expect( attachmentEnvelope.header.attachmentType, SentryAttachment.typeAttachmentDefault, @@ -83,3 +86,13 @@ void main() { }); }); } + +class Fixture { + MockTransport transport = MockTransport(); + + Hub getSut() { + final options = SentryOptions(dsn: fakeDsn); + options.transport = transport; + return Hub(options); + } +} From d921cb14ae4cffa5b78bba110ee45351228ed054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sun, 20 Jun 2021 16:59:09 +0200 Subject: [PATCH 21/34] Attachment support --- CHANGELOG.md | 1 + dart/lib/src/sentry_envelope.dart | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 149a454b70..87a2115e33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased * Feat: Add support for attachments (#505) +* Feat: Add support for attachments (#494) # 6.0.0-beta.1 diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index 6de2f69a4b..562c154a47 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -1,5 +1,4 @@ import 'dart:convert'; - import 'sentry_attachment/sentry_attachment.dart'; import 'sentry_envelope_header.dart'; import 'sentry_envelope_item.dart'; From 588a63d6b15414c17fefbe8639f797d2f50f5358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sun, 20 Jun 2021 20:07:29 +0200 Subject: [PATCH 22/34] fix pr id --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a2115e33..2ea35349aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * Feat: Add DeduplicationEventProcessor (#498) * Feat: Capture failed requests as event (#473) * Feat: `beforeSend` callback accepts async code (#494) +* Feat: Add support for attachments (#505) ## Breaking Changes: From d8fb07a401b15ecd05d7a9002c9c8680f0462dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sun, 20 Jun 2021 20:05:02 +0200 Subject: [PATCH 23/34] User Feedback --- CHANGELOG.md | 1 + dart/lib/sentry.dart | 1 + dart/lib/src/hub.dart | 9 + dart/lib/src/hub_adapter.dart | 8 +- dart/lib/src/noop_hub.dart | 5 +- dart/lib/src/sentry.dart | 4 + dart/lib/src/sentry_envelope.dart | 11 + dart/lib/src/sentry_envelope_item.dart | 20 + dart/lib/src/user_feedback.dart | 54 +++ dart/test/mocks/mock_hub.dart | 7 + flutter/example/lib/main.dart | 90 +++- flutter/example/pubspec.yaml | 3 +- flutter/lib/sentry_flutter.dart | 1 + flutter/lib/src/ui/user_feedback_dialog.dart | 458 +++++++++++++++++++ flutter/test/mocks.dart | 4 + 15 files changed, 666 insertions(+), 10 deletions(-) create mode 100644 dart/lib/src/user_feedback.dart create mode 100644 flutter/lib/src/ui/user_feedback_dialog.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea35349aa..b0358dd908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * Feat: Capture failed requests as event (#473) * Feat: `beforeSend` callback accepts async code (#494) * Feat: Add support for attachments (#505) +* Feat: Add support for User Feedback (#506) ## Breaking Changes: diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 439bf21587..568d670bd4 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -23,3 +23,4 @@ export 'src/integration.dart'; export 'src/event_processor.dart'; export 'src/http_client/sentry_http_client.dart'; export 'src/sentry_attachment/sentry_attachment.dart'; +export 'src/user_feedback.dart'; diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index ce649ab168..b416137282 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -4,7 +4,9 @@ import 'dart:collection'; import 'protocol.dart'; import 'scope.dart'; import 'sentry_client.dart'; +import 'sentry_envelope.dart'; import 'sentry_options.dart'; +import 'user_feedback.dart'; /// Configures the scope through the callback. typedef ScopeCallback = void Function(Scope); @@ -182,6 +184,13 @@ class Hub { return sentryId; } + Future captureUserFeedback(UserFeedback userFeedback) async { + final item = _peek(); + await item.client.captureEnvelope( + SentryEnvelope.fromUserFeedback(userFeedback, _options.sdk), + ); + } + Scope _cloneAndRunWithScope(Scope scope, ScopeCallback? withScope) { if (withScope != null) { scope = scope.clone(); diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index ddfdc3f054..21315aacf2 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -1,13 +1,13 @@ import 'dart:async'; - import 'hub.dart'; import 'protocol.dart'; import 'sentry.dart'; import 'sentry_client.dart'; +import 'user_feedback.dart'; /// Hub adapter to make Integrations testable class HubAdapter implements Hub { - HubAdapter._(); + const HubAdapter._(); static final HubAdapter _instance = HubAdapter._(); @@ -82,4 +82,8 @@ class HubAdapter implements Hub { @override SentryId get lastEventId => Sentry.lastEventId; + + @override + Future captureUserFeedback(UserFeedback userFeedback) => + Sentry.captureUserFeedback(userFeedback); } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index b1520f672d..421def07bf 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -1,8 +1,8 @@ import 'dart:async'; - import 'hub.dart'; import 'protocol.dart'; import 'sentry_client.dart'; +import 'user_feedback.dart'; class NoOpHub implements Hub { NoOpHub._(); @@ -62,4 +62,7 @@ class NoOpHub implements Hub { @override void addBreadcrumb(Breadcrumb crumb, {dynamic hint}) {} + + @override + Future captureUserFeedback(UserFeedback userFeedback) async {} } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 1522ec9bf4..5c68430e73 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -13,6 +13,7 @@ import 'protocol.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; import 'integration.dart'; +import 'user_feedback.dart'; /// Configuration options callback typedef OptionsConfiguration = FutureOr Function(SentryOptions); @@ -176,6 +177,9 @@ class Sentry { withScope: withScope, ); + static Future captureUserFeedback(UserFeedback userFeedback) => + _hub.captureUserFeedback(userFeedback); + /// Close the client SDK static Future close() async { final hub = _hub; diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index 562c154a47..b07131b5f1 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -4,6 +4,7 @@ import 'sentry_envelope_header.dart'; import 'sentry_envelope_item.dart'; import 'protocol/sentry_event.dart'; import 'protocol/sdk_version.dart'; +import 'user_feedback.dart'; /// Class representation of `Envelope` file. class SentryEnvelope { @@ -31,6 +32,16 @@ class SentryEnvelope { ); } + factory SentryEnvelope.fromUserFeedback( + UserFeedback feedback, + SdkVersion sdkVersion, + ) { + return SentryEnvelope( + SentryEnvelopeHeader(feedback.eventId, sdkVersion), + [SentryEnvelopeItem.fromUserFeedback(feedback)], + ); + } + /// Stream binary data representation of `Envelope` file encoded. Stream> envelopeStream() async* { yield utf8.encode(jsonEncode(header.toJson())); diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index f33ac682c1..572bc39dd8 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -4,6 +4,7 @@ import 'sentry_attachment/sentry_attachment.dart'; import 'sentry_item_type.dart'; import 'protocol/sentry_event.dart'; import 'sentry_envelope_item_header.dart'; +import 'user_feedback.dart'; /// Item holding header information and JSON encoded data. class SentryEnvelopeItem { @@ -26,6 +27,25 @@ class SentryEnvelopeItem { return SentryEnvelopeItem(header, cachedItem.getData); } + /// Create an [SentryEnvelopeItem] which sends [UserFeedback]. + factory SentryEnvelopeItem.fromUserFeedback(UserFeedback feedback) { + final cachedItem = _CachedItem(() async { + final jsonEncoded = jsonEncode(feedback.toJson()); + return utf8.encode(jsonEncoded); + }); + + final getLength = () async { + return (await cachedItem.getData()).length; + }; + + final header = SentryEnvelopeItemHeader( + SentryItemType.userFeedback, + getLength, + contentType: 'application/json', + ); + return SentryEnvelopeItem(header, cachedItem.getData); + } + /// Create an [SentryEnvelopeItem] which holds the [SentryEvent] data. factory SentryEnvelopeItem.fromEvent(SentryEvent event) { final cachedItem = _CachedItem(() async { diff --git a/dart/lib/src/user_feedback.dart b/dart/lib/src/user_feedback.dart new file mode 100644 index 0000000000..0be444ca58 --- /dev/null +++ b/dart/lib/src/user_feedback.dart @@ -0,0 +1,54 @@ +import 'protocol.dart'; + +class UserFeedback { + UserFeedback({ + required this.eventId, + this.name, + this.email, + this.comments, + }); + + factory UserFeedback.fromJson(Map json) { + return UserFeedback( + eventId: json['event_id'], + name: json['name'], + email: json['email'], + comments: json['comments'], + ); + } + + /// The eventId of the event to which the user feedback is associated. + final SentryId eventId; + + /// Recommended: The name of the user. + final String? name; + + /// Recommended: The name of the user. + final String? email; + + /// Recommended: Comments of the user about what happened. + final String? comments; + + Map toJson() { + return { + 'event_id': eventId.toString(), + if (name != null) 'name': name, + if (email != null) 'email': email, + if (comments != null) 'comments': comments, + }; + } + + UserFeedback copyWith({ + SentryId? eventId, + String? name, + String? email, + String? comments, + }) { + return UserFeedback( + eventId: eventId ?? this.eventId, + name: name ?? this.name, + email: email ?? this.email, + comments: comments ?? this.comments, + ); + } +} diff --git a/dart/test/mocks/mock_hub.dart b/dart/test/mocks/mock_hub.dart index ae6148c023..a3c463854a 100644 --- a/dart/test/mocks/mock_hub.dart +++ b/dart/test/mocks/mock_hub.dart @@ -1,4 +1,5 @@ import 'package:sentry/sentry.dart'; +import 'package:sentry/src/user_feedback.dart'; class MockHub implements Hub { List captureEventCalls = []; @@ -6,6 +7,7 @@ class MockHub implements Hub { List captureMessageCalls = []; List addBreadcrumbCalls = []; List bindClientCalls = []; + List userFeedbackCalls = []; int closeCalls = 0; bool _isEnabled = true; @@ -102,6 +104,11 @@ class MockHub implements Hub { @override // TODO: implement lastEventId SentryId get lastEventId => throw UnimplementedError(); + + @override + Future captureUserFeedback(UserFeedback userFeedback) async { + userFeedbackCalls.add(userFeedback); + } } class CaptureEventCall { diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index daf855ef0d..4f6d098308 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:universal_platform/universal_platform.dart'; import 'package:feedback/feedback.dart' as feedback; +import 'package:provider/provider.dart'; // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io const String _exampleDsn = @@ -36,11 +37,17 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { return feedback.BetterFeedback( - child: MaterialApp( - navigatorObservers: [ - SentryNavigatorObserver(), - ], - home: const MainScaffold(), + child: ChangeNotifierProvider( + create: (_) => ThemeProvider(), + child: Builder( + builder: (context) => MaterialApp( + navigatorObservers: [ + SentryNavigatorObserver(), + ], + theme: Provider.of(context).theme, + home: const MainScaffold(), + ), + ), ), ); } @@ -53,8 +60,37 @@ class MainScaffold extends StatelessWidget { @override Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + var icon = Icons.light_mode; + var theme = ThemeData.light(); + if (themeProvider.theme.brightness == Brightness.light) { + icon = Icons.dark_mode; + theme = ThemeData.dark(); + } return Scaffold( - appBar: AppBar(title: const Text('Sentry Flutter Example')), + appBar: AppBar( + title: const Text('Sentry Flutter Example'), + actions: [ + IconButton( + onPressed: () { + themeProvider.theme = theme; + }, + icon: Icon(icon), + ), + IconButton( + onPressed: () { + themeProvider.updatePrimatryColor(Colors.orange); + }, + icon: Icon(Icons.circle, color: Colors.orange), + ), + IconButton( + onPressed: () { + themeProvider.updatePrimatryColor(Colors.green); + }, + icon: Icon(Icons.circle, color: Colors.lime), + ), + ], + ), body: SingleChildScrollView( child: Column( children: [ @@ -176,6 +212,29 @@ class MainScaffold extends StatelessWidget { }); }, ), + RaisedButton( + child: const Text('Capture User Feedback'), + onPressed: () async { + final id = await Sentry.captureMessage('UserFeedback'); + await showDialog( + context: context, + builder: (context) { + return UserFeedbackDialog(eventId: id); + }, + ); + }, + ), + RaisedButton( + child: const Text('Show UserFeedback Dialog without event'), + onPressed: () async { + await showDialog( + context: context, + builder: (context) { + return UserFeedbackDialog(eventId: SentryId.empty()); + }, + ); + }, + ), if (UniversalPlatform.isIOS || UniversalPlatform.isMacOS) const CocoaExample(), if (UniversalPlatform.isAndroid) const AndroidExample(), @@ -375,3 +434,22 @@ Future makeWebRequest(BuildContext context) async { }, ); } + +class ThemeProvider extends ChangeNotifier { + ThemeData _theme = ThemeData.light(); + + ThemeData get theme => _theme; + + set theme(ThemeData theme) { + _theme = theme; + notifyListeners(); + } + + void updatePrimatryColor(MaterialColor color) { + if (theme.brightness == Brightness.light) { + theme = ThemeData(primarySwatch: color, brightness: theme.brightness); + } else { + theme = ThemeData(primarySwatch: color, brightness: theme.brightness); + } + } +} diff --git a/flutter/example/pubspec.yaml b/flutter/example/pubspec.yaml index 0f7bf84ad6..ddebf187cc 100644 --- a/flutter/example/pubspec.yaml +++ b/flutter/example/pubspec.yaml @@ -13,7 +13,8 @@ dependencies: sdk: flutter sentry_flutter: any universal_platform: ^1.0.0-nullsafety - feedback: 2.0.0 + feedback: ^2.0.0 + provider: ^5.0.0 dependency_overrides: sentry: diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index 9d4b8cc89f..0a59106995 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -6,3 +6,4 @@ export 'src/navigation/sentry_navigator_observer.dart'; export 'src/sentry_flutter.dart'; export 'src/sentry_flutter_options.dart'; export 'src/flutter_sentry_attachment.dart'; +export 'src/ui/user_feedback_dialog.dart'; diff --git a/flutter/lib/src/ui/user_feedback_dialog.dart b/flutter/lib/src/ui/user_feedback_dialog.dart new file mode 100644 index 0000000000..c07f92a0cf --- /dev/null +++ b/flutter/lib/src/ui/user_feedback_dialog.dart @@ -0,0 +1,458 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../sentry_flutter.dart'; + +class UserFeedbackDialog extends StatefulWidget { + const UserFeedbackDialog({ + Key? key, + required this.eventId, + this.hub, + }) : super(key: key); + + final SentryId eventId; + final Hub? hub; + + @override + _UserFeedbackDialogState createState() => _UserFeedbackDialogState(); +} + +class _UserFeedbackDialogState extends State { + TextEditingController nameController = TextEditingController(); + TextEditingController emailController = TextEditingController(); + TextEditingController commentController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return AlertDialog( + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "It looks like we're having some internal issues.", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline6, + ), + SizedBox(height: 4), + Text( + 'Our team has been notified. ' + "If you'd like to help, tell us what happened below.", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .subtitle1 + ?.copyWith(color: Colors.grey), + ), + Divider(height: 24), + TextField( + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: 'Name', + ), + controller: nameController, + keyboardType: TextInputType.text, + ), + SizedBox(height: 8), + TextField( + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: 'E-mail', + ), + controller: emailController, + keyboardType: TextInputType.emailAddress, + ), + SizedBox(height: 8), + TextField( + minLines: 5, + maxLines: null, + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: 'What happened?', + ), + controller: commentController, + keyboardType: TextInputType.multiline, + ), + SizedBox(height: 8), + const _PoweredBySentryMessage(), + ], + ), + ), + actions: [ + ElevatedButton( + onPressed: () async { + await (widget.hub ?? HubAdapter()) + .captureUserFeedback(UserFeedback( + eventId: widget.eventId, + comments: commentController.text, + email: emailController.text, + name: nameController.text, + )); + Navigator.pop(context); + }, + child: Text('Submit Crash Report')), + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text('Close'), + ) + ], + ); + } + + @override + void dispose() { + nameController.dispose(); + emailController.dispose(); + commentController.dispose(); + super.dispose(); + } +} + +class _PoweredBySentryMessage extends StatelessWidget { + const _PoweredBySentryMessage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Text('Crash reports powered by'), + SizedBox(width: 8), + SizedBox( + height: 30, + child: _SentryLogo(), + ), + ], + ), + ); + } +} + +class _SentryLogo extends StatelessWidget { + @override + Widget build(BuildContext context) { + var color = Colors.white; + final brightenss = Theme.of(context).brightness; + if (brightenss == Brightness.light) { + color = Color(0xff362d59).withOpacity(1.0); + } + + return FittedBox( + fit: BoxFit.contain, + child: CustomPaint( + size: Size(222, 66), + painter: _SentryLogoCustomPainter(color), + ), + ); + } +} + +// Created with https://fluttershapemaker.com/ +class _SentryLogoCustomPainter extends CustomPainter { + final Color color; + + _SentryLogoCustomPainter(this.color); + + @override + void paint(Canvas canvas, Size size) { + final path_0 = Path(); + path_0.moveTo(size.width * 0.1306306, size.height * 0.03424242); + path_0.arcToPoint(Offset(size.width * 0.09459459, size.height * 0.03424242), + radius: Radius.elliptical( + size.width * 0.02103604, size.height * 0.07075758), + rotation: 0, + largeArc: false, + clockwise: false); + path_0.lineTo(size.width * 0.06495495, size.height * 0.2050000); + path_0.arcToPoint(Offset(size.width * 0.1449099, size.height * 0.6089394), + radius: + Radius.elliptical(size.width * 0.1450901, size.height * 0.4880303), + rotation: 0, + largeArc: false, + clockwise: true); + path_0.lineTo(size.width * 0.1240991, size.height * 0.6089394); + path_0.arcToPoint(Offset(size.width * 0.05445946, size.height * 0.2646970), + radius: + Radius.elliptical(size.width * 0.1246847, size.height * 0.4193939), + rotation: 0, + largeArc: false, + clockwise: false); + path_0.lineTo(size.width * 0.02702703, size.height * 0.4242424); + path_0.arcToPoint(Offset(size.width * 0.06860360, size.height * 0.6086364), + radius: + Radius.elliptical(size.width * 0.07171171, size.height * 0.2412121), + rotation: 0, + largeArc: false, + clockwise: true); + path_0.lineTo(size.width * 0.02081081, size.height * 0.6086364); + path_0.arcToPoint(Offset(size.width * 0.01801802, size.height * 0.5918182), + radius: Radius.elliptical( + size.width * 0.003423423, size.height * 0.01151515), + rotation: 0, + largeArc: false, + clockwise: true); + path_0.lineTo(size.width * 0.03126126, size.height * 0.5160606); + path_0.arcToPoint(Offset(size.width * 0.01612613, size.height * 0.4872727), + radius: + Radius.elliptical(size.width * 0.04837838, size.height * 0.1627273), + rotation: 0, + largeArc: false, + clockwise: false); + path_0.lineTo(size.width * 0.003018018, size.height * 0.5630303); + path_0.arcToPoint(Offset(size.width * 0.01063063, size.height * 0.6575758), + radius: Radius.elliptical( + size.width * 0.02045045, size.height * 0.06878788), + rotation: 0, + largeArc: false, + clockwise: false); + path_0.arcToPoint(Offset(size.width * 0.02081081, size.height * 0.6666667), + radius: Radius.elliptical( + size.width * 0.02099099, size.height * 0.07060606), + rotation: 0, + largeArc: false, + clockwise: false); + path_0.lineTo(size.width * 0.08626126, size.height * 0.6666667); + path_0.arcToPoint(Offset(size.width * 0.05022523, size.height * 0.4043939), + radius: + Radius.elliptical(size.width * 0.08738739, size.height * 0.2939394), + rotation: 0, + largeArc: false, + clockwise: false); + path_0.lineTo(size.width * 0.06063063, size.height * 0.3437879); + path_0.arcToPoint(Offset(size.width * 0.1070270, size.height * 0.6666667), + radius: + Radius.elliptical(size.width * 0.1075225, size.height * 0.3616667), + rotation: 0, + largeArc: false, + clockwise: true); + path_0.lineTo(size.width * 0.1624775, size.height * 0.6666667); + path_0.arcToPoint(Offset(size.width * 0.08855856, size.height * 0.1848485), + radius: + Radius.elliptical(size.width * 0.1616216, size.height * 0.5436364), + rotation: 0, + largeArc: false, + clockwise: false); + path_0.lineTo(size.width * 0.1095946, size.height * 0.06363636); + path_0.arcToPoint(Offset(size.width * 0.1143243, size.height * 0.05954545), + radius: Radius.elliptical( + size.width * 0.003468468, size.height * 0.01166667), + rotation: 0, + largeArc: false, + clockwise: true); + path_0.cubicTo( + size.width * 0.1167117, + size.height * 0.06393939, + size.width * 0.2057207, + size.height * 0.5863636, + size.width * 0.2073874, + size.height * 0.5924242); + path_0.arcToPoint(Offset(size.width * 0.2043243, size.height * 0.6095455), + radius: Radius.elliptical( + size.width * 0.003423423, size.height * 0.01151515), + rotation: 0, + largeArc: false, + clockwise: true); + path_0.lineTo(size.width * 0.1828829, size.height * 0.6095455); + path_0.quadraticBezierTo(size.width * 0.1832883, size.height * 0.6384848, + size.width * 0.1828829, size.height * 0.6672727); + path_0.lineTo(size.width * 0.2044144, size.height * 0.6672727); + path_0.arcToPoint(Offset(size.width * 0.2252252, size.height * 0.5974242), + radius: Radius.elliptical( + size.width * 0.02067568, size.height * 0.06954545), + rotation: 0, + largeArc: false, + clockwise: false); + path_0.arcToPoint(Offset(size.width * 0.2224324, size.height * 0.5628788), + radius: Radius.elliptical( + size.width * 0.02022523, size.height * 0.06803030), + rotation: 0, + largeArc: false, + clockwise: false); + path_0.close(); + path_0.moveTo(size.width * 0.5600000, size.height * 0.4284848); + path_0.lineTo(size.width * 0.4935135, size.height * 0.1396970); + path_0.lineTo(size.width * 0.4769369, size.height * 0.1396970); + path_0.lineTo(size.width * 0.4769369, size.height * 0.5268182); + path_0.lineTo(size.width * 0.4937387, size.height * 0.5268182); + path_0.lineTo(size.width * 0.4937387, size.height * 0.2301515); + path_0.lineTo(size.width * 0.5621171, size.height * 0.5268182); + path_0.lineTo(size.width * 0.5768018, size.height * 0.5268182); + path_0.lineTo(size.width * 0.5768018, size.height * 0.1396970); + path_0.lineTo(size.width * 0.5600000, size.height * 0.1396970); + path_0.close(); + path_0.moveTo(size.width * 0.3925676, size.height * 0.3566667); + path_0.lineTo(size.width * 0.4521622, size.height * 0.3566667); + path_0.lineTo(size.width * 0.4521622, size.height * 0.3063636); + path_0.lineTo(size.width * 0.3925225, size.height * 0.3063636); + path_0.lineTo(size.width * 0.3925225, size.height * 0.1898485); + path_0.lineTo(size.width * 0.4597748, size.height * 0.1898485); + path_0.lineTo(size.width * 0.4597748, size.height * 0.1395455); + path_0.lineTo(size.width * 0.3754054, size.height * 0.1395455); + path_0.lineTo(size.width * 0.3754054, size.height * 0.5268182); + path_0.lineTo(size.width * 0.4606306, size.height * 0.5268182); + path_0.lineTo(size.width * 0.4606306, size.height * 0.4765152); + path_0.lineTo(size.width * 0.3925225, size.height * 0.4765152); + path_0.close(); + path_0.moveTo(size.width * 0.3224775, size.height * 0.3075758); + path_0.lineTo(size.width * 0.3224775, size.height * 0.3075758); + path_0.cubicTo( + size.width * 0.2992793, + size.height * 0.2887879, + size.width * 0.2927928, + size.height * 0.2739394, + size.width * 0.2927928, + size.height * 0.2378788); + path_0.cubicTo( + size.width * 0.2927928, + size.height * 0.2054545, + size.width * 0.3013063, + size.height * 0.1834848, + size.width * 0.3140090, + size.height * 0.1834848); + path_0.arcToPoint(Offset(size.width * 0.3458559, size.height * 0.2221212), + radius: + Radius.elliptical(size.width * 0.05432432, size.height * 0.1827273), + rotation: 0, + largeArc: false, + clockwise: true); + path_0.lineTo(size.width * 0.3548649, size.height * 0.1792424); + path_0.arcToPoint(Offset(size.width * 0.3143243, size.height * 0.1337879), + radius: + Radius.elliptical(size.width * 0.06351351, size.height * 0.2136364), + rotation: 0, + largeArc: false, + clockwise: false); + path_0.cubicTo( + size.width * 0.2915315, + size.height * 0.1337879, + size.width * 0.2756306, + size.height * 0.1792424, + size.width * 0.2756306, + size.height * 0.2439394); + path_0.cubicTo( + size.width * 0.2756306, + size.height * 0.3136364, + size.width * 0.2891441, + size.height * 0.3377273, + size.width * 0.3137387, + size.height * 0.3578788); + path_0.cubicTo( + size.width * 0.3356306, + size.height * 0.3748485, + size.width * 0.3423423, + size.height * 0.3906061, + size.width * 0.3423423, + size.height * 0.4259091); + path_0.cubicTo( + size.width * 0.3423423, + size.height * 0.4612121, + size.width * 0.3333333, + size.height * 0.4830303, + size.width * 0.3194144, + size.height * 0.4830303); + path_0.arcToPoint(Offset(size.width * 0.2820270, size.height * 0.4336364), + radius: + Radius.elliptical(size.width * 0.05558559, size.height * 0.1869697), + rotation: 0, + largeArc: false, + clockwise: true); + path_0.lineTo(size.width * 0.2718919, size.height * 0.4743939); + path_0.arcToPoint(Offset(size.width * 0.3188288, size.height * 0.5327273), + radius: + Radius.elliptical(size.width * 0.07180180, size.height * 0.2415152), + rotation: 0, + largeArc: false, + clockwise: false); + path_0.cubicTo( + size.width * 0.3435135, + size.height * 0.5327273, + size.width * 0.3593694, + size.height * 0.4880303, + size.width * 0.3593694, + size.height * 0.4189394); + path_0.cubicTo( + size.width * 0.3592342, + size.height * 0.3604545, + size.width * 0.3489640, + size.height * 0.3290909, + size.width * 0.3224775, + size.height * 0.3075758); + path_0.close(); + path_0.moveTo(size.width * 0.8815315, size.height * 0.1396970); + path_0.lineTo(size.width * 0.8468919, size.height * 0.3215152); + path_0.lineTo(size.width * 0.8124775, size.height * 0.1396970); + path_0.lineTo(size.width * 0.7923874, size.height * 0.1396970); + path_0.lineTo(size.width * 0.8378378, size.height * 0.3737879); + path_0.lineTo(size.width * 0.8378378, size.height * 0.5269697); + path_0.lineTo(size.width * 0.8551351, size.height * 0.5269697); + path_0.lineTo(size.width * 0.8551351, size.height * 0.3719697); + path_0.lineTo(size.width * 0.9009009, size.height * 0.1396970); + path_0.close(); + path_0.moveTo(size.width * 0.5904054, size.height * 0.1921212); + path_0.lineTo(size.width * 0.6281081, size.height * 0.1921212); + path_0.lineTo(size.width * 0.6281081, size.height * 0.5269697); + path_0.lineTo(size.width * 0.6454054, size.height * 0.5269697); + path_0.lineTo(size.width * 0.6454054, size.height * 0.1921212); + path_0.lineTo(size.width * 0.6831081, size.height * 0.1921212); + path_0.lineTo(size.width * 0.6831081, size.height * 0.1396970); + path_0.lineTo(size.width * 0.5904505, size.height * 0.1396970); + path_0.close(); + path_0.moveTo(size.width * 0.7631081, size.height * 0.3757576); + path_0.cubicTo( + size.width * 0.7804955, + size.height * 0.3595455, + size.width * 0.7901351, + size.height * 0.3186364, + size.width * 0.7901351, + size.height * 0.2601515); + path_0.cubicTo( + size.width * 0.7901351, + size.height * 0.1857576, + size.width * 0.7739640, + size.height * 0.1389394, + size.width * 0.7478829, + size.height * 0.1389394); + path_0.lineTo(size.width * 0.6967117, size.height * 0.1389394); + path_0.lineTo(size.width * 0.6967117, size.height * 0.5266667); + path_0.lineTo(size.width * 0.7138288, size.height * 0.5266667); + path_0.lineTo(size.width * 0.7138288, size.height * 0.3875758); + path_0.lineTo(size.width * 0.7428829, size.height * 0.3875758); + path_0.lineTo(size.width * 0.7720721, size.height * 0.5269697); + path_0.lineTo(size.width * 0.7920721, size.height * 0.5269697); + path_0.lineTo(size.width * 0.7605405, size.height * 0.3781818); + path_0.close(); + path_0.moveTo(size.width * 0.7137838, size.height * 0.3378788); + path_0.lineTo(size.width * 0.7137838, size.height * 0.1909091); + path_0.lineTo(size.width * 0.7460811, size.height * 0.1909091); + path_0.cubicTo( + size.width * 0.7629279, + size.height * 0.1909091, + size.width * 0.7725676, + size.height * 0.2177273, + size.width * 0.7725676, + size.height * 0.2642424); + path_0.cubicTo( + size.width * 0.7725676, + size.height * 0.3107576, + size.width * 0.7622523, + size.height * 0.3378788, + size.width * 0.7462613, + size.height * 0.3378788); + path_0.close(); + + final paint_0_fill = Paint()..style = PaintingStyle.fill; + paint_0_fill.color = color; + canvas.drawPath(path_0, paint_0_fill); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/flutter/test/mocks.dart b/flutter/test/mocks.dart index b51611172e..3cbf3bd00e 100644 --- a/flutter/test/mocks.dart +++ b/flutter/test/mocks.dart @@ -2,6 +2,7 @@ import 'package:mockito/annotations.dart'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/platform/platform.dart'; import 'package:sentry/src/platform_checker.dart'; +import 'package:sentry/src/user_feedback.dart'; const fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; @@ -157,4 +158,7 @@ class NoOpHub implements Hub { @override SentryId get lastEventId => SentryId.empty(); + + @override + Future captureUserFeedback(UserFeedback userFeedback) async {} } From 31e42243c8021b6c8d80b53f5fc834d07bbc8ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sun, 27 Jun 2021 15:44:24 +0200 Subject: [PATCH 24/34] wip --- dart/lib/sentry.dart | 2 +- dart/lib/src/hub.dart | 4 ++-- dart/lib/src/hub_adapter.dart | 4 ++-- dart/lib/src/noop_hub.dart | 4 ++-- dart/lib/src/sentry.dart | 4 ++-- dart/lib/src/sentry_client.dart | 11 +++++++++++ dart/lib/src/sentry_envelope.dart | 4 ++-- dart/lib/src/sentry_envelope_item.dart | 6 +++--- ...{user_feedback.dart => sentry_user_feedback.dart} | 12 ++++++------ dart/test/mocks/mock_hub.dart | 6 +++--- flutter/lib/src/ui/user_feedback_dialog.dart | 3 ++- 11 files changed, 36 insertions(+), 24 deletions(-) rename dart/lib/src/{user_feedback.dart => sentry_user_feedback.dart} (83%) diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 568d670bd4..a408f0b101 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -23,4 +23,4 @@ export 'src/integration.dart'; export 'src/event_processor.dart'; export 'src/http_client/sentry_http_client.dart'; export 'src/sentry_attachment/sentry_attachment.dart'; -export 'src/user_feedback.dart'; +export 'src/sentry_user_feedback.dart'; diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index b416137282..55d25f2a6e 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -6,7 +6,7 @@ import 'scope.dart'; import 'sentry_client.dart'; import 'sentry_envelope.dart'; import 'sentry_options.dart'; -import 'user_feedback.dart'; +import 'sentry_user_feedback.dart'; /// Configures the scope through the callback. typedef ScopeCallback = void Function(Scope); @@ -184,7 +184,7 @@ class Hub { return sentryId; } - Future captureUserFeedback(UserFeedback userFeedback) async { + Future captureUserFeedback(SentryUserFeedback userFeedback) async { final item = _peek(); await item.client.captureEnvelope( SentryEnvelope.fromUserFeedback(userFeedback, _options.sdk), diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 21315aacf2..1dcd8c1650 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -3,7 +3,7 @@ import 'hub.dart'; import 'protocol.dart'; import 'sentry.dart'; import 'sentry_client.dart'; -import 'user_feedback.dart'; +import 'sentry_user_feedback.dart'; /// Hub adapter to make Integrations testable class HubAdapter implements Hub { @@ -84,6 +84,6 @@ class HubAdapter implements Hub { SentryId get lastEventId => Sentry.lastEventId; @override - Future captureUserFeedback(UserFeedback userFeedback) => + Future captureUserFeedback(SentryUserFeedback userFeedback) => Sentry.captureUserFeedback(userFeedback); } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 421def07bf..b875110142 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'hub.dart'; import 'protocol.dart'; import 'sentry_client.dart'; -import 'user_feedback.dart'; +import 'sentry_user_feedback.dart'; class NoOpHub implements Hub { NoOpHub._(); @@ -64,5 +64,5 @@ class NoOpHub implements Hub { void addBreadcrumb(Breadcrumb crumb, {dynamic hint}) {} @override - Future captureUserFeedback(UserFeedback userFeedback) async {} + Future captureUserFeedback(SentryUserFeedback userFeedback) async {} } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 5c68430e73..b6f8d8335f 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -13,7 +13,7 @@ import 'protocol.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; import 'integration.dart'; -import 'user_feedback.dart'; +import 'sentry_user_feedback.dart'; /// Configuration options callback typedef OptionsConfiguration = FutureOr Function(SentryOptions); @@ -177,7 +177,7 @@ class Sentry { withScope: withScope, ); - static Future captureUserFeedback(UserFeedback userFeedback) => + static Future captureUserFeedback(SentryUserFeedback userFeedback) => _hub.captureUserFeedback(userFeedback); /// Close the client SDK diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index a901ebc8a4..f8876d146e 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:math'; +import 'package:sentry/sentry.dart'; + import 'event_processor.dart'; import 'transport/rate_limiter.dart'; import 'protocol.dart'; @@ -211,6 +213,15 @@ class SentryClient { return _options.transport.send(envelope); } + /// Reports the [userFeedback] to Sentry.io. + Future captureUserFeedback(SentryUserFeedback userFeedback) { + final envelope = SentryEnvelope.fromUserFeedback( + userFeedback, + _options.sdk, + ); + return _options.transport.send(envelope); + } + void close() => _options.httpClient.close(); Future _processEvent( diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index b07131b5f1..7fd4b3c4ed 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -4,7 +4,7 @@ import 'sentry_envelope_header.dart'; import 'sentry_envelope_item.dart'; import 'protocol/sentry_event.dart'; import 'protocol/sdk_version.dart'; -import 'user_feedback.dart'; +import 'sentry_user_feedback.dart'; /// Class representation of `Envelope` file. class SentryEnvelope { @@ -33,7 +33,7 @@ class SentryEnvelope { } factory SentryEnvelope.fromUserFeedback( - UserFeedback feedback, + SentryUserFeedback feedback, SdkVersion sdkVersion, ) { return SentryEnvelope( diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index 572bc39dd8..9ce077b220 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -4,7 +4,7 @@ import 'sentry_attachment/sentry_attachment.dart'; import 'sentry_item_type.dart'; import 'protocol/sentry_event.dart'; import 'sentry_envelope_item_header.dart'; -import 'user_feedback.dart'; +import 'sentry_user_feedback.dart'; /// Item holding header information and JSON encoded data. class SentryEnvelopeItem { @@ -27,8 +27,8 @@ class SentryEnvelopeItem { return SentryEnvelopeItem(header, cachedItem.getData); } - /// Create an [SentryEnvelopeItem] which sends [UserFeedback]. - factory SentryEnvelopeItem.fromUserFeedback(UserFeedback feedback) { + /// Create an [SentryEnvelopeItem] which sends [SentryUserFeedback]. + factory SentryEnvelopeItem.fromUserFeedback(SentryUserFeedback feedback) { final cachedItem = _CachedItem(() async { final jsonEncoded = jsonEncode(feedback.toJson()); return utf8.encode(jsonEncoded); diff --git a/dart/lib/src/user_feedback.dart b/dart/lib/src/sentry_user_feedback.dart similarity index 83% rename from dart/lib/src/user_feedback.dart rename to dart/lib/src/sentry_user_feedback.dart index 0be444ca58..90443cd2eb 100644 --- a/dart/lib/src/user_feedback.dart +++ b/dart/lib/src/sentry_user_feedback.dart @@ -1,15 +1,15 @@ import 'protocol.dart'; -class UserFeedback { - UserFeedback({ +class SentryUserFeedback { + SentryUserFeedback({ required this.eventId, this.name, this.email, this.comments, }); - factory UserFeedback.fromJson(Map json) { - return UserFeedback( + factory SentryUserFeedback.fromJson(Map json) { + return SentryUserFeedback( eventId: json['event_id'], name: json['name'], email: json['email'], @@ -38,13 +38,13 @@ class UserFeedback { }; } - UserFeedback copyWith({ + SentryUserFeedback copyWith({ SentryId? eventId, String? name, String? email, String? comments, }) { - return UserFeedback( + return SentryUserFeedback( eventId: eventId ?? this.eventId, name: name ?? this.name, email: email ?? this.email, diff --git a/dart/test/mocks/mock_hub.dart b/dart/test/mocks/mock_hub.dart index a3c463854a..f5765a1814 100644 --- a/dart/test/mocks/mock_hub.dart +++ b/dart/test/mocks/mock_hub.dart @@ -1,5 +1,5 @@ import 'package:sentry/sentry.dart'; -import 'package:sentry/src/user_feedback.dart'; +import 'package:sentry/src/sentry_user_feedback.dart'; class MockHub implements Hub { List captureEventCalls = []; @@ -7,7 +7,7 @@ class MockHub implements Hub { List captureMessageCalls = []; List addBreadcrumbCalls = []; List bindClientCalls = []; - List userFeedbackCalls = []; + List userFeedbackCalls = []; int closeCalls = 0; bool _isEnabled = true; @@ -106,7 +106,7 @@ class MockHub implements Hub { SentryId get lastEventId => throw UnimplementedError(); @override - Future captureUserFeedback(UserFeedback userFeedback) async { + Future captureUserFeedback(SentryUserFeedback userFeedback) async { userFeedbackCalls.add(userFeedback); } } diff --git a/flutter/lib/src/ui/user_feedback_dialog.dart b/flutter/lib/src/ui/user_feedback_dialog.dart index c07f92a0cf..fc606b8358 100644 --- a/flutter/lib/src/ui/user_feedback_dialog.dart +++ b/flutter/lib/src/ui/user_feedback_dialog.dart @@ -153,7 +153,8 @@ class _SentryLogo extends StatelessWidget { } } -// Created with https://fluttershapemaker.com/ +/// Created with https://fluttershapemaker.com/ +/// Sentry Logo comes from https://sentry.io/branding/ class _SentryLogoCustomPainter extends CustomPainter { final Color color; From 78c1779ffde6f587403c962839e0e6637ef6e66e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sun, 27 Jun 2021 15:51:27 +0200 Subject: [PATCH 25/34] wip --- dart/lib/src/hub.dart | 7 ++----- dart/lib/src/hub_adapter.dart | 2 +- dart/lib/src/noop_hub.dart | 4 +++- dart/lib/src/noop_sentry_client.dart | 5 +++++ dart/lib/src/sentry.dart | 3 ++- dart/lib/src/sentry_client.dart | 6 ++---- dart/test/mocks/mock_hub.dart | 3 ++- dart/test/mocks/mock_sentry_client.dart | 7 +++++++ 8 files changed, 24 insertions(+), 13 deletions(-) diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 55d25f2a6e..eca09fd5d7 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -4,7 +4,6 @@ import 'dart:collection'; import 'protocol.dart'; import 'scope.dart'; import 'sentry_client.dart'; -import 'sentry_envelope.dart'; import 'sentry_options.dart'; import 'sentry_user_feedback.dart'; @@ -184,11 +183,9 @@ class Hub { return sentryId; } - Future captureUserFeedback(SentryUserFeedback userFeedback) async { + Future captureUserFeedback(SentryUserFeedback userFeedback) { final item = _peek(); - await item.client.captureEnvelope( - SentryEnvelope.fromUserFeedback(userFeedback, _options.sdk), - ); + return item.client.captureUserFeedback(userFeedback); } Scope _cloneAndRunWithScope(Scope scope, ScopeCallback? withScope) { diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 1dcd8c1650..3ccf285995 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -84,6 +84,6 @@ class HubAdapter implements Hub { SentryId get lastEventId => Sentry.lastEventId; @override - Future captureUserFeedback(SentryUserFeedback userFeedback) => + Future captureUserFeedback(SentryUserFeedback userFeedback) => Sentry.captureUserFeedback(userFeedback); } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index b875110142..6c0bf0d3aa 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -64,5 +64,7 @@ class NoOpHub implements Hub { void addBreadcrumb(Breadcrumb crumb, {dynamic hint}) {} @override - Future captureUserFeedback(SentryUserFeedback userFeedback) async {} + Future captureUserFeedback(SentryUserFeedback userFeedback) async { + return SentryId.empty(); + } } diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index 40eb864597..e8d70bb9e9 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -4,6 +4,7 @@ import 'protocol.dart'; import 'scope.dart'; import 'sentry_client.dart'; import 'sentry_envelope.dart'; +import 'sentry_user_feedback.dart'; class NoOpSentryClient implements SentryClient { NoOpSentryClient._(); @@ -47,6 +48,10 @@ class NoOpSentryClient implements SentryClient { Future captureEnvelope(SentryEnvelope envelope) => Future.value(SentryId.empty()); + @override + Future captureUserFeedback(SentryUserFeedback userFeedback) => + Future.value(SentryId.empty()); + @override Future close() async { return; diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index b6f8d8335f..2e3f431503 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -177,7 +177,8 @@ class Sentry { withScope: withScope, ); - static Future captureUserFeedback(SentryUserFeedback userFeedback) => + static Future captureUserFeedback( + SentryUserFeedback userFeedback) => _hub.captureUserFeedback(userFeedback); /// Close the client SDK diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index f8876d146e..bff2dbb8f5 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -1,9 +1,7 @@ import 'dart:async'; import 'dart:math'; - -import 'package:sentry/sentry.dart'; - import 'event_processor.dart'; +import 'sentry_user_feedback.dart'; import 'transport/rate_limiter.dart'; import 'protocol.dart'; import 'scope.dart'; @@ -214,7 +212,7 @@ class SentryClient { } /// Reports the [userFeedback] to Sentry.io. - Future captureUserFeedback(SentryUserFeedback userFeedback) { + Future captureUserFeedback(SentryUserFeedback userFeedback) { final envelope = SentryEnvelope.fromUserFeedback( userFeedback, _options.sdk, diff --git a/dart/test/mocks/mock_hub.dart b/dart/test/mocks/mock_hub.dart index f5765a1814..068843281e 100644 --- a/dart/test/mocks/mock_hub.dart +++ b/dart/test/mocks/mock_hub.dart @@ -106,8 +106,9 @@ class MockHub implements Hub { SentryId get lastEventId => throw UnimplementedError(); @override - Future captureUserFeedback(SentryUserFeedback userFeedback) async { + Future captureUserFeedback(SentryUserFeedback userFeedback) async { userFeedbackCalls.add(userFeedback); + return SentryId.empty(); } } diff --git a/dart/test/mocks/mock_sentry_client.dart b/dart/test/mocks/mock_sentry_client.dart index 9b6f4175fe..6f3ba3eb4f 100644 --- a/dart/test/mocks/mock_sentry_client.dart +++ b/dart/test/mocks/mock_sentry_client.dart @@ -6,6 +6,7 @@ class MockSentryClient implements SentryClient { List captureExceptionCalls = []; List captureMessageCalls = []; List captureEnvelopeCalls = []; + List userFeedbackCalls = []; int closeCalls = 0; @override @@ -66,6 +67,12 @@ class MockSentryClient implements SentryClient { return SentryId.newId(); } + @override + Future captureUserFeedback(SentryUserFeedback userFeedback) async { + userFeedbackCalls.add(userFeedback); + return SentryId.newId(); + } + @override void close() { closeCalls = closeCalls + 1; From bfce6d60fa0ce712bb308a5dc3cbaf66a8c88594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sun, 27 Jun 2021 16:16:42 +0200 Subject: [PATCH 26/34] tests --- dart/lib/src/hub.dart | 22 +++++++-- dart/lib/src/sentry_user_feedback.dart | 4 +- dart/test/sentry_user_feedback.dart | 66 ++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 dart/test/sentry_user_feedback.dart diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index eca09fd5d7..e5f397426d 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -183,9 +183,25 @@ class Hub { return sentryId; } - Future captureUserFeedback(SentryUserFeedback userFeedback) { - final item = _peek(); - return item.client.captureUserFeedback(userFeedback); + Future captureUserFeedback(SentryUserFeedback userFeedback) async { + if (!_isEnabled) { + _options.logger( + SentryLevel.warning, + "Instance is disabled and this 'captureUserFeedback' call is a no-op.", + ); + } + try { + final item = _peek(); + + return item.client.captureUserFeedback(userFeedback); + } catch (err) { + _options.logger( + SentryLevel.error, + 'Error while capturing user feedback for id: ${userFeedback.eventId}, error: $err', + ); + } finally { + return SentryId.empty(); + } } Scope _cloneAndRunWithScope(Scope scope, ScopeCallback? withScope) { diff --git a/dart/lib/src/sentry_user_feedback.dart b/dart/lib/src/sentry_user_feedback.dart index 90443cd2eb..55074bde3e 100644 --- a/dart/lib/src/sentry_user_feedback.dart +++ b/dart/lib/src/sentry_user_feedback.dart @@ -6,11 +6,11 @@ class SentryUserFeedback { this.name, this.email, this.comments, - }); + }) : assert(eventId != SentryId.empty()); factory SentryUserFeedback.fromJson(Map json) { return SentryUserFeedback( - eventId: json['event_id'], + eventId: SentryId.fromId(json['event_id']), name: json['name'], email: json['email'], comments: json['comments'], diff --git a/dart/test/sentry_user_feedback.dart b/dart/test/sentry_user_feedback.dart new file mode 100644 index 0000000000..b634ebe3ee --- /dev/null +++ b/dart/test/sentry_user_feedback.dart @@ -0,0 +1,66 @@ +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + group('$SentryUserFeedback', () { + test('toJson', () { + final id = SentryId.newId(); + final feedback = SentryUserFeedback( + eventId: id, + comments: 'this is awesome', + email: 'sentry@example.com', + name: 'Rockstar Developer', + ); + expect(feedback.toJson(), { + 'event_id': id.toString(), + 'comments': 'this is awesome', + 'email': 'sentry@example.com', + 'name': 'Rockstar Developer', + }); + }); + + test('fromJson', () { + final id = SentryId.newId(); + final feedback = SentryUserFeedback.fromJson({ + 'event_id': id.toString(), + 'comments': 'this is awesome', + 'email': 'sentry@example.com', + 'name': 'Rockstar Developer', + }); + + expect(feedback.eventId.toString(), id.toString()); + expect(feedback.comments, 'this is awesome'); + expect(feedback.email, 'sentry@example.com'); + expect(feedback.name, 'Rockstar Developer'); + }); + + test('copyWith', () { + final id = SentryId.newId(); + final feedback = SentryUserFeedback( + eventId: id, + comments: 'this is awesome', + email: 'sentry@example.com', + name: 'Rockstar Developer', + ); + + final copyId = SentryId.newId(); + final copy = feedback.copyWith( + eventId: copyId, + comments: 'actually it is not', + email: 'example@example.com', + name: '10x developer', + ); + + expect(copy.eventId.toString(), copyId.toString()); + expect(copy.comments, 'actually it is not'); + expect(copy.email, 'example@example.com'); + expect(copy.name, '10x developer'); + }); + + test('disallow empty id', () { + final id = SentryId.empty(); + expect(() => SentryUserFeedback(eventId: id), + throwsA(isA())); + }); + }); +} From 39558bbd3bc452878d674f83270784bd48767314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sun, 27 Jun 2021 17:37:45 +0200 Subject: [PATCH 27/34] teeests --- dart/test/sentry_user_feedback.dart | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/dart/test/sentry_user_feedback.dart b/dart/test/sentry_user_feedback.dart index b634ebe3ee..f68169db33 100644 --- a/dart/test/sentry_user_feedback.dart +++ b/dart/test/sentry_user_feedback.dart @@ -1,6 +1,10 @@ import 'package:sentry/sentry.dart'; +import 'package:sentry/src/sentry_item_type.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; +import 'mocks/mock_transport.dart'; + void main() { group('$SentryUserFeedback', () { test('toJson', () { @@ -63,4 +67,32 @@ void main() { throwsA(isA())); }); }); + + group('$SentryUserFeedback to envelops', () { + test('to envelope', () { + final feedback = SentryUserFeedback(eventId: SentryId.newId()); + final envelope = SentryEnvelope.fromUserFeedback( + feedback, + SdkVersion(name: 'a', version: 'b'), + ); + + expect(envelope.items.length, 1); + expect( + envelope.items.first.header.type, + SentryItemType.userFeedback, + ); + expect(envelope.header.eventId.toString(), feedback.eventId.toString()); + }); + }); + + test('sending $SentryUserFeedback', () async { + final options = SentryOptions(dsn: fakeDsn); + final mockTransport = MockTransport(); + options.transport = mockTransport; + final hub = Hub(options); + await hub + .captureUserFeedback(SentryUserFeedback(eventId: SentryId.newId())); + + expect(mockTransport.envelopes.length, 1); + }); } From 5f5a793be007f546dd77f5808b031f491a0ac004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Sun, 27 Jun 2021 18:35:47 +0200 Subject: [PATCH 28/34] wip --- flutter/example/lib/main.dart | 2 +- flutter/lib/src/ui/user_feedback_dialog.dart | 24 +++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 4f6d098308..f02b4b7b02 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -230,7 +230,7 @@ class MainScaffold extends StatelessWidget { await showDialog( context: context, builder: (context) { - return UserFeedbackDialog(eventId: SentryId.empty()); + return UserFeedbackDialog(eventId: SentryId.newId()); }, ); }, diff --git a/flutter/lib/src/ui/user_feedback_dialog.dart b/flutter/lib/src/ui/user_feedback_dialog.dart index fc606b8358..11ce61b32b 100644 --- a/flutter/lib/src/ui/user_feedback_dialog.dart +++ b/flutter/lib/src/ui/user_feedback_dialog.dart @@ -8,7 +8,8 @@ class UserFeedbackDialog extends StatefulWidget { Key? key, required this.eventId, this.hub, - }) : super(key: key); + }) : assert(eventId != const SentryId.empty()), + super(key: key); final SentryId eventId; final Hub? hub; @@ -46,6 +47,7 @@ class _UserFeedbackDialogState extends State { ), Divider(height: 24), TextField( + key: ValueKey('sentry_name_textfield'), decoration: InputDecoration( border: OutlineInputBorder(), hintText: 'Name', @@ -55,15 +57,17 @@ class _UserFeedbackDialogState extends State { ), SizedBox(height: 8), TextField( + key: ValueKey('sentry_email_textfield'), decoration: InputDecoration( border: OutlineInputBorder(), - hintText: 'E-mail', + hintText: 'E-Mail', ), controller: emailController, keyboardType: TextInputType.emailAddress, ), SizedBox(height: 8), TextField( + key: ValueKey('sentry_comment_textfield'), minLines: 5, maxLines: null, decoration: InputDecoration( @@ -80,18 +84,20 @@ class _UserFeedbackDialogState extends State { ), actions: [ ElevatedButton( + key: ValueKey('sentry_submit_feedback_button'), onPressed: () async { - await (widget.hub ?? HubAdapter()) - .captureUserFeedback(UserFeedback( + final feedback = SentryUserFeedback( eventId: widget.eventId, comments: commentController.text, email: emailController.text, name: nameController.text, - )); + ); + await _submitUserFeedback(feedback); Navigator.pop(context); }, child: Text('Submit Crash Report')), TextButton( + key: ValueKey('sentry_close_button'), onPressed: () { Navigator.pop(context); }, @@ -101,12 +107,8 @@ class _UserFeedbackDialogState extends State { ); } - @override - void dispose() { - nameController.dispose(); - emailController.dispose(); - commentController.dispose(); - super.dispose(); + Future _submitUserFeedback(SentryUserFeedback feedback) { + return (widget.hub ?? HubAdapter()).captureUserFeedback(feedback); } } From 605f8143416a013cab3cb2e37ec9952d1986f521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Fri, 2 Jul 2021 19:53:48 +0200 Subject: [PATCH 29/34] wip --- dart/lib/src/hub.dart | 4 ++-- dart/lib/src/hub_adapter.dart | 2 +- dart/lib/src/sentry.dart | 3 +-- dart/lib/src/sentry_client.dart | 2 +- flutter/test/mocks.dart | 4 ++-- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index e5f397426d..dd56dcb0a6 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -183,7 +183,7 @@ class Hub { return sentryId; } - Future captureUserFeedback(SentryUserFeedback userFeedback) async { + Future captureUserFeedback(SentryUserFeedback userFeedback) async { if (!_isEnabled) { _options.logger( SentryLevel.warning, @@ -193,7 +193,7 @@ class Hub { try { final item = _peek(); - return item.client.captureUserFeedback(userFeedback); + await item.client.captureUserFeedback(userFeedback); } catch (err) { _options.logger( SentryLevel.error, diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 3ccf285995..3626097205 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -84,6 +84,6 @@ class HubAdapter implements Hub { SentryId get lastEventId => Sentry.lastEventId; @override - Future captureUserFeedback(SentryUserFeedback userFeedback) => + Future captureUserFeedback(SentryUserFeedback userFeedback) => Sentry.captureUserFeedback(userFeedback); } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 2e3f431503..4cd269183a 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -177,8 +177,7 @@ class Sentry { withScope: withScope, ); - static Future captureUserFeedback( - SentryUserFeedback userFeedback) => + static Future captureUserFeedback(SentryUserFeedback userFeedback) => _hub.captureUserFeedback(userFeedback); /// Close the client SDK diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index bff2dbb8f5..9b7d168e63 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -212,7 +212,7 @@ class SentryClient { } /// Reports the [userFeedback] to Sentry.io. - Future captureUserFeedback(SentryUserFeedback userFeedback) { + Future captureUserFeedback(SentryUserFeedback userFeedback) { final envelope = SentryEnvelope.fromUserFeedback( userFeedback, _options.sdk, diff --git a/flutter/test/mocks.dart b/flutter/test/mocks.dart index 3cbf3bd00e..a1642f9399 100644 --- a/flutter/test/mocks.dart +++ b/flutter/test/mocks.dart @@ -2,7 +2,7 @@ import 'package:mockito/annotations.dart'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/platform/platform.dart'; import 'package:sentry/src/platform_checker.dart'; -import 'package:sentry/src/user_feedback.dart'; +import 'package:sentry/src/sentry_user_feedback.dart'; const fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; @@ -160,5 +160,5 @@ class NoOpHub implements Hub { SentryId get lastEventId => SentryId.empty(); @override - Future captureUserFeedback(UserFeedback userFeedback) async {} + Future captureUserFeedback(SentryUserFeedback userFeedback) async {} } From c667ac75f335bed4d388a972c742779c9d4600ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Fri, 2 Jul 2021 20:18:59 +0200 Subject: [PATCH 30/34] more tests --- dart/lib/src/hub.dart | 9 ++-- dart/test/mocks/mock_transport.dart | 7 +++ ...ck.dart => sentry_user_feedback_test.dart} | 52 ++++++++++++++++--- 3 files changed, 58 insertions(+), 10 deletions(-) rename dart/test/{sentry_user_feedback.dart => sentry_user_feedback_test.dart} (69%) diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index dd56dcb0a6..23a26ba8d6 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -189,18 +189,19 @@ class Hub { SentryLevel.warning, "Instance is disabled and this 'captureUserFeedback' call is a no-op.", ); + return; } try { final item = _peek(); await item.client.captureUserFeedback(userFeedback); - } catch (err) { + } catch (exception, stacktrace) { _options.logger( SentryLevel.error, - 'Error while capturing user feedback for id: ${userFeedback.eventId}, error: $err', + 'Error while capturing user feedback for ${userFeedback.eventId}', + exception: exception, + stackTrace: stacktrace, ); - } finally { - return SentryId.empty(); } } diff --git a/dart/test/mocks/mock_transport.dart b/dart/test/mocks/mock_transport.dart index 64ed6902c5..b633639c30 100644 --- a/dart/test/mocks/mock_transport.dart +++ b/dart/test/mocks/mock_transport.dart @@ -13,3 +13,10 @@ class MockTransport implements Transport { return envelope.header.eventId ?? SentryId.empty(); } } + +class ThrowingTransport implements Transport { + @override + Future send(SentryEnvelope envelope) async { + throw Exception('foo bar'); + } +} diff --git a/dart/test/sentry_user_feedback.dart b/dart/test/sentry_user_feedback_test.dart similarity index 69% rename from dart/test/sentry_user_feedback.dart rename to dart/test/sentry_user_feedback_test.dart index f68169db33..032232dc97 100644 --- a/dart/test/sentry_user_feedback.dart +++ b/dart/test/sentry_user_feedback_test.dart @@ -86,13 +86,53 @@ void main() { }); test('sending $SentryUserFeedback', () async { - final options = SentryOptions(dsn: fakeDsn); - final mockTransport = MockTransport(); - options.transport = mockTransport; - final hub = Hub(options); - await hub + final fixture = Fixture(); + final sut = fixture.getSut(); + await sut .captureUserFeedback(SentryUserFeedback(eventId: SentryId.newId())); - expect(mockTransport.envelopes.length, 1); + expect(fixture.transport.envelopes.length, 1); + }); + + test('cannot create $SentryUserFeedback with empty id', () async { + expect( + () => SentryUserFeedback(eventId: const SentryId.empty()), + throwsA(isA()), + ); + }); + + test('do not send $SentryUserFeedback when disabled', () async { + final fixture = Fixture(); + final sut = fixture.getSut(); + await sut.close(); + await sut.captureUserFeedback( + SentryUserFeedback(eventId: SentryId.newId()), + ); + + expect(fixture.transport.envelopes.length, 0); }); + + test('captureUserFeedback does not throw', () async { + final options = SentryOptions(dsn: fakeDsn); + final transport = ThrowingTransport(); + options.transport = transport; + final sut = Hub(options); + + await expectLater(() async { + await sut.captureUserFeedback( + SentryUserFeedback(eventId: SentryId.newId()), + ); + }, returnsNormally); + }); +} + +class Fixture { + late MockTransport transport; + + Hub getSut() { + final options = SentryOptions(dsn: fakeDsn); + transport = MockTransport(); + options.transport = transport; + return Hub(options); + } } From 2b85f76282d7fcc80d06f9a1627a5ce1c25a577a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Fri, 2 Jul 2021 20:22:11 +0200 Subject: [PATCH 31/34] fix changelog --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0358dd908..7eae082f6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Unreleased * Feat: Add support for attachments (#505) -* Feat: Add support for attachments (#494) +* Feat: Add support for User Feedback (#506) # 6.0.0-beta.1 @@ -11,8 +11,6 @@ * Feat: Add DeduplicationEventProcessor (#498) * Feat: Capture failed requests as event (#473) * Feat: `beforeSend` callback accepts async code (#494) -* Feat: Add support for attachments (#505) -* Feat: Add support for User Feedback (#506) ## Breaking Changes: From a4811ca47cb7e43d43a9421d243db880f35dc687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Mon, 5 Jul 2021 08:48:04 +0200 Subject: [PATCH 32/34] Move UI to example app --- flutter/example/lib/main.dart | 1 + flutter/{lib/src/ui => example/lib}/user_feedback_dialog.dart | 2 +- flutter/lib/sentry_flutter.dart | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) rename flutter/{lib/src/ui => example/lib}/user_feedback_dialog.dart (99%) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index f02b4b7b02..8abb7580c1 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -8,6 +8,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:universal_platform/universal_platform.dart'; import 'package:feedback/feedback.dart' as feedback; import 'package:provider/provider.dart'; +import 'user_feedback_dialog.dart'; // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io const String _exampleDsn = diff --git a/flutter/lib/src/ui/user_feedback_dialog.dart b/flutter/example/lib/user_feedback_dialog.dart similarity index 99% rename from flutter/lib/src/ui/user_feedback_dialog.dart rename to flutter/example/lib/user_feedback_dialog.dart index 11ce61b32b..6781f3d886 100644 --- a/flutter/lib/src/ui/user_feedback_dialog.dart +++ b/flutter/example/lib/user_feedback_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import '../../sentry_flutter.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; class UserFeedbackDialog extends StatefulWidget { const UserFeedbackDialog({ diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index 0a59106995..9d4b8cc89f 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -6,4 +6,3 @@ export 'src/navigation/sentry_navigator_observer.dart'; export 'src/sentry_flutter.dart'; export 'src/sentry_flutter_options.dart'; export 'src/flutter_sentry_attachment.dart'; -export 'src/ui/user_feedback_dialog.dart'; From f9652e8da84e78d85ccca5a858820f5cde5cc04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Tue, 13 Jul 2021 15:09:26 +0200 Subject: [PATCH 33/34] Do not send with empty id --- dart/lib/src/hub.dart | 7 +++ dart/test/sentry_user_feedback_test.dart | 71 ++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 23a26ba8d6..73f4919673 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -191,6 +191,13 @@ class Hub { ); return; } + if (userFeedback.eventId == SentryId.empty()) { + _options.logger( + SentryLevel.warning, + 'Captured UserFeedback with empty id, dropping the feedback', + ); + return; + } try { final item = _peek(); diff --git a/dart/test/sentry_user_feedback_test.dart b/dart/test/sentry_user_feedback_test.dart index 032232dc97..700e94e35d 100644 --- a/dart/test/sentry_user_feedback_test.dart +++ b/dart/test/sentry_user_feedback_test.dart @@ -112,6 +112,19 @@ void main() { expect(fixture.transport.envelopes.length, 0); }); + test('do not send $SentryUserFeedback with empty id', () async { + final fixture = Fixture(); + final sut = fixture.getSut(); + await sut.close(); + await sut.captureUserFeedback( + SentryUserFeedbackWithoutAssert( + eventId: SentryId.empty(), + ), + ); + + expect(fixture.transport.envelopes.length, 0); + }); + test('captureUserFeedback does not throw', () async { final options = SentryOptions(dsn: fakeDsn); final transport = ThrowingTransport(); @@ -136,3 +149,61 @@ class Fixture { return Hub(options); } } + +// You cannot create an instance of SentryUserFeedback with an empty id. +// In order to test that UserFeedback with an empty id is not sent +// we need to implement it and remove the assert. +class SentryUserFeedbackWithoutAssert implements SentryUserFeedback { + SentryUserFeedbackWithoutAssert({ + required this.eventId, + this.name, + this.email, + this.comments, + }); + + factory SentryUserFeedbackWithoutAssert.fromJson(Map json) { + return SentryUserFeedbackWithoutAssert( + eventId: SentryId.fromId(json['event_id']), + name: json['name'], + email: json['email'], + comments: json['comments'], + ); + } + + @override + final SentryId eventId; + + @override + final String? name; + + @override + final String? email; + + @override + final String? comments; + + @override + Map toJson() { + return { + 'event_id': eventId.toString(), + if (name != null) 'name': name, + if (email != null) 'email': email, + if (comments != null) 'comments': comments, + }; + } + + @override + SentryUserFeedback copyWith({ + SentryId? eventId, + String? name, + String? email, + String? comments, + }) { + return SentryUserFeedback( + eventId: eventId ?? this.eventId, + name: name ?? this.name, + email: email ?? this.email, + comments: comments ?? this.comments, + ); + } +} From cb459f469a8c20b4b8b3f467cfae64ff61757eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Ueko=CC=88tter?= Date: Tue, 13 Jul 2021 15:17:53 +0200 Subject: [PATCH 34/34] fix merge --- flutter/example/pubspec.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter/example/pubspec.yaml b/flutter/example/pubspec.yaml index 7ed9da8b97..ddebf187cc 100644 --- a/flutter/example/pubspec.yaml +++ b/flutter/example/pubspec.yaml @@ -15,7 +15,6 @@ dependencies: universal_platform: ^1.0.0-nullsafety feedback: ^2.0.0 provider: ^5.0.0 - feedback: 2.0.0 dependency_overrides: sentry: