diff --git a/CHANGELOG.md b/CHANGELOG.md index a0677ec1..734419bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Improve & expose `SentryOptions` class ([#56](https://github.com/getsentry/sentry-godot/pull/56)) - Create or modify events using `SentryEvent` objects and new SDK methods: `SentrySDK.create_event()`, `SentrySDK.capture_event(event)` ([#51](https://github.com/getsentry/sentry-godot/pull/51)) +- Configure the SDK via GDScript and filter events using event hooks `before_send` and `on_crash`. The new `SentryConfiguration` class can be extended in a script and assigned in options to configure the SDK during initialization. However, due to the way scripting is initialized in the Godot Engine, this comes with a trade-off: a slightly later initialization. If a configuration script is assigned, initialization must be delayed until ScriptServer is ready to load and run the user script. ([#60](https://github.com/getsentry/sentry-godot/pull/60)) ### Dependencies diff --git a/project/example_configuration.gd b/project/example_configuration.gd new file mode 100644 index 00000000..80c58c73 --- /dev/null +++ b/project/example_configuration.gd @@ -0,0 +1,31 @@ +extends SentryConfiguration +## Example Sentry configuration script. +## +## Tip: You can assign configuration script in the project settings. + + +## Configure Sentry SDK options +func _configure(options: SentryOptions) -> void: + print("[example_configuration.gd] Configuring SDK options via GDScript") + + options.debug = true + options.release = "sentry-godot-demo@" + ProjectSettings.get_setting("application/config/version") + + # Set up event callbacks + options.before_send = _before_send + options.on_crash = _on_crash + + +## before_send callback example +func _before_send(ev: SentryEvent) -> SentryEvent: + print("[example_configuration.gd] Processing event: ", ev.id) + if ev.message == "junk": + print("[example_configuration.gd] Discarding event with message 'junk'") + return null + return ev + + +## on_crash callback example +func _on_crash(ev: SentryEvent) -> SentryEvent: + print("[example_configuration.gd] Crashing with event: ", ev.id) + return ev diff --git a/project/project.godot b/project/project.godot index 9552db1f..2b5b4e8c 100644 --- a/project/project.godot +++ b/project/project.godot @@ -38,3 +38,4 @@ ui/toolbar/run_overall=true config/dsn="https://3f1e095cf2e14598a0bd5b4ff324f712@o447951.ingest.us.sentry.io/6680910" config/debug=true +config/configuration_script="res://example_configuration.gd" diff --git a/project/test/test_event_integrity.gd b/project/test/test_event_integrity.gd new file mode 100644 index 00000000..2287fe12 --- /dev/null +++ b/project/test/test_event_integrity.gd @@ -0,0 +1,47 @@ +class_name TestEventIntegrity +extends GdUnitTestSuite + +signal callback_processed + +var created_id: String + + +func before_test() -> void: + SentrySDK._set_before_send(_before_send) + + +func after_test() -> void: + SentrySDK._unset_before_send() + + +@warning_ignore("unused_parameter") +func test_event_integrity(timeout := 10000) -> void: + var event := SentrySDK.create_event() + event.message = "integrity-check" + event.level = SentrySDK.LEVEL_DEBUG + event.logger = "custom-logger" + event.release = "custom-release" + event.dist = "custom-dist" + event.environment = "custom-environment" + created_id = event.id + + var captured_id := SentrySDK.capture_event(event) + assert_signal(self).is_emitted("callback_processed") + + assert_str(captured_id).is_not_empty() + assert_str(captured_id).is_not_equal(created_id) # event was discarded + assert_str(SentrySDK.get_last_event_id()).is_not_empty() + assert_str(captured_id).is_equal(SentrySDK.get_last_event_id()) + assert_str(created_id).is_not_equal(SentrySDK.get_last_event_id()) + + +func _before_send(event: SentryEvent) -> SentryEvent: + assert_str(event.message).is_equal("integrity-check") + assert_int(event.level).is_equal(SentrySDK.LEVEL_DEBUG) + assert_str(event.logger).is_equal("custom-logger") + assert_str(event.release).is_equal("custom-release") + assert_str(event.dist).is_equal("custom-dist") + assert_str(event.environment).is_equal("custom-environment") + assert_str(event.id).is_equal(created_id) + callback_processed.emit() + return null # discard event diff --git a/src/register_types.cpp b/src/register_types.cpp index 79e8180b..b4a71fd1 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -1,6 +1,8 @@ #include "runtime_config.h" #include "sentry/disabled_event.h" #include "sentry/native/native_event.h" +#include "sentry/util.h" +#include "sentry_configuration.h" #include "sentry_event.h" #include "sentry_logger.h" #include "sentry_options.h" @@ -18,6 +20,11 @@ namespace { void _init_logger() { if (!SentryOptions::get_singleton()->is_error_logger_enabled()) { // If error logger is disabled, don't add it to the scene tree. + sentry::util::print_debug("error logger is disabled in options"); + return; + } + if (!SentrySDK::get_singleton()->is_initialized()) { + sentry::util::print_debug("error logger is not started because SDK is not initialized"); return; } // Add experimental logger to scene tree. @@ -37,27 +44,24 @@ void initialize_module(ModuleInitializationLevel p_level) { } else if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { GDREGISTER_CLASS(SentryLoggerLimits); GDREGISTER_CLASS(SentryOptions); - SentryOptions::create_singleton(); - GDREGISTER_INTERNAL_CLASS(RuntimeConfig); + GDREGISTER_CLASS(SentryConfiguration); GDREGISTER_CLASS(SentryUser); GDREGISTER_CLASS(SentrySDK); GDREGISTER_ABSTRACT_CLASS(SentryEvent); GDREGISTER_INTERNAL_CLASS(NativeEvent); GDREGISTER_INTERNAL_CLASS(DisabledEvent); + GDREGISTER_INTERNAL_CLASS(SentryLogger); + + SentryOptions::create_singleton(); + SentrySDK *sentry_singleton = memnew(SentrySDK); Engine::get_singleton()->register_singleton("SentrySDK", SentrySDK::get_singleton()); - if (!Engine::get_singleton()->is_editor_hint() && sentry_singleton->is_enabled()) { - GDREGISTER_INTERNAL_CLASS(SentryLogger); + if (!Engine::get_singleton()->is_editor_hint()) { callable_mp_static(_init_logger).call_deferred(); } } - -#ifdef TOOLS_ENABLED - if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) { - } -#endif // TOOLS_ENABLED } void uninitialize_module(ModuleInitializationLevel p_level) { diff --git a/src/sentry/disabled_sdk.h b/src/sentry/disabled_sdk.h index 239e8ae3..ab00c7cf 100644 --- a/src/sentry/disabled_sdk.h +++ b/src/sentry/disabled_sdk.h @@ -28,6 +28,7 @@ class DisabledSDK : public InternalSDK { virtual String capture_event(const Ref &p_event) override { return ""; } virtual void initialize() override {} + virtual bool is_initialized() override { return false; } }; } // namespace sentry diff --git a/src/sentry/internal_sdk.h b/src/sentry/internal_sdk.h index 17e01226..2952a3fb 100644 --- a/src/sentry/internal_sdk.h +++ b/src/sentry/internal_sdk.h @@ -26,7 +26,6 @@ class InternalSDK { PackedStringArray post_context; }; -public: virtual void set_context(const String &p_key, const Dictionary &p_value) = 0; virtual void remove_context(const String &p_key) = 0; @@ -48,7 +47,9 @@ class InternalSDK { virtual Ref create_event() = 0; virtual String capture_event(const Ref &p_event) = 0; + virtual bool is_initialized() = 0; virtual void initialize() = 0; + virtual ~InternalSDK() = default; }; diff --git a/src/sentry/native/native_event.cpp b/src/sentry/native/native_event.cpp index 88ead5ea..8d025a17 100644 --- a/src/sentry/native/native_event.cpp +++ b/src/sentry/native/native_event.cpp @@ -111,13 +111,20 @@ String NativeEvent::get_environment() const { } NativeEvent::NativeEvent(sentry_value_t p_native_event) { - native_event = p_native_event; + if (sentry_value_refcount(p_native_event) > 0) { + sentry_value_incref(p_native_event); // acquire ownership + native_event = p_native_event; + } else { + // Shouldn't happen in healthy code. + native_event = sentry_value_new_event(); + ERR_PRINT("Sentry: Internal error: Event refcount is zero."); + } } -NativeEvent::NativeEvent() : - NativeEvent(sentry_value_new_event()) { +NativeEvent::NativeEvent() { + native_event = sentry_value_new_event(); } NativeEvent::~NativeEvent() { - sentry_value_decref(native_event); + sentry_value_decref(native_event); // release ownership } diff --git a/src/sentry/native/native_sdk.cpp b/src/sentry/native/native_sdk.cpp index a16568a1..b86bc154 100644 --- a/src/sentry/native/native_sdk.cpp +++ b/src/sentry/native/native_sdk.cpp @@ -6,6 +6,7 @@ #include "sentry/level.h" #include "sentry/native/native_event.h" #include "sentry/native/native_util.h" +#include "sentry/util.h" #include "sentry_options.h" #include @@ -53,12 +54,37 @@ inline void inject_contexts(sentry_value_t p_event) { } sentry_value_t handle_before_send(sentry_value_t event, void *hint, void *closure) { + sentry::util::print_debug("handling before_send"); inject_contexts(event); + if (const Callable &before_send = SentryOptions::get_singleton()->get_before_send(); before_send.is_valid()) { + Ref event_obj = memnew(NativeEvent(event)); + Ref processed = before_send.call(event_obj); + ERR_FAIL_COND_V_MSG(processed.is_valid() && processed != event_obj, event, "Sentry: before_send callback must return the same event object or null."); + if (processed.is_null()) { + // Discard event. + sentry::util::print_debug("event discarded by before_send callback: ", event_obj->get_id()); + sentry_value_decref(event); + return sentry_value_new_null(); + } + sentry::util::print_debug("event processed by before_send callback: ", event_obj->get_id()); + } return event; } -sentry_value_t handle_before_crash(const sentry_ucontext_t *uctx, sentry_value_t event, void *closure) { +sentry_value_t handle_on_crash(const sentry_ucontext_t *uctx, sentry_value_t event, void *closure) { inject_contexts(event); + if (const Callable &on_crash = SentryOptions::get_singleton()->get_on_crash(); on_crash.is_valid()) { + Ref event_obj = memnew(NativeEvent(event)); + Ref processed = on_crash.call(event_obj); + ERR_FAIL_COND_V_MSG(processed.is_valid() && processed != event_obj, event, "Sentry: on_crash callback must return the same event object or null."); + if (processed.is_null()) { + // Discard event. + sentry::util::print_debug("event discarded by on_crash callback: ", event_obj->get_id()); + sentry_value_decref(event); + return sentry_value_new_null(); + } + sentry::util::print_debug("event processed by on_crash callback: ", event_obj->get_id()); + } return event; } @@ -243,9 +269,10 @@ void NativeSDK::initialize() { // Hooks. sentry_options_set_before_send(options, handle_before_send, NULL); - sentry_options_set_on_crash(options, handle_before_crash, NULL); + sentry_options_set_on_crash(options, handle_on_crash, NULL); - sentry_init(options); + int err = sentry_init(options); + initialized = (err == 0); } NativeSDK::~NativeSDK() { diff --git a/src/sentry/native/native_sdk.h b/src/sentry/native/native_sdk.h index 28b5334a..2afcd3ce 100644 --- a/src/sentry/native/native_sdk.h +++ b/src/sentry/native/native_sdk.h @@ -11,6 +11,7 @@ namespace sentry { class NativeSDK : public InternalSDK { private: sentry_uuid_t last_uuid; + bool initialized = false; public: virtual void set_context(const String &p_key, const Dictionary &p_value) override; @@ -33,6 +34,7 @@ class NativeSDK : public InternalSDK { virtual String capture_event(const Ref &p_event) override; virtual void initialize() override; + virtual bool is_initialized() override { return initialized; } virtual ~NativeSDK() override; }; diff --git a/src/sentry_configuration.cpp b/src/sentry_configuration.cpp new file mode 100644 index 00000000..a8733281 --- /dev/null +++ b/src/sentry_configuration.cpp @@ -0,0 +1,20 @@ +#include "sentry_configuration.h" + +#include "sentry_sdk.h" + +void SentryConfiguration::_call_configure(const Ref &p_options) { + ERR_FAIL_COND(p_options.is_null()); + GDVIRTUAL_CALL(_configure, p_options); + ERR_FAIL_NULL(SentrySDK::get_singleton()); + SentrySDK::get_singleton()->notify_options_configured(); +} + +void SentryConfiguration::_notification(int p_what) { + if (p_what == NOTIFICATION_READY) { + _call_configure(SentryOptions::get_singleton()); + } +} + +void SentryConfiguration::_bind_methods() { + GDVIRTUAL_BIND(_configure, "options"); +} diff --git a/src/sentry_configuration.h b/src/sentry_configuration.h new file mode 100644 index 00000000..d9255ba2 --- /dev/null +++ b/src/sentry_configuration.h @@ -0,0 +1,26 @@ +#ifndef SENTRY_CONFIGURATION_H +#define SENTRY_CONFIGURATION_H + +#include "sentry_options.h" + +#include +#include + +using namespace godot; + +class SentryConfiguration : public Node { + GDCLASS(SentryConfiguration, Node); + +protected: + static void _bind_methods(); + void _notification(int p_what); + + GDVIRTUAL1(_configure, Ref); + + void _call_configure(const Ref &p_options); + +public: + SentryConfiguration() {} +}; + +#endif // SENTRY_CONFIGURATION_H diff --git a/src/sentry_options.cpp b/src/sentry_options.cpp index a6324804..5627328e 100644 --- a/src/sentry_options.cpp +++ b/src/sentry_options.cpp @@ -67,6 +67,8 @@ void SentryOptions::_define_project_settings(const Ref &p_options _define_setting(PropertyInfo(Variant::INT, "sentry/config/error_logger/limits/repeated_error_window_ms", PROPERTY_HINT_RANGE, "0,10000"), p_options->error_logger_limits->repeated_error_window_ms); _define_setting(PropertyInfo(Variant::INT, "sentry/config/error_logger/limits/throttle_events", PROPERTY_HINT_RANGE, "0,20"), p_options->error_logger_limits->throttle_events); _define_setting(PropertyInfo(Variant::INT, "sentry/config/error_logger/limits/throttle_window_ms", PROPERTY_HINT_RANGE, "0,10000"), p_options->error_logger_limits->throttle_window_ms); + + _define_setting(PropertyInfo(Variant::STRING, "sentry/config/configuration_script", PROPERTY_HINT_FILE, "*.gd"), String(p_options->configuration_script)); } void SentryOptions::_load_project_settings(const Ref &p_options) { @@ -100,6 +102,8 @@ void SentryOptions::_load_project_settings(const Ref &p_options) p_options->error_logger_limits->repeated_error_window_ms = ProjectSettings::get_singleton()->get_setting("sentry/config/error_logger/limits/repeated_error_window_ms", p_options->error_logger_limits->repeated_error_window_ms); p_options->error_logger_limits->throttle_events = ProjectSettings::get_singleton()->get_setting("sentry/config/error_logger/limits/throttle_events", p_options->error_logger_limits->throttle_events); p_options->error_logger_limits->throttle_window_ms = ProjectSettings::get_singleton()->get_setting("sentry/config/error_logger/limits/throttle_window_ms", p_options->error_logger_limits->throttle_window_ms); + + p_options->configuration_script = ProjectSettings::get_singleton()->get_setting("sentry/config/configuration_script", p_options->configuration_script); } void SentryOptions::create_singleton() { @@ -144,6 +148,9 @@ void SentryOptions::_bind_methods() { BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::OBJECT, "error_logger_limits", PROPERTY_HINT_TYPE_STRING, "SentryLoggerLimits", PROPERTY_USAGE_NONE), set_error_logger_limits, get_error_logger_limits); + BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::STRING, "before_send"), set_before_send, get_before_send); + BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::STRING, "on_crash"), set_on_crash, get_on_crash); + { using namespace sentry; BIND_BITFIELD_FLAG(MASK_NONE); diff --git a/src/sentry_options.h b/src/sentry_options.h index d93a79f2..cdfe03ee 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -56,6 +56,10 @@ class SentryOptions : public RefCounted { BitField error_logger_breadcrumb_mask = int(GodotErrorMask::MASK_ALL); Ref error_logger_limits; + String configuration_script; + Callable before_send; + Callable on_crash; + static void _define_project_settings(const Ref &p_options); static void _load_project_settings(const Ref &p_options); @@ -112,6 +116,14 @@ class SentryOptions : public RefCounted { _FORCE_INLINE_ Ref get_error_logger_limits() const { return error_logger_limits; } void set_error_logger_limits(const Ref &p_limits); + _FORCE_INLINE_ String get_configuration_script() const { return configuration_script; } + + _FORCE_INLINE_ Callable get_before_send() const { return before_send; } + _FORCE_INLINE_ void set_before_send(const Callable &p_before_send) { before_send = p_before_send; } + + _FORCE_INLINE_ Callable get_on_crash() const { return on_crash; } + _FORCE_INLINE_ void set_on_crash(const Callable &p_on_crash) { on_crash = p_on_crash; } + SentryOptions(); ~SentryOptions(); }; diff --git a/src/sentry_sdk.cpp b/src/sentry_sdk.cpp index a17cc6d2..25a3ae50 100644 --- a/src/sentry_sdk.cpp +++ b/src/sentry_sdk.cpp @@ -5,10 +5,11 @@ #include "sentry/disabled_sdk.h" #include "sentry/util.h" #include "sentry/uuid.h" -#include "sentry_options.h" +#include "sentry_configuration.h" #include #include +#include #include #ifdef NATIVE_SDK @@ -91,6 +92,48 @@ void SentrySDK::_init_contexts() { internal_sdk->set_context("environment", sentry::contexts::make_environment_context()); } +void SentrySDK::_initialize() { + sentry::util::print_debug("starting Sentry SDK version " + String(SENTRY_GODOT_SDK_VERSION)); + + if (enabled) { +#ifdef NATIVE_SDK + internal_sdk = std::make_shared(); +#else + // Unsupported platform + sentry::util::print_debug("This is an unsupported platform. Disabling Sentry SDK..."); + enabled = false; +#endif + } + + if (!enabled) { + sentry::util::print_debug("Sentry SDK is DISABLED! Operations with Sentry SDK will result in no-ops."); + internal_sdk = std::make_shared(); + return; + } + + internal_sdk->initialize(); + + // Initialize user. + set_user(runtime_config->get_user()); +} + +void SentrySDK::_check_if_configuration_succeeded() { + if (!configuration_succeeded) { + // Push error and initialize anyway. + ERR_PRINT("Sentry: Configuration via user script failed. Will try to initialize SDK anyway."); + sentry::util::print_error("initializing late because configuration via user script failed"); + _initialize(); + SentrySDK::_init_contexts(); + } +} + +void SentrySDK::notify_options_configured() { + sentry::util::print_debug("finished configuring options via user script"); + configuration_succeeded = true; + _initialize(); + SentrySDK::_init_contexts(); +} + void SentrySDK::_bind_methods() { BIND_ENUM_CONSTANT(LEVEL_DEBUG); BIND_ENUM_CONSTANT(LEVEL_INFO); @@ -109,6 +152,12 @@ 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); + + // Hidden API methods -- used in testing. + ClassDB::bind_method(D_METHOD("_set_before_send", "callable"), &SentrySDK::set_before_send); + ClassDB::bind_method(D_METHOD("_unset_before_send"), &SentrySDK::unset_before_send); + ClassDB::bind_method(D_METHOD("_set_on_crash", "callable"), &SentrySDK::set_on_crash); + ClassDB::bind_method(D_METHOD("_unset_on_crash"), &SentrySDK::unset_on_crash); } SentrySDK::SentrySDK() { @@ -117,8 +166,6 @@ SentrySDK::SentrySDK() { singleton = this; - sentry::util::print_debug("starting Sentry SDK version " + String(SENTRY_GODOT_SDK_VERSION)); - // Load the runtime configuration from the user's data directory. runtime_config.instantiate(); runtime_config->load_file(OS::get_singleton()->get_user_data_dir() + "/sentry.dat"); @@ -135,28 +182,28 @@ SentrySDK::SentrySDK() { } if (enabled) { -#ifdef NATIVE_SDK - internal_sdk = std::make_shared(); -#else - // Unsupported platform - sentry::util::print_debug("This is an unsupported platform. Disabling Sentry SDK..."); - enabled = false; -#endif - } - - if (!enabled) { - sentry::util::print_debug("Sentry SDK is DISABLED! Operations with Sentry SDK will result in no-ops."); - internal_sdk = std::make_shared(); - return; + if (SentryOptions::get_singleton()->get_configuration_script().is_empty() || Engine::get_singleton()->is_editor_hint()) { + _initialize(); + // Delay contexts initialization until the engine singletons are ready. + callable_mp(this, &SentrySDK::_init_contexts).call_deferred(); + } else { + // Register an autoload singleton, which is a user script extending the + // `SentryConfiguration` class. It will be instantiated and added to the + // scene tree by the engine shortly after ScriptServer is initialized. + // When this happens, the `SentryConfiguration` instance receives + // `NOTIFICATION_READY`, triggering our notification processing code in + // C++, which calls `_configure()` on the user script and then invokes + // `notify_options_configured()` in `SentrySDK`. This, in turn, initializes + // the internal SDK. + internal_sdk = std::make_shared(); // just in case + sentry::util::print_debug("waiting for user configuration autoload"); + ERR_FAIL_NULL(ProjectSettings::get_singleton()); + ProjectSettings::get_singleton()->set_setting("autoload/SentryConfigurationScript", + SentryOptions::get_singleton()->get_configuration_script()); + // Ensure issues with the configuration script are detected. + callable_mp(this, &SentrySDK::_check_if_configuration_succeeded).call_deferred(); + } } - - internal_sdk->initialize(); - - // Delay the contexts initialization until the engine singletons are ready. - callable_mp(this, &SentrySDK::_init_contexts).call_deferred(); - - // Initialize user. - set_user(runtime_config->get_user()); } SentrySDK::~SentrySDK() { diff --git a/src/sentry_sdk.h b/src/sentry_sdk.h index b63325a2..34845e9f 100644 --- a/src/sentry_sdk.h +++ b/src/sentry_sdk.h @@ -5,6 +5,7 @@ #include "sentry/internal_sdk.h" #include "sentry/level.h" #include "sentry_event.h" +#include "sentry_options.h" #include #include @@ -28,8 +29,11 @@ class SentrySDK : public Object { std::shared_ptr internal_sdk; Ref runtime_config; bool enabled = false; + bool configuration_succeeded = false; void _init_contexts(); + void _initialize(); + void _check_if_configuration_succeeded(); protected: static void _bind_methods(); @@ -39,9 +43,12 @@ class SentrySDK : public Object { _FORCE_INLINE_ std::shared_ptr get_internal_sdk() const { return internal_sdk; } + void notify_options_configured(); + // * Exported API bool is_enabled() const { return enabled; } + bool is_initialized() const { return internal_sdk->is_initialized(); } void add_breadcrumb(const String &p_message, const String &p_category, sentry::Level p_level, const String &p_type = "default", const Dictionary &p_data = Dictionary()); @@ -60,6 +67,14 @@ class SentrySDK : public Object { Ref create_event() const; String capture_event(const Ref &p_event); + // * Hidden API methods -- used in testing + + void set_before_send(const Callable &p_callable) { SentryOptions::get_singleton()->set_before_send(p_callable); } + void unset_before_send() { SentryOptions::get_singleton()->set_before_send(Callable()); } + + void set_on_crash(const Callable &p_callable) { SentryOptions::get_singleton()->set_on_crash(p_callable); } + void unset_on_crash() { SentryOptions::get_singleton()->set_on_crash(Callable()); } + SentrySDK(); ~SentrySDK(); };