Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Event hooks & configuration via GDScript #60

Merged
merged 23 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
Expand Up @@ -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

Expand Down
31 changes: 31 additions & 0 deletions project/example_configuration.gd
Original file line number Diff line number Diff line change
@@ -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")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could extend the sample here and provide some more options. I.e. setting the release and/or the environment. To give an idea what's there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea, although, we can't change environment until #66 is merged. Will keep it in mind.
I've updated the example configuration with setting release attribute in 46c122e

# 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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the more important uses of this callback is to allow users filtering of events. I.e. return null for dropping events entirely. Based on the native implementation it looks like this should be supported. Can we update the sample with some filtering?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, although, this would apply to everything in the demo project, including our unit tests, so it should be something harmless.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the example with filtering in 9a49f11

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
1 change: 1 addition & 0 deletions project/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ ui/toolbar/run_overall=true

config/dsn="https://[email protected]/6680910"
config/debug=true
config/configuration_script="res://example_configuration.gd"
47 changes: 47 additions & 0 deletions project/test/test_event_integrity.gd
Original file line number Diff line number Diff line change
@@ -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
22 changes: 13 additions & 9 deletions src/register_types.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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.
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/sentry/disabled_sdk.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class DisabledSDK : public InternalSDK {
virtual String capture_event(const Ref<SentryEvent> &p_event) override { return ""; }

virtual void initialize() override {}
virtual bool is_initialized() override { return false; }
};

} // namespace sentry
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/internal_sdk.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -48,7 +47,9 @@ class InternalSDK {
virtual Ref<SentryEvent> create_event() = 0;
virtual String capture_event(const Ref<SentryEvent> &p_event) = 0;

virtual bool is_initialized() = 0;
virtual void initialize() = 0;

virtual ~InternalSDK() = default;
};

Expand Down
15 changes: 11 additions & 4 deletions src/sentry/native/native_event.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
Comment on lines +114 to +121

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this change coming from?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I adjusted how ownership is handled in the NativeEvent class, just so we don't have to preclude constructor call with incref every time.

}

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
}
33 changes: 30 additions & 3 deletions src/sentry/native/native_sdk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <godot_cpp/classes/file_access.hpp>
Expand Down Expand Up @@ -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<NativeEvent> event_obj = memnew(NativeEvent(event));
Ref<NativeEvent> 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<NativeEvent> event_obj = memnew(NativeEvent(event));
Ref<NativeEvent> 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;
}

Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/native/native_sdk.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,6 +34,7 @@ class NativeSDK : public InternalSDK {
virtual String capture_event(const Ref<SentryEvent> &p_event) override;

virtual void initialize() override;
virtual bool is_initialized() override { return initialized; }

virtual ~NativeSDK() override;
};
Expand Down
20 changes: 20 additions & 0 deletions src/sentry_configuration.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include "sentry_configuration.h"

#include "sentry_sdk.h"

void SentryConfiguration::_call_configure(const Ref<SentryOptions> &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");
}
26 changes: 26 additions & 0 deletions src/sentry_configuration.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#ifndef SENTRY_CONFIGURATION_H
#define SENTRY_CONFIGURATION_H

#include "sentry_options.h"

#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/core/gdvirtual.gen.inc>

using namespace godot;

class SentryConfiguration : public Node {
GDCLASS(SentryConfiguration, Node);

protected:
static void _bind_methods();
void _notification(int p_what);

GDVIRTUAL1(_configure, Ref<SentryOptions>);

void _call_configure(const Ref<SentryOptions> &p_options);

public:
SentryConfiguration() {}
};

#endif // SENTRY_CONFIGURATION_H
7 changes: 7 additions & 0 deletions src/sentry_options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ void SentryOptions::_define_project_settings(const Ref<SentryOptions> &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<SentryOptions> &p_options) {
Expand Down Expand Up @@ -100,6 +102,8 @@ void SentryOptions::_load_project_settings(const Ref<SentryOptions> &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() {
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions src/sentry_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ class SentryOptions : public RefCounted {
BitField<GodotErrorMask> error_logger_breadcrumb_mask = int(GodotErrorMask::MASK_ALL);
Ref<SentryLoggerLimits> error_logger_limits;

String configuration_script;
Callable before_send;
Callable on_crash;

static void _define_project_settings(const Ref<SentryOptions> &p_options);
static void _load_project_settings(const Ref<SentryOptions> &p_options);

Expand Down Expand Up @@ -112,6 +116,14 @@ class SentryOptions : public RefCounted {
_FORCE_INLINE_ Ref<SentryLoggerLimits> get_error_logger_limits() const { return error_logger_limits; }
void set_error_logger_limits(const Ref<SentryLoggerLimits> &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();
};
Expand Down
Loading