From 249fad98f2a1df3b5525df72830eff6dcc3907f3 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 13 Aug 2024 11:59:22 +0200 Subject: [PATCH 01/36] add captureFeedback methods --- dart/lib/sentry.dart | 2 + dart/lib/src/hub.dart | 27 ++++++++ dart/lib/src/hub_adapter.dart | 5 ++ dart/lib/src/noop_hub.dart | 5 ++ dart/lib/src/protocol/contexts.dart | 19 +++++ dart/lib/src/protocol/sentry_feedback.dart | 81 ++++++++++++++++++++++ dart/lib/src/sentry.dart | 4 ++ dart/lib/src/sentry_client.dart | 31 +++++++++ dart/lib/src/sentry_envelope_item.dart | 5 +- dart/lib/src/sentry_options.dart | 7 ++ flutter/example/lib/main.dart | 15 ++++ 11 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 dart/lib/src/protocol/sentry_feedback.dart diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index e9cae9d666..08ff8b8727 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -58,3 +58,5 @@ export 'src/utils.dart'; export 'src/spotlight.dart'; // proxy export 'src/protocol/sentry_proxy.dart'; +// feedback +export 'src/protocol/sentry_feedback.dart'; \ No newline at end of file diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index b69bc5056f..39db31316f 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -7,6 +7,7 @@ import 'metrics/metrics_aggregator.dart'; import 'metrics/metrics_api.dart'; import 'profiling.dart'; import 'propagation_context.dart'; +import 'protocol/sentry_feedback.dart'; import 'transport/data_category.dart'; import '../sentry.dart'; @@ -274,6 +275,32 @@ class Hub { } } + Future captureFeedback(SentryFeedback feedback) async { + var sentryId = SentryId.empty(); + + if (!_isEnabled) { + _options.logger( + SentryLevel.warning, + "Instance is disabled and this 'captureFeedback' call is a no-op.", + ); + return sentryId; + } + try { + final item = _peek(); + + sentryId = await item.client.captureFeedback(feedback); + } catch (exception, stacktrace) { + _options.logger( + SentryLevel.error, + 'Error while capturing user feedback', + exception: exception, + stackTrace: stacktrace, + ); + } + + return sentryId; + } + FutureOr _cloneAndRunWithScope( Scope scope, ScopeCallback? withScope) async { if (withScope != null) { diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 6b2ece3c53..72586a3b56 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -9,6 +9,7 @@ import 'metrics/metrics_aggregator.dart'; import 'metrics/metrics_api.dart'; import 'profiling.dart'; import 'protocol.dart'; +import 'protocol/sentry_feedback.dart'; import 'scope.dart'; import 'sentry.dart'; import 'sentry_client.dart'; @@ -196,4 +197,8 @@ class HubAdapter implements Hub { @override MetricsAggregator? get metricsAggregator => Sentry.currentHub.metricsAggregator; + + @override + Future captureFeedback(SentryFeedback feedback) => + Sentry.currentHub.captureFeedback(feedback); } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 30f68ca895..deda1ed5c7 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -9,6 +9,7 @@ import 'metrics/metrics_aggregator.dart'; import 'metrics/metrics_api.dart'; import 'profiling.dart'; import 'protocol.dart'; +import 'protocol/sentry_feedback.dart'; import 'scope.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; @@ -98,6 +99,10 @@ class NoOpHub implements Hub { @override Future captureUserFeedback(SentryUserFeedback userFeedback) async {} + @override + Future captureFeedback(SentryFeedback feedback) async => + SentryId.empty(); + @override ISentrySpan startTransaction( String name, diff --git a/dart/lib/src/protocol/contexts.dart b/dart/lib/src/protocol/contexts.dart index 42d96fcc7e..10c5ab3752 100644 --- a/dart/lib/src/protocol/contexts.dart +++ b/dart/lib/src/protocol/contexts.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import '../protocol.dart'; +import 'sentry_feedback.dart'; /// The context interfaces provide additional context data. /// @@ -19,6 +20,7 @@ class Contexts extends MapView { SentryCulture? culture, SentryTraceContext? trace, SentryResponse? response, + SentryFeedback? feedback, }) : super({ SentryDevice.type: device, SentryOperatingSystem.type: operatingSystem, @@ -29,6 +31,7 @@ class Contexts extends MapView { SentryCulture.type: culture, SentryTraceContext.type: trace, SentryResponse.type: response, + SentryFeedback.type: feedback, }); /// Deserializes [Contexts] from JSON [Map]. @@ -136,6 +139,11 @@ class Contexts extends MapView { set response(SentryResponse? value) => this[SentryResponse.type] = value; + /// Feedback context for a FeedbackEvent. + SentryFeedback? get feedback => this[SentryFeedback.type]; + + set feedback(SentryFeedback? value) => this[SentryFeedback.type] = value; + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; @@ -198,6 +206,13 @@ class Contexts extends MapView { } break; + case SentryFeedback.type: + final feedbackMap = feedback?.toJson(); + if (feedbackMap?.isNotEmpty ?? false) { + json[SentryFeedback.type] = feedbackMap; + } + break; + case SentryRuntime.listType: if (runtimes.length == 1) { final runtime = runtimes[0]; @@ -249,6 +264,7 @@ class Contexts extends MapView { trace: trace?.clone(), response: response?.clone(), runtimes: runtimes.map((runtime) => runtime.clone()).toList(), + feedback: feedback?.clone(), )..addEntries( entries.where((element) => !_defaultFields.contains(element.key)), ); @@ -266,6 +282,7 @@ class Contexts extends MapView { SentryGpu? gpu, SentryTraceContext? trace, SentryResponse? response, + SentryFeedback? feedback, }) => Contexts( device: device ?? this.device, @@ -277,6 +294,7 @@ class Contexts extends MapView { culture: culture ?? this.culture, trace: trace ?? this.trace, response: response ?? this.response, + feedback: feedback ?? this.feedback, )..addEntries( entries.where((element) => !_defaultFields.contains(element.key)), ); @@ -292,5 +310,6 @@ class Contexts extends MapView { SentryCulture.type, SentryTraceContext.type, SentryResponse.type, + SentryFeedback.type, ]; } diff --git a/dart/lib/src/protocol/sentry_feedback.dart b/dart/lib/src/protocol/sentry_feedback.dart new file mode 100644 index 0000000000..46346469ae --- /dev/null +++ b/dart/lib/src/protocol/sentry_feedback.dart @@ -0,0 +1,81 @@ +import 'package:meta/meta.dart'; + +import 'access_aware_map.dart'; +import 'sentry_id.dart'; + +@immutable +class SentryFeedback { + static const type = 'feedback'; + + SentryFeedback({ + required this.message, + this.contactEmail, + this.name, + this.replayId, + this.url, + this.associatedEventId, + this.unknown, + }); + + final String message; + final String? contactEmail; + final String? name; + final String? replayId; + final String? url; + final SentryId? associatedEventId; + + @internal + final Map? unknown; + + /// Deserializes a [SentryOperatingSystem] from JSON [Map]. + factory SentryFeedback.fromJson(Map data) { + final json = AccessAwareMap(data); + + String? associatedEventId = json['associated_event_id']; + + return SentryFeedback( + message: json['message'], + contactEmail: json['contact_email'], + name: json['name'], + replayId: json['replay_id'], + url: json['url'], + associatedEventId: associatedEventId != null + ? SentryId.fromId(associatedEventId) + : null, + unknown: json.notAccessed(), + ); + } + + Map toJson() { + return { + ...?unknown, + 'message': message, + if (contactEmail != null) 'contact_email': contactEmail, + if (name != null) 'name': name, + if (replayId != null) 'replay_id': replayId, + if (url != null) 'url': url, + if (associatedEventId != null) 'associated_event_id': associatedEventId, + }; + } + + SentryFeedback copyWith({ + String? message, + String? contactEmail, + String? name, + String? replayId, + String? url, + SentryId? associatedEventId, + Map? unknown, + }) => + SentryFeedback( + message: message ?? this.message, + contactEmail: contactEmail ?? this.contactEmail, + name: name ?? this.name, + replayId: replayId ?? this.replayId, + url: url ?? this.url, + associatedEventId: associatedEventId ?? this.associatedEventId, + unknown: unknown ?? this.unknown, + ); + + SentryFeedback clone() => copyWith(); +} diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index a3ac51e818..cbac4b4ed9 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -4,6 +4,7 @@ import 'package:meta/meta.dart'; import 'dart_exception_type_identifier.dart'; import 'metrics/metrics_api.dart'; +import 'protocol/sentry_feedback.dart'; import 'run_zoned_guarded_integration.dart'; import 'event_processor/enricher/enricher_event_processor.dart'; import 'environment/environment_variables.dart'; @@ -219,6 +220,9 @@ class Sentry { static Future captureUserFeedback(SentryUserFeedback userFeedback) => _hub.captureUserFeedback(userFeedback); + static Future captureFeedback(SentryFeedback feedback) => + _hub.captureFeedback(feedback); + /// Close the client SDK static Future close() async { final hub = _hub; diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index c4ebac3db5..f81d3535e7 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -10,6 +10,7 @@ import 'hint.dart'; import 'metrics/metric.dart'; import 'metrics/metrics_aggregator.dart'; import 'protocol.dart'; +import 'protocol/sentry_feedback.dart'; import 'scope.dart'; import 'sentry_attachment/sentry_attachment.dart'; import 'sentry_baggage.dart'; @@ -213,6 +214,10 @@ class SentryClient { return event; } + if (event.type == 'feedback') { + return event; + } + if (event.exceptions?.isNotEmpty ?? false) { return event; } @@ -433,6 +438,24 @@ class SentryClient { return _attachClientReportsAndSend(envelope); } + /// Reports the [SentryFeedback] and to Sentry.io. + Future captureFeedback( + SentryFeedback feedback, { + Scope? scope, + Hint? hint, + }) { + final feedbackEvent = SentryEvent( + type: 'feedback', + contexts: Contexts(feedback: feedback), + ); + + return captureEvent( + feedbackEvent, + scope: scope, + hint: hint, + ); + } + /// Reports the [metricsBuckets] to Sentry.io. Future captureMetrics( Map> metricsBuckets) async { @@ -460,6 +483,7 @@ class SentryClient { final beforeSend = _options.beforeSend; final beforeSendTransaction = _options.beforeSendTransaction; + final beforeSendFeedback = _options.beforeSendFeedback; String beforeSendName = 'beforeSend'; try { @@ -471,6 +495,13 @@ class SentryClient { } else { processedEvent = callbackResult; } + } else if (event.type == 'feedback' && beforeSendFeedback != null) { + final callbackResult = beforeSendFeedback(event, hint); + if (callbackResult is Future) { + processedEvent = await callbackResult; + } else { + processedEvent = callbackResult; + } } else if (beforeSend != null) { final callbackResult = beforeSend(event, hint); if (callbackResult is Future) { diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index b0f19cfccd..0eab8ba532 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -66,12 +66,13 @@ class SentryEnvelopeItem { return SentryEnvelopeItem( SentryEnvelopeItemHeader( - SentryItemType.event, + event.type ?? SentryItemType.event, cachedItem.getDataLength, contentType: 'application/json', ), cachedItem.getData, - originalObject: event); + originalObject: event, + ); } /// Create a [SentryEnvelopeItem] which holds the [ClientReport] data. diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 2b1771a2b5..2ce88132a1 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -161,6 +161,8 @@ class SentryOptions { /// transaction object or nothing to skip reporting the transaction BeforeSendTransactionCallback? beforeSendTransaction; + BeforeSendFeedbackCallback? beforeSendFeedback; + /// This function is called with an SDK specific breadcrumb object before the breadcrumb is added /// to the scope. When nothing is returned from the function, the breadcrumb is dropped BeforeBreadcrumbCallback? beforeBreadcrumb; @@ -614,6 +616,11 @@ typedef BeforeSendTransactionCallback = FutureOr Function( SentryTransaction transaction, ); +typedef BeforeSendFeedbackCallback = FutureOr Function( + SentryEvent event, + Hint hint, +); + /// This function is called with an SDK specific breadcrumb object before the breadcrumb is added /// to the scope. When nothing is returned from the function, the breadcrumb is dropped typedef BeforeBreadcrumbCallback = Breadcrumb? Function( diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 82bcd0b3c8..c9c8cbcdd4 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -521,6 +521,21 @@ class MainScaffold extends StatelessWidget { text: '', buttonTitle: 'Show UserFeedback Dialog without event', ), + TooltipButton( + onPressed: () async { + final associatedEventId = await Sentry.captureMessage('Associated Event'); + await Sentry.captureFeedback( + SentryFeedback( + message: 'message', + contactEmail: 'john.appleseed@apple.com', + name: 'John Appleseed', + associatedEventId: associatedEventId, + ), + ); + }, + text: '', + buttonTitle: 'Capture Feedback', + ), TooltipButton( onPressed: () { final log = Logger('Logging'); From 76e733e34987731e3d34ec97ec3ac86ff1e5b79b Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 13 Aug 2024 13:26:07 +0200 Subject: [PATCH 02/36] add should capture feedback as event test --- dart/lib/src/sentry_client.dart | 1 + dart/test/sentry_client_test.dart | 36 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index f81d3535e7..8e79647b76 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -447,6 +447,7 @@ class SentryClient { final feedbackEvent = SentryEvent( type: 'feedback', contexts: Contexts(feedback: feedback), + level: SentryLevel.info, ); return captureEvent( diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 772699b14d..52e97ff43a 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -1406,6 +1406,42 @@ void main() { }); }); + group('SentryClient captures feedback', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('should capture feedback as event', () async { + final client = fixture.getSut(); + + final associatedEventId = SentryId.newId(); + final feedback = SentryFeedback( + message: 'fixture-message', + contactEmail: 'fixture-contactEmail', + name: 'fixture-name', + replayId: 'fixture-replayId', + url: "https://fixture-url.com", + associatedEventId: associatedEventId, + ); + await client.captureFeedback(feedback); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final envelopeItem = capturedEnvelope.items.first; + final envelopeEvent = envelopeItem.originalObject as SentryEvent?; + + expect(envelopeItem, isNotNull); + expect(envelopeEvent, isNotNull); + + expect(envelopeItem.header.type, 'feedback'); + + expect(envelopeEvent?.type, 'feedback'); + expect(envelopeEvent?.contexts.feedback?.toJson(), feedback.toJson()); + expect(envelopeEvent?.level, SentryLevel.info); + }); + }); + group('SentryClient captures envelope', () { late Fixture fixture; From 55953239335fa94fcee2f4892cddc143b8bae362 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 13 Aug 2024 13:37:18 +0200 Subject: [PATCH 03/36] add sentry_feedback_test --- dart/lib/sentry.dart | 2 +- dart/lib/src/hub.dart | 1 - dart/lib/src/protocol/sentry_feedback.dart | 8 +- dart/lib/src/sentry_client.dart | 8 +- dart/lib/src/sentry_envelope_item.dart | 14 ++-- dart/lib/src/sentry_options.dart | 4 +- dart/test/protocol/sentry_feedback_test.dart | 85 ++++++++++++++++++++ 7 files changed, 103 insertions(+), 19 deletions(-) create mode 100644 dart/test/protocol/sentry_feedback_test.dart diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 08ff8b8727..ecb44288f1 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -59,4 +59,4 @@ export 'src/spotlight.dart'; // proxy export 'src/protocol/sentry_proxy.dart'; // feedback -export 'src/protocol/sentry_feedback.dart'; \ No newline at end of file +export 'src/protocol/sentry_feedback.dart'; diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 39db31316f..628252a157 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -7,7 +7,6 @@ import 'metrics/metrics_aggregator.dart'; import 'metrics/metrics_api.dart'; import 'profiling.dart'; import 'propagation_context.dart'; -import 'protocol/sentry_feedback.dart'; import 'transport/data_category.dart'; import '../sentry.dart'; diff --git a/dart/lib/src/protocol/sentry_feedback.dart b/dart/lib/src/protocol/sentry_feedback.dart index 46346469ae..c25e0759df 100644 --- a/dart/lib/src/protocol/sentry_feedback.dart +++ b/dart/lib/src/protocol/sentry_feedback.dart @@ -39,9 +39,8 @@ class SentryFeedback { name: json['name'], replayId: json['replay_id'], url: json['url'], - associatedEventId: associatedEventId != null - ? SentryId.fromId(associatedEventId) - : null, + associatedEventId: + associatedEventId != null ? SentryId.fromId(associatedEventId) : null, unknown: json.notAccessed(), ); } @@ -54,7 +53,8 @@ class SentryFeedback { if (name != null) 'name': name, if (replayId != null) 'replay_id': replayId, if (url != null) 'url': url, - if (associatedEventId != null) 'associated_event_id': associatedEventId, + if (associatedEventId != null) + 'associated_event_id': associatedEventId.toString(), }; } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 8e79647b76..74dc8bbb1f 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -440,10 +440,10 @@ class SentryClient { /// Reports the [SentryFeedback] and to Sentry.io. Future captureFeedback( - SentryFeedback feedback, { - Scope? scope, - Hint? hint, - }) { + SentryFeedback feedback, { + Scope? scope, + Hint? hint, + }) { final feedbackEvent = SentryEvent( type: 'feedback', contexts: Contexts(feedback: feedback), diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index 0eab8ba532..c13e3c3edc 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -65,13 +65,13 @@ class SentryEnvelopeItem { _CachedItem(() async => utf8JsonEncoder.convert(event.toJson())); return SentryEnvelopeItem( - SentryEnvelopeItemHeader( - event.type ?? SentryItemType.event, - cachedItem.getDataLength, - contentType: 'application/json', - ), - cachedItem.getData, - originalObject: event, + SentryEnvelopeItemHeader( + event.type ?? SentryItemType.event, + cachedItem.getDataLength, + contentType: 'application/json', + ), + cachedItem.getData, + originalObject: event, ); } diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 2ce88132a1..0759adc098 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -617,8 +617,8 @@ typedef BeforeSendTransactionCallback = FutureOr Function( ); typedef BeforeSendFeedbackCallback = FutureOr Function( - SentryEvent event, - Hint hint, + SentryEvent event, + Hint hint, ); /// This function is called with an SDK specific breadcrumb object before the breadcrumb is added diff --git a/dart/test/protocol/sentry_feedback_test.dart b/dart/test/protocol/sentry_feedback_test.dart new file mode 100644 index 0000000000..0e57a77997 --- /dev/null +++ b/dart/test/protocol/sentry_feedback_test.dart @@ -0,0 +1,85 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; + +void main() { + final associatedEventId = SentryId.newId(); + + final feedback = SentryFeedback( + message: 'fixture-message', + contactEmail: 'fixture-contactEmail', + name: 'fixture-name', + replayId: 'fixture-replayId', + url: "https://fixture-url.com", + associatedEventId: associatedEventId, + unknown: testUnknown, + ); + + final feedbackJson = { + 'message': 'fixture-message', + 'contact_email': 'fixture-contactEmail', + 'name': 'fixture-name', + 'replay_id': 'fixture-replayId', + 'url': 'https://fixture-url.com', + 'associated_event_id': associatedEventId.toString(), + }; + feedbackJson.addAll(testUnknown); + + group('json', () { + test('toJson', () { + final json = feedback.toJson(); + + expect( + MapEquality().equals(feedbackJson, json), + true, + ); + }); + test('fromJson', () { + final feedback = SentryFeedback.fromJson(feedbackJson); + final json = feedback.toJson(); + + print(feedback); + print(json); + + expect( + MapEquality().equals(feedbackJson, json), + true, + ); + }); + }); + + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = feedback; + + final copy = data.copyWith(); + + expect( + MapEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + test('copyWith takes new values', () { + final data = feedback; + + final newAssociatedEventId = SentryId.newId(); + final copy = data.copyWith( + message: 'fixture-2-message', + contactEmail: 'fixture-2-contactEmail', + name: 'fixture-2-name', + replayId: 'fixture-2-replayId', + url: "https://fixture-2-url.com", + associatedEventId: newAssociatedEventId, + ); + + expect(copy.message, 'fixture-2-message'); + expect(copy.contactEmail, 'fixture-2-contactEmail'); + expect(copy.name, 'fixture-2-name'); + expect(copy.replayId, 'fixture-2-replayId'); + expect(copy.url, "https://fixture-2-url.com"); + expect(copy.associatedEventId, newAssociatedEventId); + }); + }); +} From 07815d8bea489ac1a6670ca42c1e5f3fe0bc7240 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 13 Aug 2024 13:43:40 +0200 Subject: [PATCH 04/36] update contexts test --- dart/lib/src/protocol/contexts.dart | 3 ++ dart/test/protocol/contexts_test.dart | 32 +++++++++++++++++++- dart/test/protocol/sentry_feedback_test.dart | 9 +++--- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/dart/lib/src/protocol/contexts.dart b/dart/lib/src/protocol/contexts.dart index 10c5ab3752..c1cbfeff54 100644 --- a/dart/lib/src/protocol/contexts.dart +++ b/dart/lib/src/protocol/contexts.dart @@ -65,6 +65,9 @@ class Contexts extends MapView { response: data[SentryResponse.type] != null ? SentryResponse.fromJson(Map.from(data[SentryResponse.type])) : null, + feedback: data[SentryFeedback.type] != null + ? SentryFeedback.fromJson(Map.from(data[SentryFeedback.type])) + : null, ); data.keys diff --git a/dart/test/protocol/contexts_test.dart b/dart/test/protocol/contexts_test.dart index b679c47679..01ad0c6fbd 100644 --- a/dart/test/protocol/contexts_test.dart +++ b/dart/test/protocol/contexts_test.dart @@ -6,6 +6,8 @@ void main() { final _traceId = SentryId.fromId('1988bb1b6f0d4c509e232f0cb9aaeaea'); final _spanId = SpanId.fromId('976e0cd945864f60'); final _parentSpanId = SpanId.fromId('c9c9fc3f9d4346df'); + final _associatedEventId = + SentryId.fromId('8a32c0f9be1d34a5efb2c4a10d80de9a'); final _trace = SentryTraceContext( traceId: _traceId, @@ -17,6 +19,15 @@ void main() { status: SpanStatus.ok(), ); + final _feedback = SentryFeedback( + message: 'fixture-message', + contactEmail: 'fixture-contactEmail', + name: 'fixture-name', + replayId: 'fixture-replayId', + url: "https://fixture-url.com", + associatedEventId: _associatedEventId, + ); + final _contexts = Contexts( device: SentryDevice(batteryLevel: 90.0), operatingSystem: SentryOperatingSystem(name: 'name'), @@ -26,6 +37,7 @@ void main() { gpu: SentryGpu(id: 1), culture: SentryCulture(locale: 'foo-bar'), trace: _trace, + feedback: _feedback, ); final _contextsJson = { @@ -44,6 +56,14 @@ void main() { 'description': 'desc', 'status': 'ok' }, + 'feedback': { + 'message': 'fixture-message', + 'contact_email': 'fixture-contactEmail', + 'name': 'fixture-name', + 'replay_id': 'fixture-replayId', + 'url': 'https://fixture-url.com', + 'associated_event_id': '8a32c0f9be1d34a5efb2c4a10d80de9a', + } }; final _contextsMutlipleRuntimes = Contexts( @@ -129,6 +149,14 @@ void main() { description: 'desc', status: SpanStatus.ok(), ); + final feedback = SentryFeedback( + message: 'fixture-2-message', + contactEmail: 'fixture-2-contactEmail', + name: 'fixture-2-name', + replayId: 'fixture-2-replayId', + url: "https://fixture-2-url.com", + associatedEventId: SentryId.fromId('1d49af08b6e2c437f9052b1ecfd83dca'), + ); final copy = data.copyWith( device: device, @@ -138,7 +166,8 @@ void main() { browser: browser, gpu: gpu, culture: culture, - trace: _trace, + trace: trace, + feedback: feedback, ); expect(device.toJson(), copy.device!.toJson()); @@ -153,6 +182,7 @@ void main() { expect(gpu.toJson(), copy.gpu!.toJson()); expect(trace.toJson(), copy.trace!.toJson()); expect('value', copy['extra']); + expect(feedback.toJson(), copy.feedback!.toJson()); }); }); } diff --git a/dart/test/protocol/sentry_feedback_test.dart b/dart/test/protocol/sentry_feedback_test.dart index 0e57a77997..0c3ed1f8be 100644 --- a/dart/test/protocol/sentry_feedback_test.dart +++ b/dart/test/protocol/sentry_feedback_test.dart @@ -5,7 +5,7 @@ import 'package:test/test.dart'; import '../mocks.dart'; void main() { - final associatedEventId = SentryId.newId(); + final associatedEventId = SentryId.fromId('8a32c0f9be1d34a5efb2c4a10d80de9a'); final feedback = SentryFeedback( message: 'fixture-message', @@ -23,7 +23,7 @@ void main() { 'name': 'fixture-name', 'replay_id': 'fixture-replayId', 'url': 'https://fixture-url.com', - 'associated_event_id': associatedEventId.toString(), + 'associated_event_id': '8a32c0f9be1d34a5efb2c4a10d80de9a', }; feedbackJson.addAll(testUnknown); @@ -64,14 +64,13 @@ void main() { test('copyWith takes new values', () { final data = feedback; - final newAssociatedEventId = SentryId.newId(); final copy = data.copyWith( message: 'fixture-2-message', contactEmail: 'fixture-2-contactEmail', name: 'fixture-2-name', replayId: 'fixture-2-replayId', url: "https://fixture-2-url.com", - associatedEventId: newAssociatedEventId, + associatedEventId: SentryId.fromId('1d49af08b6e2c437f9052b1ecfd83dca'), ); expect(copy.message, 'fixture-2-message'); @@ -79,7 +78,7 @@ void main() { expect(copy.name, 'fixture-2-name'); expect(copy.replayId, 'fixture-2-replayId'); expect(copy.url, "https://fixture-2-url.com"); - expect(copy.associatedEventId, newAssociatedEventId); + expect(copy.associatedEventId, '1d49af08b6e2c437f9052b1ecfd83dca'); }); }); } From 684ef04eb16187a08b22128e20d04093a8620d8b Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 13 Aug 2024 14:05:10 +0200 Subject: [PATCH 05/36] test before send feedback --- dart/lib/src/hub.dart | 24 +++++-- dart/lib/src/hub_adapter.dart | 12 +++- dart/lib/src/noop_hub.dart | 6 +- dart/test/sentry_client_test.dart | 104 +++++++++++++++++++++++++++--- 4 files changed, 130 insertions(+), 16 deletions(-) diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 628252a157..fb9f548911 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -274,7 +274,11 @@ class Hub { } } - Future captureFeedback(SentryFeedback feedback) async { + Future captureFeedback( + SentryFeedback feedback, { + Hint? hint, + ScopeCallback? withScope, + }) async { var sentryId = SentryId.empty(); if (!_isEnabled) { @@ -284,10 +288,22 @@ class Hub { ); return sentryId; } - try { - final item = _peek(); - sentryId = await item.client.captureFeedback(feedback); + final item = _peek(); + late Scope scope; + final s = _cloneAndRunWithScope(item.scope, withScope); + if (s is Future) { + scope = await s; + } else { + scope = s; + } + + try { + sentryId = await item.client.captureFeedback( + feedback, + hint: hint, + scope: scope, + ); } catch (exception, stacktrace) { _options.logger( SentryLevel.error, diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 72586a3b56..ad195c525c 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -199,6 +199,14 @@ class HubAdapter implements Hub { Sentry.currentHub.metricsAggregator; @override - Future captureFeedback(SentryFeedback feedback) => - Sentry.currentHub.captureFeedback(feedback); + Future captureFeedback( + SentryFeedback feedback, { + Hint? hint, + ScopeCallback? withScope, + }) => + Sentry.currentHub.captureFeedback( + feedback, + hint: hint, + withScope: withScope, + ); } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index deda1ed5c7..e7b79ec936 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -100,7 +100,11 @@ class NoOpHub implements Hub { Future captureUserFeedback(SentryUserFeedback userFeedback) async {} @override - Future captureFeedback(SentryFeedback feedback) async => + Future captureFeedback( + SentryFeedback feedback, { + Hint? hint, + ScopeCallback? withScope, + }) async => SentryId.empty(); @override diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 52e97ff43a..97e0c6a1c2 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -1193,6 +1193,61 @@ void main() { }); }); + group('SentryClient before send feedback', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('before send feedback drops event', () async { + final client = fixture.getSut( + beforeSendFeedback: beforeSendFeedbackCallbackDropEvent); + final fakeFeedback = fixture.fakeFeedback(); + await client.captureFeedback(fakeFeedback); + + expect((fixture.transport).called(0), true); + }); + + test('async before send feedback drops event', () async { + final client = fixture.getSut( + beforeSendFeedback: asyncBeforeSendFeedbackCallbackDropEvent); + final fakeFeedback = fixture.fakeFeedback(); + await client.captureFeedback(fakeFeedback); + + expect((fixture.transport).called(0), true); + }); + + test( + 'before send feedback returns an feedback event and feedback event is captured', + () async { + final client = + fixture.getSut(beforeSendFeedback: beforeSendFeedbackCallback); + final fakeFeedback = fixture.fakeFeedback(); + await client.captureFeedback(fakeFeedback); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final feedbackEvent = await eventFromEnvelope(capturedEnvelope); + + expect(feedbackEvent.tags!.containsKey('theme'), true); + }); + + test('thrown error is handled', () async { + final exception = Exception("before send exception"); + final beforeSendFeedbackCallback = (SentryEvent event, Hint hint) { + throw exception; + }; + + final client = fixture.getSut( + beforeSendFeedback: beforeSendFeedbackCallback, debug: true); + final fakeFeedback = fixture.fakeFeedback(); + await client.captureFeedback(fakeFeedback); + + expect(fixture.loggedException, exception); + expect(fixture.loggedLevel, SentryLevel.error); + }); + }); + group('SentryClient before send transaction', () { late Fixture fixture; @@ -1416,15 +1471,7 @@ void main() { test('should capture feedback as event', () async { final client = fixture.getSut(); - final associatedEventId = SentryId.newId(); - final feedback = SentryFeedback( - message: 'fixture-message', - contactEmail: 'fixture-contactEmail', - name: 'fixture-name', - replayId: 'fixture-replayId', - url: "https://fixture-url.com", - associatedEventId: associatedEventId, - ); + final feedback = fixture.fakeFeedback(); await client.captureFeedback(feedback); final capturedEnvelope = (fixture.transport).envelopes.first; @@ -1867,6 +1914,20 @@ SentryEvent? beforeSendCallbackDropEvent( ) => null; +SentryTransaction? beforeSendFeedbackCallbackDropEvent( + SentryEvent feedbackEvent, + Hint hint, +) => + null; + +Future asyncBeforeSendFeedbackCallbackDropEvent( + SentryEvent feedbackEvent, + Hint hint, +) async { + await Future.delayed(Duration(milliseconds: 200)); + return null; +} + SentryTransaction? beforeSendTransactionCallbackDropEvent( SentryTransaction event, ) => @@ -1886,6 +1947,10 @@ Future asyncBeforeSendTransactionCallbackDropEvent( return null; } +SentryEvent? beforeSendFeedbackCallback(SentryEvent event, Hint hint) { + return event.copyWith(tags: {'theme': 'material'}); +} + SentryEvent? beforeSendCallback(SentryEvent event, Hint hint) { return event ..tags!.addAll({'theme': 'material'}) @@ -1930,6 +1995,7 @@ class Fixture { double? sampleRate, BeforeSendCallback? beforeSend, BeforeSendTransactionCallback? beforeSendTransaction, + BeforeSendFeedbackCallback? beforeSendFeedback, EventProcessor? eventProcessor, bool provideMockRecorder = true, bool debug = false, @@ -1949,6 +2015,7 @@ class Fixture { options.sampleRate = sampleRate; options.beforeSend = beforeSend; options.beforeSendTransaction = beforeSendTransaction; + options.beforeSendFeedback = beforeSendFeedback; options.debug = debug; options.logger = mockLogger; @@ -1976,6 +2043,25 @@ class Fixture { ); } + SentryEvent fakeFeedbackEvent() { + return SentryEvent( + type: 'feedback', + contexts: Contexts(feedback: fakeFeedback()), + level: SentryLevel.info, + ); + } + + SentryFeedback fakeFeedback() { + return SentryFeedback( + message: 'fixture-message', + contactEmail: 'fixture-contactEmail', + name: 'fixture-name', + replayId: 'fixture-replayId', + url: "https://fixture-url.com", + associatedEventId: SentryId.fromId('1d49af08b6e2c437f9052b1ecfd83dca'), + ); + } + void mockLogger( SentryLevel level, String message, { From f3c8951aab59dc921ff2879d8f352b04e28f87c2 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 13 Aug 2024 14:10:18 +0200 Subject: [PATCH 06/36] test hint and event processors --- dart/test/sentry_client_test.dart | 78 ++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 97e0c6a1c2..0accec129b 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -1384,19 +1384,18 @@ void main() { setUp(() { fixture = Fixture(); fixture.options.addEventProcessor(FunctionEventProcessor( - (event, hint) => event - ..tags!.addAll({'theme': 'material'}) + (event, hint) => event.copyWith(tags: {'theme': 'material'}) // ignore: deprecated_member_use_from_same_package - ..extra!['host'] = '0.0.0.1' - ..modules!.addAll({'core': '1.0'}) - ..breadcrumbs!.add(Breadcrumb(message: 'processor crumb')) - ..fingerprint!.add('process') - ..sdk!.addIntegration('testIntegration') - ..sdk!.addPackage('test-pkg', '1.0'), + ..extra?['host'] = '0.0.0.1' + ..modules?.addAll({'core': '1.0'}) + ..breadcrumbs?.add(Breadcrumb(message: 'processor crumb')) + ..fingerprint?.add('process') + ..sdk?.addIntegration('testIntegration') + ..sdk?.addPackage('test-pkg', '1.0'), )); }); - test('should execute eventProcessors', () async { + test('should execute eventProcessors for event', () async { final client = fixture.getSut(); await client.captureEvent(fakeEvent); @@ -1420,7 +1419,18 @@ void main() { expect(event.fingerprint!.contains('process'), true); }); - test('should pass hint to eventProcessors', () async { + test('should execute eventProcessors for feedback', () async { + final client = fixture.getSut(); + final fakeFeedback = fixture.fakeFeedback(); + await client.captureFeedback(fakeFeedback); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final event = await eventFromEnvelope(capturedEnvelope); + + expect(event.tags?.containsKey('theme'), true); + }); + + test('should pass hint to eventProcessors for event', () async { final myHint = Hint(); myHint.set('string', 'hint'); @@ -1438,7 +1448,26 @@ void main() { expect(executed, true); }); - test('should create hint when none was provided', () async { + test('should pass hint to eventProcessors for feedback', () async { + final myHint = Hint(); + myHint.set('string', 'hint'); + + var executed = false; + + final client = + fixture.getSut(eventProcessor: FunctionEventProcessor((event, hint) { + expect(myHint, hint); + executed = true; + return event; + })); + + final fakeFeedback = fixture.fakeFeedback(); + await client.captureFeedback(fakeFeedback, hint: myHint); + + expect(executed, true); + }); + + test('should create hint when none was provided for event', () async { var executed = false; final client = @@ -1453,12 +1482,39 @@ void main() { expect(executed, true); }); + test('should create hint when none was provided for feedback event', + () async { + var executed = false; + + final client = + fixture.getSut(eventProcessor: FunctionEventProcessor((event, hint) { + expect(hint, isNotNull); + executed = true; + return event; + })); + + final fakeFeedback = fixture.fakeFeedback(); + await client.captureFeedback(fakeFeedback); + + expect(executed, true); + }); + test('event processor drops the event', () async { final client = fixture.getSut(eventProcessor: DropAllEventProcessor()); + await client.captureEvent(fakeEvent); expect((fixture.transport).called(0), true); }); + + test('event processor drops the feedback event', () async { + final client = fixture.getSut(eventProcessor: DropAllEventProcessor()); + + final fakeFeedback = fixture.fakeFeedback(); + await client.captureFeedback(fakeFeedback); + + expect((fixture.transport).called(0), true); + }); }); group('SentryClient captures feedback', () { From 95d8d9c55c1c7a0a4c020ad47a2f422cd7c118e6 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 13 Aug 2024 14:12:47 +0200 Subject: [PATCH 07/36] basic scope test --- dart/test/sentry_client_test.dart | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 0accec129b..d59e0d243a 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -818,7 +818,7 @@ void main() { scope.setUser(user); }); - test('should apply the scope', () async { + test('should apply the scope to event', () async { final client = fixture.getSut(); await client.captureEvent(event, scope: scope); @@ -840,6 +840,28 @@ void main() { eventExtraKey: eventExtraValue, }); }); + + test('should apply the scope to feedback event', () async { + final client = fixture.getSut(); + final feedback = fixture.fakeFeedback(); + await client.captureFeedback(feedback, scope: scope); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); + + expect(capturedEvent.user?.id, user.id); + expect(capturedEvent.level!.name, SentryLevel.error.name); + expect(capturedEvent.transaction, transaction); + expect(capturedEvent.fingerprint, fingerprint); + expect(capturedEvent.breadcrumbs?.first.toJson(), crumb.toJson()); + expect(capturedEvent.tags, { + scopeTagKey: scopeTagValue, + }); + // ignore: deprecated_member_use_from_same_package + expect(capturedEvent.extra, { + scopeExtraKey: scopeExtraValue, + }); + }); }); group('SentryClient : apply partial scope to the captured event', () { From ab350dc9d55e7163f37c498a188c7cc5b2e5eaa9 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 13 Aug 2024 14:22:54 +0200 Subject: [PATCH 08/36] test trace context and attachment behaviour --- dart/lib/src/sentry_client.dart | 2 +- dart/test/sentry_client_test.dart | 227 ++++++++++++++++++++---------- 2 files changed, 153 insertions(+), 76 deletions(-) diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 74dc8bbb1f..bc6ecce1f2 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -162,7 +162,7 @@ class SentryClient { } var viewHierarchy = hint.viewHierarchy; - if (viewHierarchy != null) { + if (viewHierarchy != null && event.type != 'feedback') { attachments.add(viewHierarchy); } diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index d59e0d243a..67b23a8ba2 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -1642,81 +1642,6 @@ void main() { expect(envelope.clientReport, clientReport); }); - test('captureEvent adds trace context', () async { - final client = fixture.getSut(); - - final scope = Scope(fixture.options); - scope.span = - SentrySpan(fixture.tracer, fixture.tracer.context, MockHub()); - - await client.captureEvent(fakeEvent, scope: scope); - - final envelope = fixture.transport.envelopes.first; - expect(envelope.header.traceContext, isNotNull); - }); - - test('captureEvent adds attachments from hint', () async { - final attachment = SentryAttachment.fromIntList([], "fixture-fileName"); - final hint = Hint.withAttachment(attachment); - - final sut = fixture.getSut(); - await sut.captureEvent(fakeEvent, hint: hint); - - final capturedEnvelope = (fixture.transport).envelopes.first; - final attachmentItem = IterableUtils.firstWhereOrNull( - capturedEnvelope.items, - (SentryEnvelopeItem e) => e.header.type == SentryItemType.attachment, - ); - expect(attachmentItem?.header.attachmentType, - SentryAttachment.typeAttachmentDefault); - }); - - test('captureEvent adds screenshot from hint', () async { - final client = fixture.getSut(); - final screenshot = - SentryAttachment.fromScreenshotData(Uint8List.fromList([0, 0, 0, 0])); - final hint = Hint.withScreenshot(screenshot); - - await client.captureEvent(fakeEvent, hint: hint); - - final capturedEnvelope = (fixture.transport).envelopes.first; - final attachmentItem = capturedEnvelope.items.firstWhereOrNull( - (element) => element.header.type == SentryItemType.attachment); - expect(attachmentItem?.header.fileName, 'screenshot.png'); - }); - - test('captureEvent adds viewHierarchy from hint', () async { - final client = fixture.getSut(); - final view = SentryViewHierarchy('flutter'); - final attachment = SentryAttachment.fromViewHierarchy(view); - final hint = Hint.withViewHierarchy(attachment); - - await client.captureEvent(fakeEvent, hint: hint); - - final capturedEnvelope = (fixture.transport).envelopes.first; - final attachmentItem = capturedEnvelope.items.firstWhereOrNull( - (element) => element.header.type == SentryItemType.attachment); - - expect(attachmentItem?.header.attachmentType, - SentryAttachment.typeViewHierarchy); - }); - - test('captureTransaction adds trace context', () async { - final client = fixture.getSut(); - - final tr = SentryTransaction(fixture.tracer); - - final context = SentryTraceContextHeader.fromJson({ - 'trace_id': '${tr.eventId}', - 'public_key': '123', - }); - - await client.captureTransaction(tr, traceContext: context); - - final envelope = fixture.transport.envelopes.first; - expect(envelope.header.traceContext, isNotNull); - }); - test('captureUserFeedback calls flush', () async { final client = fixture.getSut(eventProcessor: DropAllEventProcessor()); @@ -1930,6 +1855,158 @@ void main() { }); }); + group('trace context', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('captureEvent adds trace context', () async { + final client = fixture.getSut(); + + final scope = Scope(fixture.options); + scope.span = + SentrySpan(fixture.tracer, fixture.tracer.context, MockHub()); + + await client.captureEvent(fakeEvent, scope: scope); + + final envelope = fixture.transport.envelopes.first; + expect(envelope.header.traceContext, isNotNull); + }); + + test('captureTransaction adds trace context', () async { + final client = fixture.getSut(); + + final tr = SentryTransaction(fixture.tracer); + + final context = SentryTraceContextHeader.fromJson({ + 'trace_id': '${tr.eventId}', + 'public_key': '123', + }); + + await client.captureTransaction(tr, traceContext: context); + + final envelope = fixture.transport.envelopes.first; + expect(envelope.header.traceContext, isNotNull); + }); + + test('captureFeedback adds trace context', () async { + final client = fixture.getSut(); + + final scope = Scope(fixture.options); + scope.span = + SentrySpan(fixture.tracer, fixture.tracer.context, MockHub()); + + await client.captureFeedback(fixture.fakeFeedback(), scope: scope); + + final envelope = fixture.transport.envelopes.first; + expect(envelope.header.traceContext, isNotNull); + }); + }); + + group('Hint', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('captureEvent adds attachments from hint', () async { + final attachment = SentryAttachment.fromIntList([], "fixture-fileName"); + final hint = Hint.withAttachment(attachment); + + final sut = fixture.getSut(); + await sut.captureEvent(fakeEvent, hint: hint); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final attachmentItem = IterableUtils.firstWhereOrNull( + capturedEnvelope.items, + (SentryEnvelopeItem e) => e.header.type == SentryItemType.attachment, + ); + expect(attachmentItem?.header.attachmentType, + SentryAttachment.typeAttachmentDefault); + }); + + test('captureFeedback adds attachments from hint', () async { + final attachment = SentryAttachment.fromIntList([], "fixture-fileName"); + final hint = Hint.withAttachment(attachment); + + final sut = fixture.getSut(); + final fakeFeedback = fixture.fakeFeedback(); + await sut.captureFeedback(fakeFeedback, hint: hint); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final attachmentItem = IterableUtils.firstWhereOrNull( + capturedEnvelope.items, + (SentryEnvelopeItem e) => e.header.type == SentryItemType.attachment, + ); + expect(attachmentItem?.header.attachmentType, + SentryAttachment.typeAttachmentDefault); + }); + + test('captureEvent adds screenshot from hint', () async { + final client = fixture.getSut(); + final screenshot = + SentryAttachment.fromScreenshotData(Uint8List.fromList([0, 0, 0, 0])); + final hint = Hint.withScreenshot(screenshot); + + await client.captureEvent(fakeEvent, hint: hint); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final attachmentItem = capturedEnvelope.items.firstWhereOrNull( + (element) => element.header.type == SentryItemType.attachment); + expect(attachmentItem?.header.fileName, 'screenshot.png'); + }); + + test('captureFeedback adds screenshot from hint', () async { + final client = fixture.getSut(); + final screenshot = + SentryAttachment.fromScreenshotData(Uint8List.fromList([0, 0, 0, 0])); + final hint = Hint.withScreenshot(screenshot); + + final fakeFeedback = fixture.fakeFeedback(); + await client.captureFeedback(fakeFeedback, hint: hint); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final attachmentItem = capturedEnvelope.items.firstWhereOrNull( + (element) => element.header.type == SentryItemType.attachment); + expect(attachmentItem?.header.fileName, 'screenshot.png'); + }); + + test('captureEvent adds viewHierarchy from hint', () async { + final client = fixture.getSut(); + final view = SentryViewHierarchy('flutter'); + final attachment = SentryAttachment.fromViewHierarchy(view); + final hint = Hint.withViewHierarchy(attachment); + + await client.captureEvent(fakeEvent, hint: hint); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final attachmentItem = capturedEnvelope.items.firstWhereOrNull( + (element) => element.header.type == SentryItemType.attachment); + + expect(attachmentItem?.header.attachmentType, + SentryAttachment.typeViewHierarchy); + }); + + test('captureFeedback does not add viewHierarchy from hint', () async { + final client = fixture.getSut(); + final view = SentryViewHierarchy('flutter'); + final attachment = SentryAttachment.fromViewHierarchy(view); + final hint = Hint.withViewHierarchy(attachment); + + final fakeFeedback = fixture.fakeFeedback(); + await client.captureFeedback(fakeFeedback, hint: hint); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final attachmentItem = capturedEnvelope.items.firstWhereOrNull( + (element) => element.header.type == SentryItemType.attachment); + + expect(attachmentItem, isNull); + }); + }); + group('Capture metrics', () { late Fixture fixture; From 26236e3453d9b4c8fb72326f02c6122fe25354ce Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 13 Aug 2024 14:35:49 +0200 Subject: [PATCH 09/36] test sample rate for feedback and fix mock transport calls comparison --- dart/lib/src/sentry_client.dart | 2 +- dart/test/mocks/mock_transport.dart | 2 +- dart/test/sentry_client_test.dart | 15 ++++++++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index bc6ecce1f2..4bb267fb58 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -105,7 +105,7 @@ class SentryClient { return _emptySentryId; } - if (_sampleRate()) { + if (_sampleRate() && event.type != 'feedback') { _options.recorder .recordLostEvent(DiscardReason.sampleRate, _getCategory(event)); _options.logger( diff --git a/dart/test/mocks/mock_transport.dart b/dart/test/mocks/mock_transport.dart index fad9f43696..55ae027f21 100644 --- a/dart/test/mocks/mock_transport.dart +++ b/dart/test/mocks/mock_transport.dart @@ -17,7 +17,7 @@ class MockTransport implements Transport { } bool called(int calls) { - return calls == calls; + return _calls == calls; } @override diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 67b23a8ba2..3b3fae8ffc 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -1037,21 +1037,30 @@ void main() { final client = fixture.getSut(sampleRate: 1.0); await client.captureEvent(fakeEvent); - expect((fixture.transport).called(1), true); + expect(fixture.transport.called(1), true); }); test('do not capture event, sample rate is 0% disabled', () async { final client = fixture.getSut(sampleRate: 0.0); await client.captureEvent(fakeEvent); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test('captures event, sample rate is null, disabled', () async { final client = fixture.getSut(); await client.captureEvent(fakeEvent); - expect((fixture.transport).called(1), true); + expect(fixture.transport.called(1), true); + }); + + test('capture feedback event, sample rate is 0% disabled', () async { + final client = fixture.getSut(sampleRate: 0.0); + + final fakeFeedback = fixture.fakeFeedback(); + await client.captureFeedback(fakeFeedback); + + expect(fixture.transport.called(1), true); }); }); From bc22937b15be49e85b0080333e3755403bb29552 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 13 Aug 2024 14:56:00 +0200 Subject: [PATCH 10/36] add hub tests --- dart/lib/src/hub.dart | 46 +++++++++++------------ dart/test/hub_test.dart | 50 +++++++++++++++++++++++++ dart/test/mocks/mock_sentry_client.dart | 27 +++++++++++++ 3 files changed, 99 insertions(+), 24 deletions(-) diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index fb9f548911..76f7ea3ec3 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -286,33 +286,31 @@ class Hub { SentryLevel.warning, "Instance is disabled and this 'captureFeedback' call is a no-op.", ); - return sentryId; - } - - final item = _peek(); - late Scope scope; - final s = _cloneAndRunWithScope(item.scope, withScope); - if (s is Future) { - scope = await s; } else { - scope = s; - } + final item = _peek(); + late Scope scope; + final s = _cloneAndRunWithScope(item.scope, withScope); + if (s is Future) { + scope = await s; + } else { + scope = s; + } - try { - sentryId = await item.client.captureFeedback( - feedback, - hint: hint, - scope: scope, - ); - } catch (exception, stacktrace) { - _options.logger( - SentryLevel.error, - 'Error while capturing user feedback', - exception: exception, - stackTrace: stacktrace, - ); + try { + sentryId = await item.client.captureFeedback( + feedback, + hint: hint, + scope: scope, + ); + } catch (exception, stacktrace) { + _options.logger( + SentryLevel.error, + 'Error while capturing user feedback', + exception: exception, + stackTrace: stacktrace, + ); + } } - return sentryId; } diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index c53ab74e48..7cf96f2884 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -54,6 +54,24 @@ void main() { }, ); + test( + 'should capture feedback with the default scope', + () async { + final hub = fixture.getSut(); + final feedback = SentryFeedback(message: 'message'); + await hub.captureFeedback(feedback); + + var scope = fixture.client.captureFeedbackCalls.first.scope; + + expect( + fixture.client.captureFeedbackCalls.first.feedback, + feedback, + ); + + expect(scopeEquals(scope, Scope(fixture.options)), true); + }, + ); + test('should capture exception', () async { final hub = fixture.getSut(); await hub.captureException(fakeException); @@ -555,6 +573,22 @@ void main() { expect(fixture.loggedLevel, SentryLevel.error); }); + test('captureFeedback should handle thrown error in scope callback', + () async { + final hub = fixture.getSut(debug: true); + final scopeCallbackException = Exception('error in scope callback'); + + ScopeCallback scopeCallback = (Scope scope) { + throw scopeCallbackException; + }; + + final feedback = SentryFeedback(message: 'message'); + await hub.captureFeedback(feedback, withScope: scopeCallback); + + expect(fixture.loggedException, scopeCallbackException); + expect(fixture.loggedLevel, SentryLevel.error); + }); + test('captureException should handle thrown error in scope callback', () async { final hub = fixture.getSut(debug: true); @@ -643,6 +677,22 @@ void main() { expect(calls[2].scope?.user, isNull); }); + test('captureFeedback should create a new scope', () async { + final hub = fixture.getSut(); + await hub.captureFeedback(SentryFeedback(message: 'message')); + await hub.captureFeedback(SentryFeedback(message: 'message'), + withScope: (scope) async { + await scope.setUser(SentryUser(id: 'foo bar')); + }); + await hub.captureFeedback(SentryFeedback(message: 'message')); + + var calls = fixture.client.captureFeedbackCalls; + expect(calls.length, 3); + expect(calls[0].scope?.user, isNull); + expect(calls[1].scope?.user?.id, 'foo bar'); + expect(calls[2].scope?.user, isNull); + }); + test('captureException should create a new scope', () async { final hub = fixture.getSut(); await hub.captureException(Exception('0')); diff --git a/dart/test/mocks/mock_sentry_client.dart b/dart/test/mocks/mock_sentry_client.dart index 248ea19032..c9d221b25f 100644 --- a/dart/test/mocks/mock_sentry_client.dart +++ b/dart/test/mocks/mock_sentry_client.dart @@ -10,6 +10,7 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { List captureEnvelopeCalls = []; List captureTransactionCalls = []; List userFeedbackCalls = []; + List captureFeedbackCalls = []; List>> captureMetricsCalls = []; int closeCalls = 0; @@ -76,6 +77,20 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { userFeedbackCalls.add(userFeedback); } + @override + Future captureFeedback( + SentryFeedback feedback, { + Scope? scope, + Hint? hint, + }) async { + captureFeedbackCalls.add(CaptureFeedbackCall( + feedback, + scope, + hint, + )); + return SentryId.newId(); + } + @override Future captureMetrics(Map> metrics) async { captureMetricsCalls.add(metrics); @@ -113,6 +128,18 @@ class CaptureEventCall { ); } +class CaptureFeedbackCall { + final SentryFeedback feedback; + final Hint? hint; + final Scope? scope; + + CaptureFeedbackCall( + this.feedback, + this.scope, + this.hint, + ); +} + class CaptureExceptionCall { final dynamic throwable; final dynamic stackTrace; From 58d83f99a904e4f1906fc955b8c113f4d177385a Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 13 Aug 2024 14:59:11 +0200 Subject: [PATCH 11/36] add sentry tests --- dart/lib/src/sentry.dart | 8 ++++++-- dart/test/sentry_test.dart | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index cbac4b4ed9..de7e9940eb 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -220,8 +220,12 @@ class Sentry { static Future captureUserFeedback(SentryUserFeedback userFeedback) => _hub.captureUserFeedback(userFeedback); - static Future captureFeedback(SentryFeedback feedback) => - _hub.captureFeedback(feedback); + static Future captureFeedback( + SentryFeedback feedback, { + Hint? hint, + ScopeCallback? withScope, + }) => + _hub.captureFeedback(feedback, hint: hint, withScope: withScope); /// Close the client SDK static Future close() async { diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index 1b363b99a0..1dbe6ec471 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -45,6 +45,15 @@ void main() { expect(client.captureEventCalls.first.scope, isNotNull); }); + test('should capture the feedback event', () async { + final fakeFeedback = SentryFeedback(message: 'message'); + await Sentry.captureFeedback(fakeFeedback); + + expect(client.captureFeedbackCalls.length, 1); + expect(client.captureFeedbackCalls.first.feedback, fakeFeedback); + expect(client.captureFeedbackCalls.first.scope, isNotNull); + }); + test('should capture the event withScope', () async { await Sentry.captureEvent( fakeEvent, @@ -58,6 +67,19 @@ void main() { expect(client.captureEventCalls.first.scope?.user?.id, 'foo bar'); }); + test('should capture the feedback event withScope', () async { + final fakeFeedback = SentryFeedback(message: 'message'); + await Sentry.captureFeedback( + fakeFeedback, + withScope: (scope) { + scope.setUser(SentryUser(id: 'foo bar')); + }, + ); + + expect(client.captureFeedbackCalls.length, 1); + expect(client.captureFeedbackCalls.first.scope?.user?.id, 'foo bar'); + }); + test('should not capture a null exception', () async { await Sentry.captureException(null); expect(client.captureEventCalls.length, 0); From 74b5529e82f7d1745b3857ced16d5349e4b2a29b Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 13 Aug 2024 15:00:14 +0200 Subject: [PATCH 12/36] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5117d226..30d0ecdfe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ```dart SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), ``` +- Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) ### Dependencies From 9a7282c1a50298801bb23f06e0e26abf4d2164ee Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 12:31:04 +0200 Subject: [PATCH 13/36] cleanup + comments --- dart/lib/src/hub.dart | 3 ++- dart/lib/src/protocol/contexts.dart | 2 +- dart/lib/src/protocol/sentry_feedback.dart | 2 +- dart/lib/src/sentry.dart | 4 ++++ dart/lib/src/sentry_client.dart | 2 +- dart/lib/src/sentry_envelope_item.dart | 2 +- dart/lib/src/sentry_options.dart | 4 ++++ 7 files changed, 14 insertions(+), 5 deletions(-) diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 76f7ea3ec3..26359a4c46 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -274,6 +274,7 @@ class Hub { } } + /// Captures the feedback. Future captureFeedback( SentryFeedback feedback, { Hint? hint, @@ -305,7 +306,7 @@ class Hub { } catch (exception, stacktrace) { _options.logger( SentryLevel.error, - 'Error while capturing user feedback', + 'Error while capturing feedback', exception: exception, stackTrace: stacktrace, ); diff --git a/dart/lib/src/protocol/contexts.dart b/dart/lib/src/protocol/contexts.dart index c1cbfeff54..0a38e505af 100644 --- a/dart/lib/src/protocol/contexts.dart +++ b/dart/lib/src/protocol/contexts.dart @@ -142,7 +142,7 @@ class Contexts extends MapView { set response(SentryResponse? value) => this[SentryResponse.type] = value; - /// Feedback context for a FeedbackEvent. + /// Feedback context for a feedback event. SentryFeedback? get feedback => this[SentryFeedback.type]; set feedback(SentryFeedback? value) => this[SentryFeedback.type] = value; diff --git a/dart/lib/src/protocol/sentry_feedback.dart b/dart/lib/src/protocol/sentry_feedback.dart index c25e0759df..832db2316f 100644 --- a/dart/lib/src/protocol/sentry_feedback.dart +++ b/dart/lib/src/protocol/sentry_feedback.dart @@ -27,7 +27,7 @@ class SentryFeedback { @internal final Map? unknown; - /// Deserializes a [SentryOperatingSystem] from JSON [Map]. + /// Deserializes a [SentryFeedback] from JSON [Map]. factory SentryFeedback.fromJson(Map data) { final json = AccessAwareMap(data); diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index de7e9940eb..ebc917f5bb 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -22,6 +22,7 @@ import 'sentry_client.dart'; import 'sentry_options.dart'; import 'sentry_user_feedback.dart'; import 'tracing.dart'; +import 'sentry_attachment/sentry_attachment.dart'; /// Configuration options callback typedef OptionsConfiguration = FutureOr Function(SentryOptions); @@ -220,6 +221,9 @@ class Sentry { static Future captureUserFeedback(SentryUserFeedback userFeedback) => _hub.captureUserFeedback(userFeedback); + /// Reports [SentryFeedback] to Sentry.io. + /// + /// Use [withScope] to add [SentryAttachment] to the feedback. static Future captureFeedback( SentryFeedback feedback, { Hint? hint, diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 4bb267fb58..91b331c80b 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -438,7 +438,7 @@ class SentryClient { return _attachClientReportsAndSend(envelope); } - /// Reports the [SentryFeedback] and to Sentry.io. + /// Reports the [feedback] to Sentry.io. Future captureFeedback( SentryFeedback feedback, { Scope? scope, diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index c13e3c3edc..88ca36e930 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -66,7 +66,7 @@ class SentryEnvelopeItem { return SentryEnvelopeItem( SentryEnvelopeItemHeader( - event.type ?? SentryItemType.event, + event.type == 'feedback' ? 'feedback' : SentryItemType.event, cachedItem.getDataLength, contentType: 'application/json', ), diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 0759adc098..dd2379e42a 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -161,6 +161,8 @@ class SentryOptions { /// transaction object or nothing to skip reporting the transaction BeforeSendTransactionCallback? beforeSendTransaction; + /// This function is called with an SDK specific feedback event object and can return a modified + /// feedback event object or nothing to skip reporting the feedback event BeforeSendFeedbackCallback? beforeSendFeedback; /// This function is called with an SDK specific breadcrumb object before the breadcrumb is added @@ -616,6 +618,8 @@ typedef BeforeSendTransactionCallback = FutureOr Function( SentryTransaction transaction, ); +/// This function is called with an SDK specific feedback event object and can +/// return a modified feedback event object or nothing to skip reporting the feedback event typedef BeforeSendFeedbackCallback = FutureOr Function( SentryEvent event, Hint hint, From b23858368efb6572797d5ecb58922bf21c1fd1cf Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 14:01:19 +0200 Subject: [PATCH 14/36] test envelope item for feedback --- dart/test/sentry_envelope_item_test.dart | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/dart/test/sentry_envelope_item_test.dart b/dart/test/sentry_envelope_item_test.dart index c5a205c945..3f1bad3ee0 100644 --- a/dart/test/sentry_envelope_item_test.dart +++ b/dart/test/sentry_envelope_item_test.dart @@ -59,6 +59,32 @@ void main() { expect(actualData, expectedData); }); + test('fromEvent feedback', () async { + final feedback = SentryFeedback( + message: 'fixture-message', + ); + final feedbackEvent = SentryEvent( + type: 'feedback', + contexts: Contexts(feedback: feedback), + level: SentryLevel.info, + ); + final sut = SentryEnvelopeItem.fromEvent(feedbackEvent); + + final expectedData = utf8.encode(jsonEncode( + feedbackEvent.toJson(), + toEncodable: jsonSerializationFallback, + )); + final actualData = await sut.dataFactory(); + + final expectedLength = expectedData.length; + final actualLength = await sut.header.length(); + + expect(sut.header.contentType, 'application/json'); + expect(sut.header.type, 'feedback'); + expect(actualLength, expectedLength); + expect(actualData, expectedData); + }); + test('fromTransaction', () async { final context = SentryTransactionContext( 'name', From 8e12e8fc692081c524acfc824afe99d83650729d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 14:04:02 +0200 Subject: [PATCH 15/36] remove duplacte typedef --- dart/lib/src/sentry_options.dart | 9 +-------- dart/test/sentry_client_test.dart | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index dd2379e42a..87522a75b5 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -163,7 +163,7 @@ class SentryOptions { /// This function is called with an SDK specific feedback event object and can return a modified /// feedback event object or nothing to skip reporting the feedback event - BeforeSendFeedbackCallback? beforeSendFeedback; + BeforeSendCallback? beforeSendFeedback; /// This function is called with an SDK specific breadcrumb object before the breadcrumb is added /// to the scope. When nothing is returned from the function, the breadcrumb is dropped @@ -618,13 +618,6 @@ typedef BeforeSendTransactionCallback = FutureOr Function( SentryTransaction transaction, ); -/// This function is called with an SDK specific feedback event object and can -/// return a modified feedback event object or nothing to skip reporting the feedback event -typedef BeforeSendFeedbackCallback = FutureOr Function( - SentryEvent event, - Hint hint, -); - /// This function is called with an SDK specific breadcrumb object before the breadcrumb is added /// to the scope. When nothing is returned from the function, the breadcrumb is dropped typedef BeforeBreadcrumbCallback = Breadcrumb? Function( diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 3b3fae8ffc..9bf7cf3e9f 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -2159,7 +2159,7 @@ class Fixture { double? sampleRate, BeforeSendCallback? beforeSend, BeforeSendTransactionCallback? beforeSendTransaction, - BeforeSendFeedbackCallback? beforeSendFeedback, + BeforeSendCallback? beforeSendFeedback, EventProcessor? eventProcessor, bool provideMockRecorder = true, bool debug = false, From ed773ef87430664ace7aaa6c26c0c8fe589398ee Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 14:05:39 +0200 Subject: [PATCH 16/36] fix test expectation --- dart/test/protocol/sentry_feedback_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dart/test/protocol/sentry_feedback_test.dart b/dart/test/protocol/sentry_feedback_test.dart index 0c3ed1f8be..ae02472f32 100644 --- a/dart/test/protocol/sentry_feedback_test.dart +++ b/dart/test/protocol/sentry_feedback_test.dart @@ -78,7 +78,7 @@ void main() { expect(copy.name, 'fixture-2-name'); expect(copy.replayId, 'fixture-2-replayId'); expect(copy.url, "https://fixture-2-url.com"); - expect(copy.associatedEventId, '1d49af08b6e2c437f9052b1ecfd83dca'); + expect(copy.associatedEventId.toString(), '1d49af08b6e2c437f9052b1ecfd83dca'); }); }); } From ee716966dd9897c8042f56883d600fcd36c75a23 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 14:10:52 +0200 Subject: [PATCH 17/36] Deprecate captureUserFeedback --- CHANGELOG.md | 4 ++++ dart/lib/src/hub.dart | 2 ++ dart/lib/src/sentry.dart | 2 ++ dart/lib/src/sentry_client.dart | 2 ++ dart/lib/src/sentry_envelope.dart | 1 + dart/lib/src/sentry_user_feedback.dart | 1 + dart/test/protocol/sentry_feedback_test.dart | 3 ++- 7 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b851b6dfd..7b817b8a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), ``` - Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) +### Deprecated + +- The function `captureUserFeedback` is deprecated. Use `captureFeedback` instead. ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) + ### Improvements - Debouncing of SentryWidgetsBindingObserver.didChangeMetrics with delay of 100ms. ([#2232](https://github.com/getsentry/sentry-dart/pull/2232)) diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 26359a4c46..dea6ef79d9 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -245,6 +245,8 @@ class Hub { return sentryId; } + @Deprecated( + 'Will be removed in a future version. Use [captureFeedback] instead') Future captureUserFeedback(SentryUserFeedback userFeedback) async { if (!_isEnabled) { _options.logger( diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index ebc917f5bb..b3faf735f0 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -218,6 +218,8 @@ class Sentry { /// Reports a [userFeedback] to Sentry.io. /// /// First capture an event and use the [SentryId] to create a [SentryUserFeedback] + @Deprecated( + 'Will be removed in a future version. Use [captureFeedback] instead') static Future captureUserFeedback(SentryUserFeedback userFeedback) => _hub.captureUserFeedback(userFeedback); diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 91b331c80b..6b9a5643a8 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -429,6 +429,8 @@ class SentryClient { } /// Reports the [userFeedback] to Sentry.io. + @Deprecated( + 'Will be removed in a future version. Use [captureFeedback] instead') Future captureUserFeedback(SentryUserFeedback userFeedback) { final envelope = SentryEnvelope.fromUserFeedback( userFeedback, diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index fb7cd1543a..ce27f03d12 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -59,6 +59,7 @@ class SentryEnvelope { ); } + @Deprecated('Will be removed in a future version.') factory SentryEnvelope.fromUserFeedback( SentryUserFeedback feedback, SdkVersion sdkVersion, { diff --git a/dart/lib/src/sentry_user_feedback.dart b/dart/lib/src/sentry_user_feedback.dart index 055199ed61..722a0983f1 100644 --- a/dart/lib/src/sentry_user_feedback.dart +++ b/dart/lib/src/sentry_user_feedback.dart @@ -3,6 +3,7 @@ import 'package:meta/meta.dart'; import 'protocol.dart'; import 'protocol/access_aware_map.dart'; +@Deprecated('Will be removed in a future version. Use [SentryFeedback] instead') class SentryUserFeedback { SentryUserFeedback({ required this.eventId, diff --git a/dart/test/protocol/sentry_feedback_test.dart b/dart/test/protocol/sentry_feedback_test.dart index ae02472f32..f32fa2f3a8 100644 --- a/dart/test/protocol/sentry_feedback_test.dart +++ b/dart/test/protocol/sentry_feedback_test.dart @@ -78,7 +78,8 @@ void main() { expect(copy.name, 'fixture-2-name'); expect(copy.replayId, 'fixture-2-replayId'); expect(copy.url, "https://fixture-2-url.com"); - expect(copy.associatedEventId.toString(), '1d49af08b6e2c437f9052b1ecfd83dca'); + expect(copy.associatedEventId.toString(), + '1d49af08b6e2c437f9052b1ecfd83dca'); }); }); } From 4b403ddf8623cd80c86bf03abea4b469c7fa02fb Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 14:13:14 +0200 Subject: [PATCH 18/36] update depraction info in cl --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b817b8a05..16aadcd9fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,10 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), ``` - Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) - -### Deprecated - -- The function `captureUserFeedback` is deprecated. Use `captureFeedback` instead. ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) + - Deprecated `Sentry.captureUserFeedback`, use `captureFeedback` instead. + - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. + - Deprecated `SentryClient.captureUserFeedback`, use `captureFeedback` instead. + - Deprecated `SentryUserFeedback`, use `SentryFeedback` instead. ### Improvements From b01cef2ace4d7cb4feed7f133285ef3d16fc680b Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 15:01:56 +0200 Subject: [PATCH 19/36] format --- flutter/example/lib/main.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index c9c8cbcdd4..9d85a89b9a 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -523,14 +523,15 @@ class MainScaffold extends StatelessWidget { ), TooltipButton( onPressed: () async { - final associatedEventId = await Sentry.captureMessage('Associated Event'); + final associatedEventId = + await Sentry.captureMessage('Associated Event'); await Sentry.captureFeedback( - SentryFeedback( - message: 'message', - contactEmail: 'john.appleseed@apple.com', - name: 'John Appleseed', - associatedEventId: associatedEventId, - ), + SentryFeedback( + message: 'message', + contactEmail: 'john.appleseed@apple.com', + name: 'John Appleseed', + associatedEventId: associatedEventId, + ), ); }, text: '', From 1733c9ec407aac1fdc50a4aad36ac25ec1ff30da Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 15:06:45 +0200 Subject: [PATCH 20/36] add missing option in test --- dart/test/sentry_client_test.dart | 39 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 9bf7cf3e9f..314b2aa164 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -1070,6 +1070,7 @@ void main() { setUp(() { fixture = Fixture(); fixture.options.ignoreErrors = ["my-error", "^error-.*\$"]; + fixture.options.ignoreTransactions = ["my-transaction", "^transaction-.*\$"]; }); test('drop event if error message fully matches ignoreErrors value', @@ -1079,7 +1080,7 @@ void main() { final client = fixture.getSut(); await client.captureEvent(event); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test('drop event if error message partially matches ignoreErrors value', @@ -1089,7 +1090,7 @@ void main() { final client = fixture.getSut(); await client.captureEvent(event); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test( @@ -1100,7 +1101,7 @@ void main() { final client = fixture.getSut(); await client.captureEvent(event); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test('send event if error message does not match ignoreErrors value', @@ -1110,7 +1111,7 @@ void main() { final client = fixture.getSut(); await client.captureEvent(event); - expect((fixture.transport).called(1), true); + expect(fixture.transport.called(1), true); }); test('send event if no values are set for ignoreErrors', () async { @@ -1120,7 +1121,7 @@ void main() { final client = fixture.getSut(); await client.captureEvent(event); - expect((fixture.transport).called(1), true); + expect(fixture.transport.called(1), true); }); }); @@ -1142,7 +1143,7 @@ void main() { fakeTransaction.tracer.name = "my-transaction"; await client.captureTransaction(fakeTransaction); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test('drop transaction if name partially matches ignoreTransaction value', @@ -1152,7 +1153,7 @@ void main() { fakeTransaction.tracer.name = "this is a transaction-test"; await client.captureTransaction(fakeTransaction); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test( @@ -1163,7 +1164,7 @@ void main() { fakeTransaction.tracer.name = "transaction-test message"; await client.captureTransaction(fakeTransaction); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test('send transaction if name does not match ignoreTransaction value', @@ -1173,7 +1174,7 @@ void main() { fakeTransaction.tracer.name = "capture"; await client.captureTransaction(fakeTransaction); - expect((fixture.transport).called(1), true); + expect(fixture.transport.called(1), true); }); test('send transaction if no values are set for ignoreTransaction', @@ -1184,7 +1185,7 @@ void main() { fakeTransaction.tracer.name = "this is a test transaction"; await client.captureTransaction(fakeTransaction); - expect((fixture.transport).called(1), true); + expect(fixture.transport.called(1), true); }); }); @@ -1205,7 +1206,7 @@ void main() { final client = fixture.getSut(); await client.captureEvent(event); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test('record ignored exceptions dropping event', () async { @@ -1237,7 +1238,7 @@ void main() { final fakeFeedback = fixture.fakeFeedback(); await client.captureFeedback(fakeFeedback); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test('async before send feedback drops event', () async { @@ -1246,7 +1247,7 @@ void main() { final fakeFeedback = fixture.fakeFeedback(); await client.captureFeedback(fakeFeedback); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test( @@ -1292,7 +1293,7 @@ void main() { final fakeTransaction = fixture.fakeTransaction(); await client.captureTransaction(fakeTransaction); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test('async before send transaction drops event', () async { @@ -1301,7 +1302,7 @@ void main() { final fakeTransaction = fixture.fakeTransaction(); await client.captureTransaction(fakeTransaction); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test( @@ -1358,7 +1359,7 @@ void main() { final client = fixture.getSut(beforeSend: beforeSendCallbackDropEvent); await client.captureEvent(fakeEvent); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test('async before send drops event', () async { @@ -1366,7 +1367,7 @@ void main() { fixture.getSut(beforeSend: asyncBeforeSendCallbackDropEvent); await client.captureEvent(fakeEvent); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test('before send returns an event and event is captured', () async { @@ -1535,7 +1536,7 @@ void main() { await client.captureEvent(fakeEvent); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); test('event processor drops the feedback event', () async { @@ -1544,7 +1545,7 @@ void main() { final fakeFeedback = fixture.fakeFeedback(); await client.captureFeedback(fakeFeedback); - expect((fixture.transport).called(0), true); + expect(fixture.transport.called(0), true); }); }); From 7b2523553a29f26f2443ad72da91fe6076778e34 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 15:08:49 +0200 Subject: [PATCH 21/36] run format --- dart/test/sentry_client_test.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 314b2aa164..fb2de9085d 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -1070,7 +1070,10 @@ void main() { setUp(() { fixture = Fixture(); fixture.options.ignoreErrors = ["my-error", "^error-.*\$"]; - fixture.options.ignoreTransactions = ["my-transaction", "^transaction-.*\$"]; + fixture.options.ignoreTransactions = [ + "my-transaction", + "^transaction-.*\$" + ]; }); test('drop event if error message fully matches ignoreErrors value', From d5c1f0da8bb7ba91c07cf80b7ade95818d21c407 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 15:11:14 +0200 Subject: [PATCH 22/36] organize imports --- dart/lib/src/sentry_envelope.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index ce27f03d12..9c02bfa8f1 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -1,15 +1,16 @@ import 'dart:convert'; + import 'client_reports/client_report.dart'; import 'metrics/metric.dart'; import 'protocol.dart'; -import 'sentry_item_type.dart'; -import 'sentry_options.dart'; -import 'sentry_trace_context_header.dart'; -import 'utils.dart'; import 'sentry_attachment/sentry_attachment.dart'; import 'sentry_envelope_header.dart'; import 'sentry_envelope_item.dart'; +import 'sentry_item_type.dart'; +import 'sentry_options.dart'; +import 'sentry_trace_context_header.dart'; import 'sentry_user_feedback.dart'; +import 'utils.dart'; /// Class representation of `Envelope` file. class SentryEnvelope { From 4785da92534cb118d149095f4f821af3e24b2690 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 15:38:49 +0200 Subject: [PATCH 23/36] add missing method --- dart/lib/src/noop_sentry_client.dart | 6 ++++++ dart/test/sentry_client_test.dart | 6 +----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index 5a8c04bbae..4c80946ab9 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -6,6 +6,7 @@ import 'hint.dart'; import 'metrics/metric.dart'; import 'metrics/metrics_aggregator.dart'; import 'protocol.dart'; +import 'protocol/sentry_feedback.dart'; import 'scope.dart'; import 'sentry_client.dart'; import 'sentry_envelope.dart'; @@ -77,4 +78,9 @@ class NoOpSentryClient implements SentryClient { @override @internal MetricsAggregator? get metricsAggregator => null; + + @override + Future captureFeedback(SentryFeedback feedback, + {Scope? scope, Hint? hint}) async => + SentryId.empty(); } diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index fb2de9085d..2e611f9700 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -13,8 +13,8 @@ import 'package:sentry/src/sentry_item_type.dart'; import 'package:sentry/src/sentry_stack_trace_factory.dart'; import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry/src/transport/data_category.dart'; -import 'package:sentry/src/utils/iterable_utils.dart'; import 'package:sentry/src/transport/spotlight_http_transport.dart'; +import 'package:sentry/src/utils/iterable_utils.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -1070,10 +1070,6 @@ void main() { setUp(() { fixture = Fixture(); fixture.options.ignoreErrors = ["my-error", "^error-.*\$"]; - fixture.options.ignoreTransactions = [ - "my-transaction", - "^transaction-.*\$" - ]; }); test('drop event if error message fully matches ignoreErrors value', From 4ae6696aa14bfd91595ed0a71e050b0b6cd4a614 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 15:40:47 +0200 Subject: [PATCH 24/36] fix test epectation --- dart/test/sentry_client_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 2e611f9700..233f628930 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -1149,7 +1149,7 @@ void main() { () async { final client = fixture.getSut(); final fakeTransaction = fixture.fakeTransaction(); - fakeTransaction.tracer.name = "this is a transaction-test"; + fakeTransaction.tracer.name = "this is a my-transaction-test"; await client.captureTransaction(fakeTransaction); expect(fixture.transport.called(0), true); From 06a4a5fc7ed80dd6be132924e65e87ac083376de Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 15:56:00 +0200 Subject: [PATCH 25/36] ignore deprecations internally --- dart/lib/src/hub_adapter.dart | 6 ++++-- dart/lib/src/noop_hub.dart | 1 + dart/lib/src/noop_sentry_client.dart | 1 + dart/lib/src/sentry_envelope_item.dart | 5 +++-- dart/test/mocks/mock_hub.dart | 3 +++ dart/test/mocks/mock_sentry_client.dart | 3 +++ dart/test/sentry_client_test.dart | 6 ++++++ dart/test/sentry_envelope_item_test.dart | 2 ++ dart/test/sentry_envelope_test.dart | 3 +++ dart/test/sentry_user_feedback_test.dart | 21 +++++++++++++++++++ .../integration_test/integration_test.dart | 1 + flutter/example/lib/user_feedback_dialog.dart | 4 +++- 12 files changed, 51 insertions(+), 5 deletions(-) diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index ad195c525c..40684cf3c9 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:meta/meta.dart'; -import 'hint.dart'; +import 'hint.dart'; import 'hub.dart'; import 'metrics/metric.dart'; import 'metrics/metrics_aggregator.dart'; @@ -13,8 +13,8 @@ import 'protocol/sentry_feedback.dart'; import 'scope.dart'; import 'sentry.dart'; import 'sentry_client.dart'; -import 'sentry_user_feedback.dart'; import 'sentry_options.dart'; +import 'sentry_user_feedback.dart'; import 'tracing.dart'; /// Hub adapter to make Integrations testable @@ -118,7 +118,9 @@ class HubAdapter implements Hub { ISentrySpan? getSpan() => Sentry.currentHub.getSpan(); @override + // ignore: deprecated_member_use_from_same_package Future captureUserFeedback(SentryUserFeedback userFeedback) => + // ignore: deprecated_member_use_from_same_package Sentry.captureUserFeedback(userFeedback); @override diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index e7b79ec936..10f34a6d42 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -97,6 +97,7 @@ class NoOpHub implements Hub { SentryId.empty(); @override + // ignore: deprecated_member_use_from_same_package Future captureUserFeedback(SentryUserFeedback userFeedback) async {} @override diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index 4c80946ab9..f2854f4f7e 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -56,6 +56,7 @@ class NoOpSentryClient implements SentryClient { SentryId.empty(); @override + // ignore: deprecated_member_use_from_same_package Future captureUserFeedback(SentryUserFeedback userFeedback) async {} @override diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index 88ca36e930..3f7ed19d32 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -4,11 +4,11 @@ import 'dart:convert'; import 'client_reports/client_report.dart'; import 'metrics/metric.dart'; import 'protocol.dart'; -import 'utils.dart'; import 'sentry_attachment/sentry_attachment.dart'; -import 'sentry_item_type.dart'; import 'sentry_envelope_item_header.dart'; +import 'sentry_item_type.dart'; import 'sentry_user_feedback.dart'; +import 'utils.dart'; /// Item holding header information and JSON encoded data. class SentryEnvelopeItem { @@ -46,6 +46,7 @@ class SentryEnvelopeItem { } /// Create a [SentryEnvelopeItem] which sends [SentryUserFeedback]. + @Deprecated('Will be removed in a future version.') factory SentryEnvelopeItem.fromUserFeedback(SentryUserFeedback feedback) { final cachedItem = _CachedItem(() async => utf8JsonEncoder.convert(feedback.toJson())); diff --git a/dart/test/mocks/mock_hub.dart b/dart/test/mocks/mock_hub.dart index c076251736..2bdec695a2 100644 --- a/dart/test/mocks/mock_hub.dart +++ b/dart/test/mocks/mock_hub.dart @@ -13,6 +13,8 @@ class MockHub with NoSuchMethodProvider implements Hub { List captureMessageCalls = []; List addBreadcrumbCalls = []; List bindClientCalls = []; + + // ignore: deprecated_member_use_from_same_package List userFeedbackCalls = []; List captureTransactionCalls = []; List captureMetricsCalls = []; @@ -133,6 +135,7 @@ class MockHub with NoSuchMethodProvider implements Hub { } @override + // ignore: deprecated_member_use_from_same_package Future captureUserFeedback(SentryUserFeedback userFeedback) async { userFeedbackCalls.add(userFeedback); } diff --git a/dart/test/mocks/mock_sentry_client.dart b/dart/test/mocks/mock_sentry_client.dart index c9d221b25f..c0e4ba9ffe 100644 --- a/dart/test/mocks/mock_sentry_client.dart +++ b/dart/test/mocks/mock_sentry_client.dart @@ -9,6 +9,8 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { List captureMessageCalls = []; List captureEnvelopeCalls = []; List captureTransactionCalls = []; + + // ignore: deprecated_member_use_from_same_package List userFeedbackCalls = []; List captureFeedbackCalls = []; List>> captureMetricsCalls = []; @@ -73,6 +75,7 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { } @override + // ignore: deprecated_member_use_from_same_package Future captureUserFeedback(SentryUserFeedback userFeedback) async { userFeedbackCalls.add(userFeedback); } diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 233f628930..f928d5413d 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -1655,12 +1655,14 @@ void main() { final client = fixture.getSut(eventProcessor: DropAllEventProcessor()); final id = SentryId.newId(); + // ignore: deprecated_member_use_from_same_package final feedback = SentryUserFeedback( eventId: id, comments: 'this is awesome', email: 'sentry@example.com', name: 'Rockstar Developer', ); + // ignore: deprecated_member_use_from_same_package await client.captureUserFeedback(feedback); expect(fixture.recorder.flushCalled, true); @@ -1676,12 +1678,14 @@ void main() { final client = fixture.getSut(eventProcessor: DropAllEventProcessor()); final id = SentryId.newId(); + // ignore: deprecated_member_use_from_same_package final feedback = SentryUserFeedback( eventId: id, comments: 'this is awesome', email: 'sentry@example.com', name: 'Rockstar Developer', ); + // ignore: deprecated_member_use_from_same_package await client.captureUserFeedback(feedback); final envelope = fixture.transport.envelopes.first; @@ -1844,10 +1848,12 @@ void main() { test('user feedback envelope contains dsn', () async { final client = fixture.getSut(); final event = SentryEvent(); + // ignore: deprecated_member_use_from_same_package final feedback = SentryUserFeedback( eventId: event.eventId, name: 'test', ); + // ignore: deprecated_member_use_from_same_package await client.captureUserFeedback(feedback); final capturedEnvelope = (fixture.transport).envelopes.first; diff --git a/dart/test/sentry_envelope_item_test.dart b/dart/test/sentry_envelope_item_test.dart index 3f1bad3ee0..9074ee712c 100644 --- a/dart/test/sentry_envelope_item_test.dart +++ b/dart/test/sentry_envelope_item_test.dart @@ -139,11 +139,13 @@ void main() { }); test('fromUserFeedback', () async { + // ignore: deprecated_member_use_from_same_package final userFeedback = SentryUserFeedback( eventId: SentryId.newId(), name: 'name', comments: 'comments', email: 'email'); + // ignore: deprecated_member_use_from_same_package final sut = SentryEnvelopeItem.fromUserFeedback(userFeedback); final expectedData = utf8.encode(jsonEncode( diff --git a/dart/test/sentry_envelope_test.dart b/dart/test/sentry_envelope_test.dart index a24cab20c7..303d3cab63 100644 --- a/dart/test/sentry_envelope_test.dart +++ b/dart/test/sentry_envelope_test.dart @@ -132,10 +132,12 @@ void main() { test('fromUserFeedback', () async { final eventId = SentryId.newId(); + // ignore: deprecated_member_use_from_same_package final userFeedback = SentryUserFeedback( eventId: eventId, name: 'name', email: 'email', comments: 'comments'); final sdkVersion = SdkVersion(name: 'fixture-name', version: 'fixture-version'); + // ignore: deprecated_member_use_from_same_package final sut = SentryEnvelope.fromUserFeedback( userFeedback, sdkVersion, @@ -143,6 +145,7 @@ void main() { ); final expectedEnvelopeItem = + // ignore: deprecated_member_use_from_same_package SentryEnvelopeItem.fromUserFeedback(userFeedback); expect(sut.header.eventId, eventId); diff --git a/dart/test/sentry_user_feedback_test.dart b/dart/test/sentry_user_feedback_test.dart index cb406f65b1..9c5c0c711d 100644 --- a/dart/test/sentry_user_feedback_test.dart +++ b/dart/test/sentry_user_feedback_test.dart @@ -7,9 +7,11 @@ import 'mocks.dart'; import 'mocks/mock_transport.dart'; void main() { + // ignore: deprecated_member_use_from_same_package group('$SentryUserFeedback', () { final id = SentryId.newId(); + // ignore: deprecated_member_use_from_same_package final feedback = SentryUserFeedback( eventId: id, comments: 'this is awesome', @@ -45,6 +47,7 @@ void main() { test('copyWith', () { final id = SentryId.newId(); + // ignore: deprecated_member_use_from_same_package final feedback = SentryUserFeedback( eventId: id, comments: 'this is awesome', @@ -68,17 +71,21 @@ void main() { test('disallow empty id', () { final id = SentryId.empty(); + // ignore: deprecated_member_use_from_same_package expect(() => SentryUserFeedback(eventId: id), throwsA(isA())); }); }); + // ignore: deprecated_member_use_from_same_package group('$SentryUserFeedback to envelops', () { test('to envelope', () { + // ignore: deprecated_member_use_from_same_package final feedback = SentryUserFeedback( eventId: SentryId.newId(), name: 'test', ); + // ignore: deprecated_member_use_from_same_package final envelope = SentryEnvelope.fromUserFeedback( feedback, SdkVersion(name: 'a', version: 'b'), @@ -95,9 +102,11 @@ void main() { }); }); + // ignore: deprecated_member_use_from_same_package test('sending $SentryUserFeedback', () async { final fixture = Fixture(); final sut = fixture.getSut(); + // ignore: deprecated_member_use_from_same_package await sut.captureUserFeedback(SentryUserFeedback( eventId: SentryId.newId(), name: 'test', @@ -106,18 +115,23 @@ void main() { expect(fixture.transport.envelopes.length, 1); }); + // ignore: deprecated_member_use_from_same_package test('cannot create $SentryUserFeedback with empty id', () async { expect( + // ignore: deprecated_member_use_from_same_package () => SentryUserFeedback(eventId: const SentryId.empty()), throwsA(isA()), ); }); + // ignore: deprecated_member_use_from_same_package test('do not send $SentryUserFeedback when disabled', () async { final fixture = Fixture(); final sut = fixture.getSut(); await sut.close(); + // ignore: deprecated_member_use_from_same_package await sut.captureUserFeedback( + // ignore: deprecated_member_use_from_same_package SentryUserFeedback( eventId: SentryId.newId(), name: 'test', @@ -127,10 +141,12 @@ void main() { expect(fixture.transport.envelopes.length, 0); }); + // ignore: deprecated_member_use_from_same_package test('do not send $SentryUserFeedback with empty id', () async { final fixture = Fixture(); final sut = fixture.getSut(); await sut.close(); + // ignore: deprecated_member_use_from_same_package await sut.captureUserFeedback( SentryUserFeedbackWithoutAssert( eventId: SentryId.empty(), @@ -147,7 +163,9 @@ void main() { final sut = Hub(options); await expectLater(() async { + // ignore: deprecated_member_use_from_same_package await sut.captureUserFeedback( + // ignore: deprecated_member_use_from_same_package SentryUserFeedback(eventId: SentryId.newId(), name: 'name'), ); }, returnsNormally); @@ -168,6 +186,7 @@ class Fixture { // 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. +// ignore: deprecated_member_use_from_same_package class SentryUserFeedbackWithoutAssert implements SentryUserFeedback { SentryUserFeedbackWithoutAssert({ required this.eventId, @@ -204,12 +223,14 @@ class SentryUserFeedbackWithoutAssert implements SentryUserFeedback { } @override + // ignore: deprecated_member_use_from_same_package SentryUserFeedback copyWith({ SentryId? eventId, String? name, String? email, String? comments, }) { + // ignore: deprecated_member_use_from_same_package return SentryUserFeedback( eventId: eventId ?? this.eventId, name: name ?? this.name, diff --git a/flutter/example/integration_test/integration_test.dart b/flutter/example/integration_test/integration_test.dart index 7a961083b5..d1ae5f876c 100644 --- a/flutter/example/integration_test/integration_test.dart +++ b/flutter/example/integration_test/integration_test.dart @@ -85,6 +85,7 @@ void main() { testWidgets('setup sentry and capture user feedback', (tester) async { await setupSentryAndApp(tester); + // ignore: deprecated_member_use_from_same_package final feedback = SentryUserFeedback( eventId: SentryId.newId(), name: 'fixture-name', diff --git a/flutter/example/lib/user_feedback_dialog.dart b/flutter/example/lib/user_feedback_dialog.dart index 495c9c7b7a..a219db6376 100644 --- a/flutter/example/lib/user_feedback_dialog.dart +++ b/flutter/example/lib/user_feedback_dialog.dart @@ -1,7 +1,6 @@ // ignore_for_file: library_private_types_in_public_api import 'package:flutter/material.dart'; - import 'package:sentry_flutter/sentry_flutter.dart'; class UserFeedbackDialog extends StatefulWidget { @@ -86,6 +85,7 @@ class _UserFeedbackDialogState extends State { ElevatedButton( key: const ValueKey('sentry_submit_feedback_button'), onPressed: () async { + // ignore: deprecated_member_use final feedback = SentryUserFeedback( eventId: widget.eventId, comments: commentController.text, @@ -108,7 +108,9 @@ class _UserFeedbackDialogState extends State { ); } + // ignore: deprecated_member_use Future _submitUserFeedback(SentryUserFeedback feedback) { + // ignore: deprecated_member_use return (widget.hub ?? HubAdapter()).captureUserFeedback(feedback); } } From 4bb3cebc08e8b41d156d28c696d035c3b08460fe Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 16:12:36 +0200 Subject: [PATCH 26/36] add to integration test, fix analyze errors --- dart/lib/src/sentry.dart | 2 +- dio/test/mocks/mock_hub.dart | 4 +++- .../integration_test/integration_test.dart | 17 +++++++++++++++++ isar/test/mocks/mocks.mocks.dart | 1 + sqflite/test/mocks/mocks.mocks.dart | 1 + 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index b3faf735f0..fc3a62cf8d 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -226,7 +226,7 @@ class Sentry { /// Reports [SentryFeedback] to Sentry.io. /// /// Use [withScope] to add [SentryAttachment] to the feedback. - static Future captureFeedback( + static Future captureFeedback( SentryFeedback feedback, { Hint? hint, ScopeCallback? withScope, diff --git a/dio/test/mocks/mock_hub.dart b/dio/test/mocks/mock_hub.dart index 377e1efb79..910e6bb199 100644 --- a/dio/test/mocks/mock_hub.dart +++ b/dio/test/mocks/mock_hub.dart @@ -1,5 +1,4 @@ import 'package:meta/meta.dart'; - import 'package:sentry/sentry.dart'; import 'no_such_method_provider.dart'; @@ -10,6 +9,8 @@ class MockHub with NoSuchMethodProvider implements Hub { List captureMessageCalls = []; List addBreadcrumbCalls = []; List bindClientCalls = []; + + // ignore: deprecated_member_use List userFeedbackCalls = []; List captureTransactionCalls = []; int closeCalls = 0; @@ -121,6 +122,7 @@ class MockHub with NoSuchMethodProvider implements Hub { } @override + // ignore: deprecated_member_use Future captureUserFeedback(SentryUserFeedback userFeedback) async { userFeedbackCalls.add(userFeedback); } diff --git a/flutter/example/integration_test/integration_test.dart b/flutter/example/integration_test/integration_test.dart index d1ae5f876c..a14b8f4eda 100644 --- a/flutter/example/integration_test/integration_test.dart +++ b/flutter/example/integration_test/integration_test.dart @@ -86,14 +86,31 @@ void main() { await setupSentryAndApp(tester); // ignore: deprecated_member_use_from_same_package + // ignore: deprecated_member_use final feedback = SentryUserFeedback( eventId: SentryId.newId(), name: 'fixture-name', email: 'fixture@email.com', comments: 'fixture-comments'); + // ignore: deprecated_member_use await Sentry.captureUserFeedback(feedback); }); + testWidgets('setup sentry and capture feedback', (tester) async { + await setupSentryAndApp(tester); + + // ignore: deprecated_member_use_from_same_package + // ignore: deprecated_member_use + final associatedEventId = await Sentry.captureMessage("Associated"); + final feedback = SentryFeedback( + message: 'message', + contactEmail: 'john.appleseed@apple.com', + name: 'John Appleseed', + associatedEventId: associatedEventId, + ); + await Sentry.captureFeedback(feedback); + }); + testWidgets('setup sentry and close', (tester) async { await setupSentryAndApp(tester); diff --git a/isar/test/mocks/mocks.mocks.dart b/isar/test/mocks/mocks.mocks.dart index 4f7adfed86..dc4cbd87a4 100644 --- a/isar/test/mocks/mocks.mocks.dart +++ b/isar/test/mocks/mocks.mocks.dart @@ -286,6 +286,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i3.Future<_i2.SentryId>); @override + // ignore: deprecated_member_use _i3.Future captureUserFeedback(_i2.SentryUserFeedback? userFeedback) => (super.noSuchMethod( Invocation.method( diff --git a/sqflite/test/mocks/mocks.mocks.dart b/sqflite/test/mocks/mocks.mocks.dart index 6c4c5e362e..11300b2dff 100644 --- a/sqflite/test/mocks/mocks.mocks.dart +++ b/sqflite/test/mocks/mocks.mocks.dart @@ -1453,6 +1453,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { )), ) as _i4.Future<_i2.SentryId>); @override + // ignore: deprecated_member_use _i4.Future captureUserFeedback(_i2.SentryUserFeedback? userFeedback) => (super.noSuchMethod( Invocation.method( From 5a18fdf94b309493d40009b5a63e30b995c50ffc Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 5 Sep 2024 10:24:53 +0200 Subject: [PATCH 27/36] fix cl --- CHANGELOG.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ba4f59ef..2a9c65e8e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,14 @@ ); ``` +- Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) + - Deprecated `Sentry.captureUserFeedback`, use `captureFeedback` instead. + - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. + - Deprecated `SentryClient.captureUserFeedback`, use `captureFeedback` instead. + - Deprecated `SentryUserFeedback`, use `SentryFeedback` instead. + - This will ignore the Routes and prevent the Route from being pushed to the Sentry server. + - Ignored routes will also create no TTID and TTFD spans. + ### Dependencies - Bump Cocoa SDK from v8.35.1 to v8.36.0 ([#2252](https://github.com/getsentry/sentry-dart/pull/2252)) @@ -56,17 +64,6 @@ ```dart SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), ``` -- Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) - - Deprecated `Sentry.captureUserFeedback`, use `captureFeedback` instead. - - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. - - Deprecated `SentryClient.captureUserFeedback`, use `captureFeedback` instead. - - Deprecated `SentryUserFeedback`, use `SentryFeedback` instead. - - This will ignore the Routes and prevent the Route from being pushed to the Sentry server. - - Ignored routes will also create no TTID and TTFD spans. - - ```dart - SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), - ``` ### Improvements From 995d2889ef7a9c8f244e5fedf8767026a9795735 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 23 Sep 2024 11:22:28 +0200 Subject: [PATCH 28/36] disable fixture.options.automatedTestMode --- dart/test/hub_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index 734a019987..12eaf8d227 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -577,6 +577,7 @@ void main() { test('captureFeedback should handle thrown error in scope callback', () async { + fixture.options.automatedTestMode = false; final hub = fixture.getSut(debug: true); final scopeCallbackException = Exception('error in scope callback'); From 3114a70e44e090feb2d43a8375f26f702c0be4b3 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 23 Sep 2024 11:32:43 +0200 Subject: [PATCH 29/36] update test --- dart/test/sentry_client_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 18baf98770..b4f2dd87a6 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -1268,6 +1268,7 @@ void main() { }); test('thrown error is handled', () async { + fixture.options.automatedTestMode = false; final exception = Exception("before send exception"); final beforeSendFeedbackCallback = (SentryEvent event, Hint hint) { throw exception; From 55c68701564d1f4b00861f2fd964aa66d982372f Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 23 Sep 2024 11:52:00 +0200 Subject: [PATCH 30/36] fix cl --- CHANGELOG.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b07ff35225..98720b8dda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +## Features + +- Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) + - Deprecated `Sentry.captureUserFeedback`, use `captureFeedback` instead. + - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. + - Deprecated `SentryClient.captureUserFeedback`, use `captureFeedback` instead. + - Deprecated `SentryUserFeedback`, use `SentryFeedback` instead. + ### Enhancements - Improve app start integration ([#2266](https://github.com/getsentry/sentry-dart/pull/2266)) @@ -38,14 +46,6 @@ appRunner: () => runApp(MyApp()), ); ``` - -- Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) - - Deprecated `Sentry.captureUserFeedback`, use `captureFeedback` instead. - - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. - - Deprecated `SentryClient.captureUserFeedback`, use `captureFeedback` instead. - - Deprecated `SentryUserFeedback`, use `SentryFeedback` instead. - - This will ignore the Routes and prevent the Route from being pushed to the Sentry server. - - Ignored routes will also create no TTID and TTFD spans. - Collect touch breadcrumbs for all buttons, not just those with `key` specified. ([#2242](https://github.com/getsentry/sentry-dart/pull/2242)) - Add `enableDartSymbolication` option to Sentry.init() for **Flutter iOS, macOS and Android** ([#2256](https://github.com/getsentry/sentry-dart/pull/2256)) - This flag enables symbolication of Dart stack traces when native debug images are not available. From f1a82814c22659c081774ba0f22366753956d4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Mon, 23 Sep 2024 13:38:18 +0200 Subject: [PATCH 31/36] Add `SentryFeedbackWidget` (#2240) --- CHANGELOG.md | 10 + flutter/example/lib/main.dart | 45 +- flutter/example/lib/user_feedback_dialog.dart | 464 ------------------ flutter/lib/sentry_flutter.dart | 1 + .../src/feedback/sentry_feedback_widget.dart | 252 ++++++++++ .../feedback/sentry_feedback_widget_test.dart | 205 ++++++++ flutter/test/mocks.mocks.dart | 56 +++ 7 files changed, 533 insertions(+), 500 deletions(-) delete mode 100644 flutter/example/lib/user_feedback_dialog.dart create mode 100644 flutter/lib/src/feedback/sentry_feedback_widget.dart create mode 100644 flutter/test/feedback/sentry_feedback_widget_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 98720b8dda..39377dee75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. - Deprecated `SentryClient.captureUserFeedback`, use `captureFeedback` instead. - Deprecated `SentryUserFeedback`, use `SentryFeedback` instead. +- Add `SentryFeedbackWidget` ([#2240](https://github.com/getsentry/sentry-dart/pull/2240)) +```dart +Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SentryFeedbackWidget(associatedEventId: id), + fullscreenDialog: true, + ), +); +``` ### Enhancements diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 5620c6de74..91c7c1094c 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -28,7 +28,6 @@ import 'auto_close_screen.dart'; import 'drift/connection/connection.dart'; import 'drift/database.dart'; import 'isar/user.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 = @@ -453,7 +452,7 @@ class MainScaffold extends StatelessWidget { Sentry.captureMessage( 'This message has an attachment', withScope: (scope) { - const txt = 'Lorem Ipsum dolar sit amet'; + const txt = 'Lorem Ipsum dolor sit amet'; scope.addAttachment( SentryAttachment.fromIntList( utf8.encode(txt), @@ -501,43 +500,17 @@ class MainScaffold extends StatelessWidget { onPressed: () async { final id = await Sentry.captureMessage('UserFeedback'); if (!context.mounted) return; - await showDialog( - context: context, - builder: (context) { - return UserFeedbackDialog(eventId: id); - }, - ); - }, - text: - 'Shows a custom user feedback dialog without an ongoing event that captures and sends user feedback data to Sentry.', - buttonTitle: 'Capture User Feedback', - ), - TooltipButton( - onPressed: () async { - await showDialog( - context: context, - builder: (context) { - return UserFeedbackDialog(eventId: SentryId.newId()); - }, - ); - }, - text: '', - buttonTitle: 'Show UserFeedback Dialog without event', - ), - TooltipButton( - onPressed: () async { - final associatedEventId = - await Sentry.captureMessage('Associated Event'); - await Sentry.captureFeedback( - SentryFeedback( - message: 'message', - contactEmail: 'john.appleseed@apple.com', - name: 'John Appleseed', - associatedEventId: associatedEventId, + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + SentryFeedbackWidget(associatedEventId: id), + fullscreenDialog: true, ), ); }, - text: '', + text: + 'Shows a custom feedback dialog without an ongoing event that captures and sends user feedback data to Sentry.', buttonTitle: 'Capture Feedback', ), TooltipButton( diff --git a/flutter/example/lib/user_feedback_dialog.dart b/flutter/example/lib/user_feedback_dialog.dart deleted file mode 100644 index ac7bd4308c..0000000000 --- a/flutter/example/lib/user_feedback_dialog.dart +++ /dev/null @@ -1,464 +0,0 @@ -// ignore_for_file: library_private_types_in_public_api - -import 'package:flutter/material.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - -class UserFeedbackDialog extends StatefulWidget { - const UserFeedbackDialog({ - super.key, - required this.eventId, - this.hub, - }) : assert(eventId != const SentryId.empty()); - - 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.titleLarge, - ), - const 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 - .titleMedium - ?.copyWith(color: Colors.grey), - ), - const Divider(height: 24), - TextField( - key: const ValueKey('sentry_name_textfield'), - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: 'Name', - ), - controller: nameController, - keyboardType: TextInputType.text, - ), - const SizedBox(height: 8), - TextField( - key: const ValueKey('sentry_email_textfield'), - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: 'E-Mail', - ), - controller: emailController, - keyboardType: TextInputType.emailAddress, - ), - const SizedBox(height: 8), - TextField( - key: const ValueKey('sentry_comment_textfield'), - minLines: 5, - maxLines: null, - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: 'What happened?', - ), - controller: commentController, - keyboardType: TextInputType.multiline, - ), - const SizedBox(height: 8), - const _PoweredBySentryMessage(), - ], - ), - ), - actions: [ - ElevatedButton( - key: const ValueKey('sentry_submit_feedback_button'), - onPressed: () async { - // ignore: deprecated_member_use - final feedback = SentryUserFeedback( - eventId: widget.eventId, - comments: commentController.text, - email: emailController.text, - name: nameController.text, - ); - await _submitUserFeedback(feedback); - // ignore: use_build_context_synchronously - Navigator.pop(context); - }, - child: const Text('Submit Crash Report')), - TextButton( - key: const ValueKey('sentry_close_button'), - onPressed: () { - Navigator.pop(context); - }, - child: const Text('Close'), - ) - ], - ); - } - - // ignore: deprecated_member_use - Future _submitUserFeedback(SentryUserFeedback feedback) { - // ignore: deprecated_member_use - return (widget.hub ?? HubAdapter()).captureUserFeedback(feedback); - } -} - -class _PoweredBySentryMessage extends StatelessWidget { - const _PoweredBySentryMessage(); - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Crash reports powered by'), - const 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 = const Color(0xff362d59); - } - - return FittedBox( - fit: BoxFit.contain, - child: CustomPaint( - size: const Size(222, 66), - painter: _SentryLogoCustomPainter(color), - ), - ); - } -} - -/// Created with https://fluttershapemaker.com/ -/// Sentry Logo comes from https://sentry.io/branding/ -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 paint0Fill = Paint()..style = PaintingStyle.fill; - paint0Fill.color = color; - canvas.drawPath(path_0, paint0Fill); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return true; - } -} diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index c74013e81e..d1ee9c080c 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -18,3 +18,4 @@ export 'src/user_interaction/sentry_user_interaction_widget.dart'; export 'src/binding_wrapper.dart'; export 'src/sentry_widget.dart'; export 'src/navigation/sentry_display_widget.dart'; +export 'src/feedback/sentry_feedback_widget.dart'; diff --git a/flutter/lib/src/feedback/sentry_feedback_widget.dart b/flutter/lib/src/feedback/sentry_feedback_widget.dart new file mode 100644 index 0000000000..3112bb8cee --- /dev/null +++ b/flutter/lib/src/feedback/sentry_feedback_widget.dart @@ -0,0 +1,252 @@ +// ignore_for_file: library_private_types_in_public_api + +import 'package:flutter/material.dart'; +import '../../sentry_flutter.dart'; + +class SentryFeedbackWidget extends StatefulWidget { + SentryFeedbackWidget({ + super.key, + this.associatedEventId, + Hub? hub, + this.title = 'Report a Bug', + this.nameLabel = 'Name', + this.namePlaceholder = 'Your Name', + this.emailLabel = 'Email', + this.emailPlaceholder = 'your.email@example.org', + this.messageLabel = 'Description', + this.messagePlaceholder = 'What\'s the bug? What did you expect?', + this.submitButtonLabel = 'Send Bug Report', + this.cancelButtonLabel = 'Cancel', + this.validationErrorLabel = 'Can\'t be empty', + this.isRequiredLabel = '(required)', + this.isNameRequired = false, + this.isEmailRequired = false, + }) : assert(associatedEventId != const SentryId.empty()), + _hub = hub ?? HubAdapter(); + + final SentryId? associatedEventId; + final Hub _hub; + + final String title; + + final String nameLabel; + final String namePlaceholder; + final String emailLabel; + final String emailPlaceholder; + final String messageLabel; + final String messagePlaceholder; + + final String submitButtonLabel; + final String cancelButtonLabel; + final String validationErrorLabel; + + final String isRequiredLabel; + + final bool isNameRequired; + final bool isEmailRequired; + + @override + _SentryFeedbackWidgetState createState() => _SentryFeedbackWidgetState(); +} + +class _SentryFeedbackWidgetState extends State { + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _messageController = TextEditingController(); + + final GlobalKey _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Text( + key: const ValueKey('sentry_feedback_name_label'), + widget.nameLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + const SizedBox(width: 4), + if (widget.isNameRequired) + Text( + key: const ValueKey( + 'sentry_feedback_name_required_label'), + widget.isRequiredLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + const SizedBox(height: 4), + TextFormField( + key: const ValueKey('sentry_feedback_name_textfield'), + style: Theme.of(context).textTheme.bodyLarge, + controller: _nameController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: widget.namePlaceholder, + ), + keyboardType: TextInputType.text, + validator: (String? value) { + return _errorText(value, widget.isNameRequired); + }, + autovalidateMode: AutovalidateMode.onUserInteraction, + ), + const SizedBox(height: 16), + Row( + children: [ + Text( + key: const ValueKey('sentry_feedback_email_label'), + widget.emailLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + const SizedBox(width: 4), + if (widget.isEmailRequired) + Text( + key: const ValueKey( + 'sentry_feedback_email_required_label'), + widget.isRequiredLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + const SizedBox(height: 4), + TextFormField( + key: const ValueKey('sentry_feedback_email_textfield'), + controller: _emailController, + style: Theme.of(context).textTheme.bodyLarge, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: widget.emailPlaceholder, + ), + keyboardType: TextInputType.emailAddress, + validator: (String? value) { + return _errorText(value, widget.isEmailRequired); + }, + autovalidateMode: AutovalidateMode.onUserInteraction, + ), + const SizedBox(height: 16), + Row( + children: [ + Text( + key: + const ValueKey('sentry_feedback_message_label'), + widget.messageLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + const SizedBox(width: 4), + Text( + key: const ValueKey( + 'sentry_feedback_message_required_label'), + widget.isRequiredLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + const SizedBox(height: 4), + TextFormField( + key: + const ValueKey('sentry_feedback_message_textfield'), + controller: _messageController, + style: Theme.of(context).textTheme.bodyLarge, + minLines: 5, + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: widget.messagePlaceholder, + ), + keyboardType: TextInputType.multiline, + validator: (String? value) { + return _errorText(value, true); + }, + autovalidateMode: AutovalidateMode.onUserInteraction, + ), + ], + ), + ), + ), + ), + const SizedBox(height: 8), + Column( + children: [ + SizedBox( + width: double.infinity, + child: ElevatedButton( + key: const ValueKey('sentry_feedback_submit_button'), + onPressed: () async { + if (!_formKey.currentState!.validate()) { + return; + } + final feedback = SentryFeedback( + message: _messageController.text, + contactEmail: _emailController.text, + name: _nameController.text, + associatedEventId: widget.associatedEventId, + ); + await _captureFeedback(feedback); + + bool mounted; + try { + mounted = (this as dynamic).mounted as bool; + } on NoSuchMethodError catch (_) { + mounted = false; + } + if (mounted) { + // ignore: use_build_context_synchronously + await Navigator.maybePop(context); + } + }, + child: Text(widget.submitButtonLabel), + ), + ), + SizedBox( + width: double.infinity, + child: TextButton( + key: const ValueKey('sentry_feedback_close_button'), + onPressed: () { + Navigator.pop(context); + }, + child: Text(widget.cancelButtonLabel), + ), + ), + ], + ), + ], + ), + ), + ); + } + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _messageController.dispose(); + super.dispose(); + } + + String? _errorText(String? value, bool isRequired) { + if (isRequired && (value == null || value.isEmpty)) { + return widget.validationErrorLabel; + } + return null; + } + + Future _captureFeedback(SentryFeedback feedback) { + return widget._hub.captureFeedback(feedback); + } +} diff --git a/flutter/test/feedback/sentry_feedback_widget_test.dart b/flutter/test/feedback/sentry_feedback_widget_test.dart new file mode 100644 index 0000000000..668b4e247e --- /dev/null +++ b/flutter/test/feedback/sentry_feedback_widget_test.dart @@ -0,0 +1,205 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../mocks.mocks.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$SentryFeedbackWidget validation', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + testWidgets('does not call hub on submit if not valid', (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget(hub: hub), + ); + + await tester.tap(find.text('Send Bug Report')); + await tester.pumpAndSettle(); + + verifyNever( + fixture.hub.captureFeedback( + captureAny, + hint: anyNamed('hint'), + withScope: anyNamed('withScope'), + ), + ); + }); + + testWidgets('shows error on submit if message not valid', (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget(hub: hub), + ); + + await tester.tap(find.text('Send Bug Report')); + await tester.pumpAndSettle(); + + expect(find.text('Can\'t be empty'), findsOne); + expect(find.text('(required)'), findsOne); + }); + + testWidgets('shows error on submit if name not valid', (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget( + hub: hub, + isNameRequired: true, + ), + ); + + await tester.tap(find.text('Send Bug Report')); + await tester.pumpAndSettle(); + + expect(find.text('Can\'t be empty'), findsExactly(2)); + expect(find.text('(required)'), findsExactly(2)); + }); + + testWidgets('shows error on submit if email not valid', (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget( + hub: hub, + isEmailRequired: true, + ), + ); + + await tester.tap(find.text('Send Bug Report')); + await tester.pumpAndSettle(); + + expect(find.text('Can\'t be empty'), findsExactly(2)); + expect(find.text('(required)'), findsExactly(2)); + }); + + testWidgets('shows error on submit if name and email not valid', + (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget( + hub: hub, + isNameRequired: true, + isEmailRequired: true, + ), + ); + + await tester.tap(find.text('Send Bug Report')); + await tester.pumpAndSettle(); + + expect(find.text('Can\'t be empty'), findsExactly(3)); + expect(find.text('(required)'), findsExactly(3)); + }); + }); + + group('$SentryFeedbackWidget submit', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + testWidgets('does call hub captureFeedback on submit', (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget( + hub: hub, + associatedEventId: + SentryId.fromId('1988bb1b6f0d4c509e232f0cb9aaeaea'), + ), + ); + + when(fixture.hub.captureFeedback( + any, + hint: anyNamed('hint'), + withScope: anyNamed('withScope'), + )).thenAnswer( + (_) async => SentryId.fromId('1988bb1b6f0d4c509e232f0cb9aaeaea')); + + await tester.enterText( + find.byKey(ValueKey('sentry_feedback_name_textfield')), + "fixture-name"); + await tester.enterText( + find.byKey(ValueKey('sentry_feedback_email_textfield')), + "fixture-email"); + await tester.enterText( + find.byKey(ValueKey('sentry_feedback_message_textfield')), + "fixture-message"); + await tester.tap(find.text('Send Bug Report')); + await tester.pumpAndSettle(); + + verify(fixture.hub.captureFeedback( + argThat(predicate((feedback) => + feedback.name == 'fixture-name' && + feedback.contactEmail == 'fixture-email' && + feedback.message == 'fixture-message' && + feedback.associatedEventId == + SentryId.fromId('1988bb1b6f0d4c509e232f0cb9aaeaea'))), + hint: anyNamed('hint'), + withScope: anyNamed('withScope'), + )).called(1); + }); + }); + + group('$SentryFeedbackWidget localization', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + testWidgets('sets labels and hints from parameters', (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget( + hub: hub, + title: 'fixture-title', + nameLabel: 'fixture-nameLabel', + namePlaceholder: 'fixture-namePlaceholder', + emailLabel: 'fixture-emailLabel', + emailPlaceholder: 'fixture-emailPlaceholder', + messageLabel: 'fixture-messageLabel', + messagePlaceholder: 'fixture-messagePlaceholder', + submitButtonLabel: 'fixture-submitButtonLabel', + cancelButtonLabel: 'fixture-cancelButtonLabel', + isRequiredLabel: 'fixture-isRequiredLabel', + validationErrorLabel: 'fixture-validationErrorLabel', + ), + ); + + expect(find.text('fixture-title'), findsOne); + expect(find.text('fixture-nameLabel'), findsOne); + expect(find.text('fixture-namePlaceholder'), findsOne); + expect(find.text('fixture-emailLabel'), findsOne); + expect(find.text('fixture-emailPlaceholder'), findsOne); + expect(find.text('fixture-messageLabel'), findsOne); + expect(find.text('fixture-messagePlaceholder'), findsOne); + expect(find.text('fixture-submitButtonLabel'), findsOne); + expect(find.text('fixture-cancelButtonLabel'), findsOne); + expect(find.text('fixture-isRequiredLabel'), findsOne); + + await tester.tap(find.text('fixture-submitButtonLabel')); + await tester.pumpAndSettle(); + + expect(find.text('fixture-validationErrorLabel'), findsOne); + }); + }); +} + +class Fixture { + var hub = MockHub(); + + Future pumpFeedbackWidget( + WidgetTester tester, Widget Function(Hub) builder) async { + await tester.pumpWidget( + MaterialApp( + home: builder(hub), + ), + ); + } +} diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index 3f94ca0274..f2cf885b18 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -1172,6 +1172,34 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { returnValueForMissingStub: _i7.Future.value(), ) as _i7.Future); + @override + _i7.Future<_i2.SentryId> captureFeedback( + _i2.SentryFeedback? feedback, { + _i2.Scope? scope, + _i2.Hint? hint, + }) => + (super.noSuchMethod( + Invocation.method( + #captureFeedback, + [feedback], + { + #scope: scope, + #hint: hint, + }, + ), + returnValue: _i7.Future<_i2.SentryId>.value(_FakeSentryId_5( + this, + Invocation.method( + #captureFeedback, + [feedback], + { + #scope: scope, + #hint: hint, + }, + ), + )), + ) as _i7.Future<_i2.SentryId>); + @override _i7.Future<_i2.SentryId> captureMetrics( Map>? metricsBuckets) => @@ -1759,6 +1787,34 @@ class MockHub extends _i1.Mock implements _i2.Hub { returnValueForMissingStub: _i7.Future.value(), ) as _i7.Future); + @override + _i7.Future<_i2.SentryId> captureFeedback( + _i2.SentryFeedback? feedback, { + _i2.Hint? hint, + _i2.ScopeCallback? withScope, + }) => + (super.noSuchMethod( + Invocation.method( + #captureFeedback, + [feedback], + { + #hint: hint, + #withScope: withScope, + }, + ), + returnValue: _i7.Future<_i2.SentryId>.value(_FakeSentryId_5( + this, + Invocation.method( + #captureFeedback, + [feedback], + { + #hint: hint, + #withScope: withScope, + }, + ), + )), + ) as _i7.Future<_i2.SentryId>); + @override _i7.Future addBreadcrumb( _i2.Breadcrumb? crumb, { From eb98dd2fd3f93eedf3f93741502c4fde4415593c Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 1 Oct 2024 11:43:17 +0200 Subject: [PATCH 32/36] fix cl --- CHANGELOG.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60baa39dda..fddaa0e31c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,10 @@ ## Unreleased -## Features +### Features +- Emit `transaction.data` inside `contexts.trace.data` ([#2284](https://github.com/getsentry/sentry-dart/pull/2284)) +- Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291) - Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) - Deprecated `Sentry.captureUserFeedback`, use `captureFeedback` instead. - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. @@ -19,10 +21,6 @@ Navigator.push( ), ); ``` -### Features - -- Emit `transaction.data` inside `contexts.trace.data` ([#2284](https://github.com/getsentry/sentry-dart/pull/2284)) -- Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291) ### Enhancements From 02b7b75d6e50c8af41328e26a4e02b3a4458343f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 10 Oct 2024 16:53:22 +0200 Subject: [PATCH 33/36] Update CHANGELOG.md --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 586735730e..1f77e4e710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,14 @@ ### Features - Emit `transaction.data` inside `contexts.trace.data` ([#2284](https://github.com/getsentry/sentry-dart/pull/2284)) -- Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291) +- Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291)) +- Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291)) +- Windows native error & obfuscation support ([#2286](https://github.com/getsentry/sentry-dart/pull/2286)) - Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) - Deprecated `Sentry.captureUserFeedback`, use `captureFeedback` instead. - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. - Deprecated `SentryClient.captureUserFeedback`, use `captureFeedback` instead. - Deprecated `SentryUserFeedback`, use `SentryFeedback` instead. -- Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291)) -- Windows native error & obfuscation support ([#2286](https://github.com/getsentry/sentry-dart/pull/2286)) - Add `SentryFeedbackWidget` ([#2240](https://github.com/getsentry/sentry-dart/pull/2240)) ```dart Navigator.push( From f187ee909ad107dccc0e743e47d0cfc7f14ae907 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 10 Oct 2024 16:53:39 +0200 Subject: [PATCH 34/36] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f77e4e710..1d60df9d7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ - Emit `transaction.data` inside `contexts.trace.data` ([#2284](https://github.com/getsentry/sentry-dart/pull/2284)) - Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291)) -- Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291)) - Windows native error & obfuscation support ([#2286](https://github.com/getsentry/sentry-dart/pull/2286)) - Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) - Deprecated `Sentry.captureUserFeedback`, use `captureFeedback` instead. From b0b273cbe6ee592910b9cd47a949f16294e12932 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 10 Oct 2024 17:05:38 +0200 Subject: [PATCH 35/36] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d60df9d7d..67046a39ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,7 +105,6 @@ Navigator.push( - Add `SentryFlutter.nativeCrash()` using MethodChannels for Android and iOS ([#2239](https://github.com/getsentry/sentry-dart/pull/2239)) - This can be used to test if native crash reporting works - - Add `ignoreRoutes` parameter to `SentryNavigatorObserver`. ([#2218](https://github.com/getsentry/sentry-dart/pull/2218)) - This will ignore the Routes and prevent the Route from being pushed to the Sentry server. - Ignored routes will also create no TTID and TTFD spans. From 48bc130e1154c8bc41df374a8a196e1232976355 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 10 Oct 2024 17:06:11 +0200 Subject: [PATCH 36/36] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67046a39ba..30d8755575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,7 +72,6 @@ Navigator.push( ``` - Support allowUrls and denyUrls for Flutter Web ([#2227](https://github.com/getsentry/sentry-dart/pull/2227)) - ```dart await SentryFlutter.init( (options) {