From 76d524c9521703a5dc59631f992d1313e8b7184f Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Mon, 10 Aug 2026 19:48:28 +0200 Subject: [PATCH 01/36] Initial API draft --- src/register_types.cpp | 2 ++ src/sentry/sentry_sdk.h | 9 +++++++- src/sentry/sentry_span.cpp | 13 +++++++++++ src/sentry/sentry_span.h | 47 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/sentry/sentry_span.cpp create mode 100644 src/sentry/sentry_span.h diff --git a/src/register_types.cpp b/src/register_types.cpp index b2272b8e8..1a8631025 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -21,6 +21,7 @@ #include "sentry/sentry_scope.h" #include "sentry/sentry_scope_observer.h" #include "sentry/sentry_sdk.h" +#include "sentry/sentry_span.h" #include "sentry/sentry_unit.h" #include "sentry/sentry_user.h" @@ -96,6 +97,7 @@ void register_runtime_classes() { GDREGISTER_ABSTRACT_CLASS(SentryLog); GDREGISTER_ABSTRACT_CLASS(SentryMetric); GDREGISTER_ABSTRACT_CLASS(SentryScope); + GDREGISTER_ABSTRACT_CLASS(SentrySpan); GDREGISTER_INTERNAL_CLASS(DisabledEvent); GDREGISTER_INTERNAL_CLASS(SentryEventProcessor); GDREGISTER_INTERNAL_CLASS(ScreenshotProcessor); diff --git a/src/sentry/sentry_sdk.h b/src/sentry/sentry_sdk.h index 8a3425135..c8848befd 100644 --- a/src/sentry/sentry_sdk.h +++ b/src/sentry/sentry_sdk.h @@ -12,6 +12,7 @@ #include "sentry/sentry_metrics.h" #include "sentry/sentry_options.h" #include "sentry/sentry_scope.h" +#include "sentry/sentry_span.h" #include #include @@ -128,9 +129,15 @@ class SentrySDK : public Object { // * Scopes Ref get_current_scope() const; - Variant with_scope(const Callable &p_callable); + // * Spans + + Ref start_span(const String &p_name, const Ref &p_parent_span = SentrySpan::unassigned(), + const Dictionary &p_attributes = {}, bool p_active = true); + Variant with_span(const String &p_name, const Callable &p_callable); + Ref get_active_span() const; + // * Hidden API methods -- used in testing void set_before_send(const Callable &p_callable) { options->set_before_send(p_callable); } diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp new file mode 100644 index 000000000..dde029117 --- /dev/null +++ b/src/sentry/sentry_span.cpp @@ -0,0 +1,13 @@ +#include "sentry_span.h" + +namespace sentry { + +Ref SentrySpan::unassigned() { + static Ref sentinel; + if (sentinel.is_null()) { + sentinel.instantiate(); + } + return sentinel; +} + +} // namespace sentry diff --git a/src/sentry/sentry_span.h b/src/sentry/sentry_span.h new file mode 100644 index 000000000..a5359d28b --- /dev/null +++ b/src/sentry/sentry_span.h @@ -0,0 +1,47 @@ +#pragma once + +#include "sentry/util/thread_guard.h" + +#include + +using namespace godot; + +namespace sentry { + +class SentrySpan : public RefCounted { + GDCLASS(SentrySpan, RefCounted); + +public: + enum SpanStatus { + SPAN_UNSET = -1, + SPAN_OK = 0, + SPAN_ERROR = 1, + }; + +private: + // SentrySpanImpl *_impl; + + SENTRY_THREAD_OWNER; + +protected: + static void _bind_methods(); + +public: + // Returns a sentinel value that indicates an unassigned span. + static Ref unassigned(); + + void set_attribute(const String &p_key, const Variant &p_value); + void set_attributes(const Dictionary &p_attributes); + Variant get_attribute(const String &p_key) const; + Dictionary get_attributes() const; + + void set_status(SpanStatus p_status); + SpanStatus get_status() const; + + void set_name(const String &p_name); + String get_name() const; + + void end(); +}; + +} // namespace sentry From 85afcf4de8dbf96656939fd00888592e86d71ae2 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Mon, 10 Aug 2026 20:12:00 +0200 Subject: [PATCH 02/36] Define implementation interface and forwarding --- src/sentry/disabled/disabled_sdk.h | 2 + src/sentry/disabled/disabled_span.h | 31 +++++++++++ src/sentry/internal_sdk.h | 2 + src/sentry/sentry_span.cpp | 85 +++++++++++++++++++++++++++++ src/sentry/sentry_span.h | 23 ++++++-- src/sentry/sentry_span_impl.h | 32 +++++++++++ src/sentry/span_status.h | 13 +++++ 7 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 src/sentry/disabled/disabled_span.h create mode 100644 src/sentry/sentry_span_impl.h create mode 100644 src/sentry/span_status.h diff --git a/src/sentry/disabled/disabled_sdk.h b/src/sentry/disabled/disabled_sdk.h index c31c2b9d9..73960d89f 100644 --- a/src/sentry/disabled/disabled_sdk.h +++ b/src/sentry/disabled/disabled_sdk.h @@ -3,6 +3,7 @@ #include "disabled_breadcrumb.h" #include "disabled_event.h" #include "disabled_scope.h" +#include "disabled_span.h" #include "sentry/internal_sdk.h" namespace sentry { @@ -41,6 +42,7 @@ class DisabledSDK : public InternalSDK { virtual void remove_attribute(const String &p_name) override {} virtual SentryScopeImpl *create_scope() override { return memnew(DisabledScope); } + virtual SentrySpanImpl *create_span() override { return memnew(DisabledSpan); } // Nothing is captured, so nothing is lost by discarding scope writes. virtual bool supports_scopes() const override { return true; } diff --git a/src/sentry/disabled/disabled_span.h b/src/sentry/disabled/disabled_span.h new file mode 100644 index 000000000..8b2926973 --- /dev/null +++ b/src/sentry/disabled/disabled_span.h @@ -0,0 +1,31 @@ +#pragma once + +#include "sentry/sentry_span_impl.h" + +namespace sentry { + +// Span implementation that is used when the SDK is disabled. +// Nothing is sent, but values are kept so that getters don't contradict setters. +class DisabledSpan : public SentrySpanImpl { + SENTRY_CASTABLE(DisabledSpan, SentrySpanImpl); + +private: + Dictionary attributes; + SpanStatus status = SPAN_UNSET; + String name; + +public: + virtual void set_attribute(const String &p_key, const Variant &p_value) override { attributes[p_key] = p_value; } + virtual Variant get_attribute(const String &p_key) const override { return attributes.get(p_key, Variant()); } + virtual Dictionary get_attributes() const override { return attributes.duplicate(); } + + virtual void set_status(SpanStatus p_status) override { status = p_status; } + virtual SpanStatus get_status() const override { return status; } + + virtual void set_name(const String &p_name) override { name = p_name; } + virtual String get_name() const override { return name; } + + virtual void end() override {} +}; + +} //namespace sentry diff --git a/src/sentry/internal_sdk.h b/src/sentry/internal_sdk.h index d43212180..b1dc6a4ca 100644 --- a/src/sentry/internal_sdk.h +++ b/src/sentry/internal_sdk.h @@ -7,6 +7,7 @@ #include "sentry/sentry_event.h" #include "sentry/sentry_feedback.h" #include "sentry/sentry_scope.h" +#include "sentry/sentry_span.h" #include "sentry/sentry_user.h" #include @@ -52,6 +53,7 @@ class InternalSDK { virtual void remove_attribute(const String &p_name) = 0; virtual SentryScopeImpl *create_scope() = 0; + virtual SentrySpanImpl *create_span() = 0; // Whether local scopes are as capable as the rest of this backend. // False means the backend captures events but silently discards scope diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index dde029117..993d5dba2 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -1,5 +1,10 @@ #include "sentry_span.h" +#include "sentry_sdk.h" // Needed for VariantCaster + +#define WRONG_THREAD_MSG \ + "Sentry: Span methods must be called on the thread that created the span." + namespace sentry { Ref SentrySpan::unassigned() { @@ -10,4 +15,84 @@ Ref SentrySpan::unassigned() { return sentinel; } +void SentrySpan::set_attribute(const String &p_key, const Variant &p_value) { + ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); + ERR_FAIL_COND_MSG(p_key.is_empty(), "Sentry: Can't set attribute with an empty key."); + _impl->set_attribute(p_key, p_value); +} + +void SentrySpan::set_attributes(const Dictionary &p_attributes) { + ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); + const Array &keys = p_attributes.keys(); + for (int i = 0; i < keys.size(); i++) { + const Variant &key = keys[i]; + String name = key; + ERR_CONTINUE_MSG(name.is_empty(), "Sentry: Can't set attribute with an empty key."); + _impl->set_attribute(name, p_attributes[key]); + } +} + +Variant SentrySpan::get_attribute(const String &p_key) const { + ERR_SENTRY_THREAD_GUARD_V(Variant(), WRONG_THREAD_MSG); + ERR_FAIL_COND_V_MSG(p_key.is_empty(), Variant(), "Sentry: Can't get attribute with an empty key."); + return _impl->get_attribute(p_key); +} + +Dictionary SentrySpan::get_attributes() const { + ERR_SENTRY_THREAD_GUARD_V(Dictionary(), WRONG_THREAD_MSG); + return _impl->get_attributes(); +} + +void SentrySpan::set_status(SpanStatus p_status) { + ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); + _impl->set_status(p_status); +} + +SentrySpan::SpanStatus SentrySpan::get_status() const { + ERR_SENTRY_THREAD_GUARD_V(SPAN_UNSET, WRONG_THREAD_MSG); + return _impl->get_status(); +} + +void SentrySpan::set_name(const String &p_name) { + ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); + ERR_FAIL_COND_MSG(p_name.is_empty(), "Sentry: Can't set an empty span name."); + _impl->set_name(p_name); +} + +String SentrySpan::get_name() const { + ERR_SENTRY_THREAD_GUARD_V(String(), WRONG_THREAD_MSG); + return _impl->get_name(); +} + +void SentrySpan::end() { + ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); + _impl->end(); +} + +SentrySpan::SentrySpan() { + _impl = INTERNAL_SDK()->create_span(); +} + +SentrySpan::SentrySpan(SentrySpanImpl *p_impl) : + _impl(p_impl) { +} + +SentrySpan::~SentrySpan() { + memdelete(_impl); +} + +void SentrySpan::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_attribute", "key", "value"), &SentrySpan::set_attribute); + ClassDB::bind_method(D_METHOD("set_attributes", "attributes"), &SentrySpan::set_attributes); + ClassDB::bind_method(D_METHOD("get_attribute", "key"), &SentrySpan::get_attribute); + ClassDB::bind_method(D_METHOD("get_attributes"), &SentrySpan::get_attributes); + ClassDB::bind_method(D_METHOD("set_status", "status"), &SentrySpan::set_status); + ClassDB::bind_method(D_METHOD("get_status"), &SentrySpan::get_status); + ClassDB::bind_method(D_METHOD("set_name", "name"), &SentrySpan::set_name); + ClassDB::bind_method(D_METHOD("get_name"), &SentrySpan::get_name); + ClassDB::bind_method(D_METHOD("end"), &SentrySpan::end); +} + } // namespace sentry + +#undef WRONG_THREAD_MSG diff --git a/src/sentry/sentry_span.h b/src/sentry/sentry_span.h index a5359d28b..6c20e2a81 100644 --- a/src/sentry/sentry_span.h +++ b/src/sentry/sentry_span.h @@ -1,5 +1,7 @@ #pragma once +#include "sentry/sentry_span_impl.h" +#include "sentry/span_status.h" #include "sentry/util/thread_guard.h" #include @@ -8,18 +10,19 @@ using namespace godot; namespace sentry { +// Godot-exported representation of a Sentry span. +// Platform-specific behavior is provided by SentrySpanImpl subclasses. class SentrySpan : public RefCounted { GDCLASS(SentrySpan, RefCounted); public: - enum SpanStatus { - SPAN_UNSET = -1, - SPAN_OK = 0, - SPAN_ERROR = 1, - }; + // SentrySpan.SpanStatus is defined in sentry/span_status.h. + // Godot extensions can't expose global enums; they must belong to a class. + // This alias avoids circular dependencies with headers that use SpanStatus. + using SpanStatus = sentry::SpanStatus; private: - // SentrySpanImpl *_impl; + SentrySpanImpl *_impl; SENTRY_THREAD_OWNER; @@ -42,6 +45,14 @@ class SentrySpan : public RefCounted { String get_name() const; void end(); + + SentrySpanImpl *get_implementation() const { return _impl; } + + SentrySpan(); + SentrySpan(SentrySpanImpl *p_impl); + ~SentrySpan(); }; } // namespace sentry + +VARIANT_ENUM_CAST(sentry::SentrySpan::SpanStatus); diff --git a/src/sentry/sentry_span_impl.h b/src/sentry/sentry_span_impl.h new file mode 100644 index 000000000..ea72c59ef --- /dev/null +++ b/src/sentry/sentry_span_impl.h @@ -0,0 +1,32 @@ +#pragma once + +#include "sentry/castable.h" +#include "sentry/span_status.h" + +#include + +using namespace godot; + +namespace sentry { + +// Base class for Sentry span implementations; see Godot-facing SentrySpan. +// Kept as a pure C++ class instead of a Godot class to avoid ClassDB +// registration and reduce overhead. +// Lifetime governed by SentrySpan. +class SentrySpanImpl : public Castable { + SENTRY_CASTABLE(SentrySpanImpl, Castable); + +public: + virtual void set_attribute(const String &p_key, const Variant &p_value) = 0; + virtual Variant get_attribute(const String &p_key) const = 0; + virtual Dictionary get_attributes() const = 0; + virtual void set_status(SpanStatus p_status) = 0; + virtual SpanStatus get_status() const = 0; + virtual void set_name(const String &p_name) = 0; + virtual String get_name() const = 0; + virtual void end() = 0; + + virtual ~SentrySpanImpl() = default; +}; + +} //namespace sentry diff --git a/src/sentry/span_status.h b/src/sentry/span_status.h new file mode 100644 index 000000000..ff0a4e44b --- /dev/null +++ b/src/sentry/span_status.h @@ -0,0 +1,13 @@ +#pragma once + +namespace sentry { + +// Represents the outcome of a span. +// In the public API, it is exposed as SentrySpan.SpanStatus enum. +enum SpanStatus { + SPAN_UNSET = -1, + SPAN_OK = 0, + SPAN_ERROR = 1, +}; + +} // namespace sentry From 49f0d48430eb1a1968365458ca8335b0ba966433 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Mon, 10 Aug 2026 20:23:14 +0200 Subject: [PATCH 03/36] Add stubs for native implemention --- src/sentry/android/android_sdk.cpp | 5 +++ src/sentry/android/android_sdk.h | 1 + src/sentry/cocoa/cocoa_sdk.h | 1 + src/sentry/cocoa/cocoa_sdk.mm | 5 +++ src/sentry/javascript/javascript_sdk.cpp | 5 +++ src/sentry/javascript/javascript_sdk.h | 1 + src/sentry/native/native_sdk.cpp | 5 +++ src/sentry/native/native_sdk.h | 1 + src/sentry/native/native_span.cpp | 49 ++++++++++++++++++++++++ src/sentry/native/native_span.h | 28 ++++++++++++++ 10 files changed, 101 insertions(+) create mode 100644 src/sentry/native/native_span.cpp create mode 100644 src/sentry/native/native_span.h diff --git a/src/sentry/android/android_sdk.cpp b/src/sentry/android/android_sdk.cpp index b36450d4d..350dc713b 100644 --- a/src/sentry/android/android_sdk.cpp +++ b/src/sentry/android/android_sdk.cpp @@ -8,6 +8,7 @@ #include "android_string_names.h" #include "android_util.h" #include "sentry/common_defs.h" +#include "sentry/disabled/disabled_span.h" #include "sentry/logging/print.h" #include "sentry/processing/process_event.h" #include "sentry/processing/process_log.h" @@ -333,6 +334,10 @@ SentryScopeImpl *AndroidSDK::create_scope() { return memnew(AndroidScope(android_plugin, handle)); } +SentrySpanImpl *AndroidSDK::create_span() { + return memnew(DisabledSpan); +} + void AndroidSDK::set_trace(const String &p_trace_id, const String &p_parent_span_id) { ERR_FAIL_COND(p_trace_id.is_empty()); diff --git a/src/sentry/android/android_sdk.h b/src/sentry/android/android_sdk.h index 94179220e..2f45758b1 100644 --- a/src/sentry/android/android_sdk.h +++ b/src/sentry/android/android_sdk.h @@ -96,6 +96,7 @@ class AndroidSDK : public InternalSDK { virtual void remove_attribute(const String &p_name) override; virtual SentryScopeImpl *create_scope() override; + virtual SentrySpanImpl *create_span() override; virtual bool supports_scopes() const override { return true; } diff --git a/src/sentry/cocoa/cocoa_sdk.h b/src/sentry/cocoa/cocoa_sdk.h index 090cf7469..4053d9e83 100644 --- a/src/sentry/cocoa/cocoa_sdk.h +++ b/src/sentry/cocoa/cocoa_sdk.h @@ -47,6 +47,7 @@ class CocoaSDK : public InternalSDK { virtual void remove_attribute(const String &p_name) override; virtual SentryScopeImpl *create_scope() override; + virtual SentrySpanImpl *create_span() override; virtual void set_trace(const String &p_trace_id, const String &p_parent_span_id) override; diff --git a/src/sentry/cocoa/cocoa_sdk.mm b/src/sentry/cocoa/cocoa_sdk.mm index 620cd984c..e82a6f4e4 100644 --- a/src/sentry/cocoa/cocoa_sdk.mm +++ b/src/sentry/cocoa/cocoa_sdk.mm @@ -9,6 +9,7 @@ #include "gen/sdk_version.gen.h" #include "sentry/common_defs.h" #include "sentry/disabled/disabled_scope.h" +#include "sentry/disabled/disabled_span.h" #include "sentry/logging/print.h" #include "sentry/processing/process_event.h" #include "sentry/processing/process_log.h" @@ -345,6 +346,10 @@ void _add_default_attachments(SentryObjCScope *p_scope) { return memnew(DisabledScope); } +SentrySpanImpl *CocoaSDK::create_span() { + return memnew(DisabledSpan); +} + void CocoaSDK::set_trace(const String &p_trace_id, const String &p_parent_span_id) { ERR_FAIL_COND(p_trace_id.is_empty()); diff --git a/src/sentry/javascript/javascript_sdk.cpp b/src/sentry/javascript/javascript_sdk.cpp index aa0e5d7b1..dceb6c8ef 100644 --- a/src/sentry/javascript/javascript_sdk.cpp +++ b/src/sentry/javascript/javascript_sdk.cpp @@ -1,6 +1,7 @@ #include "javascript_sdk.h" #include "sentry/disabled/disabled_scope.h" +#include "sentry/disabled/disabled_span.h" #include "sentry/javascript/javascript_breadcrumb.h" #include "sentry/javascript/javascript_event.h" #include "sentry/javascript/javascript_interop.h" @@ -309,6 +310,10 @@ SentryScopeImpl *JavaScriptSDK::create_scope() { return memnew(JavaScriptScope(scope_obj)); } +SentrySpanImpl *JavaScriptSDK::create_span() { + return memnew(DisabledSpan); +} + void JavaScriptSDK::set_trace(const String &p_trace_id, const String &p_parent_span_id) { SENTRY_PRINT_ONCE(sentry::LEVEL_DEBUG, "Setting trace info not implemented on Web platform - skipped."); } diff --git a/src/sentry/javascript/javascript_sdk.h b/src/sentry/javascript/javascript_sdk.h index bf912fee4..e312135fa 100644 --- a/src/sentry/javascript/javascript_sdk.h +++ b/src/sentry/javascript/javascript_sdk.h @@ -44,6 +44,7 @@ class JavaScriptSDK : public InternalSDK { virtual void remove_attribute(const String &p_name) override; virtual SentryScopeImpl *create_scope() override; + virtual SentrySpanImpl *create_span() override; virtual bool supports_scopes() const override { return true; } diff --git a/src/sentry/native/native_sdk.cpp b/src/sentry/native/native_sdk.cpp index 010f0a151..53e611ffe 100644 --- a/src/sentry/native/native_sdk.cpp +++ b/src/sentry/native/native_sdk.cpp @@ -9,6 +9,7 @@ #include "sentry/native/native_log.h" #include "sentry/native/native_metric.h" #include "sentry/native/native_scope.h" +#include "sentry/native/native_span.h" #include "sentry/native/native_util.h" #include "sentry/native/platform_detection.h" #include "sentry/processing/process_event.h" @@ -375,6 +376,10 @@ SentryScopeImpl *NativeSDK::create_scope() { return memnew(NativeScope); } +SentrySpanImpl *NativeSDK::create_span() { + return memnew(NativeSpan); +} + void NativeSDK::set_trace(const String &p_trace_id, const String &p_parent_span_id) { ERR_FAIL_COND(p_trace_id.is_empty()); if (p_parent_span_id.is_empty()) { diff --git a/src/sentry/native/native_sdk.h b/src/sentry/native/native_sdk.h index 5e4a22d21..a24081539 100644 --- a/src/sentry/native/native_sdk.h +++ b/src/sentry/native/native_sdk.h @@ -48,6 +48,7 @@ class NativeSDK : public InternalSDK { virtual void remove_attribute(const String &p_name) override; virtual SentryScopeImpl *create_scope() override; + virtual SentrySpanImpl *create_span() override; virtual bool supports_scopes() const override { return true; } diff --git a/src/sentry/native/native_span.cpp b/src/sentry/native/native_span.cpp new file mode 100644 index 000000000..eefdd3fa8 --- /dev/null +++ b/src/sentry/native/native_span.cpp @@ -0,0 +1,49 @@ +#include "native_span.h" + +#include + +namespace sentry::native { + +void NativeSpan::set_attribute(const String &p_key, const Variant &p_value) { + WARN_PRINT_ONCE("Sentry: Not implemented"); +} + +Variant NativeSpan::get_attribute(const String &p_key) const { + WARN_PRINT_ONCE("Sentry: Not implemented"); + return Variant(); +} + +Dictionary NativeSpan::get_attributes() const { + WARN_PRINT_ONCE("Sentry: Not implemented"); + return Dictionary(); +} + +void NativeSpan::set_status(SpanStatus p_status) { + WARN_PRINT_ONCE("Sentry: Not implemented"); +} + +SpanStatus NativeSpan::get_status() const { + WARN_PRINT_ONCE("Sentry: Not implemented"); + return SPAN_UNSET; +} + +void NativeSpan::set_name(const String &p_name) { + WARN_PRINT_ONCE("Sentry: Not implemented"); +} + +String NativeSpan::get_name() const { + WARN_PRINT_ONCE("Sentry: Not implemented"); + return String(); +} + +void NativeSpan::end() { + WARN_PRINT_ONCE("Sentry: Not implemented"); +} + +NativeSpan::NativeSpan() { +} + +NativeSpan::~NativeSpan() { +} + +} //namespace sentry::native diff --git a/src/sentry/native/native_span.h b/src/sentry/native/native_span.h new file mode 100644 index 000000000..74e36168a --- /dev/null +++ b/src/sentry/native/native_span.h @@ -0,0 +1,28 @@ +#pragma once + +#include "sentry/sentry_span_impl.h" + +namespace sentry::native { + +// Stub: spans are not implemented on this platform yet. +class NativeSpan : public SentrySpanImpl { + SENTRY_CASTABLE(NativeSpan, SentrySpanImpl); + +public: + virtual void set_attribute(const String &p_key, const Variant &p_value) override; + virtual Variant get_attribute(const String &p_key) const override; + virtual Dictionary get_attributes() const override; + + virtual void set_status(SpanStatus p_status) override; + virtual SpanStatus get_status() const override; + + virtual void set_name(const String &p_name) override; + virtual String get_name() const override; + + virtual void end() override; + + NativeSpan(); + virtual ~NativeSpan() override; +}; + +} //namespace sentry::native From 0d6c6d08435a0d0db9c133427bfbf304ea76dcd4 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Mon, 10 Aug 2026 20:50:53 +0200 Subject: [PATCH 04/36] Stubs for future top-level API --- src/register_types.cpp | 2 +- src/sentry/sentry_sdk.cpp | 19 +++++++++++++++++++ src/sentry/sentry_span.cpp | 7 +++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/register_types.cpp b/src/register_types.cpp index 1a8631025..faf0be341 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -90,6 +90,7 @@ void register_runtime_classes() { GDREGISTER_CLASS(SentryBadCode); GDREGISTER_CLASS(SentryUnit); GDREGISTER_CLASS(SentryFeedback); + GDREGISTER_ABSTRACT_CLASS(SentrySpan); GDREGISTER_CLASS(SentrySDK); GDREGISTER_ABSTRACT_CLASS(SentryAttachment); GDREGISTER_ABSTRACT_CLASS(SentryEvent); @@ -97,7 +98,6 @@ void register_runtime_classes() { GDREGISTER_ABSTRACT_CLASS(SentryLog); GDREGISTER_ABSTRACT_CLASS(SentryMetric); GDREGISTER_ABSTRACT_CLASS(SentryScope); - GDREGISTER_ABSTRACT_CLASS(SentrySpan); GDREGISTER_INTERNAL_CLASS(DisabledEvent); GDREGISTER_INTERNAL_CLASS(SentryEventProcessor); GDREGISTER_INTERNAL_CLASS(ScreenshotProcessor); diff --git a/src/sentry/sentry_sdk.cpp b/src/sentry/sentry_sdk.cpp index 54bdde57b..256b18445 100644 --- a/src/sentry/sentry_sdk.cpp +++ b/src/sentry/sentry_sdk.cpp @@ -175,6 +175,21 @@ Variant SentrySDK::with_scope(const Callable &p_callable) { return result; } +Ref SentrySDK::start_span(const String &p_name, const Ref &p_parent_span, const Dictionary &p_attributes, bool p_active) { + WARN_PRINT_ONCE("Sentry: Not implemented"); + return Ref(); +} + +Variant SentrySDK::with_span(const String &p_name, const Callable &p_callable) { + WARN_PRINT_ONCE("Sentry: Not implemented"); + return Variant(); +} + +Ref SentrySDK::get_active_span() const { + WARN_PRINT_ONCE("Sentry: Not implemented"); + return Ref(); +} + void SentrySDK::init(const Callable &p_configuration_callback) { ERR_FAIL_COND_MSG(OS::get_singleton()->get_thread_caller_id() != OS::get_singleton()->get_main_thread_id(), "Sentry: init() must be called from the main thread."); @@ -606,6 +621,10 @@ void SentrySDK::_bind_methods() { ClassDB::bind_method(D_METHOD("get_current_scope"), &SentrySDK::get_current_scope); ClassDB::bind_method(D_METHOD("with_scope", "callable"), &SentrySDK::with_scope); + ClassDB::bind_method(D_METHOD("start_span", "name", "parent_span", "attributes", "active"), &SentrySDK::start_span, DEFVAL(SentrySpan::unassigned()), DEFVAL(Dictionary()), DEFVAL(true)); + ClassDB::bind_method(D_METHOD("with_span", "name", "callable"), &SentrySDK::with_span); + ClassDB::bind_method(D_METHOD("get_active_span"), &SentrySDK::get_active_span); + // 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); diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index 993d5dba2..aaa6cdc28 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -1,5 +1,6 @@ #include "sentry_span.h" +#include "sentry/disabled/disabled_span.h" #include "sentry_sdk.h" // Needed for VariantCaster #define WRONG_THREAD_MSG \ @@ -8,10 +9,8 @@ namespace sentry { Ref SentrySpan::unassigned() { - static Ref sentinel; - if (sentinel.is_null()) { - sentinel.instantiate(); - } + // FYI: Internal SDK is not initialized yet when this static is created. + static Ref sentinel = Ref(memnew(SentrySpan(memnew(DisabledSpan)))); return sentinel; } From 4c4ed2fe787b7a436782a712fe48bd36e45520dc Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Mon, 10 Aug 2026 22:06:16 +0200 Subject: [PATCH 05/36] Stub SentrySpan::start_child() --- src/sentry/disabled/disabled_span.h | 2 ++ src/sentry/native/native_span.cpp | 7 +++++++ src/sentry/native/native_span.h | 12 ++++++++++++ src/sentry/sentry_span.cpp | 5 +++++ src/sentry/sentry_span.h | 4 ++++ src/sentry/sentry_span_impl.h | 2 ++ 6 files changed, 32 insertions(+) diff --git a/src/sentry/disabled/disabled_span.h b/src/sentry/disabled/disabled_span.h index 8b2926973..79c53c0f8 100644 --- a/src/sentry/disabled/disabled_span.h +++ b/src/sentry/disabled/disabled_span.h @@ -26,6 +26,8 @@ class DisabledSpan : public SentrySpanImpl { virtual String get_name() const override { return name; } virtual void end() override {} + + virtual SentrySpanImpl *start_child(const String &p_name) override { return memnew(DisabledSpan); } }; } //namespace sentry diff --git a/src/sentry/native/native_span.cpp b/src/sentry/native/native_span.cpp index eefdd3fa8..2f94b25c5 100644 --- a/src/sentry/native/native_span.cpp +++ b/src/sentry/native/native_span.cpp @@ -1,5 +1,7 @@ #include "native_span.h" +#include "sentry/disabled/disabled_span.h" + #include namespace sentry::native { @@ -40,6 +42,11 @@ void NativeSpan::end() { WARN_PRINT_ONCE("Sentry: Not implemented"); } +SentrySpanImpl *NativeSpan::start_child(const String &p_name) { + WARN_PRINT_ONCE("Sentry: Not implemented"); + return memnew(DisabledSpan); +} + NativeSpan::NativeSpan() { } diff --git a/src/sentry/native/native_span.h b/src/sentry/native/native_span.h index 74e36168a..3e05a9d28 100644 --- a/src/sentry/native/native_span.h +++ b/src/sentry/native/native_span.h @@ -1,5 +1,6 @@ #pragma once +#include "sentry.h" #include "sentry/sentry_span_impl.h" namespace sentry::native { @@ -8,6 +9,15 @@ namespace sentry::native { class NativeSpan : public SentrySpanImpl { SENTRY_CASTABLE(NativeSpan, SentrySpanImpl); +private: + // Native emulates the span-first API, until the day span-first is actually supported. + // Root spans are backed by transactions, while child spans use native spans. + union { + sentry_transaction_t *transaction; + sentry_span_t *span; + } _data; + bool _is_transaction = true; + public: virtual void set_attribute(const String &p_key, const Variant &p_value) override; virtual Variant get_attribute(const String &p_key) const override; @@ -21,6 +31,8 @@ class NativeSpan : public SentrySpanImpl { virtual void end() override; + virtual SentrySpanImpl *start_child(const String &p_name) override; + NativeSpan(); virtual ~NativeSpan() override; }; diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index aaa6cdc28..28d7a13db 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -68,6 +68,11 @@ void SentrySpan::end() { _impl->end(); } +Ref SentrySpan::start_child(const String &p_name) { + ERR_FAIL_COND_V_MSG(p_name.is_empty(), Ref(), "Sentry: Can't start a child span with an empty name."); + return memnew(SentrySpan(_impl->start_child(p_name))); +} + SentrySpan::SentrySpan() { _impl = INTERNAL_SDK()->create_span(); } diff --git a/src/sentry/sentry_span.h b/src/sentry/sentry_span.h index 6c20e2a81..7f8bd16f3 100644 --- a/src/sentry/sentry_span.h +++ b/src/sentry/sentry_span.h @@ -46,6 +46,10 @@ class SentrySpan : public RefCounted { void end(); + // *** Not exposed in the public API + + Ref start_child(const String &p_name); + SentrySpanImpl *get_implementation() const { return _impl; } SentrySpan(); diff --git a/src/sentry/sentry_span_impl.h b/src/sentry/sentry_span_impl.h index ea72c59ef..0600f78a1 100644 --- a/src/sentry/sentry_span_impl.h +++ b/src/sentry/sentry_span_impl.h @@ -26,6 +26,8 @@ class SentrySpanImpl : public Castable { virtual String get_name() const = 0; virtual void end() = 0; + virtual SentrySpanImpl *start_child(const String &p_name) = 0; + virtual ~SentrySpanImpl() = default; }; From f85e5b43f3cb4fdf378fa91deb8a35ad4a62927b Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Mon, 10 Aug 2026 22:08:35 +0200 Subject: [PATCH 06/36] Expose span status enum constants to scripting --- src/sentry/sentry_span.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index 28d7a13db..a57d6f5be 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -95,6 +95,10 @@ void SentrySpan::_bind_methods() { ClassDB::bind_method(D_METHOD("set_name", "name"), &SentrySpan::set_name); ClassDB::bind_method(D_METHOD("get_name"), &SentrySpan::get_name); ClassDB::bind_method(D_METHOD("end"), &SentrySpan::end); + + BIND_ENUM_CONSTANT(SPAN_UNSET); + BIND_ENUM_CONSTANT(SPAN_OK); + BIND_ENUM_CONSTANT(SPAN_ERROR); } } // namespace sentry From 7d7faa50a178679b067a96fe38b7d0690bf9f56f Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Mon, 10 Aug 2026 22:14:05 +0200 Subject: [PATCH 07/36] Align enum to JS SDK and OpenTelemetry --- src/sentry/disabled/disabled_span.h | 2 +- src/sentry/native/native_span.cpp | 2 +- src/sentry/sentry_span.cpp | 8 ++++---- src/sentry/span_status.h | 7 ++++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/sentry/disabled/disabled_span.h b/src/sentry/disabled/disabled_span.h index 79c53c0f8..2ca76611f 100644 --- a/src/sentry/disabled/disabled_span.h +++ b/src/sentry/disabled/disabled_span.h @@ -11,7 +11,7 @@ class DisabledSpan : public SentrySpanImpl { private: Dictionary attributes; - SpanStatus status = SPAN_UNSET; + SpanStatus status = SPAN_STATUS_UNSET; String name; public: diff --git a/src/sentry/native/native_span.cpp b/src/sentry/native/native_span.cpp index 2f94b25c5..3dd42695a 100644 --- a/src/sentry/native/native_span.cpp +++ b/src/sentry/native/native_span.cpp @@ -26,7 +26,7 @@ void NativeSpan::set_status(SpanStatus p_status) { SpanStatus NativeSpan::get_status() const { WARN_PRINT_ONCE("Sentry: Not implemented"); - return SPAN_UNSET; + return SPAN_STATUS_UNSET; } void NativeSpan::set_name(const String &p_name) { diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index a57d6f5be..f37c51bc6 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -48,7 +48,7 @@ void SentrySpan::set_status(SpanStatus p_status) { } SentrySpan::SpanStatus SentrySpan::get_status() const { - ERR_SENTRY_THREAD_GUARD_V(SPAN_UNSET, WRONG_THREAD_MSG); + ERR_SENTRY_THREAD_GUARD_V(SPAN_STATUS_UNSET, WRONG_THREAD_MSG); return _impl->get_status(); } @@ -96,9 +96,9 @@ void SentrySpan::_bind_methods() { ClassDB::bind_method(D_METHOD("get_name"), &SentrySpan::get_name); ClassDB::bind_method(D_METHOD("end"), &SentrySpan::end); - BIND_ENUM_CONSTANT(SPAN_UNSET); - BIND_ENUM_CONSTANT(SPAN_OK); - BIND_ENUM_CONSTANT(SPAN_ERROR); + BIND_ENUM_CONSTANT(SPAN_STATUS_UNSET); + BIND_ENUM_CONSTANT(SPAN_STATUS_OK); + BIND_ENUM_CONSTANT(SPAN_STATUS_ERROR); } } // namespace sentry diff --git a/src/sentry/span_status.h b/src/sentry/span_status.h index ff0a4e44b..55341eacb 100644 --- a/src/sentry/span_status.h +++ b/src/sentry/span_status.h @@ -4,10 +4,11 @@ namespace sentry { // Represents the outcome of a span. // In the public API, it is exposed as SentrySpan.SpanStatus enum. +// Values match OpenTelemetry's SpanStatusCode, as sentry-javascript does. enum SpanStatus { - SPAN_UNSET = -1, - SPAN_OK = 0, - SPAN_ERROR = 1, + SPAN_STATUS_UNSET = 0, + SPAN_STATUS_OK = 1, + SPAN_STATUS_ERROR = 2, }; } // namespace sentry From 282140d1eaf421bd51e6742051d3af59f0e872cd Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Mon, 10 Aug 2026 22:38:10 +0200 Subject: [PATCH 08/36] Refine span creation APIs --- src/sentry/android/android_sdk.cpp | 2 +- src/sentry/android/android_sdk.h | 2 +- src/sentry/cocoa/cocoa_sdk.h | 2 +- src/sentry/cocoa/cocoa_sdk.mm | 2 +- src/sentry/disabled/disabled_sdk.h | 2 +- src/sentry/disabled/disabled_span.h | 2 +- src/sentry/internal_sdk.h | 2 +- src/sentry/javascript/javascript_sdk.cpp | 2 +- src/sentry/javascript/javascript_sdk.h | 2 +- src/sentry/native/native_sdk.cpp | 2 +- src/sentry/native/native_sdk.h | 2 +- src/sentry/native/native_span.cpp | 2 +- src/sentry/native/native_span.h | 2 +- src/sentry/sentry_span.cpp | 12 +++++++++--- src/sentry/sentry_span.h | 3 ++- src/sentry/sentry_span_impl.h | 2 +- 16 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/sentry/android/android_sdk.cpp b/src/sentry/android/android_sdk.cpp index 350dc713b..0ef16ec27 100644 --- a/src/sentry/android/android_sdk.cpp +++ b/src/sentry/android/android_sdk.cpp @@ -334,7 +334,7 @@ SentryScopeImpl *AndroidSDK::create_scope() { return memnew(AndroidScope(android_plugin, handle)); } -SentrySpanImpl *AndroidSDK::create_span() { +SentrySpanImpl *AndroidSDK::create_span(const String &p_name, const Dictionary &p_attributes) { return memnew(DisabledSpan); } diff --git a/src/sentry/android/android_sdk.h b/src/sentry/android/android_sdk.h index 2f45758b1..61fbf9c2a 100644 --- a/src/sentry/android/android_sdk.h +++ b/src/sentry/android/android_sdk.h @@ -96,7 +96,7 @@ class AndroidSDK : public InternalSDK { virtual void remove_attribute(const String &p_name) override; virtual SentryScopeImpl *create_scope() override; - virtual SentrySpanImpl *create_span() override; + virtual SentrySpanImpl *create_span(const String &p_name, const Dictionary &p_attributes) override; virtual bool supports_scopes() const override { return true; } diff --git a/src/sentry/cocoa/cocoa_sdk.h b/src/sentry/cocoa/cocoa_sdk.h index 4053d9e83..8a7c5bc02 100644 --- a/src/sentry/cocoa/cocoa_sdk.h +++ b/src/sentry/cocoa/cocoa_sdk.h @@ -47,7 +47,7 @@ class CocoaSDK : public InternalSDK { virtual void remove_attribute(const String &p_name) override; virtual SentryScopeImpl *create_scope() override; - virtual SentrySpanImpl *create_span() override; + virtual SentrySpanImpl *create_span(const String &p_name, const Dictionary &p_attributes) override; virtual void set_trace(const String &p_trace_id, const String &p_parent_span_id) override; diff --git a/src/sentry/cocoa/cocoa_sdk.mm b/src/sentry/cocoa/cocoa_sdk.mm index e82a6f4e4..0a65791dc 100644 --- a/src/sentry/cocoa/cocoa_sdk.mm +++ b/src/sentry/cocoa/cocoa_sdk.mm @@ -346,7 +346,7 @@ void _add_default_attachments(SentryObjCScope *p_scope) { return memnew(DisabledScope); } -SentrySpanImpl *CocoaSDK::create_span() { +SentrySpanImpl *CocoaSDK::create_span(const String &p_name, const Dictionary &p_attributes) { return memnew(DisabledSpan); } diff --git a/src/sentry/disabled/disabled_sdk.h b/src/sentry/disabled/disabled_sdk.h index 73960d89f..7ab913fb8 100644 --- a/src/sentry/disabled/disabled_sdk.h +++ b/src/sentry/disabled/disabled_sdk.h @@ -42,7 +42,7 @@ class DisabledSDK : public InternalSDK { virtual void remove_attribute(const String &p_name) override {} virtual SentryScopeImpl *create_scope() override { return memnew(DisabledScope); } - virtual SentrySpanImpl *create_span() override { return memnew(DisabledSpan); } + virtual SentrySpanImpl *create_span(const String &p_name, const Dictionary &p_attributes) override { return memnew(DisabledSpan); } // Nothing is captured, so nothing is lost by discarding scope writes. virtual bool supports_scopes() const override { return true; } diff --git a/src/sentry/disabled/disabled_span.h b/src/sentry/disabled/disabled_span.h index 2ca76611f..520dcf3b0 100644 --- a/src/sentry/disabled/disabled_span.h +++ b/src/sentry/disabled/disabled_span.h @@ -27,7 +27,7 @@ class DisabledSpan : public SentrySpanImpl { virtual void end() override {} - virtual SentrySpanImpl *start_child(const String &p_name) override { return memnew(DisabledSpan); } + virtual SentrySpanImpl *start_child(const String &p_name, const Dictionary &p_attributes) override { return memnew(DisabledSpan); } }; } //namespace sentry diff --git a/src/sentry/internal_sdk.h b/src/sentry/internal_sdk.h index b1dc6a4ca..ac68f5db5 100644 --- a/src/sentry/internal_sdk.h +++ b/src/sentry/internal_sdk.h @@ -53,7 +53,7 @@ class InternalSDK { virtual void remove_attribute(const String &p_name) = 0; virtual SentryScopeImpl *create_scope() = 0; - virtual SentrySpanImpl *create_span() = 0; + virtual SentrySpanImpl *create_span(const String &p_name, const Dictionary &p_attributes) = 0; // Whether local scopes are as capable as the rest of this backend. // False means the backend captures events but silently discards scope diff --git a/src/sentry/javascript/javascript_sdk.cpp b/src/sentry/javascript/javascript_sdk.cpp index dceb6c8ef..509ed5f54 100644 --- a/src/sentry/javascript/javascript_sdk.cpp +++ b/src/sentry/javascript/javascript_sdk.cpp @@ -310,7 +310,7 @@ SentryScopeImpl *JavaScriptSDK::create_scope() { return memnew(JavaScriptScope(scope_obj)); } -SentrySpanImpl *JavaScriptSDK::create_span() { +SentrySpanImpl *JavaScriptSDK::create_span(const String &p_name, const Dictionary &p_attributes) { return memnew(DisabledSpan); } diff --git a/src/sentry/javascript/javascript_sdk.h b/src/sentry/javascript/javascript_sdk.h index e312135fa..f3524206e 100644 --- a/src/sentry/javascript/javascript_sdk.h +++ b/src/sentry/javascript/javascript_sdk.h @@ -44,7 +44,7 @@ class JavaScriptSDK : public InternalSDK { virtual void remove_attribute(const String &p_name) override; virtual SentryScopeImpl *create_scope() override; - virtual SentrySpanImpl *create_span() override; + virtual SentrySpanImpl *create_span(const String &p_name, const Dictionary &p_attributes) override; virtual bool supports_scopes() const override { return true; } diff --git a/src/sentry/native/native_sdk.cpp b/src/sentry/native/native_sdk.cpp index 53e611ffe..e5694e70b 100644 --- a/src/sentry/native/native_sdk.cpp +++ b/src/sentry/native/native_sdk.cpp @@ -376,7 +376,7 @@ SentryScopeImpl *NativeSDK::create_scope() { return memnew(NativeScope); } -SentrySpanImpl *NativeSDK::create_span() { +SentrySpanImpl *NativeSDK::create_span(const String &p_name, const Dictionary &p_attributes) { return memnew(NativeSpan); } diff --git a/src/sentry/native/native_sdk.h b/src/sentry/native/native_sdk.h index a24081539..dee741143 100644 --- a/src/sentry/native/native_sdk.h +++ b/src/sentry/native/native_sdk.h @@ -48,7 +48,7 @@ class NativeSDK : public InternalSDK { virtual void remove_attribute(const String &p_name) override; virtual SentryScopeImpl *create_scope() override; - virtual SentrySpanImpl *create_span() override; + virtual SentrySpanImpl *create_span(const String &p_name, const Dictionary &p_attributes) override; virtual bool supports_scopes() const override { return true; } diff --git a/src/sentry/native/native_span.cpp b/src/sentry/native/native_span.cpp index 3dd42695a..cbc9f1353 100644 --- a/src/sentry/native/native_span.cpp +++ b/src/sentry/native/native_span.cpp @@ -42,7 +42,7 @@ void NativeSpan::end() { WARN_PRINT_ONCE("Sentry: Not implemented"); } -SentrySpanImpl *NativeSpan::start_child(const String &p_name) { +SentrySpanImpl *NativeSpan::start_child(const String &p_name, const Dictionary &p_attributes) { WARN_PRINT_ONCE("Sentry: Not implemented"); return memnew(DisabledSpan); } diff --git a/src/sentry/native/native_span.h b/src/sentry/native/native_span.h index 3e05a9d28..9c44e97c0 100644 --- a/src/sentry/native/native_span.h +++ b/src/sentry/native/native_span.h @@ -31,7 +31,7 @@ class NativeSpan : public SentrySpanImpl { virtual void end() override; - virtual SentrySpanImpl *start_child(const String &p_name) override; + virtual SentrySpanImpl *start_child(const String &p_name, const Dictionary &p_attributes) override; NativeSpan(); virtual ~NativeSpan() override; diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index f37c51bc6..34311c1d1 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -68,13 +68,19 @@ void SentrySpan::end() { _impl->end(); } -Ref SentrySpan::start_child(const String &p_name) { +Ref SentrySpan::start_child(const String &p_name, const Dictionary &p_attributes) { ERR_FAIL_COND_V_MSG(p_name.is_empty(), Ref(), "Sentry: Can't start a child span with an empty name."); - return memnew(SentrySpan(_impl->start_child(p_name))); + return memnew(SentrySpan(_impl->start_child(p_name, p_attributes))); } SentrySpan::SentrySpan() { - _impl = INTERNAL_SDK()->create_span(); + // Inert by default: the only paths here are unassigned() and a accidental + // instantiation by the engine or user, neither of which should start a live span. + _impl = memnew(DisabledSpan); +} + +SentrySpan::SentrySpan(const String &p_name, const Dictionary &p_attributes) { + _impl = INTERNAL_SDK()->create_span(p_name, p_attributes); } SentrySpan::SentrySpan(SentrySpanImpl *p_impl) : diff --git a/src/sentry/sentry_span.h b/src/sentry/sentry_span.h index 7f8bd16f3..991722c41 100644 --- a/src/sentry/sentry_span.h +++ b/src/sentry/sentry_span.h @@ -48,11 +48,12 @@ class SentrySpan : public RefCounted { // *** Not exposed in the public API - Ref start_child(const String &p_name); + Ref start_child(const String &p_name, const Dictionary &p_attributes); SentrySpanImpl *get_implementation() const { return _impl; } SentrySpan(); + SentrySpan(const String &p_name, const Dictionary &p_attributes); SentrySpan(SentrySpanImpl *p_impl); ~SentrySpan(); }; diff --git a/src/sentry/sentry_span_impl.h b/src/sentry/sentry_span_impl.h index 0600f78a1..e98570fb3 100644 --- a/src/sentry/sentry_span_impl.h +++ b/src/sentry/sentry_span_impl.h @@ -26,7 +26,7 @@ class SentrySpanImpl : public Castable { virtual String get_name() const = 0; virtual void end() = 0; - virtual SentrySpanImpl *start_child(const String &p_name) = 0; + virtual SentrySpanImpl *start_child(const String &p_name, const Dictionary &p_attributes) = 0; virtual ~SentrySpanImpl() = default; }; From 6ff96e5cfc11c0b894c18c53d5bbef15a01acd30 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Tue, 11 Aug 2026 16:22:14 +0200 Subject: [PATCH 09/36] Scope-span bookkeeping --- src/sentry/sentry_scope.cpp | 19 ++++++++++++++++++- src/sentry/sentry_scope.h | 10 ++++++++++ src/sentry/sentry_sdk.cpp | 24 ++++++++++++++++++++---- src/sentry/sentry_span.cpp | 7 ++++++- src/sentry/sentry_span.h | 10 ++++++++++ 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/sentry/sentry_scope.cpp b/src/sentry/sentry_scope.cpp index e89335301..06e46a940 100644 --- a/src/sentry/sentry_scope.cpp +++ b/src/sentry/sentry_scope.cpp @@ -62,9 +62,26 @@ void SentryScope::clear() { _impl->clear(); } +void SentryScope::set_span(const Ref &p_span) { + ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); + ERR_FAIL_COND_MSG(p_span.is_null(), "Sentry: Can't bind a null span to the scope."); + p_span->set_previous(get_span()); + _span = p_span; +} + +Ref SentryScope::get_span() const { + ERR_SENTRY_THREAD_GUARD_V(Ref(), WRONG_THREAD_MSG); + while (_span.is_valid() && _span->is_ended()) { + _span = _span->get_previous(); + } + return _span; +} + Ref SentryScope::clone() const { ERR_SENTRY_THREAD_GUARD_V(Ref(), WRONG_THREAD_MSG); - return Ref(memnew(SentryScope(_impl->clone()))); + Ref copy = Ref(memnew(SentryScope(_impl->clone()))); + copy->_span = get_span(); + return copy; } void SentryScope::_bind_methods() { diff --git a/src/sentry/sentry_scope.h b/src/sentry/sentry_scope.h index 9025921b8..4fdeb33b3 100644 --- a/src/sentry/sentry_scope.h +++ b/src/sentry/sentry_scope.h @@ -4,6 +4,7 @@ #include "sentry/sentry_attachment.h" #include "sentry/sentry_breadcrumb.h" #include "sentry/sentry_scope_impl.h" +#include "sentry/sentry_span.h" #include "sentry/sentry_user.h" #include "sentry/util/thread_guard.h" @@ -22,6 +23,10 @@ class SentryScope : public RefCounted { private: SentryScopeImpl *_impl; + // Scope's bound span slot managed primarily by SentrySDK. + // Always access through get_span() due to deferred ended span resolution. + mutable Ref _span; + SENTRY_THREAD_OWNER; protected: @@ -40,6 +45,11 @@ class SentryScope : public RefCounted { Ref clone() const; + // *** Not exposed in the public API + + void set_span(const Ref &p_span); + Ref get_span() const; + SentryScopeImpl *get_implementation() const { return _impl; } SentryScope(); diff --git a/src/sentry/sentry_sdk.cpp b/src/sentry/sentry_sdk.cpp index 256b18445..1bf8c9336 100644 --- a/src/sentry/sentry_sdk.cpp +++ b/src/sentry/sentry_sdk.cpp @@ -176,8 +176,25 @@ Variant SentrySDK::with_scope(const Callable &p_callable) { } Ref SentrySDK::start_span(const String &p_name, const Ref &p_parent_span, const Dictionary &p_attributes, bool p_active) { - WARN_PRINT_ONCE("Sentry: Not implemented"); - return Ref(); + ERR_FAIL_COND_V_MSG(p_name.is_empty(), Ref(), "Sentry: Can't start a span with an empty name."); + + // The unassigned sentinel means "inherit the active span", while an explicit null forces a segment (new root-level span). + Ref parent = p_parent_span; + if (parent == SentrySpan::unassigned()) { + parent = get_active_span(); + } + + Ref span; + if (parent.is_valid()) { + span = parent->start_child(p_name, p_attributes); + } else { + span = Ref(memnew(SentrySpan(p_name, p_attributes))); + } + + if (p_active) { + get_current_scope()->set_span(span); + } + return span; } Variant SentrySDK::with_span(const String &p_name, const Callable &p_callable) { @@ -186,8 +203,7 @@ Variant SentrySDK::with_span(const String &p_name, const Callable &p_callable) { } Ref SentrySDK::get_active_span() const { - WARN_PRINT_ONCE("Sentry: Not implemented"); - return Ref(); + return get_current_scope()->get_span(); } void SentrySDK::init(const Callable &p_configuration_callback) { diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index 34311c1d1..37218af18 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -65,12 +65,17 @@ String SentrySpan::get_name() const { void SentrySpan::end() { ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); + if (_ended) { + return; + } + _ended = true; _impl->end(); } Ref SentrySpan::start_child(const String &p_name, const Dictionary &p_attributes) { ERR_FAIL_COND_V_MSG(p_name.is_empty(), Ref(), "Sentry: Can't start a child span with an empty name."); - return memnew(SentrySpan(_impl->start_child(p_name, p_attributes))); + SentrySpanImpl *child_impl = _impl->start_child(p_name, p_attributes); + return memnew(SentrySpan(child_impl)); } SentrySpan::SentrySpan() { diff --git a/src/sentry/sentry_span.h b/src/sentry/sentry_span.h index 991722c41..1fbb448e3 100644 --- a/src/sentry/sentry_span.h +++ b/src/sentry/sentry_span.h @@ -24,6 +24,12 @@ class SentrySpan : public RefCounted { private: SentrySpanImpl *_impl; + // The span this one displaced when it was bound to a scope, assigned by SentrySDK. + // Scopes resolve their slot through this chain, so it must outlive this span's end(). + Ref _previous; + + bool _ended = false; + SENTRY_THREAD_OWNER; protected: @@ -50,6 +56,10 @@ class SentrySpan : public RefCounted { Ref start_child(const String &p_name, const Dictionary &p_attributes); + _FORCE_INLINE_ bool is_ended() const { return _ended; } + _FORCE_INLINE_ void set_previous(const Ref &p_span) { _previous = p_span; } + _FORCE_INLINE_ Ref get_previous() const { return _previous; } + SentrySpanImpl *get_implementation() const { return _impl; } SentrySpan(); From 619f2a14bb33983c8b17c1ef5df469462f7f2590 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Wed, 12 Aug 2026 11:16:14 +0200 Subject: [PATCH 10/36] Warnings on currently unsupported platforms --- src/sentry/android/android_sdk.cpp | 1 + src/sentry/cocoa/cocoa_sdk.mm | 1 + src/sentry/javascript/javascript_sdk.cpp | 1 + 3 files changed, 3 insertions(+) diff --git a/src/sentry/android/android_sdk.cpp b/src/sentry/android/android_sdk.cpp index 0ef16ec27..adbfc7321 100644 --- a/src/sentry/android/android_sdk.cpp +++ b/src/sentry/android/android_sdk.cpp @@ -335,6 +335,7 @@ SentryScopeImpl *AndroidSDK::create_scope() { } SentrySpanImpl *AndroidSDK::create_span(const String &p_name, const Dictionary &p_attributes) { + WARN_PRINT_ONCE("Sentry: Spans are not implemented on this platform yet - nothing will be recorded."); return memnew(DisabledSpan); } diff --git a/src/sentry/cocoa/cocoa_sdk.mm b/src/sentry/cocoa/cocoa_sdk.mm index 0a65791dc..f0f35999e 100644 --- a/src/sentry/cocoa/cocoa_sdk.mm +++ b/src/sentry/cocoa/cocoa_sdk.mm @@ -347,6 +347,7 @@ void _add_default_attachments(SentryObjCScope *p_scope) { } SentrySpanImpl *CocoaSDK::create_span(const String &p_name, const Dictionary &p_attributes) { + WARN_PRINT_ONCE("Sentry: Spans are not implemented on this platform yet - nothing will be recorded."); return memnew(DisabledSpan); } diff --git a/src/sentry/javascript/javascript_sdk.cpp b/src/sentry/javascript/javascript_sdk.cpp index 509ed5f54..031c4ba13 100644 --- a/src/sentry/javascript/javascript_sdk.cpp +++ b/src/sentry/javascript/javascript_sdk.cpp @@ -311,6 +311,7 @@ SentryScopeImpl *JavaScriptSDK::create_scope() { } SentrySpanImpl *JavaScriptSDK::create_span(const String &p_name, const Dictionary &p_attributes) { + WARN_PRINT_ONCE("Sentry: Spans are not implemented on this platform yet - nothing will be recorded."); return memnew(DisabledSpan); } From 5ed14615692c57647f613a711f7d24a741ee0972 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Wed, 12 Aug 2026 12:12:59 +0200 Subject: [PATCH 11/36] Drop getters --- src/sentry/disabled/disabled_span.h | 19 +++---------------- src/sentry/native/native_span.h | 6 ------ src/sentry/sentry_span.cpp | 25 ------------------------- src/sentry/sentry_span.h | 4 ---- src/sentry/sentry_span_impl.h | 4 ---- 5 files changed, 3 insertions(+), 55 deletions(-) diff --git a/src/sentry/disabled/disabled_span.h b/src/sentry/disabled/disabled_span.h index 520dcf3b0..b8dde3e01 100644 --- a/src/sentry/disabled/disabled_span.h +++ b/src/sentry/disabled/disabled_span.h @@ -5,26 +5,13 @@ namespace sentry { // Span implementation that is used when the SDK is disabled. -// Nothing is sent, but values are kept so that getters don't contradict setters. class DisabledSpan : public SentrySpanImpl { SENTRY_CASTABLE(DisabledSpan, SentrySpanImpl); -private: - Dictionary attributes; - SpanStatus status = SPAN_STATUS_UNSET; - String name; - public: - virtual void set_attribute(const String &p_key, const Variant &p_value) override { attributes[p_key] = p_value; } - virtual Variant get_attribute(const String &p_key) const override { return attributes.get(p_key, Variant()); } - virtual Dictionary get_attributes() const override { return attributes.duplicate(); } - - virtual void set_status(SpanStatus p_status) override { status = p_status; } - virtual SpanStatus get_status() const override { return status; } - - virtual void set_name(const String &p_name) override { name = p_name; } - virtual String get_name() const override { return name; } - + virtual void set_attribute(const String &p_key, const Variant &p_value) override {} + virtual void set_status(SpanStatus p_status) override {} + virtual void set_name(const String &p_name) override {} virtual void end() override {} virtual SentrySpanImpl *start_child(const String &p_name, const Dictionary &p_attributes) override { return memnew(DisabledSpan); } diff --git a/src/sentry/native/native_span.h b/src/sentry/native/native_span.h index 9c44e97c0..2e464b128 100644 --- a/src/sentry/native/native_span.h +++ b/src/sentry/native/native_span.h @@ -20,14 +20,8 @@ class NativeSpan : public SentrySpanImpl { public: virtual void set_attribute(const String &p_key, const Variant &p_value) override; - virtual Variant get_attribute(const String &p_key) const override; - virtual Dictionary get_attributes() const override; - virtual void set_status(SpanStatus p_status) override; - virtual SpanStatus get_status() const override; - virtual void set_name(const String &p_name) override; - virtual String get_name() const override; virtual void end() override; diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index 37218af18..3378ac215 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -31,38 +31,17 @@ void SentrySpan::set_attributes(const Dictionary &p_attributes) { } } -Variant SentrySpan::get_attribute(const String &p_key) const { - ERR_SENTRY_THREAD_GUARD_V(Variant(), WRONG_THREAD_MSG); - ERR_FAIL_COND_V_MSG(p_key.is_empty(), Variant(), "Sentry: Can't get attribute with an empty key."); - return _impl->get_attribute(p_key); -} - -Dictionary SentrySpan::get_attributes() const { - ERR_SENTRY_THREAD_GUARD_V(Dictionary(), WRONG_THREAD_MSG); - return _impl->get_attributes(); -} - void SentrySpan::set_status(SpanStatus p_status) { ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); _impl->set_status(p_status); } -SentrySpan::SpanStatus SentrySpan::get_status() const { - ERR_SENTRY_THREAD_GUARD_V(SPAN_STATUS_UNSET, WRONG_THREAD_MSG); - return _impl->get_status(); -} - void SentrySpan::set_name(const String &p_name) { ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); ERR_FAIL_COND_MSG(p_name.is_empty(), "Sentry: Can't set an empty span name."); _impl->set_name(p_name); } -String SentrySpan::get_name() const { - ERR_SENTRY_THREAD_GUARD_V(String(), WRONG_THREAD_MSG); - return _impl->get_name(); -} - void SentrySpan::end() { ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); if (_ended) { @@ -99,12 +78,8 @@ SentrySpan::~SentrySpan() { void SentrySpan::_bind_methods() { ClassDB::bind_method(D_METHOD("set_attribute", "key", "value"), &SentrySpan::set_attribute); ClassDB::bind_method(D_METHOD("set_attributes", "attributes"), &SentrySpan::set_attributes); - ClassDB::bind_method(D_METHOD("get_attribute", "key"), &SentrySpan::get_attribute); - ClassDB::bind_method(D_METHOD("get_attributes"), &SentrySpan::get_attributes); ClassDB::bind_method(D_METHOD("set_status", "status"), &SentrySpan::set_status); - ClassDB::bind_method(D_METHOD("get_status"), &SentrySpan::get_status); ClassDB::bind_method(D_METHOD("set_name", "name"), &SentrySpan::set_name); - ClassDB::bind_method(D_METHOD("get_name"), &SentrySpan::get_name); ClassDB::bind_method(D_METHOD("end"), &SentrySpan::end); BIND_ENUM_CONSTANT(SPAN_STATUS_UNSET); diff --git a/src/sentry/sentry_span.h b/src/sentry/sentry_span.h index 1fbb448e3..e7e34426e 100644 --- a/src/sentry/sentry_span.h +++ b/src/sentry/sentry_span.h @@ -41,14 +41,10 @@ class SentrySpan : public RefCounted { void set_attribute(const String &p_key, const Variant &p_value); void set_attributes(const Dictionary &p_attributes); - Variant get_attribute(const String &p_key) const; - Dictionary get_attributes() const; void set_status(SpanStatus p_status); - SpanStatus get_status() const; void set_name(const String &p_name); - String get_name() const; void end(); diff --git a/src/sentry/sentry_span_impl.h b/src/sentry/sentry_span_impl.h index e98570fb3..a0eb44d90 100644 --- a/src/sentry/sentry_span_impl.h +++ b/src/sentry/sentry_span_impl.h @@ -18,12 +18,8 @@ class SentrySpanImpl : public Castable { public: virtual void set_attribute(const String &p_key, const Variant &p_value) = 0; - virtual Variant get_attribute(const String &p_key) const = 0; - virtual Dictionary get_attributes() const = 0; virtual void set_status(SpanStatus p_status) = 0; - virtual SpanStatus get_status() const = 0; virtual void set_name(const String &p_name) = 0; - virtual String get_name() const = 0; virtual void end() = 0; virtual SentrySpanImpl *start_child(const String &p_name, const Dictionary &p_attributes) = 0; From 5afdfe103c1802b7534c1e17dc09f7401d3a3c31 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Wed, 12 Aug 2026 12:38:31 +0200 Subject: [PATCH 12/36] Native implementation --- src/sentry/native/native_sdk.cpp | 4 +- src/sentry/native/native_span.cpp | 108 +++++++++++++++++++++++------- src/sentry/native/native_span.h | 17 +++-- src/sentry/sentry_span.cpp | 6 +- src/sentry/sentry_span.h | 3 + src/sentry/sentry_span_impl.cpp | 11 +++ src/sentry/sentry_span_impl.h | 2 + 7 files changed, 116 insertions(+), 35 deletions(-) create mode 100644 src/sentry/sentry_span_impl.cpp diff --git a/src/sentry/native/native_sdk.cpp b/src/sentry/native/native_sdk.cpp index e5694e70b..20ae427db 100644 --- a/src/sentry/native/native_sdk.cpp +++ b/src/sentry/native/native_sdk.cpp @@ -377,7 +377,7 @@ SentryScopeImpl *NativeSDK::create_scope() { } SentrySpanImpl *NativeSDK::create_span(const String &p_name, const Dictionary &p_attributes) { - return memnew(NativeSpan); + return memnew(NativeSpan(p_name, p_attributes)); } void NativeSDK::set_trace(const String &p_trace_id, const String &p_parent_span_id) { @@ -402,6 +402,8 @@ void NativeSDK::init() { sentry_options_set_dist(options, SENTRY_OPTIONS()->get_dist().utf8()); sentry_options_set_environment(options, SENTRY_OPTIONS()->get_environment().utf8()); sentry_options_set_sample_rate(options, SENTRY_OPTIONS()->get_sample_rate()); + // TODO: Replace with SENTRY_OPTIONS() value once exposed. + sentry_options_set_traces_sample_rate(options, 1.0); sentry_options_set_max_breadcrumbs(options, SENTRY_OPTIONS()->get_max_breadcrumbs()); sentry_options_set_shutdown_timeout(options, SENTRY_OPTIONS()->get_shutdown_timeout_ms()); sentry_options_set_sdk_name(options, "sentry.native.godot"); diff --git a/src/sentry/native/native_span.cpp b/src/sentry/native/native_span.cpp index cbc9f1353..420a0109b 100644 --- a/src/sentry/native/native_span.cpp +++ b/src/sentry/native/native_span.cpp @@ -1,56 +1,112 @@ #include "native_span.h" -#include "sentry/disabled/disabled_span.h" +#include "sentry/native/native_util.h" #include -namespace sentry::native { +namespace { -void NativeSpan::set_attribute(const String &p_key, const Variant &p_value) { - WARN_PRINT_ONCE("Sentry: Not implemented"); -} +constexpr const char *OP_KEY = "sentry.op"; -Variant NativeSpan::get_attribute(const String &p_key) const { - WARN_PRINT_ONCE("Sentry: Not implemented"); - return Variant(); +inline CharString _get_op(const Dictionary &p_attributes) { + return String(p_attributes.get(OP_KEY, String())).utf8(); } -Dictionary NativeSpan::get_attributes() const { - WARN_PRINT_ONCE("Sentry: Not implemented"); - return Dictionary(); +} // unnamed namespace + +namespace sentry::native { + +void NativeSpan::set_attribute(const String &p_key, const Variant &p_value) { + if (!_is_live()) { + return; + } + if (_transaction) { + sentry_transaction_set_data(_transaction, p_key.utf8(), variant_to_sentry_value(p_value)); + } else { + sentry_span_set_data(_span, p_key.utf8(), variant_to_sentry_value(p_value)); + } } void NativeSpan::set_status(SpanStatus p_status) { - WARN_PRINT_ONCE("Sentry: Not implemented"); + if (!_is_live()) { + return; + } + if (p_status == SPAN_STATUS_UNSET) { + WARN_PRINT_ONCE("Sentry: Clearing a span status is not supported on this platform."); + return; + } + sentry_span_status_t native_status = p_status == SPAN_STATUS_OK ? SENTRY_SPAN_STATUS_OK : SENTRY_SPAN_STATUS_INTERNAL_ERROR; + if (_transaction) { + sentry_transaction_set_status(_transaction, native_status); + } else { + sentry_span_set_status(_span, native_status); + } } -SpanStatus NativeSpan::get_status() const { - WARN_PRINT_ONCE("Sentry: Not implemented"); - return SPAN_STATUS_UNSET; +void NativeSpan::set_name(const String &p_name) { + if (!_is_live()) { + return; + } + if (_transaction) { + sentry_transaction_set_name(_transaction, p_name.utf8()); + } else { + WARN_PRINT_ONCE("Sentry: Renaming a child span is not supported on this platform - the name is fixed at creation."); + } } -void NativeSpan::set_name(const String &p_name) { - WARN_PRINT_ONCE("Sentry: Not implemented"); +void NativeSpan::end() { + if (!_is_live()) { + return; + } + if (_transaction) { + sentry_transaction_finish(_transaction); + _transaction = nullptr; + } else { + sentry_span_finish(_span); + _span = nullptr; + } } -String NativeSpan::get_name() const { - WARN_PRINT_ONCE("Sentry: Not implemented"); - return String(); +SentrySpanImpl *NativeSpan::start_child(const String &p_name, const Dictionary &p_attributes) { + if (!_is_live()) { + return SentrySpanImpl::noop(); + } + sentry_span_t *child = _transaction + ? sentry_transaction_start_child(_transaction, _get_op(p_attributes), p_name.utf8()) + : sentry_span_start_child(_span, _get_op(p_attributes), p_name.utf8()); + return memnew(NativeSpan(child, p_attributes)); } -void NativeSpan::end() { - WARN_PRINT_ONCE("Sentry: Not implemented"); +void NativeSpan::_apply_attributes(const Dictionary &p_attributes) { + const Array &keys = p_attributes.keys(); + for (int i = 0; i < keys.size(); i++) { + const Variant &key = keys[i]; + String name = key; + ERR_CONTINUE_MSG(name.is_empty(), "Sentry: Can't set attribute with an empty key."); + set_attribute(name, p_attributes[key]); + } } -SentrySpanImpl *NativeSpan::start_child(const String &p_name, const Dictionary &p_attributes) { - WARN_PRINT_ONCE("Sentry: Not implemented"); - return memnew(DisabledSpan); +NativeSpan::NativeSpan(const String &p_name, const Dictionary &p_attributes) { + sentry_transaction_context_t *context = sentry_transaction_context_new(p_name.utf8(), _get_op(p_attributes)); + _transaction = sentry_transaction_start(context, sentry_value_new_null()); + _apply_attributes(p_attributes); } -NativeSpan::NativeSpan() { +NativeSpan::NativeSpan(sentry_span_t *p_span, const Dictionary &p_attributes) : + _span(p_span) { + _apply_attributes(p_attributes); } NativeSpan::~NativeSpan() { + if (!_is_live()) { + return; + } + if (_transaction) { + sentry_transaction_discard(_transaction); + } else { + sentry_span_discard(_span); + } } } //namespace sentry::native diff --git a/src/sentry/native/native_span.h b/src/sentry/native/native_span.h index 2e464b128..199723c0f 100644 --- a/src/sentry/native/native_span.h +++ b/src/sentry/native/native_span.h @@ -5,18 +5,19 @@ namespace sentry::native { -// Stub: spans are not implemented on this platform yet. class NativeSpan : public SentrySpanImpl { SENTRY_CASTABLE(NativeSpan, SentrySpanImpl); private: // Native emulates the span-first API, until the day span-first is actually supported. // Root spans are backed by transactions, while child spans use native spans. - union { - sentry_transaction_t *transaction; - sentry_span_t *span; - } _data; - bool _is_transaction = true; + sentry_transaction_t *_transaction = nullptr; + sentry_span_t *_span = nullptr; + + // Finishing hands the handle over to sentry-native, which frees it. + _FORCE_INLINE_ bool _is_live() const { return _transaction || _span; } + + void _apply_attributes(const Dictionary &p_attributes); public: virtual void set_attribute(const String &p_key, const Variant &p_value) override; @@ -27,7 +28,9 @@ class NativeSpan : public SentrySpanImpl { virtual SentrySpanImpl *start_child(const String &p_name, const Dictionary &p_attributes) override; - NativeSpan(); + NativeSpan() = delete; + NativeSpan(const String &p_name, const Dictionary &p_attributes); + NativeSpan(sentry_span_t *p_span, const Dictionary &p_attributes); virtual ~NativeSpan() override; }; diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index 3378ac215..d3371a7b2 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -8,9 +8,13 @@ namespace sentry { +Ref SentrySpan::noop() { + return Ref(memnew(SentrySpan(memnew(DisabledSpan)))); +} + Ref SentrySpan::unassigned() { // FYI: Internal SDK is not initialized yet when this static is created. - static Ref sentinel = Ref(memnew(SentrySpan(memnew(DisabledSpan)))); + static Ref sentinel = noop(); return sentinel; } diff --git a/src/sentry/sentry_span.h b/src/sentry/sentry_span.h index e7e34426e..e3bd81baf 100644 --- a/src/sentry/sentry_span.h +++ b/src/sentry/sentry_span.h @@ -36,6 +36,9 @@ class SentrySpan : public RefCounted { static void _bind_methods(); public: + // Returns a no-op span that does nothing. + static Ref noop(); + // Returns a sentinel value that indicates an unassigned span. static Ref unassigned(); diff --git a/src/sentry/sentry_span_impl.cpp b/src/sentry/sentry_span_impl.cpp new file mode 100644 index 000000000..0e1cb9e11 --- /dev/null +++ b/src/sentry/sentry_span_impl.cpp @@ -0,0 +1,11 @@ +#include "sentry_span_impl.h" + +#include "sentry/disabled/disabled_span.h" + +namespace sentry { + +SentrySpanImpl *SentrySpanImpl::noop() { + return memnew(DisabledSpan); +} + +} //namespace sentry diff --git a/src/sentry/sentry_span_impl.h b/src/sentry/sentry_span_impl.h index a0eb44d90..be59ca0a8 100644 --- a/src/sentry/sentry_span_impl.h +++ b/src/sentry/sentry_span_impl.h @@ -17,6 +17,8 @@ class SentrySpanImpl : public Castable { SENTRY_CASTABLE(SentrySpanImpl, Castable); public: + static SentrySpanImpl *noop(); + virtual void set_attribute(const String &p_key, const Variant &p_value) = 0; virtual void set_status(SpanStatus p_status) = 0; virtual void set_name(const String &p_name) = 0; From 2ce8fcb969ed93fc553de71432014b6832fc0f03 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Wed, 12 Aug 2026 12:40:32 +0200 Subject: [PATCH 13/36] Use noop() --- src/sentry/android/android_sdk.cpp | 3 +-- src/sentry/cocoa/cocoa_sdk.mm | 3 +-- src/sentry/javascript/javascript_sdk.cpp | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/sentry/android/android_sdk.cpp b/src/sentry/android/android_sdk.cpp index adbfc7321..5a7bece22 100644 --- a/src/sentry/android/android_sdk.cpp +++ b/src/sentry/android/android_sdk.cpp @@ -8,7 +8,6 @@ #include "android_string_names.h" #include "android_util.h" #include "sentry/common_defs.h" -#include "sentry/disabled/disabled_span.h" #include "sentry/logging/print.h" #include "sentry/processing/process_event.h" #include "sentry/processing/process_log.h" @@ -336,7 +335,7 @@ SentryScopeImpl *AndroidSDK::create_scope() { SentrySpanImpl *AndroidSDK::create_span(const String &p_name, const Dictionary &p_attributes) { WARN_PRINT_ONCE("Sentry: Spans are not implemented on this platform yet - nothing will be recorded."); - return memnew(DisabledSpan); + return SentrySpanImpl::noop(); } void AndroidSDK::set_trace(const String &p_trace_id, const String &p_parent_span_id) { diff --git a/src/sentry/cocoa/cocoa_sdk.mm b/src/sentry/cocoa/cocoa_sdk.mm index f0f35999e..25ec9595f 100644 --- a/src/sentry/cocoa/cocoa_sdk.mm +++ b/src/sentry/cocoa/cocoa_sdk.mm @@ -9,7 +9,6 @@ #include "gen/sdk_version.gen.h" #include "sentry/common_defs.h" #include "sentry/disabled/disabled_scope.h" -#include "sentry/disabled/disabled_span.h" #include "sentry/logging/print.h" #include "sentry/processing/process_event.h" #include "sentry/processing/process_log.h" @@ -348,7 +347,7 @@ void _add_default_attachments(SentryObjCScope *p_scope) { SentrySpanImpl *CocoaSDK::create_span(const String &p_name, const Dictionary &p_attributes) { WARN_PRINT_ONCE("Sentry: Spans are not implemented on this platform yet - nothing will be recorded."); - return memnew(DisabledSpan); + return SentrySpanImpl::noop(); } void CocoaSDK::set_trace(const String &p_trace_id, const String &p_parent_span_id) { diff --git a/src/sentry/javascript/javascript_sdk.cpp b/src/sentry/javascript/javascript_sdk.cpp index 031c4ba13..1b024cae0 100644 --- a/src/sentry/javascript/javascript_sdk.cpp +++ b/src/sentry/javascript/javascript_sdk.cpp @@ -1,7 +1,6 @@ #include "javascript_sdk.h" #include "sentry/disabled/disabled_scope.h" -#include "sentry/disabled/disabled_span.h" #include "sentry/javascript/javascript_breadcrumb.h" #include "sentry/javascript/javascript_event.h" #include "sentry/javascript/javascript_interop.h" @@ -312,7 +311,7 @@ SentryScopeImpl *JavaScriptSDK::create_scope() { SentrySpanImpl *JavaScriptSDK::create_span(const String &p_name, const Dictionary &p_attributes) { WARN_PRINT_ONCE("Sentry: Spans are not implemented on this platform yet - nothing will be recorded."); - return memnew(DisabledSpan); + return SentrySpanImpl::noop(); } void JavaScriptSDK::set_trace(const String &p_trace_id, const String &p_parent_span_id) { From ad95234487cb6885c9e6dfb337eb1115a49a57c2 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Wed, 12 Aug 2026 12:43:38 +0200 Subject: [PATCH 14/36] Drop set_name() --- src/sentry/disabled/disabled_span.h | 1 - src/sentry/native/native_span.cpp | 11 ----------- src/sentry/native/native_span.h | 1 - src/sentry/sentry_span.cpp | 7 ------- src/sentry/sentry_span.h | 2 -- src/sentry/sentry_span_impl.h | 1 - 6 files changed, 23 deletions(-) diff --git a/src/sentry/disabled/disabled_span.h b/src/sentry/disabled/disabled_span.h index b8dde3e01..3159b71d7 100644 --- a/src/sentry/disabled/disabled_span.h +++ b/src/sentry/disabled/disabled_span.h @@ -11,7 +11,6 @@ class DisabledSpan : public SentrySpanImpl { public: virtual void set_attribute(const String &p_key, const Variant &p_value) override {} virtual void set_status(SpanStatus p_status) override {} - virtual void set_name(const String &p_name) override {} virtual void end() override {} virtual SentrySpanImpl *start_child(const String &p_name, const Dictionary &p_attributes) override { return memnew(DisabledSpan); } diff --git a/src/sentry/native/native_span.cpp b/src/sentry/native/native_span.cpp index 420a0109b..9d9cd77d2 100644 --- a/src/sentry/native/native_span.cpp +++ b/src/sentry/native/native_span.cpp @@ -43,17 +43,6 @@ void NativeSpan::set_status(SpanStatus p_status) { } } -void NativeSpan::set_name(const String &p_name) { - if (!_is_live()) { - return; - } - if (_transaction) { - sentry_transaction_set_name(_transaction, p_name.utf8()); - } else { - WARN_PRINT_ONCE("Sentry: Renaming a child span is not supported on this platform - the name is fixed at creation."); - } -} - void NativeSpan::end() { if (!_is_live()) { return; diff --git a/src/sentry/native/native_span.h b/src/sentry/native/native_span.h index 199723c0f..2ddf7dc27 100644 --- a/src/sentry/native/native_span.h +++ b/src/sentry/native/native_span.h @@ -22,7 +22,6 @@ class NativeSpan : public SentrySpanImpl { public: virtual void set_attribute(const String &p_key, const Variant &p_value) override; virtual void set_status(SpanStatus p_status) override; - virtual void set_name(const String &p_name) override; virtual void end() override; diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index d3371a7b2..ff3b54814 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -40,12 +40,6 @@ void SentrySpan::set_status(SpanStatus p_status) { _impl->set_status(p_status); } -void SentrySpan::set_name(const String &p_name) { - ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); - ERR_FAIL_COND_MSG(p_name.is_empty(), "Sentry: Can't set an empty span name."); - _impl->set_name(p_name); -} - void SentrySpan::end() { ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); if (_ended) { @@ -83,7 +77,6 @@ void SentrySpan::_bind_methods() { ClassDB::bind_method(D_METHOD("set_attribute", "key", "value"), &SentrySpan::set_attribute); ClassDB::bind_method(D_METHOD("set_attributes", "attributes"), &SentrySpan::set_attributes); ClassDB::bind_method(D_METHOD("set_status", "status"), &SentrySpan::set_status); - ClassDB::bind_method(D_METHOD("set_name", "name"), &SentrySpan::set_name); ClassDB::bind_method(D_METHOD("end"), &SentrySpan::end); BIND_ENUM_CONSTANT(SPAN_STATUS_UNSET); diff --git a/src/sentry/sentry_span.h b/src/sentry/sentry_span.h index e3bd81baf..6a9b3ad4b 100644 --- a/src/sentry/sentry_span.h +++ b/src/sentry/sentry_span.h @@ -47,8 +47,6 @@ class SentrySpan : public RefCounted { void set_status(SpanStatus p_status); - void set_name(const String &p_name); - void end(); // *** Not exposed in the public API diff --git a/src/sentry/sentry_span_impl.h b/src/sentry/sentry_span_impl.h index be59ca0a8..38f169e5c 100644 --- a/src/sentry/sentry_span_impl.h +++ b/src/sentry/sentry_span_impl.h @@ -21,7 +21,6 @@ class SentrySpanImpl : public Castable { virtual void set_attribute(const String &p_key, const Variant &p_value) = 0; virtual void set_status(SpanStatus p_status) = 0; - virtual void set_name(const String &p_name) = 0; virtual void end() = 0; virtual SentrySpanImpl *start_child(const String &p_name, const Dictionary &p_attributes) = 0; From 03af4a4b26f8f4679884e2cb70272a010823b890 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Wed, 12 Aug 2026 22:22:03 +0200 Subject: [PATCH 15/36] Bind active span to scope --- src/sentry/native/native_scope.cpp | 9 +++++++++ src/sentry/native/native_scope.h | 1 + src/sentry/native/native_span.cpp | 8 ++++++++ src/sentry/native/native_span.h | 2 ++ src/sentry/sentry_scope.cpp | 24 ++++++++++++++++++++---- src/sentry/sentry_scope.h | 5 ++++- src/sentry/sentry_scope_impl.h | 6 ++++++ 7 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/sentry/native/native_scope.cpp b/src/sentry/native/native_scope.cpp index e1bf2ed4a..9f0a54efb 100644 --- a/src/sentry/native/native_scope.cpp +++ b/src/sentry/native/native_scope.cpp @@ -1,6 +1,7 @@ #include "native_scope.h" #include "sentry/native/native_breadcrumb.h" +#include "sentry/native/native_span.h" #include "sentry/native/native_util.h" namespace sentry::native { @@ -74,6 +75,14 @@ SentryScopeImpl *NativeScope::clone() const { return memnew(NativeScope(sentry_scope_clone(_scope))); } +void NativeScope::set_span(SentrySpanImpl *p_span) { + if (NativeSpan *native_span = Castable::cast_to(p_span)) { + native_span->bind_to_scope(_scope); + } else { + sentry_scope_set_span(_scope, nullptr); + } +} + NativeScope::NativeScope() { _scope = sentry_scope_new(); } diff --git a/src/sentry/native/native_scope.h b/src/sentry/native/native_scope.h index fb2d5a00b..318e65c92 100644 --- a/src/sentry/native/native_scope.h +++ b/src/sentry/native/native_scope.h @@ -26,6 +26,7 @@ class NativeScope : public SentryScopeImpl { virtual void add_attachment(const Ref &p_attachment) override; virtual void clear() override; virtual SentryScopeImpl *clone() const override; + virtual void set_span(SentrySpanImpl *p_span) override; NativeScope(); NativeScope(sentry_scope_t *p_scope); diff --git a/src/sentry/native/native_span.cpp b/src/sentry/native/native_span.cpp index 9d9cd77d2..1dc5117a0 100644 --- a/src/sentry/native/native_span.cpp +++ b/src/sentry/native/native_span.cpp @@ -66,6 +66,14 @@ SentrySpanImpl *NativeSpan::start_child(const String &p_name, const Dictionary & return memnew(NativeSpan(child, p_attributes)); } +void NativeSpan::bind_to_scope(sentry_scope_t *p_scope) { + if (_transaction) { + sentry_scope_set_transaction_object(p_scope, _transaction); + } else { + sentry_scope_set_span(p_scope, _span); + } +} + void NativeSpan::_apply_attributes(const Dictionary &p_attributes) { const Array &keys = p_attributes.keys(); for (int i = 0; i < keys.size(); i++) { diff --git a/src/sentry/native/native_span.h b/src/sentry/native/native_span.h index 2ddf7dc27..31feede6a 100644 --- a/src/sentry/native/native_span.h +++ b/src/sentry/native/native_span.h @@ -27,6 +27,8 @@ class NativeSpan : public SentrySpanImpl { virtual SentrySpanImpl *start_child(const String &p_name, const Dictionary &p_attributes) override; + void bind_to_scope(sentry_scope_t *p_scope); + NativeSpan() = delete; NativeSpan(const String &p_name, const Dictionary &p_attributes); NativeSpan(sentry_span_t *p_span, const Dictionary &p_attributes); diff --git a/src/sentry/sentry_scope.cpp b/src/sentry/sentry_scope.cpp index 06e46a940..bd3f5986f 100644 --- a/src/sentry/sentry_scope.cpp +++ b/src/sentry/sentry_scope.cpp @@ -67,23 +67,39 @@ void SentryScope::set_span(const Ref &p_span) { ERR_FAIL_COND_MSG(p_span.is_null(), "Sentry: Can't bind a null span to the scope."); p_span->set_previous(get_span()); _span = p_span; + _impl->set_span(p_span->get_implementation()); } Ref SentryScope::get_span() const { ERR_SENTRY_THREAD_GUARD_V(Ref(), WRONG_THREAD_MSG); - while (_span.is_valid() && _span->is_ended()) { - _span = _span->get_previous(); - } + _sync_active_span(); return _span; } Ref SentryScope::clone() const { ERR_SENTRY_THREAD_GUARD_V(Ref(), WRONG_THREAD_MSG); + _sync_active_span(); Ref copy = Ref(memnew(SentryScope(_impl->clone()))); - copy->_span = get_span(); + copy->_span = _span; return copy; } +SentryScopeImpl *SentryScope::get_implementation() const { + _sync_active_span(); + return _impl; +} + +void SentryScope::_sync_active_span() const { + bool restored = false; + while (_span.is_valid() && _span->is_ended()) { + _span = _span->get_previous(); + restored = true; + } + if (restored) { + _impl->set_span(_span.is_valid() ? _span->get_implementation() : nullptr); + } +} + void SentryScope::_bind_methods() { ClassDB::bind_method(D_METHOD("set_context", "key", "value"), &SentryScope::set_context); ClassDB::bind_method(D_METHOD("set_tag", "key", "value"), &SentryScope::set_tag); diff --git a/src/sentry/sentry_scope.h b/src/sentry/sentry_scope.h index 4fdeb33b3..3daac6b4c 100644 --- a/src/sentry/sentry_scope.h +++ b/src/sentry/sentry_scope.h @@ -29,6 +29,9 @@ class SentryScope : public RefCounted { SENTRY_THREAD_OWNER; + // Unwinds finished active spans to the nearest live ancestor and syncs with the backing scope. + void _sync_active_span() const; + protected: static void _bind_methods(); @@ -50,7 +53,7 @@ class SentryScope : public RefCounted { void set_span(const Ref &p_span); Ref get_span() const; - SentryScopeImpl *get_implementation() const { return _impl; } + SentryScopeImpl *get_implementation() const; SentryScope(); SentryScope(SentryScopeImpl *p_impl); diff --git a/src/sentry/sentry_scope_impl.h b/src/sentry/sentry_scope_impl.h index ea7d4d955..6c88045ce 100644 --- a/src/sentry/sentry_scope_impl.h +++ b/src/sentry/sentry_scope_impl.h @@ -13,6 +13,8 @@ using namespace godot; namespace sentry { +class SentrySpanImpl; + // Base class for Sentry scope implementations; see Godot-facing SentryScope. // Splitting the implementation from SentryScope avoids a factory method // (i.e. SentryScope.create() or SentrySDK.create_scope()). @@ -34,6 +36,10 @@ class SentryScopeImpl : public Castable { virtual void clear() = 0; virtual SentryScopeImpl *clone() const = 0; + // Binds the span that stamps everything captured through this scope; null clears the binding. + // Backends without span support leave it unimplemented. + virtual void set_span(SentrySpanImpl *p_span) {} + virtual ~SentryScopeImpl() = default; }; From 02ee6ddd8db59b750d6ab31af6f9689bbc1e026c Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Wed, 12 Aug 2026 22:49:55 +0200 Subject: [PATCH 16/36] noop() => create_noop() --- src/sentry/android/android_sdk.cpp | 2 +- src/sentry/cocoa/cocoa_sdk.mm | 2 +- src/sentry/javascript/javascript_sdk.cpp | 2 +- src/sentry/native/native_span.cpp | 2 +- src/sentry/sentry_sdk.cpp | 2 ++ src/sentry/sentry_span.cpp | 4 ++-- src/sentry/sentry_span.h | 2 +- src/sentry/sentry_span_impl.cpp | 2 +- src/sentry/sentry_span_impl.h | 2 +- 9 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/sentry/android/android_sdk.cpp b/src/sentry/android/android_sdk.cpp index 5a7bece22..9d8eeb868 100644 --- a/src/sentry/android/android_sdk.cpp +++ b/src/sentry/android/android_sdk.cpp @@ -335,7 +335,7 @@ SentryScopeImpl *AndroidSDK::create_scope() { SentrySpanImpl *AndroidSDK::create_span(const String &p_name, const Dictionary &p_attributes) { WARN_PRINT_ONCE("Sentry: Spans are not implemented on this platform yet - nothing will be recorded."); - return SentrySpanImpl::noop(); + return SentrySpanImpl::create_noop(); } void AndroidSDK::set_trace(const String &p_trace_id, const String &p_parent_span_id) { diff --git a/src/sentry/cocoa/cocoa_sdk.mm b/src/sentry/cocoa/cocoa_sdk.mm index 25ec9595f..4d53fd634 100644 --- a/src/sentry/cocoa/cocoa_sdk.mm +++ b/src/sentry/cocoa/cocoa_sdk.mm @@ -347,7 +347,7 @@ void _add_default_attachments(SentryObjCScope *p_scope) { SentrySpanImpl *CocoaSDK::create_span(const String &p_name, const Dictionary &p_attributes) { WARN_PRINT_ONCE("Sentry: Spans are not implemented on this platform yet - nothing will be recorded."); - return SentrySpanImpl::noop(); + return SentrySpanImpl::create_noop(); } void CocoaSDK::set_trace(const String &p_trace_id, const String &p_parent_span_id) { diff --git a/src/sentry/javascript/javascript_sdk.cpp b/src/sentry/javascript/javascript_sdk.cpp index 1b024cae0..a96499db8 100644 --- a/src/sentry/javascript/javascript_sdk.cpp +++ b/src/sentry/javascript/javascript_sdk.cpp @@ -311,7 +311,7 @@ SentryScopeImpl *JavaScriptSDK::create_scope() { SentrySpanImpl *JavaScriptSDK::create_span(const String &p_name, const Dictionary &p_attributes) { WARN_PRINT_ONCE("Sentry: Spans are not implemented on this platform yet - nothing will be recorded."); - return SentrySpanImpl::noop(); + return SentrySpanImpl::create_noop(); } void JavaScriptSDK::set_trace(const String &p_trace_id, const String &p_parent_span_id) { diff --git a/src/sentry/native/native_span.cpp b/src/sentry/native/native_span.cpp index 1dc5117a0..52f6fe8a1 100644 --- a/src/sentry/native/native_span.cpp +++ b/src/sentry/native/native_span.cpp @@ -58,7 +58,7 @@ void NativeSpan::end() { SentrySpanImpl *NativeSpan::start_child(const String &p_name, const Dictionary &p_attributes) { if (!_is_live()) { - return SentrySpanImpl::noop(); + return SentrySpanImpl::create_noop(); } sentry_span_t *child = _transaction ? sentry_transaction_start_child(_transaction, _get_op(p_attributes), p_name.utf8()) diff --git a/src/sentry/sentry_sdk.cpp b/src/sentry/sentry_sdk.cpp index 1bf8c9336..37fb70064 100644 --- a/src/sentry/sentry_sdk.cpp +++ b/src/sentry/sentry_sdk.cpp @@ -192,6 +192,8 @@ Ref SentrySDK::start_span(const String &p_name, const Refset_span(span); } return span; diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index ff3b54814..bcd4a01bd 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -8,13 +8,13 @@ namespace sentry { -Ref SentrySpan::noop() { +Ref SentrySpan::create_noop() { return Ref(memnew(SentrySpan(memnew(DisabledSpan)))); } Ref SentrySpan::unassigned() { // FYI: Internal SDK is not initialized yet when this static is created. - static Ref sentinel = noop(); + static Ref sentinel = create_noop(); return sentinel; } diff --git a/src/sentry/sentry_span.h b/src/sentry/sentry_span.h index 6a9b3ad4b..870d75910 100644 --- a/src/sentry/sentry_span.h +++ b/src/sentry/sentry_span.h @@ -37,7 +37,7 @@ class SentrySpan : public RefCounted { public: // Returns a no-op span that does nothing. - static Ref noop(); + static Ref create_noop(); // Returns a sentinel value that indicates an unassigned span. static Ref unassigned(); diff --git a/src/sentry/sentry_span_impl.cpp b/src/sentry/sentry_span_impl.cpp index 0e1cb9e11..d013dabaf 100644 --- a/src/sentry/sentry_span_impl.cpp +++ b/src/sentry/sentry_span_impl.cpp @@ -4,7 +4,7 @@ namespace sentry { -SentrySpanImpl *SentrySpanImpl::noop() { +SentrySpanImpl *SentrySpanImpl::create_noop() { return memnew(DisabledSpan); } diff --git a/src/sentry/sentry_span_impl.h b/src/sentry/sentry_span_impl.h index 38f169e5c..5ef88b1f2 100644 --- a/src/sentry/sentry_span_impl.h +++ b/src/sentry/sentry_span_impl.h @@ -17,7 +17,7 @@ class SentrySpanImpl : public Castable { SENTRY_CASTABLE(SentrySpanImpl, Castable); public: - static SentrySpanImpl *noop(); + static SentrySpanImpl *create_noop(); virtual void set_attribute(const String &p_key, const Variant &p_value) = 0; virtual void set_status(SpanStatus p_status) = 0; From 7cd5cf54844b846ecc3f542eb240195d7e74f7a1 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Thu, 13 Aug 2026 12:18:03 +0200 Subject: [PATCH 17/36] SentryScope::clear() should unref active span --- src/sentry/sentry_scope.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/sentry_scope.cpp b/src/sentry/sentry_scope.cpp index bd3f5986f..c748a8b0e 100644 --- a/src/sentry/sentry_scope.cpp +++ b/src/sentry/sentry_scope.cpp @@ -59,6 +59,7 @@ void SentryScope::add_attachment(const Ref &p_attachment) { void SentryScope::clear() { ERR_SENTRY_THREAD_GUARD(WRONG_THREAD_MSG); + _span.unref(); _impl->clear(); } From ea9df7ba828fd33f0b50995d40a1dbbe85b9b651 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Thu, 13 Aug 2026 12:59:15 +0200 Subject: [PATCH 18/36] Fix crash due to unassigned sentinel outliving ObjectDB --- src/register_types.cpp | 2 ++ .../engine_lifecycle/engine_lifecycle.cpp | 16 +++++++++++++ .../engine_lifecycle/engine_lifecycle.h | 8 +++++++ src/sentry/sentry_span.cpp | 24 ++++++++++++++++--- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/register_types.cpp b/src/register_types.cpp index faf0be341..f6bfcb976 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -2,6 +2,7 @@ #include "sentry/disabled/disabled_event.h" #include "sentry/dotnet/dotnet_before_send_processor.h" #include "sentry/dotnet/dotnet_scope_observer.h" +#include "sentry/engine_lifecycle/engine_lifecycle.h" #include "sentry/engine_lifecycle/sentry_scene_tree_watcher.h" #include "sentry/logging/sentry_godot_logger.h" #include "sentry/processing/screenshot_processor.h" @@ -181,6 +182,7 @@ void uninitialize_module(ModuleInitializationLevel p_level) { if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { SentrySDK::destroy_singleton(); SentryUnit::destroy_singleton(); + engine_lifecycle::notify_module_terminating(); } } diff --git a/src/sentry/engine_lifecycle/engine_lifecycle.cpp b/src/sentry/engine_lifecycle/engine_lifecycle.cpp index ddea2b625..804c9d508 100644 --- a/src/sentry/engine_lifecycle/engine_lifecycle.cpp +++ b/src/sentry/engine_lifecycle/engine_lifecycle.cpp @@ -21,6 +21,9 @@ std::atomic _singletons_ready{ false }; // Shutdown subscribers, notified while script runtime is still alive. LocalVector _shutdown_callbacks; +// Termination subscribers, notified as the extension is torn down. +LocalVector _module_termination_callbacks; + // Whether the lifecycle watch has already been started. bool _watch_started = false; @@ -71,4 +74,17 @@ void remove_shutdown_callback(const Callable &p_callback) { _shutdown_callbacks.erase(p_callback); } +void add_module_termination_callback(const Callable &p_callback) { + _module_termination_callbacks.push_back(p_callback); +} + +void notify_module_terminating() { + for (const Callable &callback : _module_termination_callbacks) { + callback.call(); + } + + _module_termination_callbacks.clear(); + _shutdown_callbacks.clear(); +} + } // namespace sentry::engine_lifecycle diff --git a/src/sentry/engine_lifecycle/engine_lifecycle.h b/src/sentry/engine_lifecycle/engine_lifecycle.h index 573b8817d..1a5e335fc 100644 --- a/src/sentry/engine_lifecycle/engine_lifecycle.h +++ b/src/sentry/engine_lifecycle/engine_lifecycle.h @@ -24,4 +24,12 @@ void add_shutdown_callback(const Callable &p_callback); // Unregisters shutdown callback. void remove_shutdown_callback(const Callable &p_callback); +// Registers a callback to be invoked once when this extension is deinitialized. +// Useful for releasing statics. +void add_module_termination_callback(const Callable &p_callback); + +// Called from register_types.cpp when the module is deinitialized. +// Runs all registered module termination callbacks and then releases all callbacks. +void notify_module_terminating(); + } // namespace sentry::engine_lifecycle diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index bcd4a01bd..283b82b64 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -1,11 +1,24 @@ #include "sentry_span.h" #include "sentry/disabled/disabled_span.h" +#include "sentry/engine_lifecycle/engine_lifecycle.h" #include "sentry_sdk.h" // Needed for VariantCaster +#include + #define WRONG_THREAD_MSG \ "Sentry: Span methods must be called on the thread that created the span." +namespace { + +Ref _unassigned_sentinel; + +void _release_unassigned_sentinel() { + _unassigned_sentinel.unref(); +} + +} // unnamed namespace + namespace sentry { Ref SentrySpan::create_noop() { @@ -13,9 +26,14 @@ Ref SentrySpan::create_noop() { } Ref SentrySpan::unassigned() { - // FYI: Internal SDK is not initialized yet when this static is created. - static Ref sentinel = create_noop(); - return sentinel; + // FYI: Internal SDK is not initialized yet when this is first called, which + // happens while binding methods, since it is the default value for start_span(). + if (_unassigned_sentinel.is_null()) { + _unassigned_sentinel = create_noop(); + // Holding it until process exit would outlive ObjectDB and crash on teardown. + engine_lifecycle::add_module_termination_callback(callable_mp_static(&_release_unassigned_sentinel)); + } + return _unassigned_sentinel; } void SentrySpan::set_attribute(const String &p_key, const Variant &p_value) { From 1525525edfd32d6fedf69917723641fd9919244a Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Thu, 13 Aug 2026 13:15:55 +0200 Subject: [PATCH 19/36] Add tests --- project/test/suites/test_span.gd | 173 +++++++++++++++++++++++++++ project/test/suites/test_span.gd.uid | 1 + 2 files changed, 174 insertions(+) create mode 100644 project/test/suites/test_span.gd create mode 100644 project/test/suites/test_span.gd.uid diff --git a/project/test/suites/test_span.gd b/project/test/suites/test_span.gd new file mode 100644 index 000000000..87f61872d --- /dev/null +++ b/project/test/suites/test_span.gd @@ -0,0 +1,173 @@ +extends SentryTestSuite +## Verifies events captured while a span is active are stamped with that span. + + +# TODO: widen the platform list as spans are implemented on other backends. +func before(_do_skip = OS.get_name() not in ["Windows", "Linux"], + _skip_reason = "Spans are not implemented on this platform yet.") -> void: + super() + + +func _span_id(json: String) -> Variant: + var data: Variant = JSON.parse_string(json) + return data.get("contexts", {}).get("trace", {}).get("span_id") + + +func _trace_id(json: String) -> Variant: + var data: Variant = JSON.parse_string(json) + return data.get("contexts", {}).get("trace", {}).get("trace_id") + + +func test_active_span_stamps_event() -> void: + var json_before: String = await capture_event_and_get_json(SentrySDK.create_event()) + assert_object(SentrySDK.get_active_span()).is_null() + + var span := SentrySDK.start_span("test.active") + assert_object(SentrySDK.get_active_span()).is_same(span) + var json_in_span: String = await capture_event_and_get_json(SentrySDK.create_event()) + + span.end() + assert_object(SentrySDK.get_active_span()).is_null() + var json_after: String = await capture_event_and_get_json(SentrySDK.create_event()) + + assert_json(json_in_span).describe("an active span stamps the event it encloses") \ + .at("/contexts/trace/span_id") \ + .is_not_equal(_span_id(json_before)) \ + .verify() + + assert_json(json_after).describe("ending the span stops it stamping later events") \ + .at("/contexts/trace/span_id") \ + .is_equal(_span_id(json_before)) \ + .verify() + + +func test_events_under_one_span_share_span_id() -> void: + var span := SentrySDK.start_span("test.shared") + assert_object(SentrySDK.get_active_span()).is_same(span) + var json_first: String = await capture_event_and_get_json(SentrySDK.create_event()) + var json_second: String = await capture_event_and_get_json(SentrySDK.create_event()) + span.end() + + assert_json(json_second).describe("events captured under the same span carry the same span_id") \ + .at("/contexts/trace/span_id") \ + .is_equal(_span_id(json_first)) \ + .verify() + + +func test_nested_span_stamps_and_restores_its_parent() -> void: + var parent := SentrySDK.start_span("test.parent") + assert_object(SentrySDK.get_active_span()).is_same(parent) + var json_in_parent: String = await capture_event_and_get_json(SentrySDK.create_event()) + + var child := SentrySDK.start_span("test.child") + assert_object(SentrySDK.get_active_span()).is_same(child) + var json_in_child: String = await capture_event_and_get_json(SentrySDK.create_event()) + + child.end() + assert_object(SentrySDK.get_active_span()).is_same(parent) + var json_after_child: String = await capture_event_and_get_json(SentrySDK.create_event()) + parent.end() + + assert_json(json_in_child).describe("the innermost active span stamps the event") \ + .at("/contexts/trace/span_id") \ + .is_not_equal(_span_id(json_in_parent)) \ + .verify() + + assert_json(json_after_child).describe("ending a child span hands stamping back to its parent") \ + .at("/contexts/trace/span_id") \ + .is_equal(_span_id(json_in_parent)) \ + .verify() + + +func test_inactive_span_does_not_stamp() -> void: + var json_before: String = await capture_event_and_get_json(SentrySDK.create_event()) + + var span := SentrySDK.start_span("test.inactive", null, {}, false) + assert_object(SentrySDK.get_active_span()).is_not_same(span) + var json_alongside: String = await capture_event_and_get_json(SentrySDK.create_event()) + span.end() + + assert_json(json_alongside).describe("a span started with active=false leaves events unstamped") \ + .at("/contexts/trace/span_id") \ + .is_equal(_span_id(json_before)) \ + .verify() + + +func test_forked_scope_inherits_active_span() -> void: + var span := SentrySDK.start_span("test.forked") + SentrySDK.capture_event(SentrySDK.create_event()) + + SentrySDK.with_scope(func(_scope: SentryScope) -> void: + assert_object(SentrySDK.get_active_span()).is_same(span) + SentrySDK.capture_event(SentrySDK.create_event()) + ) + span.end() + + var json_outside: String = await wait_for_captured_event_json() + var json_in_fork: String = await wait_for_captured_event_json() + + assert_json(json_in_fork).describe("a forked scope stamps with the span that was active when it forked") \ + .at("/contexts/trace/span_id") \ + .is_equal(_span_id(json_outside)) \ + .verify() + + +func test_scope_forked_after_span_ended_is_not_stamped() -> void: + var span := SentrySDK.start_span("test.ended_before_fork") + SentrySDK.capture_event(SentrySDK.create_event()) + span.end() + + SentrySDK.with_scope(func(_scope: SentryScope) -> void: + assert_object(SentrySDK.get_active_span()).is_null() + SentrySDK.capture_event(SentrySDK.create_event()) + ) + + var json_in_span: String = await wait_for_captured_event_json() + var json_in_fork: String = await wait_for_captured_event_json() + + assert_json(json_in_fork).describe("a scope forked after the span ended does not inherit its binding") \ + .at("/contexts/trace/span_id") \ + .is_not_equal(_span_id(json_in_span)) \ + .verify() + + +func test_scope_clear_drops_the_span() -> void: + var span := SentrySDK.start_span("test.cleared") + SentrySDK.capture_event(SentrySDK.create_event()) + + SentrySDK.with_scope(func(scope: SentryScope) -> void: + scope.clear() + assert_object(SentrySDK.get_active_span()) \ + .override_failure_message("clear() must drop the active span, or the scope and the backend disagree about it") \ + .is_null() + SentrySDK.capture_event(SentrySDK.create_event()) + ) + span.end() + + var json_in_span: String = await wait_for_captured_event_json() + var json_after_clear: String = await wait_for_captured_event_json() + + assert_json(json_after_clear).describe("clear() unbinds the span, so later events are not stamped with it") \ + .at("/contexts/trace/span_id") \ + .is_not_equal(_span_id(json_in_span)) \ + .verify() + + +func test_span_stays_on_the_current_trace() -> void: + var json_before: String = await capture_event_and_get_json(SentrySDK.create_event()) + + var span := SentrySDK.start_span("test.trace") + var json_in_span: String = await capture_event_and_get_json(SentrySDK.create_event()) + + span.end() + var json_after: String = await capture_event_and_get_json(SentrySDK.create_event()) + + assert_json(json_in_span).describe("starting a span does not move events off the current trace") \ + .at("/contexts/trace/trace_id") \ + .is_equal(_trace_id(json_before)) \ + .verify() + + assert_json(json_after).describe("ending a span does not move later events off the current trace") \ + .at("/contexts/trace/trace_id") \ + .is_equal(_trace_id(json_before)) \ + .verify() diff --git a/project/test/suites/test_span.gd.uid b/project/test/suites/test_span.gd.uid new file mode 100644 index 000000000..d3f057d17 --- /dev/null +++ b/project/test/suites/test_span.gd.uid @@ -0,0 +1 @@ +uid://dymejc5sck73m From 816bd5ee9436a6daf86ca90dfd4cf2ff78624742 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Thu, 13 Aug 2026 13:22:09 +0200 Subject: [PATCH 20/36] Better descriptions --- project/test/suites/test_span.gd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/project/test/suites/test_span.gd b/project/test/suites/test_span.gd index 87f61872d..52eab337b 100644 --- a/project/test/suites/test_span.gd +++ b/project/test/suites/test_span.gd @@ -35,7 +35,7 @@ func test_active_span_stamps_event() -> void: .is_not_equal(_span_id(json_before)) \ .verify() - assert_json(json_after).describe("ending the span stops it stamping later events") \ + assert_json(json_after).describe("events captured after the span ends carry the same id as before it started") \ .at("/contexts/trace/span_id") \ .is_equal(_span_id(json_before)) \ .verify() @@ -138,7 +138,7 @@ func test_scope_clear_drops_the_span() -> void: SentrySDK.with_scope(func(scope: SentryScope) -> void: scope.clear() assert_object(SentrySDK.get_active_span()) \ - .override_failure_message("clear() must drop the active span, or the scope and the backend disagree about it") \ + .override_failure_message("clear() must drop the active span") \ .is_null() SentrySDK.capture_event(SentrySDK.create_event()) ) @@ -147,7 +147,7 @@ func test_scope_clear_drops_the_span() -> void: var json_in_span: String = await wait_for_captured_event_json() var json_after_clear: String = await wait_for_captured_event_json() - assert_json(json_after_clear).describe("clear() unbinds the span, so later events are not stamped with it") \ + assert_json(json_after_clear).describe("later events are not stamped with previously cleared span") \ .at("/contexts/trace/span_id") \ .is_not_equal(_span_id(json_in_span)) \ .verify() From 14f8e5d2519442e123175f947395713daede5046 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Thu, 13 Aug 2026 13:30:33 +0200 Subject: [PATCH 21/36] Implement with_span() --- src/sentry/sentry_sdk.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/sentry/sentry_sdk.cpp b/src/sentry/sentry_sdk.cpp index 37fb70064..9c7a50159 100644 --- a/src/sentry/sentry_sdk.cpp +++ b/src/sentry/sentry_sdk.cpp @@ -200,8 +200,22 @@ Ref SentrySDK::start_span(const String &p_name, const Ref forked_scope = _push_scope(); + Ref active_span = start_span(p_name); + Variant result = p_callable.call(active_span); + static bool first_warning = true; + if (first_warning) { + if (Object *obj = result.get_validated_object(); + unlikely(obj != nullptr && obj->get_class() == "GDScriptFunctionState")) { + first_warning = false; + WARN_PRINT("Sentry: with_span() does not support await - the span is only active until the first await."); + } + } + active_span->end(); + _pop_scope(forked_scope); + return result; } Ref SentrySDK::get_active_span() const { From 9849482cc2fa6175ae6b52e6e7618e3ecbc0d0ce Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Thu, 13 Aug 2026 13:44:26 +0200 Subject: [PATCH 22/36] Add with_span tests --- project/test/suites/test_span.gd | 96 ++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/project/test/suites/test_span.gd b/project/test/suites/test_span.gd index 52eab337b..29f6d97df 100644 --- a/project/test/suites/test_span.gd +++ b/project/test/suites/test_span.gd @@ -171,3 +171,99 @@ func test_span_stays_on_the_current_trace() -> void: .at("/contexts/trace/trace_id") \ .is_equal(_trace_id(json_before)) \ .verify() + + +func test_with_span_forks_the_current_scope() -> void: + SentrySDK.with_span("test.with_span_fork", func(_span: SentrySpan) -> void: + SentrySDK.get_current_scope().set_tag("scoped", "in_span") + SentrySDK.capture_event(SentrySDK.create_event()) + ) + var json_in_span: String = await wait_for_captured_event_json() + + var json_after: String = await capture_event_and_get_json(SentrySDK.create_event()) + + assert_json(json_in_span).describe("set_tag() reaches the event captured inside with_span") \ + .at("/tags") \ + .must_contain("scoped", "in_span") \ + .verify() + + assert_json(json_after).describe("scope writes inside with_span do not outlive it") \ + .at("/tags") \ + .must_not_contain("scoped") \ + .verify() + + +func test_with_span_stamps_events_and_clears_the_slot() -> void: + var json_before: String = await capture_event_and_get_json(SentrySDK.create_event()) + + SentrySDK.with_span("test.with_span", func(span: SentrySpan) -> void: + assert_object(SentrySDK.get_active_span()).is_same(span) + SentrySDK.capture_event(SentrySDK.create_event()) + ) + assert_object(SentrySDK.get_active_span()).is_null() + + var json_inside: String = await wait_for_captured_event_json() + var json_after: String = await capture_event_and_get_json(SentrySDK.create_event()) + + assert_json(json_inside).describe("events captured inside with_span carry its span") \ + .at("/contexts/trace/span_id") \ + .is_not_equal(_span_id(json_before)) \ + .verify() + + assert_json(json_after).describe("events captured after with_span returns carry the same id as before it started") \ + .at("/contexts/trace/span_id") \ + .is_equal(_span_id(json_before)) \ + .verify() + + +func test_with_span_returns_the_callable_result() -> void: + var result: Variant = SentrySDK.with_span("test.with_span_result", func(_span: SentrySpan) -> int: + return 42 + ) + + assert_int(result).is_equal(42) + + +func test_nested_with_span_stamps_with_the_inner_span() -> void: + SentrySDK.with_span("test.with_span_outer", func(outer: SentrySpan) -> void: + SentrySDK.capture_event(SentrySDK.create_event()) + SentrySDK.with_span("test.with_span_inner", func(_inner: SentrySpan) -> void: + SentrySDK.capture_event(SentrySDK.create_event()) + ) + assert_object(SentrySDK.get_active_span()).is_same(outer) + ) + + var json_outer: String = await wait_for_captured_event_json() + var json_inner: String = await wait_for_captured_event_json() + + assert_json(json_inner).describe("a nested with_span stamps events with the inner span") \ + .at("/contexts/trace/span_id") \ + .is_not_equal(_span_id(json_outer)) \ + .verify() + + +func test_with_span_inside_a_started_span_nests_under_it() -> void: + var span := SentrySDK.start_span("test.manual_parent") + SentrySDK.capture_event(SentrySDK.create_event()) + + SentrySDK.with_span("test.with_span_child", func(_child: SentrySpan) -> void: + SentrySDK.capture_event(SentrySDK.create_event()) + ) + assert_object(SentrySDK.get_active_span()).is_same(span) + + SentrySDK.capture_event(SentrySDK.create_event()) + span.end() + + var json_in_parent: String = await wait_for_captured_event_json() + var json_in_child: String = await wait_for_captured_event_json() + var json_after_child: String = await wait_for_captured_event_json() + + assert_json(json_in_child).describe("with_span inside an active span stamps with its own span") \ + .at("/contexts/trace/span_id") \ + .is_not_equal(_span_id(json_in_parent)) \ + .verify() + + assert_json(json_after_child).describe("the enclosing span stamps again once with_span returns") \ + .at("/contexts/trace/span_id") \ + .is_equal(_span_id(json_in_parent)) \ + .verify() From 4fe2686869da8a359bd610eaac74aec80f752a33 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Thu, 13 Aug 2026 14:01:33 +0200 Subject: [PATCH 23/36] Thread guard --- src/sentry/sentry_sdk.cpp | 4 ++++ src/sentry/sentry_span.cpp | 1 + 2 files changed, 5 insertions(+) diff --git a/src/sentry/sentry_sdk.cpp b/src/sentry/sentry_sdk.cpp index 9c7a50159..2f2376303 100644 --- a/src/sentry/sentry_sdk.cpp +++ b/src/sentry/sentry_sdk.cpp @@ -187,6 +187,10 @@ Ref SentrySDK::start_span(const String &p_name, const Ref span; if (parent.is_valid()) { span = parent->start_child(p_name, p_attributes); + if (span.is_null()) { + // start_child() already reported why. + return span; + } } else { span = Ref(memnew(SentrySpan(p_name, p_attributes))); } diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index 283b82b64..95ff0d555 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -68,6 +68,7 @@ void SentrySpan::end() { } Ref SentrySpan::start_child(const String &p_name, const Dictionary &p_attributes) { + ERR_SENTRY_THREAD_GUARD_V(Ref(), WRONG_THREAD_MSG); ERR_FAIL_COND_V_MSG(p_name.is_empty(), Ref(), "Sentry: Can't start a child span with an empty name."); SentrySpanImpl *child_impl = _impl->start_child(p_name, p_attributes); return memnew(SentrySpan(child_impl)); From cb064ec44a925fa7b1cf8ab1139657cf4400e265 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Thu, 13 Aug 2026 14:06:10 +0200 Subject: [PATCH 24/36] Put attributes before parent_span in start_span() --- project/test/suites/test_span.gd | 2 +- src/sentry/sentry_sdk.cpp | 4 ++-- src/sentry/sentry_sdk.h | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/project/test/suites/test_span.gd b/project/test/suites/test_span.gd index 29f6d97df..bb936dd30 100644 --- a/project/test/suites/test_span.gd +++ b/project/test/suites/test_span.gd @@ -82,7 +82,7 @@ func test_nested_span_stamps_and_restores_its_parent() -> void: func test_inactive_span_does_not_stamp() -> void: var json_before: String = await capture_event_and_get_json(SentrySDK.create_event()) - var span := SentrySDK.start_span("test.inactive", null, {}, false) + var span := SentrySDK.start_span("test.inactive", {}, null, false) assert_object(SentrySDK.get_active_span()).is_not_same(span) var json_alongside: String = await capture_event_and_get_json(SentrySDK.create_event()) span.end() diff --git a/src/sentry/sentry_sdk.cpp b/src/sentry/sentry_sdk.cpp index 2f2376303..0c525bde3 100644 --- a/src/sentry/sentry_sdk.cpp +++ b/src/sentry/sentry_sdk.cpp @@ -175,7 +175,7 @@ Variant SentrySDK::with_scope(const Callable &p_callable) { return result; } -Ref SentrySDK::start_span(const String &p_name, const Ref &p_parent_span, const Dictionary &p_attributes, bool p_active) { +Ref SentrySDK::start_span(const String &p_name, const Dictionary &p_attributes, const Ref &p_parent_span, bool p_active) { ERR_FAIL_COND_V_MSG(p_name.is_empty(), Ref(), "Sentry: Can't start a span with an empty name."); // The unassigned sentinel means "inherit the active span", while an explicit null forces a segment (new root-level span). @@ -657,7 +657,7 @@ void SentrySDK::_bind_methods() { ClassDB::bind_method(D_METHOD("get_current_scope"), &SentrySDK::get_current_scope); ClassDB::bind_method(D_METHOD("with_scope", "callable"), &SentrySDK::with_scope); - ClassDB::bind_method(D_METHOD("start_span", "name", "parent_span", "attributes", "active"), &SentrySDK::start_span, DEFVAL(SentrySpan::unassigned()), DEFVAL(Dictionary()), DEFVAL(true)); + ClassDB::bind_method(D_METHOD("start_span", "name", "attributes", "parent_span", "active"), &SentrySDK::start_span, DEFVAL(Dictionary()), DEFVAL(SentrySpan::unassigned()), DEFVAL(true)); ClassDB::bind_method(D_METHOD("with_span", "name", "callable"), &SentrySDK::with_span); ClassDB::bind_method(D_METHOD("get_active_span"), &SentrySDK::get_active_span); diff --git a/src/sentry/sentry_sdk.h b/src/sentry/sentry_sdk.h index c8848befd..ccdc1d93a 100644 --- a/src/sentry/sentry_sdk.h +++ b/src/sentry/sentry_sdk.h @@ -133,8 +133,8 @@ class SentrySDK : public Object { // * Spans - Ref start_span(const String &p_name, const Ref &p_parent_span = SentrySpan::unassigned(), - const Dictionary &p_attributes = {}, bool p_active = true); + Ref start_span(const String &p_name, const Dictionary &p_attributes = {}, + const Ref &p_parent_span = SentrySpan::unassigned(), bool p_active = true); Variant with_span(const String &p_name, const Callable &p_callable); Ref get_active_span() const; From 9423b89fa59fb1ce9b7d98b03b1b2ca766f57578 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Thu, 13 Aug 2026 16:00:53 +0200 Subject: [PATCH 25/36] Fork the scope when starting an active span --- src/sentry/sentry_sdk.cpp | 39 ++++++++++++++++++++++++++++---------- src/sentry/sentry_sdk.h | 6 +++++- src/sentry/sentry_span.cpp | 9 +++++++++ src/sentry/sentry_span.h | 11 +++++++++++ 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/sentry/sentry_sdk.cpp b/src/sentry/sentry_sdk.cpp index 0c525bde3..d4b1f0b0a 100644 --- a/src/sentry/sentry_sdk.cpp +++ b/src/sentry/sentry_sdk.cpp @@ -156,12 +156,20 @@ Ref SentrySDK::get_current_scope() const { return current_scopes.back()->get(); } +Ref SentrySDK::_push_scope(const Ref &p_source) { + constexpr int SCOPE_DEPTH_WARNING_THRESHOLD = 64; + if (unlikely(current_scopes.size() >= SCOPE_DEPTH_WARNING_THRESHOLD)) { + WARN_PRINT_ONCE("Sentry: Scope stack is growing unusually deep. This may indicate that spans are not being ended."); + } + return current_scopes.push_back(p_source->clone())->get(); +} + Variant SentrySDK::with_scope(const Callable &p_callable) { if (unlikely(!internal_sdk->supports_scopes())) { WARN_PRINT_ONCE("Sentry: Scopes are not supported on this platform yet - writes to the scope will be discarded."); } - Ref scope = _push_scope(); + Ref scope = _push_scope(get_current_scope()); Variant result = p_callable.call(scope); static bool first_warning = true; if (first_warning) { @@ -179,10 +187,8 @@ Ref SentrySDK::start_span(const String &p_name, const Dictionary &p_ ERR_FAIL_COND_V_MSG(p_name.is_empty(), Ref(), "Sentry: Can't start a span with an empty name."); // The unassigned sentinel means "inherit the active span", while an explicit null forces a segment (new root-level span). - Ref parent = p_parent_span; - if (parent == SentrySpan::unassigned()) { - parent = get_active_span(); - } + const bool parent_given = p_parent_span != SentrySpan::unassigned(); + Ref parent = parent_given ? p_parent_span : get_active_span(); Ref span; if (parent.is_valid()) { @@ -196,17 +202,31 @@ Ref SentrySDK::start_span(const String &p_name, const Dictionary &p_ } if (p_active) { - // PONDERING: I'm not sure if the current scope should be forked here. - // If the caller forgets to call end(), the scope would stick around until the current thread dies. - get_current_scope()->set_span(span); + // When a new span is started, the current scope of the parent span MUST be forked. + Ref source; + if (parent_given && parent.is_valid()) { + source = parent->get_associated_scope(); + } + if (source.is_null()) { + // The parent was never active, or is no longer. + source = get_current_scope(); + } + Ref forked_scope = _push_scope(source); + forked_scope->set_span(span); + span->set_associated_scope(forked_scope); } return span; } +void SentrySDK::notify_span_ended(const SentrySpan *p_span) { + if (Ref scope = p_span->get_associated_scope(); scope.is_valid()) { + _pop_scope(scope); + } +} + Variant SentrySDK::with_span(const String &p_name, const Callable &p_callable) { ERR_FAIL_COND_V_MSG(p_name.is_empty(), Variant(), "Sentry: Can't start a span with an empty name."); - Ref forked_scope = _push_scope(); Ref active_span = start_span(p_name); Variant result = p_callable.call(active_span); static bool first_warning = true; @@ -218,7 +238,6 @@ Variant SentrySDK::with_span(const String &p_name, const Callable &p_callable) { } } active_span->end(); - _pop_scope(forked_scope); return result; } diff --git a/src/sentry/sentry_sdk.h b/src/sentry/sentry_sdk.h index ccdc1d93a..264afe375 100644 --- a/src/sentry/sentry_sdk.h +++ b/src/sentry/sentry_sdk.h @@ -73,7 +73,7 @@ class SentrySDK : public Object { // Marks every thread's scope stack as stale. void _invalidate_scopes(); - _FORCE_INLINE_ Ref _push_scope() { return current_scopes.push_back(Ref(get_current_scope()->clone()))->get(); } + Ref _push_scope(const Ref &p_source); _FORCE_INLINE_ void _pop_scope(const Ref &p_scope) { current_scopes.erase(p_scope); } protected: @@ -144,10 +144,14 @@ class SentrySDK : public Object { void unset_before_send() { options->set_before_send(Callable()); } Callable get_before_send() { return options->get_before_send(); } + // * Not exposed in the public API + void prepare_and_auto_initialize(); _FORCE_INLINE_ TraceContext get_trace_context() const { return trace_context; } + void notify_span_ended(const SentrySpan *p_span); + SentrySDK(); ~SentrySDK(); }; diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index 95ff0d555..9407bb86a 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -64,9 +64,18 @@ void SentrySpan::end() { return; } _ended = true; + SentrySDK::get_singleton()->notify_span_ended(this); _impl->end(); } +void SentrySpan::set_associated_scope(const Ref &p_scope) { + _scope_id = p_scope.is_valid() ? p_scope->get_instance_id() : 0; +} + +Ref SentrySpan::get_associated_scope() const { + return Ref(Object::cast_to(ObjectDB::get_instance(_scope_id))); +} + Ref SentrySpan::start_child(const String &p_name, const Dictionary &p_attributes) { ERR_SENTRY_THREAD_GUARD_V(Ref(), WRONG_THREAD_MSG); ERR_FAIL_COND_V_MSG(p_name.is_empty(), Ref(), "Sentry: Can't start a child span with an empty name."); diff --git a/src/sentry/sentry_span.h b/src/sentry/sentry_span.h index 870d75910..c8f90d002 100644 --- a/src/sentry/sentry_span.h +++ b/src/sentry/sentry_span.h @@ -10,6 +10,8 @@ using namespace godot; namespace sentry { +class SentryScope; + // Godot-exported representation of a Sentry span. // Platform-specific behavior is provided by SentrySpanImpl subclasses. class SentrySpan : public RefCounted { @@ -28,6 +30,8 @@ class SentrySpan : public RefCounted { // Scopes resolve their slot through this chain, so it must outlive this span's end(). Ref _previous; + uint64_t _scope_id = 0; + bool _ended = false; SENTRY_THREAD_OWNER; @@ -57,6 +61,13 @@ class SentrySpan : public RefCounted { _FORCE_INLINE_ void set_previous(const Ref &p_span) { _previous = p_span; } _FORCE_INLINE_ Ref get_previous() const { return _previous; } + // Stores the ID of the scope fork created when this span becomes active. + // The forked scope is stored as a weak reference, because the scope holds this span strongly. + void set_associated_scope(const Ref &p_scope); + + // Returns null if this span was never active, or if its fork is already gone (unlikely). + Ref get_associated_scope() const; + SentrySpanImpl *get_implementation() const { return _impl; } SentrySpan(); From d1d1753136b90dbf12bf2c16a8d17033605d55e1 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Thu, 13 Aug 2026 16:25:11 +0200 Subject: [PATCH 26/36] Add span scope tests --- project/test/suites/test_span.gd | 120 +++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 20 deletions(-) diff --git a/project/test/suites/test_span.gd b/project/test/suites/test_span.gd index bb936dd30..50acae668 100644 --- a/project/test/suites/test_span.gd +++ b/project/test/suites/test_span.gd @@ -8,6 +8,11 @@ func before(_do_skip = OS.get_name() not in ["Windows", "Linux"], super() +func after_test() -> void: + super() + SentrySDK.get_current_scope().clear() + + func _span_id(json: String) -> Variant: var data: Variant = JSON.parse_string(json) return data.get("contexts", {}).get("trace", {}).get("span_id") @@ -131,6 +136,81 @@ func test_scope_forked_after_span_ended_is_not_stamped() -> void: .verify() +func test_span_inherits_the_scope_it_started_from() -> void: + SentrySDK.get_current_scope().set_tag("before_span", "current") + + var span := SentrySDK.start_span("test.inherits_scope") + var json_in_span: String = await capture_event_and_get_json(SentrySDK.create_event()) + span.end() + + assert_json(json_in_span).describe("a span carries scope data written before it started") \ + .at("/tags") \ + .must_contain("before_span", "current") \ + .verify() + + +func test_scope_write_inside_a_span_does_not_outlive_it() -> void: + var span := SentrySDK.start_span("test.scope_write") + SentrySDK.get_current_scope().set_tag("spanned", "in_span") + SentrySDK.capture_event(SentrySDK.create_event()) + span.end() + + var json_in_span: String = await wait_for_captured_event_json() + var json_after: String = await capture_event_and_get_json(SentrySDK.create_event()) + + assert_json(json_in_span).describe("set_tag() on the current scope reaches the event captured inside the span") \ + .at("/tags") \ + .must_contain("spanned", "in_span") \ + .verify() + + assert_json(json_after).describe("scope writes made while a span was active do not outlive it") \ + .at("/tags") \ + .must_not_contain("spanned") \ + .verify() + + +func test_ending_spans_leaves_the_scope_stack_where_it_started() -> void: + SentrySDK.get_current_scope().set_tag("root", "root_scope") + + for i in 4: + var span := SentrySDK.start_span("test.balanced_" + str(i)) + SentrySDK.get_current_scope().set_tag("span_" + str(i), "fork") + span.end() + + var json_after: String = await capture_event_and_get_json(SentrySDK.create_event()) + + assert_json(json_after).describe("the scope in effect after a batch of spans is the one they started from") \ + .at("/tags") \ + .must_contain("root", "root_scope") \ + .must_not_contain("span_0") \ + .must_not_contain("span_1") \ + .must_not_contain("span_2") \ + .must_not_contain("span_3") \ + .verify() + + +func test_explicit_parent_inherits_the_parents_scope() -> void: + var parent := SentrySDK.start_span("test.explicit_parent") + SentrySDK.get_current_scope().set_tag("parent_tag", "parent") + + var unrelated := SentrySDK.start_span("test.unrelated") + SentrySDK.get_current_scope().set_tag("unrelated_tag", "unrelated") + + var child := SentrySDK.start_span("test.explicit_child", {}, parent) + SentrySDK.capture_event(SentrySDK.create_event()) + child.end() + unrelated.end() + parent.end() + + var json_in_child: String = await wait_for_captured_event_json() + + assert_json(json_in_child).describe("an explicitly parented span forks the parent's scope rather than the caller's") \ + .at("/tags") \ + .must_contain("parent_tag", "parent") \ + .must_not_contain("unrelated_tag") \ + .verify() + + func test_scope_clear_drops_the_span() -> void: var span := SentrySDK.start_span("test.cleared") SentrySDK.capture_event(SentrySDK.create_event()) @@ -173,26 +253,6 @@ func test_span_stays_on_the_current_trace() -> void: .verify() -func test_with_span_forks_the_current_scope() -> void: - SentrySDK.with_span("test.with_span_fork", func(_span: SentrySpan) -> void: - SentrySDK.get_current_scope().set_tag("scoped", "in_span") - SentrySDK.capture_event(SentrySDK.create_event()) - ) - var json_in_span: String = await wait_for_captured_event_json() - - var json_after: String = await capture_event_and_get_json(SentrySDK.create_event()) - - assert_json(json_in_span).describe("set_tag() reaches the event captured inside with_span") \ - .at("/tags") \ - .must_contain("scoped", "in_span") \ - .verify() - - assert_json(json_after).describe("scope writes inside with_span do not outlive it") \ - .at("/tags") \ - .must_not_contain("scoped") \ - .verify() - - func test_with_span_stamps_events_and_clears_the_slot() -> void: var json_before: String = await capture_event_and_get_json(SentrySDK.create_event()) @@ -216,6 +276,26 @@ func test_with_span_stamps_events_and_clears_the_slot() -> void: .verify() +func test_with_span_tolerates_the_callable_ending_the_span() -> void: + SentrySDK.with_scope(func(scope: SentryScope) -> void: + scope.set_tag("enclosing", "scope") + + SentrySDK.with_span("test.with_span_early_end", func(span: SentrySpan) -> void: + span.end() + assert_object(SentrySDK.get_active_span()).is_null() + ) + + SentrySDK.capture_event(SentrySDK.create_event()) + ) + + var json_after: String = await wait_for_captured_event_json() + + assert_json(json_after).describe("a callable that ends its own span leaves the enclosing scope intact") \ + .at("/tags") \ + .must_contain("enclosing", "scope") \ + .verify() + + func test_with_span_returns_the_callable_result() -> void: var result: Variant = SentrySDK.with_span("test.with_span_result", func(_span: SentrySpan) -> int: return 42 From 8ad10e8913405ddcec34ccce459633abd703f0d5 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 14 Aug 2026 11:17:07 +0200 Subject: [PATCH 27/36] Corrections --- src/sentry/sentry_sdk.cpp | 2 +- src/sentry/sentry_span.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sentry/sentry_sdk.cpp b/src/sentry/sentry_sdk.cpp index d4b1f0b0a..09dfcee6f 100644 --- a/src/sentry/sentry_sdk.cpp +++ b/src/sentry/sentry_sdk.cpp @@ -159,7 +159,7 @@ Ref SentrySDK::get_current_scope() const { Ref SentrySDK::_push_scope(const Ref &p_source) { constexpr int SCOPE_DEPTH_WARNING_THRESHOLD = 64; if (unlikely(current_scopes.size() >= SCOPE_DEPTH_WARNING_THRESHOLD)) { - WARN_PRINT_ONCE("Sentry: Scope stack is growing unusually deep. This may indicate that spans are not being ended."); + WARN_PRINT_ONCE("Sentry: Scope stack is growing unusually deep. This may indicate that spans are not being ended, or that with_scope() calls are nesting without bound."); } return current_scopes.push_back(p_source->clone())->get(); } diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index 9407bb86a..a514a2b9f 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -64,7 +64,9 @@ void SentrySpan::end() { return; } _ended = true; - SentrySDK::get_singleton()->notify_span_ended(this); + if (SentrySDK *sdk = SentrySDK::get_singleton()) { + sdk->notify_span_ended(this); + } _impl->end(); } From 4074e7f718eee6f5c917faf18a08103d70c5d0ec Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 14 Aug 2026 11:32:06 +0200 Subject: [PATCH 28/36] Add class docs --- doc_classes/SentrySDK.xml | 48 +++++++++++++++++++++++++++ doc_classes/SentrySpan.xml | 67 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 doc_classes/SentrySpan.xml diff --git a/doc_classes/SentrySDK.xml b/doc_classes/SentrySDK.xml index 38c28bede..8991daf36 100644 --- a/doc_classes/SentrySDK.xml +++ b/doc_classes/SentrySDK.xml @@ -72,6 +72,14 @@ Creates a new [SentryEvent] object. You can capture the event with [method SentrySDK.capture_event]. + + + + Returns the span currently bound to the calling thread's current scope, or [code]null[/code] if no span is active. Events captured while a span is active are associated with that span's trace context. + Active spans are created by [method SentrySDK.start_span] and [method SentrySDK.with_span]. When an active span ends, the SDK restores the previously active span, if any. + [b]Note:[/b] The returned span belongs to the calling thread. Span methods must be called from the thread that created the span. + + @@ -185,6 +193,28 @@ Assigns user data. See [SentryUser]. + + + + + + + + Starts a span named [param name] and returns it. Use the returned [SentrySpan] to attach attributes, set a status, and call [method SentrySpan.end] when the measured operation finishes. + If [param active] is [code]true[/code], the SDK forks the current scope and makes the new span active on the fork, so telemetry captured while the span is active is associated with it. Writes to the current scope during the span are discarded when the span ends, and the scope that was current when the span started becomes current again. Pass [code]false[/code] to record a span without making it current. An inactive span is not bound to a scope, so it does not affect [method SentrySDK.get_active_span] and does not stamp telemetry captured alongside it. + [codeblock] + var span := SentrySDK.start_span("load_level", { + "sentry.op": "asset.load", + "level": level_name + }) + load_level(level_name) + span.set_status(SentrySpan.SPAN_STATUS_OK) + span.end() + [/codeblock] + When [param parent_span] is omitted, the new span becomes a child of [method SentrySDK.get_active_span] if one exists; otherwise it starts a new root span. Pass [code]null[/code] explicitly to force a new root span. Pass a [SentrySpan] to create the new span under that parent, even if another span is currently active. When such a span is active, it forks the explicit parent's scope instead of the current scope, so it inherits the parent's scope data. + The [param attributes] dictionary is added to the span when it starts. The special [code]sentry.op[/code] attribute sets the Sentry operation name used for the span. Empty span names are rejected. Empty attribute keys are skipped and reported as errors. + + @@ -212,6 +242,24 @@ [b]Note:[/b] The SDK does not support scopes on macOS and iOS yet. On those platforms it still captures telemetry, but discards the data written to the forked scope and prints a warning. + + + + + + Starts an active span named [param name], calls [param callable] with that [SentrySpan], ends the span when the callable returns, and returns the callable's return value. + Use this helper when the operation fits in one synchronous callable. Use [method SentrySDK.start_span] instead when you need to set initial attributes, choose a parent span, keep a span open across multiple functions, or create an inactive span. + [codeblock] + var result: Variant = SentrySDK.with_span("generate_chunk", func(span: SentrySpan) -> Variant: + span.set_attribute("chunk_x", chunk_x) + span.set_attribute("chunk_y", chunk_y) + return generate_chunk(chunk_x, chunk_y) + ) + [/codeblock] + Nested calls create nested spans. When the callable returns, the enclosing active span and scope are restored. + [b]Note:[/b] [method SentrySDK.with_span] covers the synchronous part of [param callable] only. If the callable awaits, the span is ended at the first [code]await[/code] and a warning is printed. + + diff --git a/doc_classes/SentrySpan.xml b/doc_classes/SentrySpan.xml new file mode 100644 index 000000000..51c0f945e --- /dev/null +++ b/doc_classes/SentrySpan.xml @@ -0,0 +1,67 @@ + + + + Represents a timed operation in a Sentry trace. + + + A span measures one operation and records metadata about that operation. Spans can be nested, letting Sentry show how work is structured inside a trace. + Create spans with [method SentrySDK.start_span] or [method SentrySDK.with_span]. While a span is active, events captured on the same thread are associated with that span's trace context. You can add details with [method set_attribute] and [method set_attributes], set the outcome with [method set_status], and finish the span with [method end]. + [codeblock] + SentrySDK.with_span("load_level", func(span: SentrySpan) -> void: + span.set_attribute("level", level_name) + load_level(level_name) + span.set_status(SentrySpan.SPAN_STATUS_OK) + ) + [/codeblock] + Always end spans created with [method SentrySDK.start_span]. If a span is dropped before [method end] is called, the SDK may discard it instead of sending a completed duration. [method SentrySDK.with_span] ends the span for you. + [b]Note:[/b] Spans are thread-local. Call span methods only from the thread that created the span. + [b]Note:[/b] On platforms where spans are not implemented yet, the SDK returns a no-op span and prints a one-time warning. Calling methods on that span is safe, but no span is recorded. + + + + + + + + Finishes the span and records its duration. If this span is active, ending it removes the scope fork created for the span and restores the previous active span, if there was one. + Calling [method end] more than once is safe; calls after the first one do nothing. + + + + + + + + Sets one attribute on the span. Attributes are searchable metadata that help describe the operation, such as the level being loaded, the asset path, or a gameplay flag. + The [param key] must not be empty. Supported value types are [bool], [int], [float], and [String]. Other types will be stringified. + + + + + + + Sets multiple attributes on the span from [param attributes]. Each dictionary entry becomes one span attribute. + Empty keys are skipped and reported as errors. Values use the same conversion rules as [method set_attribute]. + + + + + + + Sets the outcome of the span. Use [constant SentrySpan.SPAN_STATUS_OK] for successful operations and [constant SentrySpan.SPAN_STATUS_ERROR] for failed operations. + If no status is set, the span remains [constant SentrySpan.SPAN_STATUS_UNSET]. Clearing a status by passing [constant SentrySpan.SPAN_STATUS_UNSET] after a span has started is not supported on all platforms and may be ignored with a warning. + + + + + + No explicit status has been set for the span. + + + The operation completed successfully. + + + The operation failed. + + + From 4b61e90718f814e8a4c44c681c55c41bf8701a19 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 14 Aug 2026 11:54:46 +0200 Subject: [PATCH 29/36] Restrict span attributes to supported value types Span attributes went through variant_to_sentry_value, so dictionaries and arrays were serialized structurally while every other attribute surface stringifies them. Documenting that would promise behavior the span-first attribute protocol will not keep. Split the type switch out of variant_to_attribute so spans can reuse it without the type tag that belongs on logs and metrics. --- src/sentry/native/native_span.cpp | 4 ++-- src/sentry/native/native_util.cpp | 14 +++++++++----- src/sentry/native/native_util.h | 4 ++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/sentry/native/native_span.cpp b/src/sentry/native/native_span.cpp index 52f6fe8a1..0451444cc 100644 --- a/src/sentry/native/native_span.cpp +++ b/src/sentry/native/native_span.cpp @@ -21,9 +21,9 @@ void NativeSpan::set_attribute(const String &p_key, const Variant &p_value) { return; } if (_transaction) { - sentry_transaction_set_data(_transaction, p_key.utf8(), variant_to_sentry_value(p_value)); + sentry_transaction_set_data(_transaction, p_key.utf8(), variant_to_attribute_value(p_value)); } else { - sentry_span_set_data(_span, p_key.utf8(), variant_to_sentry_value(p_value)); + sentry_span_set_data(_span, p_key.utf8(), variant_to_attribute_value(p_value)); } } diff --git a/src/sentry/native/native_util.cpp b/src/sentry/native/native_util.cpp index f02f14177..b2478e59a 100644 --- a/src/sentry/native/native_util.cpp +++ b/src/sentry/native/native_util.cpp @@ -157,23 +157,27 @@ Level cstring_to_level(const CharString &p_cstring) { } } -sentry_value_t variant_to_attribute(const Variant &p_value) { +sentry_value_t variant_to_attribute_value(const Variant &p_value) { switch (p_value.get_type()) { case Variant::BOOL: { - return sentry_value_new_attribute(sentry_value_new_bool((bool)p_value), NULL); + return sentry_value_new_bool((bool)p_value); } break; case Variant::INT: { - return sentry_value_new_attribute(sentry_value_new_int64((int64_t)p_value), NULL); + return sentry_value_new_int64((int64_t)p_value); } break; case Variant::FLOAT: { - return sentry_value_new_attribute(sentry_value_new_double((double)p_value), NULL); + return sentry_value_new_double((double)p_value); } break; default: { - return sentry_value_new_attribute(sentry_value_new_string(p_value.stringify().utf8()), NULL); + return sentry_value_new_string(p_value.stringify().utf8()); } break; } } +sentry_value_t variant_to_attribute(const Variant &p_value) { + return sentry_value_new_attribute(variant_to_attribute_value(p_value), NULL); +} + sentry_value_t dictionary_to_attributes(const Dictionary &p_attributes) { if (p_attributes.is_empty()) { return sentry_value_new_null(); diff --git a/src/sentry/native/native_util.h b/src/sentry/native/native_util.h index 4011451de..9277b599f 100644 --- a/src/sentry/native/native_util.h +++ b/src/sentry/native/native_util.h @@ -38,6 +38,10 @@ _FORCE_INLINE_ void sentry_value_set_or_remove_string_by_key(sentry_value_t valu } } +// Converts a Variant to an attribute-supported value type, stringifying unsupported types. +// Returns the raw value for use where no attribute type tag is stored. +sentry_value_t variant_to_attribute_value(const Variant &p_value); + sentry_value_t variant_to_attribute(const Variant &p_value); sentry_value_t dictionary_to_attributes(const Dictionary &p_attributes); From 11d7983adc085a3d58ecea5afb572b2af2680cbb Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 14 Aug 2026 12:12:35 +0200 Subject: [PATCH 30/36] Doc fixes --- doc_classes/SentrySDK.xml | 2 ++ doc_classes/SentrySpan.xml | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/doc_classes/SentrySDK.xml b/doc_classes/SentrySDK.xml index 8991daf36..d25375d89 100644 --- a/doc_classes/SentrySDK.xml +++ b/doc_classes/SentrySDK.xml @@ -213,6 +213,7 @@ [/codeblock] When [param parent_span] is omitted, the new span becomes a child of [method SentrySDK.get_active_span] if one exists; otherwise it starts a new root span. Pass [code]null[/code] explicitly to force a new root span. Pass a [SentrySpan] to create the new span under that parent, even if another span is currently active. When such a span is active, it forks the explicit parent's scope instead of the current scope, so it inherits the parent's scope data. The [param attributes] dictionary is added to the span when it starts. The special [code]sentry.op[/code] attribute sets the Sentry operation name used for the span. Empty span names are rejected. Empty attribute keys are skipped and reported as errors. + [b]Note:[/b] The SDK does not support spans on macOS, iOS, Android and Web yet. On those platforms it returns a no-op span and prints a warning. @@ -258,6 +259,7 @@ [/codeblock] Nested calls create nested spans. When the callable returns, the enclosing active span and scope are restored. [b]Note:[/b] [method SentrySDK.with_span] covers the synchronous part of [param callable] only. If the callable awaits, the span is ended at the first [code]await[/code] and a warning is printed. + [b]Note:[/b] The SDK does not support spans on macOS, iOS, Android and Web yet. On those platforms it returns a no-op span and prints a warning. diff --git a/doc_classes/SentrySpan.xml b/doc_classes/SentrySpan.xml index 51c0f945e..ef16bf0be 100644 --- a/doc_classes/SentrySpan.xml +++ b/doc_classes/SentrySpan.xml @@ -13,7 +13,7 @@ span.set_status(SentrySpan.SPAN_STATUS_OK) ) [/codeblock] - Always end spans created with [method SentrySDK.start_span]. If a span is dropped before [method end] is called, the SDK may discard it instead of sending a completed duration. [method SentrySDK.with_span] ends the span for you. + Always end spans created with [method SentrySDK.start_span]. A span that never ends is never sent, and an active one keeps its scope fork alive, so spans left unended accumulate on the thread's scope stack until the SDK warns that it has grown unusually deep. [method SentrySDK.with_span] ends the span for you. [b]Note:[/b] Spans are thread-local. Call span methods only from the thread that created the span. [b]Note:[/b] On platforms where spans are not implemented yet, the SDK returns a no-op span and prints a one-time warning. Calling methods on that span is safe, but no span is recorded. @@ -24,7 +24,7 @@ Finishes the span and records its duration. If this span is active, ending it removes the scope fork created for the span and restores the previous active span, if there was one. - Calling [method end] more than once is safe; calls after the first one do nothing. + Calling [method end] more than once is safe. Calls after the first one do nothing. @@ -49,13 +49,13 @@ Sets the outcome of the span. Use [constant SentrySpan.SPAN_STATUS_OK] for successful operations and [constant SentrySpan.SPAN_STATUS_ERROR] for failed operations. - If no status is set, the span remains [constant SentrySpan.SPAN_STATUS_UNSET]. Clearing a status by passing [constant SentrySpan.SPAN_STATUS_UNSET] after a span has started is not supported on all platforms and may be ignored with a warning. + Sentry reports a span that was never given a status as successful, so set [constant SentrySpan.SPAN_STATUS_ERROR] on the paths that fail. Clearing a status by passing [constant SentrySpan.SPAN_STATUS_UNSET] after a span has started is not supported on all platforms and may be ignored with a warning. - No explicit status has been set for the span. + No explicit status has been set for the span. Sentry reports such a span as successful. The operation completed successfully. From 4d5b61a29ebe5ed068eaaf4a8bdf52df2358bffb Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 14 Aug 2026 12:50:50 +0200 Subject: [PATCH 31/36] Main thread note --- src/sentry/engine_lifecycle/engine_lifecycle.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sentry/engine_lifecycle/engine_lifecycle.h b/src/sentry/engine_lifecycle/engine_lifecycle.h index 1a5e335fc..def1c63ca 100644 --- a/src/sentry/engine_lifecycle/engine_lifecycle.h +++ b/src/sentry/engine_lifecycle/engine_lifecycle.h @@ -18,14 +18,14 @@ void mark_engine_singletons_as_ready(); bool are_engine_singletons_ready(); // Registers a callback to be invoked once when the engine begins shutting down, -// shortly before the script runtime is torn down. +// shortly before the script runtime is torn down. Must be called from the main thread. void add_shutdown_callback(const Callable &p_callback); -// Unregisters shutdown callback. +// Unregisters shutdown callback. Must be called from the main thread. void remove_shutdown_callback(const Callable &p_callback); // Registers a callback to be invoked once when this extension is deinitialized. -// Useful for releasing statics. +// Useful for releasing statics. Must be called from the main thread. void add_module_termination_callback(const Callable &p_callback); // Called from register_types.cpp when the module is deinitialized. From 9feb86107116efcc438b23f939e04dc16c3cd67c Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 14 Aug 2026 13:09:08 +0200 Subject: [PATCH 32/36] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f365e0c85..f74cafc39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ - `SentrySDK.with_scope()` runs a callable with a forked scope, `SentrySDK.get_current_scope()` returns the scope active on the calling thread, and the new `SentryScope` class carries tags, contexts, user, level, fingerprint, breadcrumbs, and attributes on top of the data set globally - Not supported on macOS and iOS yet, where telemetry is still captured but the scope data is discarded with a warning - Add `SentryScope.add_attachment()` to send a file or a block of bytes with the events captured within a scope instead of with every event ([#856](https://github.com/getsentry/sentry-godot/pull/856)) +- Add Spans support to the GDScript API for measuring operations and grouping telemetry captured while they run ([#863](https://github.com/getsentry/sentry-godot/pull/863)) + - `SentrySDK.start_span()` starts a span and makes it active, `SentrySDK.with_span()` runs a callable with an active span and ends it on return, and `SentrySDK.get_active_span()` returns the active span for the calling thread + - The new `SentrySpan` class carries attributes and status; call `SentrySpan.end()` to finish the operation + - Events captured during an active span are associated with that operation + - Not supported on macOS, iOS, Android, or Web yet, where the SDK returns a no-op span with a warning ### Improvements From e1fa9b60f612eb270f1779a36f7860f36e6fcac2 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 14 Aug 2026 13:39:13 +0200 Subject: [PATCH 33/36] Expose traces_sample_rate option --- CHANGELOG.md | 1 + doc_classes/SentryOptions.xml | 4 ++++ doc_classes/SentrySDK.xml | 2 ++ project/test/suites/test_options.gd | 7 +++++++ src/sentry/native/native_sdk.cpp | 3 +-- src/sentry/sentry_options.cpp | 3 +++ src/sentry/sentry_options.h | 4 ++++ 7 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f74cafc39..7d6ca84a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - `SentrySDK.start_span()` starts a span and makes it active, `SentrySDK.with_span()` runs a callable with an active span and ends it on return, and `SentrySDK.get_active_span()` returns the active span for the calling thread - The new `SentrySpan` class carries attributes and status; call `SentrySpan.end()` to finish the operation - Events captured during an active span are associated with that operation + - `SentryOptions.traces_sample_rate` controls the share of traces sent to Sentry. It defaults to `0.0`; set it above `0.0` to enable performance tracing and send spans - Not supported on macOS, iOS, Android, or Web yet, where the SDK returns a no-op span with a warning ### Improvements diff --git a/doc_classes/SentryOptions.xml b/doc_classes/SentryOptions.xml index 8c433a05b..a4e3707ef 100644 --- a/doc_classes/SentryOptions.xml +++ b/doc_classes/SentryOptions.xml @@ -165,6 +165,10 @@ The maximum time in milliseconds the SDK will wait for pending events to be sent when [method SentrySDK.close] is called. If the timeout expires, the SDK will perform a forced shutdown and any unsent events may be lost. + + Configures the sample rate for performance traces, in the range of 0.0 to 1.0. The default is 0.0, which disables tracing and prevents spans from being sent. If set to 1.0, all traces are sent. If set to 0.1, approximately 10% of traces are sent. + The sampling decision is made randomly per trace, so either all spans in a trace are sent or none of them are. See [method SentrySDK.start_span] for information on recording spans. + diff --git a/doc_classes/SentrySDK.xml b/doc_classes/SentrySDK.xml index d25375d89..7151bf1f7 100644 --- a/doc_classes/SentrySDK.xml +++ b/doc_classes/SentrySDK.xml @@ -213,6 +213,7 @@ [/codeblock] When [param parent_span] is omitted, the new span becomes a child of [method SentrySDK.get_active_span] if one exists; otherwise it starts a new root span. Pass [code]null[/code] explicitly to force a new root span. Pass a [SentrySpan] to create the new span under that parent, even if another span is currently active. When such a span is active, it forks the explicit parent's scope instead of the current scope, so it inherits the parent's scope data. The [param attributes] dictionary is added to the span when it starts. The special [code]sentry.op[/code] attribute sets the Sentry operation name used for the span. Empty span names are rejected. Empty attribute keys are skipped and reported as errors. + [b]Note:[/b] Spans are only sent to Sentry if [member SentryOptions.traces_sample_rate] is raised above its default of 0.0. The rest of the API works either way, so telemetry captured during a span is associated with it even when the span itself is not sent. [b]Note:[/b] The SDK does not support spans on macOS, iOS, Android and Web yet. On those platforms it returns a no-op span and prints a warning. @@ -259,6 +260,7 @@ [/codeblock] Nested calls create nested spans. When the callable returns, the enclosing active span and scope are restored. [b]Note:[/b] [method SentrySDK.with_span] covers the synchronous part of [param callable] only. If the callable awaits, the span is ended at the first [code]await[/code] and a warning is printed. + [b]Note:[/b] Spans are only sent to Sentry if [member SentryOptions.traces_sample_rate] is raised above its default of 0.0. [b]Note:[/b] The SDK does not support spans on macOS, iOS, Android and Web yet. On those platforms it returns a no-op span and prints a warning. diff --git a/project/test/suites/test_options.gd b/project/test/suites/test_options.gd index f30cb417f..18dd1edd1 100644 --- a/project/test/suites/test_options.gd +++ b/project/test/suites/test_options.gd @@ -56,6 +56,13 @@ func test_sample_rate() -> void: assert_float(options.sample_rate).is_equal_approx(0.5, 0.01) +## SentryOptions.traces_sample_rate should default to 0.0 and be set to the specified value. +func test_traces_sample_rate() -> void: + assert_float(options.traces_sample_rate).is_equal_approx(0.0, 0.01) + options.traces_sample_rate = 0.5 + assert_float(options.traces_sample_rate).is_equal_approx(0.5, 0.01) + + ## SentryOptions.max_breadcrumbs should be set to the specified value. func test_max_breadcrumbs() -> void: options.max_breadcrumbs = 42 diff --git a/src/sentry/native/native_sdk.cpp b/src/sentry/native/native_sdk.cpp index 20ae427db..30fc6e211 100644 --- a/src/sentry/native/native_sdk.cpp +++ b/src/sentry/native/native_sdk.cpp @@ -402,8 +402,7 @@ void NativeSDK::init() { sentry_options_set_dist(options, SENTRY_OPTIONS()->get_dist().utf8()); sentry_options_set_environment(options, SENTRY_OPTIONS()->get_environment().utf8()); sentry_options_set_sample_rate(options, SENTRY_OPTIONS()->get_sample_rate()); - // TODO: Replace with SENTRY_OPTIONS() value once exposed. - sentry_options_set_traces_sample_rate(options, 1.0); + sentry_options_set_traces_sample_rate(options, SENTRY_OPTIONS()->get_traces_sample_rate()); sentry_options_set_max_breadcrumbs(options, SENTRY_OPTIONS()->get_max_breadcrumbs()); sentry_options_set_shutdown_timeout(options, SENTRY_OPTIONS()->get_shutdown_timeout_ms()); sentry_options_set_sdk_name(options, "sentry.native.godot"); diff --git a/src/sentry/sentry_options.cpp b/src/sentry/sentry_options.cpp index eb55bb486..7d992955e 100644 --- a/src/sentry/sentry_options.cpp +++ b/src/sentry/sentry_options.cpp @@ -129,6 +129,7 @@ void SentryOptions::_define_project_settings(const Ref &p_options _define_setting(PropertyInfo(Variant::INT, "sentry/options/debug_printing", PROPERTY_HINT_ENUM, "Off,On,Auto"), (int)SentryOptions::DEBUG_DEFAULT); _define_setting(sentry::make_level_enum_property("sentry/options/diagnostic_level"), p_options->diagnostic_level); _define_setting(PropertyInfo(Variant::FLOAT, "sentry/options/sample_rate", PROPERTY_HINT_RANGE, "0.0,1.0"), p_options->sample_rate, false); + _define_setting(PropertyInfo(Variant::FLOAT, "sentry/options/traces_sample_rate", PROPERTY_HINT_RANGE, "0.0,1.0"), p_options->traces_sample_rate, false); _define_setting(PropertyInfo(Variant::INT, "sentry/options/max_breadcrumbs", PROPERTY_HINT_RANGE, "0, 500"), p_options->max_breadcrumbs, false); _define_setting(PropertyInfo(Variant::INT, "sentry/options/shutdown_timeout_ms", PROPERTY_HINT_RANGE, "0,30000"), p_options->shutdown_timeout_ms, false); _define_setting("sentry/options/send_default_pii", p_options->send_default_pii); @@ -221,6 +222,7 @@ void SentryOptions::_load_project_settings(const Ref &p_options) p_options->diagnostic_level = (sentry::Level)(int)ProjectSettings::get_singleton()->get_setting("sentry/options/diagnostic_level", p_options->diagnostic_level); p_options->sample_rate = ProjectSettings::get_singleton()->get_setting("sentry/options/sample_rate", p_options->sample_rate); + p_options->traces_sample_rate = ProjectSettings::get_singleton()->get_setting("sentry/options/traces_sample_rate", p_options->traces_sample_rate); p_options->max_breadcrumbs = ProjectSettings::get_singleton()->get_setting("sentry/options/max_breadcrumbs", p_options->max_breadcrumbs); p_options->shutdown_timeout_ms = ProjectSettings::get_singleton()->get_setting("sentry/options/shutdown_timeout_ms", p_options->shutdown_timeout_ms); p_options->send_default_pii = ProjectSettings::get_singleton()->get_setting("sentry/options/send_default_pii", p_options->send_default_pii); @@ -415,6 +417,7 @@ void SentryOptions::_bind_methods() { BIND_PROPERTY(SentryOptions, sentry::make_level_enum_property("diagnostic_level"), set_diagnostic_level, get_diagnostic_level); BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::STRING, "environment"), set_environment, get_environment); BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::FLOAT, "sample_rate"), set_sample_rate, get_sample_rate); + BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::FLOAT, "traces_sample_rate"), set_traces_sample_rate, get_traces_sample_rate); BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::INT, "max_breadcrumbs"), set_max_breadcrumbs, get_max_breadcrumbs); BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::INT, "shutdown_timeout_ms", PROPERTY_HINT_RANGE, "0,30000"), set_shutdown_timeout_ms, get_shutdown_timeout_ms); BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::BOOL, "send_default_pii"), set_send_default_pii, is_send_default_pii_enabled); diff --git a/src/sentry/sentry_options.h b/src/sentry/sentry_options.h index 72a38babf..f7cb862c7 100644 --- a/src/sentry/sentry_options.h +++ b/src/sentry/sentry_options.h @@ -113,6 +113,7 @@ class SentryOptions : public RefCounted { sentry::Level diagnostic_level = sentry::LEVEL_DEBUG; String environment = "{auto}"; double sample_rate = 1.0; + double traces_sample_rate = 0.0; int max_breadcrumbs = 100; int shutdown_timeout_ms = 2000; bool send_default_pii = false; @@ -183,6 +184,9 @@ class SentryOptions : public RefCounted { _FORCE_INLINE_ double get_sample_rate() const { return sample_rate; } _FORCE_INLINE_ void set_sample_rate(double p_sample_rate) { sample_rate = p_sample_rate; } + _FORCE_INLINE_ double get_traces_sample_rate() const { return traces_sample_rate; } + _FORCE_INLINE_ void set_traces_sample_rate(double p_traces_sample_rate) { traces_sample_rate = p_traces_sample_rate; } + _FORCE_INLINE_ int get_max_breadcrumbs() const { return max_breadcrumbs; } _FORCE_INLINE_ void set_max_breadcrumbs(int p_max_breadcrumbs) { max_breadcrumbs = p_max_breadcrumbs; } From 334d6b54052d252894bfb01b408b6a3e8a01c0b9 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 14 Aug 2026 13:42:19 +0200 Subject: [PATCH 34/36] Enable tracing in the demo project --- project/project.godot | 1 + 1 file changed, 1 insertion(+) diff --git a/project/project.godot b/project/project.godot index c5a639acb..920853499 100644 --- a/project/project.godot +++ b/project/project.godot @@ -59,6 +59,7 @@ textures/vram_compression/import_etc2_astc=true options/auto_init=false options/dsn="https://3f1e095cf2e14598a0bd5b4ff324f712@o447951.ingest.us.sentry.io/6680910" schema_version=4 +options/traces_sample_rate=1.0 options/attach_scene_tree=true godot_logger/include_variables=true godot_logger/logs=143 From 7dbcf452cf4fbfc8478b8fb520ef9cbc76e8a0b1 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 14 Aug 2026 13:52:33 +0200 Subject: [PATCH 35/36] Pass traces_sample_rate across the C# interop boundary --- src/sentry/dotnet/csharp_interop.cpp | 4 ++++ .../dotnet/managed/Sentry.Godot/Interop/NativeBridge.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/sentry/dotnet/csharp_interop.cpp b/src/sentry/dotnet/csharp_interop.cpp index 2b2663425..3cc71d222 100644 --- a/src/sentry/dotnet/csharp_interop.cpp +++ b/src/sentry/dotnet/csharp_interop.cpp @@ -138,6 +138,7 @@ struct NativeOptions { uint8_t debug; int32_t diagnostic_level; double sample_rate; + double traces_sample_rate; int32_t max_breadcrumbs; double shutdown_timeout_ms; uint8_t send_default_pii; @@ -201,6 +202,7 @@ struct ManagedOptions { uint8_t debug; int32_t diagnostic_level; double sample_rate; + double traces_sample_rate; int32_t max_breadcrumbs; double shutdown_timeout_ms; uint8_t send_default_pii; @@ -249,6 +251,7 @@ static void _apply_managed_options(const ManagedOptions &data, Refset_debug_enabled(data.debug); options->set_diagnostic_level((Level)data.diagnostic_level); options->set_sample_rate(data.sample_rate); + options->set_traces_sample_rate(data.traces_sample_rate); options->set_max_breadcrumbs(data.max_breadcrumbs); options->set_shutdown_timeout_ms(data.shutdown_timeout_ms); options->set_send_default_pii(data.send_default_pii); @@ -287,6 +290,7 @@ void _populate_options_data(NativeOptions &r_data, const Ref &opt r_data.debug = options->is_debug_enabled(); r_data.diagnostic_level = options->get_diagnostic_level(); r_data.sample_rate = options->get_sample_rate(); + r_data.traces_sample_rate = options->get_traces_sample_rate(); r_data.max_breadcrumbs = options->get_max_breadcrumbs(); r_data.shutdown_timeout_ms = options->get_shutdown_timeout_ms(); r_data.send_default_pii = options->is_send_default_pii_enabled(); diff --git a/src/sentry/dotnet/managed/Sentry.Godot/Interop/NativeBridge.cs b/src/sentry/dotnet/managed/Sentry.Godot/Interop/NativeBridge.cs index 4b4217745..730a1c4ec 100644 --- a/src/sentry/dotnet/managed/Sentry.Godot/Interop/NativeBridge.cs +++ b/src/sentry/dotnet/managed/Sentry.Godot/Interop/NativeBridge.cs @@ -117,6 +117,7 @@ private struct NativeOptions public byte debug; public int diagnostic_level; public double sample_rate; + public double traces_sample_rate; public int max_breadcrumbs; public double shutdown_timeout_ms; public byte send_default_pii; @@ -155,6 +156,7 @@ private unsafe struct ManagedOptions public byte debug; public int diagnostic_level; public double sample_rate; + public double traces_sample_rate; public int max_breadcrumbs; public double shutdown_timeout_ms; public byte send_default_pii; @@ -514,6 +516,7 @@ private static void ApplyNativeOptions(NativeOptions data, SentryGodotOptions op opts.Debug = data.debug != 0; opts.DiagnosticLevel = (SentryLevel)data.diagnostic_level; opts.SampleRate = (float)data.sample_rate; + opts.TracesSampleRate = data.traces_sample_rate; opts.MaxBreadcrumbs = data.max_breadcrumbs; opts.ShutdownTimeout = TimeSpan.FromMilliseconds(data.shutdown_timeout_ms); opts.SendDefaultPii = data.send_default_pii != 0; @@ -813,6 +816,7 @@ public static unsafe void InitNativeSdk(SentryGodotOptions opts) debug = (byte)(opts.Debug ? 1 : 0), diagnostic_level = (int)opts.DiagnosticLevel, sample_rate = opts.SampleRate ?? 1.0, + traces_sample_rate = opts.TracesSampleRate ?? 0.0, max_breadcrumbs = opts.MaxBreadcrumbs, shutdown_timeout_ms = opts.ShutdownTimeout.TotalMilliseconds, send_default_pii = (byte)(opts.SendDefaultPii ? 1 : 0), From c107ac7c0492ade5b9caeb46a551f01be446fda5 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Tue, 18 Aug 2026 17:32:39 +0200 Subject: [PATCH 36/36] Drop SPAN_STATUS_UNSET --- doc_classes/SentrySpan.xml | 9 +++------ src/sentry/native/native_span.cpp | 4 ---- src/sentry/sentry_span.cpp | 1 - src/sentry/span_status.h | 6 ++---- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/doc_classes/SentrySpan.xml b/doc_classes/SentrySpan.xml index ef16bf0be..3630e6a51 100644 --- a/doc_classes/SentrySpan.xml +++ b/doc_classes/SentrySpan.xml @@ -49,18 +49,15 @@ Sets the outcome of the span. Use [constant SentrySpan.SPAN_STATUS_OK] for successful operations and [constant SentrySpan.SPAN_STATUS_ERROR] for failed operations. - Sentry reports a span that was never given a status as successful, so set [constant SentrySpan.SPAN_STATUS_ERROR] on the paths that fail. Clearing a status by passing [constant SentrySpan.SPAN_STATUS_UNSET] after a span has started is not supported on all platforms and may be ignored with a warning. + Sentry reports a span that was never given a status as successful, so set [constant SentrySpan.SPAN_STATUS_ERROR] on the paths that fail. - - No explicit status has been set for the span. Sentry reports such a span as successful. - - + The operation completed successfully. - + The operation failed. diff --git a/src/sentry/native/native_span.cpp b/src/sentry/native/native_span.cpp index 0451444cc..c5fef27ae 100644 --- a/src/sentry/native/native_span.cpp +++ b/src/sentry/native/native_span.cpp @@ -31,10 +31,6 @@ void NativeSpan::set_status(SpanStatus p_status) { if (!_is_live()) { return; } - if (p_status == SPAN_STATUS_UNSET) { - WARN_PRINT_ONCE("Sentry: Clearing a span status is not supported on this platform."); - return; - } sentry_span_status_t native_status = p_status == SPAN_STATUS_OK ? SENTRY_SPAN_STATUS_OK : SENTRY_SPAN_STATUS_INTERNAL_ERROR; if (_transaction) { sentry_transaction_set_status(_transaction, native_status); diff --git a/src/sentry/sentry_span.cpp b/src/sentry/sentry_span.cpp index a514a2b9f..f17ac9f84 100644 --- a/src/sentry/sentry_span.cpp +++ b/src/sentry/sentry_span.cpp @@ -109,7 +109,6 @@ void SentrySpan::_bind_methods() { ClassDB::bind_method(D_METHOD("set_status", "status"), &SentrySpan::set_status); ClassDB::bind_method(D_METHOD("end"), &SentrySpan::end); - BIND_ENUM_CONSTANT(SPAN_STATUS_UNSET); BIND_ENUM_CONSTANT(SPAN_STATUS_OK); BIND_ENUM_CONSTANT(SPAN_STATUS_ERROR); } diff --git a/src/sentry/span_status.h b/src/sentry/span_status.h index 55341eacb..a95379f84 100644 --- a/src/sentry/span_status.h +++ b/src/sentry/span_status.h @@ -4,11 +4,9 @@ namespace sentry { // Represents the outcome of a span. // In the public API, it is exposed as SentrySpan.SpanStatus enum. -// Values match OpenTelemetry's SpanStatusCode, as sentry-javascript does. enum SpanStatus { - SPAN_STATUS_UNSET = 0, - SPAN_STATUS_OK = 1, - SPAN_STATUS_ERROR = 2, + SPAN_STATUS_OK = 0, + SPAN_STATUS_ERROR = 1, }; } // namespace sentry