diff --git a/CHANGELOG.md b/CHANGELOG.md
index b77ba342..3344b7d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
- Detect when we're inside message logging to prevent SDK print operations through the Godot logger which cause runtime errors. ([#414](https://github.com/getsentry/sentry-godot/pull/414))
- Relax throttling limits on app startup ([#423](https://github.com/getsentry/sentry-godot/pull/423))
- Set app hang timeout to 5s on Apple platforms ([#416](https://github.com/getsentry/sentry-godot/pull/416))
+- Add app hang tracking options and disable this feature by default ([#429](https://github.com/getsentry/sentry-godot/pull/429))
### Dependencies
diff --git a/android_lib/src/main/java/io/sentry/godotplugin/SentryAndroidGodotPlugin.kt b/android_lib/src/main/java/io/sentry/godotplugin/SentryAndroidGodotPlugin.kt
index f2e4ec19..861fcfbb 100644
--- a/android_lib/src/main/java/io/sentry/godotplugin/SentryAndroidGodotPlugin.kt
+++ b/android_lib/src/main/java/io/sentry/godotplugin/SentryAndroidGodotPlugin.kt
@@ -160,45 +160,56 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
@UsedByGodot
fun init(
+ optionsData: Dictionary,
beforeSendHandlerId: Long,
- dsn: String,
- debug: Boolean,
- release: String,
- dist: String,
- environment: String,
- sampleRate: Float,
- maxBreadcrumbs: Int,
- enableLogs: Boolean,
beforeSendLogHandlerId: Long
) {
Log.v(TAG, "Initializing Sentry Android")
- SentryAndroid.init(godot.getActivity()!!.applicationContext) { options ->
- options.dsn = dsn.ifEmpty { null }
- options.isDebug = debug
- options.release = release.ifEmpty { null }
- options.dist = dist.ifEmpty { null }
- options.environment = environment.ifEmpty { null }
- options.sampleRate = sampleRate.toDouble()
- options.maxBreadcrumbs = maxBreadcrumbs
- options.sdkVersion?.name = "sentry.java.android.godot"
- options.nativeSdkName = "sentry.native.android.godot"
- options.logs.isEnabled = enableLogs
- options.beforeSend =
- SentryOptions.BeforeSendCallback { event: SentryEvent, hint: Hint ->
- Log.v(TAG, "beforeSend: ${event.eventId} isCrashed: ${event.isCrashed}")
- val handle: Int = registerEvent(event)
- Callable.call(beforeSendHandlerId, "before_send", handle)
- eventsByHandle.get()?.remove(handle) // Returns the event or null if it was discarded.
- }
- if (beforeSendLogHandlerId != 0L) {
- options.logs.beforeSend =
- SentryOptions.Logs.BeforeSendLogCallback { logEvent ->
- val handle: Int = registerLog(logEvent)
- Callable.call(beforeSendLogHandlerId, "before_send_log", handle)
- logsByHandle.get()?.remove(handle) // Returns the log or null if it was discarded.
+
+ try {
+ val dsn = optionsData["dsn"] as String
+ val debug = optionsData["debug"] as Boolean
+ val release = optionsData["release"] as String
+ val dist = optionsData["dist"] as String
+ val environment = optionsData["environment"] as String
+ val sampleRate = optionsData["sample_rate"] as Double
+ val maxBreadcrumbs = optionsData["max_breadcrumbs"].toIntOrThrow()
+ val enableLogs = optionsData["enable_logs"] as Boolean
+ val appHangTracking = optionsData["app_hang_tracking"] as Boolean
+ val appHangTimeoutSec = optionsData["app_hang_timeout_sec"] as Double
+
+ SentryAndroid.init(godot.getActivity()!!.applicationContext) { options ->
+ options.dsn = dsn.ifEmpty { null }
+ options.isDebug = debug
+ options.release = release.ifEmpty { null }
+ options.dist = dist.ifEmpty { null }
+ options.environment = environment.ifEmpty { null }
+ options.sampleRate = sampleRate.toDouble()
+ options.maxBreadcrumbs = maxBreadcrumbs
+ options.sdkVersion?.name = "sentry.java.android.godot"
+ options.nativeSdkName = "sentry.native.android.godot"
+ options.logs.isEnabled = enableLogs
+ options.isAnrEnabled = appHangTracking
+ options.anrTimeoutIntervalMillis = (appHangTimeoutSec * 1000.0).toLong()
+ options.beforeSend =
+ SentryOptions.BeforeSendCallback { event: SentryEvent, hint: Hint ->
+ Log.v(TAG, "beforeSend: ${event.eventId} isCrashed: ${event.isCrashed}")
+ val handle: Int = registerEvent(event)
+ Callable.call(beforeSendHandlerId, "before_send", handle)
+ eventsByHandle.get()?.remove(handle) // Returns the event or null if it was discarded.
}
+ if (beforeSendLogHandlerId != 0L) {
+ options.logs.beforeSend =
+ SentryOptions.Logs.BeforeSendLogCallback { logEvent ->
+ val handle: Int = registerLog(logEvent)
+ Callable.call(beforeSendLogHandlerId, "before_send_log", handle)
+ logsByHandle.get()?.remove(handle) // Returns the log or null if it was discarded.
+ }
+ }
}
-
+ } catch (e: Exception) {
+ Log.e(TAG, "Error initializing Sentry for Android", e)
+ return
}
}
diff --git a/android_lib/src/main/java/io/sentry/godotplugin/UtilityFunctions.kt b/android_lib/src/main/java/io/sentry/godotplugin/UtilityFunctions.kt
index 7de69bc1..b4183949 100644
--- a/android_lib/src/main/java/io/sentry/godotplugin/UtilityFunctions.kt
+++ b/android_lib/src/main/java/io/sentry/godotplugin/UtilityFunctions.kt
@@ -50,8 +50,14 @@ fun Long.microsecondsToTimestamp(): Date {
return Date(millis)
}
-
fun Date.toMicros(): Long {
val date: Date = this@toMicros
return date.time * 1000
}
+
+fun Any?.toIntOrThrow(): Int =
+ when (this) {
+ is Int -> this
+ is Long -> this.toInt()
+ else -> throw IllegalArgumentException("Expected Int or Long, got ${this?.let { it::class } ?: "null"}")
+ }
diff --git a/doc_classes/SentryOptions.xml b/doc_classes/SentryOptions.xml
index bd7d0636..7d332ec6 100644
--- a/doc_classes/SentryOptions.xml
+++ b/doc_classes/SentryOptions.xml
@@ -13,6 +13,13 @@
+
+ Specifies the timeout duration in seconds after which the application is considered to have hanged. When [member app_hang_tracking] is enabled, if the main thread is blocked for longer than this duration, it will be reported as an application hang event to Sentry.
+
+
+ If [code]true[/code], enables automatic detection and reporting of application hangs. The SDK will monitor the main thread and report hang events when it becomes unresponsive for longer than the duration specified in [member app_hang_timeout_sec]. This helps identify performance issues where the application becomes frozen or unresponsive.
+ [b]Note:[/b] This feature is only supported on Android, iOS, and macOS platforms.
+
If [code]true[/code], the SDK will attach the Godot log file to the event.
diff --git a/src/sentry/android/android_sdk.cpp b/src/sentry/android/android_sdk.cpp
index e5a47f95..a0d9560b 100644
--- a/src/sentry/android/android_sdk.cpp
+++ b/src/sentry/android/android_sdk.cpp
@@ -226,16 +226,21 @@ void AndroidSDK::init(const PackedStringArray &p_global_attachments, const Calla
is_view_hierarchy ? "event.view_hierarchy" : String());
}
+ Dictionary optionsData;
+ optionsData["dsn"] = SentryOptions::get_singleton()->get_dsn();
+ optionsData["debug"] = SentryOptions::get_singleton()->is_debug_enabled();
+ optionsData["release"] = SentryOptions::get_singleton()->get_release();
+ optionsData["dist"] = SentryOptions::get_singleton()->get_dist();
+ optionsData["environment"] = SentryOptions::get_singleton()->get_environment();
+ optionsData["sample_rate"] = SentryOptions::get_singleton()->get_sample_rate();
+ optionsData["max_breadcrumbs"] = SentryOptions::get_singleton()->get_max_breadcrumbs();
+ optionsData["enable_logs"] = SentryOptions::get_singleton()->get_experimental()->get_enable_logs();
+ optionsData["app_hang_tracking"] = SentryOptions::get_singleton()->is_app_hang_tracking_enabled();
+ optionsData["app_hang_timeout_sec"] = SentryOptions::get_singleton()->get_app_hang_timeout_sec();
+
android_plugin->call(ANDROID_SN(init),
+ optionsData,
before_send_handler->get_instance_id(),
- SentryOptions::get_singleton()->get_dsn(),
- SentryOptions::get_singleton()->is_debug_enabled(),
- SentryOptions::get_singleton()->get_release(),
- SentryOptions::get_singleton()->get_dist(),
- SentryOptions::get_singleton()->get_environment(),
- SentryOptions::get_singleton()->get_sample_rate(),
- SentryOptions::get_singleton()->get_max_breadcrumbs(),
- SentryOptions::get_singleton()->get_experimental()->get_enable_logs(),
SentryOptions::get_singleton()->get_experimental()->before_send_log.is_valid() ? before_send_log_handler->get_instance_id() : 0);
if (is_enabled()) {
diff --git a/src/sentry/cocoa/cocoa_sdk.mm b/src/sentry/cocoa/cocoa_sdk.mm
index 0efe32f2..3c33fd75 100644
--- a/src/sentry/cocoa/cocoa_sdk.mm
+++ b/src/sentry/cocoa/cocoa_sdk.mm
@@ -258,10 +258,12 @@
options.dist = string_to_objc(dist);
}
+ options.enableAppHangTracking = SentryOptions::get_singleton()->is_app_hang_tracking_enabled();
+ options.appHangTimeoutInterval = SentryOptions::get_singleton()->get_app_hang_timeout_sec();
+
// NOTE: This only works for captureMessage(), unfortunately.
options.attachStacktrace = false;
- options.appHangTimeoutInterval = 5; // 5 seconds
options.experimental.enableLogs = SentryOptions::get_singleton()->get_experimental()->get_enable_logs();
options.initialScope = ^(objc::SentryScope *scope) {
diff --git a/src/sentry/sentry_options.cpp b/src/sentry/sentry_options.cpp
index 58a535ef..4a91a0bf 100644
--- a/src/sentry/sentry_options.cpp
+++ b/src/sentry/sentry_options.cpp
@@ -79,6 +79,9 @@ void SentryOptions::_define_project_settings(const Ref &p_options
_define_setting("sentry/options/attach_log", p_options->attach_log, false);
_define_setting("sentry/options/attach_scene_tree", p_options->attach_scene_tree);
+ _define_setting("sentry/options/app_hang/tracking", p_options->app_hang_tracking, false);
+ _define_setting("sentry/options/app_hang/timeout_sec", p_options->app_hang_timeout_sec, false);
+
_define_setting("sentry/logger/logger_enabled", p_options->logger_enabled);
_define_setting("sentry/logger/include_source", p_options->logger_include_source, false);
_define_setting("sentry/logger/include_variables", p_options->logger_include_variables, false);
@@ -121,6 +124,9 @@ void SentryOptions::_load_project_settings(const Ref &p_options)
p_options->attach_log = ProjectSettings::get_singleton()->get_setting("sentry/options/attach_log", p_options->attach_log);
p_options->attach_scene_tree = ProjectSettings::get_singleton()->get_setting("sentry/options/attach_scene_tree", p_options->attach_scene_tree);
+ p_options->app_hang_tracking = ProjectSettings::get_singleton()->get_setting("sentry/options/app_hang/tracking", p_options->app_hang_tracking);
+ p_options->app_hang_timeout_sec = ProjectSettings::get_singleton()->get_setting("sentry/options/app_hang/timeout_sec", p_options->app_hang_timeout_sec);
+
p_options->logger_enabled = ProjectSettings::get_singleton()->get_setting("sentry/logger/logger_enabled", p_options->logger_enabled);
p_options->logger_include_source = ProjectSettings::get_singleton()->get_setting("sentry/logger/include_source", p_options->logger_include_source);
p_options->logger_include_variables = ProjectSettings::get_singleton()->get_setting("sentry/logger/include_variables", p_options->logger_include_variables);
@@ -203,6 +209,9 @@ void SentryOptions::_bind_methods() {
BIND_PROPERTY(SentryOptions, sentry::make_level_enum_property("screenshot_level"), set_screenshot_level, get_screenshot_level);
BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::BOOL, "attach_scene_tree"), set_attach_scene_tree, is_attach_scene_tree_enabled);
+ BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::BOOL, "app_hang_tracking"), set_app_hang_tracking, is_app_hang_tracking_enabled);
+ BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::FLOAT, "app_hang_timeout_sec"), set_app_hang_timeout_sec, get_app_hang_timeout_sec);
+
BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::BOOL, "logger_enabled"), set_logger_enabled, is_logger_enabled);
BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::BOOL, "logger_include_source"), set_logger_include_source, is_logger_include_source_enabled);
BIND_PROPERTY(SentryOptions, PropertyInfo(Variant::BOOL, "logger_include_variables"), set_logger_include_variables, is_logger_include_variables_enabled);
diff --git a/src/sentry/sentry_options.h b/src/sentry/sentry_options.h
index 927eac54..0830e1f3 100644
--- a/src/sentry/sentry_options.h
+++ b/src/sentry/sentry_options.h
@@ -79,6 +79,9 @@ class SentryOptions : public RefCounted {
sentry::Level screenshot_level = sentry::LEVEL_FATAL;
bool attach_scene_tree = false;
+ bool app_hang_tracking = false;
+ double app_hang_timeout_sec = 5.0;
+
bool logger_enabled = true;
bool logger_include_source = true;
bool logger_include_variables = false;
@@ -152,6 +155,12 @@ class SentryOptions : public RefCounted {
_FORCE_INLINE_ void set_attach_scene_tree(bool p_enable) { attach_scene_tree = p_enable; }
_FORCE_INLINE_ bool is_attach_scene_tree_enabled() const { return attach_scene_tree; }
+ _FORCE_INLINE_ bool is_app_hang_tracking_enabled() const { return app_hang_tracking; }
+ _FORCE_INLINE_ void set_app_hang_tracking(bool p_enabled) { app_hang_tracking = p_enabled; }
+
+ _FORCE_INLINE_ double get_app_hang_timeout_sec() const { return app_hang_timeout_sec; }
+ _FORCE_INLINE_ void set_app_hang_timeout_sec(double p_seconds) { app_hang_timeout_sec = p_seconds; }
+
_FORCE_INLINE_ bool is_logger_enabled() const { return logger_enabled; }
_FORCE_INLINE_ void set_logger_enabled(bool p_enabled) { logger_enabled = p_enabled; }