Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 93 additions & 10 deletions plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,38 @@ void FAndroidSentrySubsystem::InitWithSettings(const USentrySettings* settings,

if (IsEnabled() && bNdkAppHangTracking)
{
bAppIsActive.AtomicSet(true);
bAppIsForeground.AtomicSet(true);

// OnEndFrame is broadcast on the game thread, so the first heartbeat latches it as the
// monitored thread and every subsequent frame refreshes the heartbeat sentry-native watches
// for staleness before capturing an app-hang event.
OnEndFrameDelegateHandle = FCoreDelegates::OnEndFrame.AddRaw(this, &FAndroidSentrySubsystem::PumpAppHangHeartbeat);
OnEndFrameDelegateHandle = FCoreDelegates::OnEndFrame.AddLambda([this]()
{
// Prevent a late frame callback from rearming the watchdog after the app becomes inactive.
if (bAppIsActive && bAppIsForeground)
{
PumpAppHangHeartbeat();
}
});
OnWillDeactivateDelegateHandle = FCoreDelegates::ApplicationWillDeactivateDelegate.AddLambda([this]()
{
bAppIsActive.AtomicSet(false);
PauseAppHangTracking();
});
OnWillEnterBackgroundDelegateHandle = FCoreDelegates::ApplicationWillEnterBackgroundDelegate.AddLambda([this]()
{
bAppIsForeground.AtomicSet(false);
PauseAppHangTracking();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Android pause silently never applies

High Severity

On Android, backgrounding stops heartbeats via bAppIsActive/bAppIsForeground while PauseAppHangTracking only runs if dlsym finds sentry_app_hang_pause. The bundled Java SDK NDK is still native 0.16.0, so that symbol is missing, pause is a silent no-op, and the watchdog stays armed without heartbeats—so false app-hang events can still fire (or start firing if frames continue in the background).

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1e1ac54. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair. We would need to wait for a sentry-java release. Or bump it's dependency.

});
OnHasEnteredForegroundDelegateHandle = FCoreDelegates::ApplicationHasEnteredForegroundDelegate.AddLambda([this]()
{
bAppIsForeground.AtomicSet(true);
});
OnHasReactivatedDelegateHandle = FCoreDelegates::ApplicationHasReactivatedDelegate.AddLambda([this]()
{
bAppIsActive.AtomicSet(true);
});
}
}

Expand All @@ -153,6 +181,30 @@ void FAndroidSentrySubsystem::Close()
OnEndFrameDelegateHandle.Reset();
}

if (OnWillDeactivateDelegateHandle.IsValid())
{
FCoreDelegates::ApplicationWillDeactivateDelegate.Remove(OnWillDeactivateDelegateHandle);
OnWillDeactivateDelegateHandle.Reset();
}

if (OnWillEnterBackgroundDelegateHandle.IsValid())
{
FCoreDelegates::ApplicationWillEnterBackgroundDelegate.Remove(OnWillEnterBackgroundDelegateHandle);
OnWillEnterBackgroundDelegateHandle.Reset();
}

if (OnHasEnteredForegroundDelegateHandle.IsValid())
{
FCoreDelegates::ApplicationHasEnteredForegroundDelegate.Remove(OnHasEnteredForegroundDelegateHandle);
OnHasEnteredForegroundDelegateHandle.Reset();
}

if (OnHasReactivatedDelegateHandle.IsValid())
{
FCoreDelegates::ApplicationHasReactivatedDelegate.Remove(OnHasReactivatedDelegateHandle);
OnHasReactivatedDelegateHandle.Reset();
}

FSentryJavaObjectWrapper::CallStaticMethod<void>(SentryJavaClasses::Sentry, "flush", "(J)V", (jlong)3000);
FSentryJavaObjectWrapper::CallStaticMethod<void>(SentryJavaClasses::Sentry, "close", "()V");
}
Expand Down Expand Up @@ -551,24 +603,55 @@ FString FAndroidSentrySubsystem::TryCaptureScreenshot() const

void FAndroidSentrySubsystem::PumpAppHangHeartbeat()
{
if (!AppHangHeartbeatFunc)
ResolveAppHangFunctions();

if (AppHangHeartbeatFunc)
{
AppHangHeartbeatFunc();
}
}

void FAndroidSentrySubsystem::PauseAppHangTracking()
{
ResolveAppHangFunctions();

if (AppHangPauseFunc)
{
AppHangPauseFunc();
}
}

void FAndroidSentrySubsystem::ResolveAppHangFunctions()
{
if (AppHangHeartbeatFunc && AppHangPauseFunc)
{
// libsentry.so is loaded asynchronously by the Java SDK (io.sentry.ndk.SentryNdk), so resolve
// the symbol lazily against the already-loaded library rather than linking it at build time.
if (void* libsentryHandle = dlopen("libsentry.so", RTLD_NOLOAD | RTLD_NOW))
return;
}

// libsentry.so is loaded asynchronously by the Java SDK (io.sentry.ndk.SentryNdk), so resolve
// symbols lazily against the already-loaded library rather than linking them at build time.
if (void* libsentryHandle = dlopen("libsentry.so", RTLD_NOLOAD | RTLD_NOW))
{
if (!AppHangHeartbeatFunc)
{
AppHangHeartbeatFunc = reinterpret_cast<void (*)()>(dlsym(libsentryHandle, "sentry_app_hang_heartbeat"));
dlclose(libsentryHandle);

if (AppHangHeartbeatFunc)
{
UE_LOG(LogSentrySdk, Log, TEXT("Resolved sentry_app_hang_heartbeat for NDK app-hang tracking."));
}
}
}

if (AppHangHeartbeatFunc)
{
AppHangHeartbeatFunc();
if (!AppHangPauseFunc)
{
AppHangPauseFunc = reinterpret_cast<void (*)()>(dlsym(libsentryHandle, "sentry_app_hang_pause"));

if (AppHangPauseFunc)
{
UE_LOG(LogSentrySdk, Log, TEXT("Resolved sentry_app_hang_pause for NDK app-hang tracking."));
}
}

dlclose(libsentryHandle);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#pragma once

#include "HAL/ThreadSafeBool.h"
#include "Interface/SentrySubsystemInterface.h"

class FAndroidSentrySubsystem : public ISentrySubsystem
Expand Down Expand Up @@ -63,15 +64,24 @@ class FAndroidSentrySubsystem : public ISentrySubsystem

private:
void PumpAppHangHeartbeat();
void PauseAppHangTracking();
void ResolveAppHangFunctions();

void (*AppHangHeartbeatFunc)() = nullptr;
void (*AppHangPauseFunc)() = nullptr;

bool bNdkAppHangTracking = false;
FThreadSafeBool bAppIsActive;
FThreadSafeBool bAppIsForeground;

bool isScreenshotAttachmentEnabled = false;

FDelegateHandle OnHandleSystemErrorDelegateHandle;
FDelegateHandle OnEndFrameDelegateHandle;
FDelegateHandle OnWillDeactivateDelegateHandle;
FDelegateHandle OnWillEnterBackgroundDelegateHandle;
FDelegateHandle OnHasEnteredForegroundDelegateHandle;
FDelegateHandle OnHasReactivatedDelegateHandle;
};

typedef FAndroidSentrySubsystem FPlatformSentrySubsystem;
Original file line number Diff line number Diff line change
Expand Up @@ -628,11 +628,36 @@ void FGenericPlatformSentrySubsystem::InitWithSettings(const USentrySettings* se

if (bNativeHangTracking && isEnabled)
{
bAppIsActive.AtomicSet(true);
bAppIsForeground.AtomicSet(true);

// OnEndFrame is broadcast on the game thread, so the first invocation latches it as the monitored
// thread and every subsequent frame refreshes the heartbeat the daemon watches for staleness
AppHangHeartbeatHandle = FCoreDelegates::OnEndFrame.AddLambda([]()
AppHangHeartbeatHandle = FCoreDelegates::OnEndFrame.AddLambda([this]()
{
// Prevent a late frame callback from rearming the watchdog after the app becomes inactive.
if (bAppIsActive && bAppIsForeground)
{
sentry_app_hang_heartbeat();
}
});
AppHangWillDeactivateHandle = FCoreDelegates::ApplicationWillDeactivateDelegate.AddLambda([this]()
{
sentry_app_hang_heartbeat();
bAppIsActive.AtomicSet(false);
sentry_app_hang_pause();
});
AppHangWillEnterBackgroundHandle = FCoreDelegates::ApplicationWillEnterBackgroundDelegate.AddLambda([this]()
{
bAppIsForeground.AtomicSet(false);
sentry_app_hang_pause();
});
AppHangHasEnteredForegroundHandle = FCoreDelegates::ApplicationHasEnteredForegroundDelegate.AddLambda([this]()
{
bAppIsForeground.AtomicSet(true);
});
AppHangHasReactivatedHandle = FCoreDelegates::ApplicationHasReactivatedDelegate.AddLambda([this]()
{
bAppIsActive.AtomicSet(true);
});
Comment thread
bitsandfoxes marked this conversation as resolved.
}

Expand Down Expand Up @@ -686,6 +711,26 @@ void FGenericPlatformSentrySubsystem::Close()
FCoreDelegates::OnEndFrame.Remove(AppHangHeartbeatHandle);
AppHangHeartbeatHandle.Reset();
}
if (AppHangWillDeactivateHandle.IsValid())
{
FCoreDelegates::ApplicationWillDeactivateDelegate.Remove(AppHangWillDeactivateHandle);
AppHangWillDeactivateHandle.Reset();
}
if (AppHangWillEnterBackgroundHandle.IsValid())
{
FCoreDelegates::ApplicationWillEnterBackgroundDelegate.Remove(AppHangWillEnterBackgroundHandle);
AppHangWillEnterBackgroundHandle.Reset();
}
if (AppHangHasEnteredForegroundHandle.IsValid())
{
FCoreDelegates::ApplicationHasEnteredForegroundDelegate.Remove(AppHangHasEnteredForegroundHandle);
AppHangHasEnteredForegroundHandle.Reset();
}
if (AppHangHasReactivatedHandle.IsValid())
{
FCoreDelegates::ApplicationHasReactivatedDelegate.Remove(AppHangHasReactivatedHandle);
AppHangHasReactivatedHandle.Reset();
}

isEnabled = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,14 @@ class FGenericPlatformSentrySubsystem : public ISentrySubsystem

bool bNativeHangTracking;

FThreadSafeBool bAppIsActive;
FThreadSafeBool bAppIsForeground;

FDelegateHandle AppHangHeartbeatHandle;
FDelegateHandle AppHangWillDeactivateHandle;
FDelegateHandle AppHangWillEnterBackgroundHandle;
FDelegateHandle AppHangHasEnteredForegroundHandle;
FDelegateHandle AppHangHasReactivatedHandle;

FDateTime initTimestamp;

Expand Down
Loading