Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Unreleased

* Feat: Add support for attachments (#505)
* Feat: Add support for User Feedback (#506)

# 6.0.0-beta.1

Expand Down
1 change: 1 addition & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export 'src/integration.dart';
export 'src/event_processor.dart';
export 'src/http_client/sentry_http_client.dart';
export 'src/sentry_attachment/sentry_attachment.dart';
export 'src/sentry_user_feedback.dart';
30 changes: 30 additions & 0 deletions dart/lib/src/hub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'protocol.dart';
import 'scope.dart';
import 'sentry_client.dart';
import 'sentry_options.dart';
import 'sentry_user_feedback.dart';

/// Configures the scope through the callback.
typedef ScopeCallback = void Function(Scope);
Expand Down Expand Up @@ -182,6 +183,35 @@ class Hub {
return sentryId;
}

Future<void> captureUserFeedback(SentryUserFeedback userFeedback) async {
if (!_isEnabled) {
_options.logger(
SentryLevel.warning,
"Instance is disabled and this 'captureUserFeedback' call is a no-op.",
);
return;
}
if (userFeedback.eventId == SentryId.empty()) {
_options.logger(
SentryLevel.warning,
'Captured UserFeedback with empty id, dropping the feedback',
);
return;
}
try {
final item = _peek();

await item.client.captureUserFeedback(userFeedback);
} catch (exception, stacktrace) {
_options.logger(
SentryLevel.error,
'Error while capturing user feedback for ${userFeedback.eventId}',
exception: exception,
stackTrace: stacktrace,
);
}
}

Scope _cloneAndRunWithScope(Scope scope, ScopeCallback? withScope) {
if (withScope != null) {
scope = scope.clone();
Expand Down
8 changes: 6 additions & 2 deletions dart/lib/src/hub_adapter.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import 'dart:async';

import 'hub.dart';
import 'protocol.dart';
import 'sentry.dart';
import 'sentry_client.dart';
import 'sentry_user_feedback.dart';

/// Hub adapter to make Integrations testable
class HubAdapter implements Hub {
HubAdapter._();
const HubAdapter._();

static final HubAdapter _instance = HubAdapter._();

Expand Down Expand Up @@ -82,4 +82,8 @@ class HubAdapter implements Hub {

@override
SentryId get lastEventId => Sentry.lastEventId;

@override
Future captureUserFeedback(SentryUserFeedback userFeedback) =>
Sentry.captureUserFeedback(userFeedback);
}
7 changes: 6 additions & 1 deletion dart/lib/src/noop_hub.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'dart:async';

import 'hub.dart';
import 'protocol.dart';
import 'sentry_client.dart';
import 'sentry_user_feedback.dart';

class NoOpHub implements Hub {
NoOpHub._();
Expand Down Expand Up @@ -62,4 +62,9 @@ class NoOpHub implements Hub {

@override
void addBreadcrumb(Breadcrumb crumb, {dynamic hint}) {}

@override
Future<SentryId> captureUserFeedback(SentryUserFeedback userFeedback) async {
return SentryId.empty();
}
}
5 changes: 5 additions & 0 deletions dart/lib/src/noop_sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'protocol.dart';
import 'scope.dart';
import 'sentry_client.dart';
import 'sentry_envelope.dart';
import 'sentry_user_feedback.dart';

class NoOpSentryClient implements SentryClient {
NoOpSentryClient._();
Expand Down Expand Up @@ -47,6 +48,10 @@ class NoOpSentryClient implements SentryClient {
Future<SentryId> captureEnvelope(SentryEnvelope envelope) =>
Future.value(SentryId.empty());

@override
Future<SentryId> captureUserFeedback(SentryUserFeedback userFeedback) =>
Future.value(SentryId.empty());

@override
Future<void> close() async {
return;
Expand Down
4 changes: 4 additions & 0 deletions dart/lib/src/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'protocol.dart';
import 'sentry_client.dart';
import 'sentry_options.dart';
import 'integration.dart';
import 'sentry_user_feedback.dart';

/// Configuration options callback
typedef OptionsConfiguration = FutureOr<void> Function(SentryOptions);
Expand Down Expand Up @@ -176,6 +177,9 @@ class Sentry {
withScope: withScope,
);

static Future captureUserFeedback(SentryUserFeedback userFeedback) =>
_hub.captureUserFeedback(userFeedback);

/// Close the client SDK
static Future<void> close() async {
final hub = _hub;
Expand Down
11 changes: 10 additions & 1 deletion dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:math';

import 'event_processor.dart';
import 'sentry_user_feedback.dart';
import 'transport/rate_limiter.dart';
import 'protocol.dart';
import 'scope.dart';
Expand Down Expand Up @@ -211,6 +211,15 @@ class SentryClient {
return _options.transport.send(envelope);
}

/// Reports the [userFeedback] to Sentry.io.
Future<void> captureUserFeedback(SentryUserFeedback userFeedback) {
final envelope = SentryEnvelope.fromUserFeedback(
userFeedback,
_options.sdk,
);
return _options.transport.send(envelope);
}

void close() => _options.httpClient.close();

Future<SentryEvent?> _processEvent(
Expand Down
11 changes: 11 additions & 0 deletions dart/lib/src/sentry_envelope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'sentry_envelope_header.dart';
import 'sentry_envelope_item.dart';
import 'protocol/sentry_event.dart';
import 'protocol/sdk_version.dart';
import 'sentry_user_feedback.dart';

/// Class representation of `Envelope` file.
class SentryEnvelope {
Expand Down Expand Up @@ -32,6 +33,16 @@ class SentryEnvelope {
);
}

factory SentryEnvelope.fromUserFeedback(
SentryUserFeedback feedback,
SdkVersion sdkVersion,
) {
return SentryEnvelope(
SentryEnvelopeHeader(feedback.eventId, sdkVersion),
[SentryEnvelopeItem.fromUserFeedback(feedback)],
);
}

/// Stream binary data representation of `Envelope` file encoded.
Stream<List<int>> envelopeStream() async* {
yield utf8.encode(jsonEncode(header.toJson()));
Expand Down
20 changes: 20 additions & 0 deletions dart/lib/src/sentry_envelope_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'sentry_attachment/sentry_attachment.dart';
import 'sentry_item_type.dart';
import 'protocol/sentry_event.dart';
import 'sentry_envelope_item_header.dart';
import 'sentry_user_feedback.dart';

/// Item holding header information and JSON encoded data.
class SentryEnvelopeItem {
Expand All @@ -26,6 +27,25 @@ class SentryEnvelopeItem {
return SentryEnvelopeItem(header, cachedItem.getData);
}

/// Create an [SentryEnvelopeItem] which sends [SentryUserFeedback].
factory SentryEnvelopeItem.fromUserFeedback(SentryUserFeedback feedback) {
final cachedItem = _CachedItem(() async {
final jsonEncoded = jsonEncode(feedback.toJson());
return utf8.encode(jsonEncoded);
});

final getLength = () async {
return (await cachedItem.getData()).length;
};

final header = SentryEnvelopeItemHeader(
SentryItemType.userFeedback,
getLength,
contentType: 'application/json',
);
return SentryEnvelopeItem(header, cachedItem.getData);
}

/// Create an [SentryEnvelopeItem] which holds the [SentryEvent] data.
factory SentryEnvelopeItem.fromEvent(SentryEvent event) {
final cachedItem = _CachedItem(() async {
Expand Down
54 changes: 54 additions & 0 deletions dart/lib/src/sentry_user_feedback.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'protocol.dart';

class SentryUserFeedback {
SentryUserFeedback({
required this.eventId,
this.name,
this.email,
this.comments,
}) : assert(eventId != SentryId.empty());

factory SentryUserFeedback.fromJson(Map<String, dynamic> json) {
return SentryUserFeedback(
eventId: SentryId.fromId(json['event_id']),
name: json['name'],
email: json['email'],
comments: json['comments'],
);
}

/// The eventId of the event to which the user feedback is associated.
final SentryId eventId;

/// Recommended: The name of the user.
final String? name;

/// Recommended: The name of the user.
final String? email;

/// Recommended: Comments of the user about what happened.
final String? comments;

Map<String, dynamic> toJson() {
return <String, dynamic>{
'event_id': eventId.toString(),
if (name != null) 'name': name,
if (email != null) 'email': email,
if (comments != null) 'comments': comments,
};
}

SentryUserFeedback copyWith({
SentryId? eventId,
String? name,
String? email,
String? comments,
}) {
return SentryUserFeedback(
eventId: eventId ?? this.eventId,
name: name ?? this.name,
email: email ?? this.email,
comments: comments ?? this.comments,
);
}
}
8 changes: 8 additions & 0 deletions dart/test/mocks/mock_hub.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import 'package:sentry/sentry.dart';
import 'package:sentry/src/sentry_user_feedback.dart';

class MockHub implements Hub {
List<CaptureEventCall> captureEventCalls = [];
List<CaptureExceptionCall> captureExceptionCalls = [];
List<CaptureMessageCall> captureMessageCalls = [];
List<AddBreadcrumbCall> addBreadcrumbCalls = [];
List<SentryClient?> bindClientCalls = [];
List<SentryUserFeedback> userFeedbackCalls = [];
int closeCalls = 0;
bool _isEnabled = true;

Expand Down Expand Up @@ -102,6 +104,12 @@ class MockHub implements Hub {
@override
// TODO: implement lastEventId
SentryId get lastEventId => throw UnimplementedError();

@override
Future<SentryId> captureUserFeedback(SentryUserFeedback userFeedback) async {
userFeedbackCalls.add(userFeedback);
return SentryId.empty();
}
}

class CaptureEventCall {
Expand Down
7 changes: 7 additions & 0 deletions dart/test/mocks/mock_sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class MockSentryClient implements SentryClient {
List<CaptureExceptionCall> captureExceptionCalls = [];
List<CaptureMessageCall> captureMessageCalls = [];
List<CaptureEnvelopeCall> captureEnvelopeCalls = [];
List<SentryUserFeedback> userFeedbackCalls = [];
int closeCalls = 0;

@override
Expand Down Expand Up @@ -66,6 +67,12 @@ class MockSentryClient implements SentryClient {
return SentryId.newId();
}

@override
Future<SentryId> captureUserFeedback(SentryUserFeedback userFeedback) async {
userFeedbackCalls.add(userFeedback);
return SentryId.newId();
}

@override
void close() {
closeCalls = closeCalls + 1;
Expand Down
7 changes: 7 additions & 0 deletions dart/test/mocks/mock_transport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@ class MockTransport implements Transport {
return envelope.header.eventId ?? SentryId.empty();
}
}

class ThrowingTransport implements Transport {
@override
Future<SentryId> send(SentryEnvelope envelope) async {
throw Exception('foo bar');
}
}
Loading