diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6c16cb5..65f3d347 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Features
+
+- User feedback API ([#204](https://github.com/getsentry/sentry-godot/pull/204))
+
### Dependencies
- Bump gdUnit 4 from v5.0.0 to v5.0.4 ([#193](https://github.com/getsentry/sentry-godot/pull/193), [#197](https://github.com/getsentry/sentry-godot/pull/197))
diff --git a/doc_classes/SentryFeedback.xml b/doc_classes/SentryFeedback.xml
new file mode 100644
index 00000000..305f2c05
--- /dev/null
+++ b/doc_classes/SentryFeedback.xml
@@ -0,0 +1,28 @@
+
+
+
+ Represents user feedback in Sentry.
+
+
+
+
+
+
+
+ The identifier of an error event in the same project. [i]Optional[/i].
+ Use this to explicitly link a related error in the feedback UI.
+
+
+ The email of the user who submitted the feedback. [i]Optional[/i].
+ If excluded, Sentry attempts to fill this in with user context. Anonymous feedbacks (no name or email) are still accepted.
+
+
+ Comments of the user, describing what happened and/or sharing feedback. [i]Required[/i].
+ The max length is 4096 characters.
+
+
+ The name of the user who submitted the feedback. [i]Optional[/i].
+ If excluded, Sentry attempts to fill this in with user context. Anonymous feedbacks (no name or email) are still accepted.
+
+
+
diff --git a/doc_classes/SentrySDK.xml b/doc_classes/SentrySDK.xml
index de4196c7..18d1ef65 100644
--- a/doc_classes/SentrySDK.xml
+++ b/doc_classes/SentrySDK.xml
@@ -32,6 +32,13 @@
Captures [param event] and sends it to Sentry, returning the event ID. You can create an event with [method SentrySDK.create_event].
+
+
+
+
+ Captures user [param feedback] and sends it to Sentry. The feedback can optionally be associated with a specific error event if the [member SentryFeedback.associated_event_id] is set.
+
+
diff --git a/project/test/suites/test_feedback.gd b/project/test/suites/test_feedback.gd
new file mode 100644
index 00000000..42b53360
--- /dev/null
+++ b/project/test/suites/test_feedback.gd
@@ -0,0 +1,23 @@
+extends GdUnitTestSuite
+## Test Feedback class.
+@warning_ignore("unused_parameter")
+
+
+func test_feedback_properties() -> void:
+ var feedback := SentryFeedback.new()
+
+ assert_str(feedback.associated_event_id).is_empty()
+ feedback.associated_event_id = "082ce03eface41dd94b8c6b005382d5e"
+ assert_str(feedback.associated_event_id).is_equal("082ce03eface41dd94b8c6b005382d5e")
+
+ assert_str(feedback.name).is_empty()
+ feedback.name = "Bob"
+ assert_str(feedback.name).is_equal(feedback.name)
+
+ assert_str(feedback.contact_email).is_empty()
+ feedback.contact_email = "bob@example.com"
+ assert_str(feedback.contact_email).is_equal("bob@example.com")
+
+ assert_str(feedback.message).is_empty()
+ feedback.message = "something happened"
+ assert_str(feedback.message).is_equal("something happened")
diff --git a/src/register_types.cpp b/src/register_types.cpp
index 72f76c8c..d6c1b7f7 100644
--- a/src/register_types.cpp
+++ b/src/register_types.cpp
@@ -3,6 +3,7 @@
#include "sentry/util/print.h"
#include "sentry_configuration.h"
#include "sentry_event.h"
+#include "sentry_feedback.h"
#include "sentry_logger.h"
#include "sentry_options.h"
#include "sentry_sdk.h"
@@ -51,6 +52,7 @@ void initialize_module(ModuleInitializationLevel p_level) {
GDREGISTER_INTERNAL_CLASS(RuntimeConfig);
GDREGISTER_CLASS(SentryConfiguration);
GDREGISTER_CLASS(SentryUser);
+ GDREGISTER_CLASS(SentryFeedback);
GDREGISTER_CLASS(SentrySDK);
GDREGISTER_ABSTRACT_CLASS(SentryEvent);
GDREGISTER_INTERNAL_CLASS(DisabledEvent);
diff --git a/src/sentry/disabled_sdk.h b/src/sentry/disabled_sdk.h
index 57e725a2..48f24ca2 100644
--- a/src/sentry/disabled_sdk.h
+++ b/src/sentry/disabled_sdk.h
@@ -26,6 +26,8 @@ class DisabledSDK : public InternalSDK {
virtual Ref create_event() override { return memnew(DisabledEvent); }
virtual String capture_event(const Ref &p_event) override { return ""; }
+ virtual void capture_feedback(const Ref &p_feedback) override {}
+
virtual void initialize() override {}
};
diff --git a/src/sentry/internal_sdk.h b/src/sentry/internal_sdk.h
index 1afed376..2c62471c 100644
--- a/src/sentry/internal_sdk.h
+++ b/src/sentry/internal_sdk.h
@@ -3,6 +3,7 @@
#include "sentry/level.h"
#include "sentry_event.h"
+#include "sentry_feedback.h"
#include "sentry_user.h"
#include
@@ -34,6 +35,8 @@ class InternalSDK {
virtual Ref create_event() = 0;
virtual String capture_event(const Ref &p_event) = 0;
+ virtual void capture_feedback(const Ref &p_feedback) = 0;
+
virtual void initialize() = 0;
virtual ~InternalSDK() = default;
diff --git a/src/sentry/native/native_sdk.cpp b/src/sentry/native/native_sdk.cpp
index 2ab0ca86..c0b5f145 100644
--- a/src/sentry/native/native_sdk.cpp
+++ b/src/sentry/native/native_sdk.cpp
@@ -340,6 +340,26 @@ String NativeSDK::capture_event(const Ref &p_event) {
return _uuid_as_string(uuid);
}
+void NativeSDK::capture_feedback(const Ref &p_feedback) {
+ ERR_FAIL_COND(p_feedback.is_null());
+ ERR_FAIL_COND(p_feedback->get_message().is_empty());
+
+ sentry_uuid_t event_uuid;
+ if (p_feedback->get_associated_event_id().is_empty()) {
+ event_uuid = sentry_uuid_nil();
+ } else {
+ event_uuid = sentry_uuid_from_string(p_feedback->get_associated_event_id().ascii());
+ }
+
+ sentry_value_t uf = sentry_value_new_user_feedback(
+ &event_uuid,
+ p_feedback->get_name().utf8(),
+ p_feedback->get_contact_email().utf8(),
+ p_feedback->get_message().utf8());
+
+ sentry_capture_user_feedback(uf);
+}
+
void NativeSDK::initialize() {
ERR_FAIL_NULL(OS::get_singleton());
ERR_FAIL_NULL(ProjectSettings::get_singleton());
diff --git a/src/sentry/native/native_sdk.h b/src/sentry/native/native_sdk.h
index 65cc8cf0..1326f2e0 100644
--- a/src/sentry/native/native_sdk.h
+++ b/src/sentry/native/native_sdk.h
@@ -33,6 +33,8 @@ class NativeSDK : public InternalSDK {
virtual Ref create_event() override;
virtual String capture_event(const Ref &p_event) override;
+ virtual void capture_feedback(const Ref &p_feedback) override;
+
virtual void initialize() override;
virtual ~NativeSDK() override;
diff --git a/src/sentry_feedback.cpp b/src/sentry_feedback.cpp
new file mode 100644
index 00000000..6b36c462
--- /dev/null
+++ b/src/sentry_feedback.cpp
@@ -0,0 +1,10 @@
+#include "sentry_feedback.h"
+
+#include "sentry/simple_bind.h"
+
+void SentryFeedback::_bind_methods() {
+ BIND_PROPERTY(SentryFeedback, PropertyInfo(Variant::STRING, "name"), set_name, get_name);
+ BIND_PROPERTY(SentryFeedback, PropertyInfo(Variant::STRING, "contact_email"), set_contact_email, get_contact_email);
+ BIND_PROPERTY(SentryFeedback, PropertyInfo(Variant::STRING, "message"), set_message, get_message);
+ BIND_PROPERTY(SentryFeedback, PropertyInfo(Variant::STRING, "associated_event_id"), set_associated_event_id, get_associated_event_id);
+}
diff --git a/src/sentry_feedback.h b/src/sentry_feedback.h
new file mode 100644
index 00000000..8ab6bf3b
--- /dev/null
+++ b/src/sentry_feedback.h
@@ -0,0 +1,34 @@
+#ifndef SENTRY_FEEDBACK_H
+#define SENTRY_FEEDBACK_H
+
+#include
+
+using namespace godot;
+
+class SentryFeedback : public RefCounted {
+ GDCLASS(SentryFeedback, RefCounted);
+
+private:
+ String name;
+ String contact_email;
+ String message;
+ String associated_event_id;
+
+protected:
+ static void _bind_methods();
+
+public:
+ String get_name() const { return name; }
+ void set_name(const String &p_name) { name = p_name; }
+
+ String get_contact_email() const { return contact_email; }
+ void set_contact_email(const String &p_contact_email) { contact_email = p_contact_email; }
+
+ String get_message() const { return message; }
+ void set_message(const String &p_message) { message = p_message; }
+
+ String get_associated_event_id() const { return associated_event_id; }
+ void set_associated_event_id(const String &p_associated_event_id) { associated_event_id = p_associated_event_id; }
+};
+
+#endif // SENTRY_FEEDBACK_H
diff --git a/src/sentry_sdk.cpp b/src/sentry_sdk.cpp
index aaf9db0f..e0c8e751 100644
--- a/src/sentry_sdk.cpp
+++ b/src/sentry_sdk.cpp
@@ -73,6 +73,15 @@ String SentrySDK::capture_event(const Ref &p_event) {
return internal_sdk->capture_event(p_event);
}
+void SentrySDK::capture_feedback(const Ref &p_feedback) {
+ ERR_FAIL_COND_MSG(p_feedback.is_null(), "Sentry: Can't capture feedback - feedback object is null.");
+ ERR_FAIL_COND_MSG(p_feedback->get_message().is_empty(), "Sentry: Can't capture feedback - feedback message is empty.");
+ if (p_feedback->get_message().length() > 4096) {
+ WARN_PRINT("Sentry: Feedback message is too long (max 4096 characters).");
+ }
+ return internal_sdk->capture_feedback(p_feedback);
+}
+
void SentrySDK::set_tag(const String &p_key, const String &p_value) {
ERR_FAIL_COND_MSG(p_key.is_empty(), "Sentry: Can't set tag with an empty key.");
internal_sdk->set_tag(p_key, p_value);
@@ -194,6 +203,7 @@ void SentrySDK::_bind_methods() {
ClassDB::bind_method(D_METHOD("remove_user"), &SentrySDK::remove_user);
ClassDB::bind_method(D_METHOD("create_event"), &SentrySDK::create_event);
ClassDB::bind_method(D_METHOD("capture_event", "event"), &SentrySDK::capture_event);
+ ClassDB::bind_method(D_METHOD("capture_feedback", "feedback"), &SentrySDK::capture_feedback);
// Hidden API methods -- used in testing.
ClassDB::bind_method(D_METHOD("_set_before_send", "callable"), &SentrySDK::set_before_send);
diff --git a/src/sentry_sdk.h b/src/sentry_sdk.h
index 019dfe33..1b0ee1a7 100644
--- a/src/sentry_sdk.h
+++ b/src/sentry_sdk.h
@@ -69,6 +69,8 @@ class SentrySDK : public Object {
Ref create_event() const;
String capture_event(const Ref &p_event);
+ void capture_feedback(const Ref &p_feedback);
+
// * Hidden API methods -- used in testing
void set_before_send(const Callable &p_callable) { SentryOptions::get_singleton()->set_before_send(p_callable); }