Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3c6fd49
refactor
bitsandfoxes May 29, 2026
a26c756
linter
bitsandfoxes Jun 1, 2026
0668a93
restored default fatal behaviour
bitsandfoxes Jun 1, 2026
caeb8fb
fix naming
bitsandfoxes Jun 1, 2026
0a3a646
perf(native): keep breadcrumbs off the per-mutation scope flush
bitsandfoxes Jun 1, 2026
57af004
read breadcrumbs from ring file
bitsandfoxes Jun 1, 2026
0a57db6
minified change
bitsandfoxes Jun 1, 2026
64a1d71
merged scope changes
bitsandfoxes Jun 1, 2026
b157d91
minified changes here too
bitsandfoxes Jun 1, 2026
1326bc5
bail with breadcrumbs disabled
bitsandfoxes Jun 3, 2026
4fb6895
fixed serialization regression
bitsandfoxes Jun 3, 2026
8a5a6be
fixed scope sync limit
bitsandfoxes Jun 3, 2026
5676031
added app hang feature, macOS only
bitsandfoxes Jun 3, 2026
156bb8b
styling
bitsandfoxes Jun 8, 2026
1533aa0
added tests
bitsandfoxes Jun 8, 2026
884d859
Merge branch 'feat/native-daemon-reads-breadcrumbs' into feat/app-han…
bitsandfoxes Jun 8, 2026
f198020
collapsed heartbeat api into one
bitsandfoxes Jun 8, 2026
bcb1bda
tightening
bitsandfoxes Jun 8, 2026
e10826c
merged master
bitsandfoxes Jun 8, 2026
741c669
reverted erronous name change
bitsandfoxes Jun 8, 2026
f5c4eaa
cleanup
bitsandfoxes Jun 8, 2026
ad72414
get rid of strikes, update event message
bitsandfoxes Jun 8, 2026
55ee3a2
updated changelog
bitsandfoxes Jun 8, 2026
edeb2aa
consent
bitsandfoxes Jun 8, 2026
a09d42a
addressed bot review
bitsandfoxes Jun 8, 2026
103c9d3
pretty
bitsandfoxes Jun 9, 2026
b28784f
skip 0 timeout
bitsandfoxes Jun 9, 2026
bbf0c1e
sigterm handling
bitsandfoxes Jun 9, 2026
b1674db
review
bitsandfoxes Jun 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Apple: use `os_sync_wait_on_address` for the level-triggered waitable flag in the batcher on modern macOS(14.4+) and iOS(17.4+). ([#1765](https://github.com/getsentry/sentry-native/pull/1765))
- Native/macOS: add thread names. ([#1766](https://github.com/getsentry/sentry-native/pull/1766))
- Add Upload-Metadata header to TUS requests. ([#1795](https://github.com/getsentry/sentry-native/pull/1795))
- Native/macOS: add opt-in app-hang detection. When enabled, the out-of-process crash daemon monitors a heartbeat emitted via `sentry_app_hang_heartbeat()` and captures an `AppHang` event with a full stack trace if the monitored thread stops responding for longer than the configured timeout. Configure with `sentry_options_set_app_hang_enabled()` and `sentry_options_set_app_hang_timeout_ms()`. ([#1780](https://github.com/getsentry/sentry-native/pull/1780))

**Fixes**:

Expand Down
44 changes: 44 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,26 @@ run_threads(thread_func_t func)
}
#endif

#if defined(SENTRY_PLATFORM_MACOS)
static void *
app_hang_demo_thread(void *arg)
{
(void)arg;
/* The first heartbeat latches this thread as the monitored target */
for (int i = 0; i < 10; i++) {
sentry_app_hang_heartbeat();
usleep(50 * 1000);
}

sentry_add_breadcrumb(
sentry_value_new_breadcrumb(NULL, "app-hang demo: about to freeze"));
sentry_add_breadcrumb(create_debug_crumb("app-hang demo breadcrumb"));
/* Freeze for 3x the configured timeout (3000 ms). */
usleep(3000 * 1000);
return NULL;
}
#endif

int
main(int argc, char **argv)
{
Expand Down Expand Up @@ -863,6 +883,13 @@ main(int argc, char **argv)
options, SENTRY_CRASH_UPLOAD_MODE_ASYNC);
}

#if defined(SENTRY_PLATFORM_MACOS)
if (has_arg(argc, argv, "app-hang")) {
sentry_options_set_app_hang_enabled(options, 1);
sentry_options_set_app_hang_timeout_ms(options, 1000);
}
#endif

// E2E test mode: generate unique test ID for event correlation
char e2e_test_id[37] = { 0 };
if (has_arg(argc, argv, "e2e-test")) {
Expand All @@ -874,6 +901,23 @@ main(int argc, char **argv)
return EXIT_FAILURE;
}

#if defined(SENTRY_PLATFORM_MACOS)
/* app-hang: spawn the demo thread BEFORE any other post-init work so it
* begins heartbeating immediately. The thread freezes for 3x the timeout,
* giving the daemon time to detect the hang and ship the envelope. We wait
* for it here so main does not exit before the transport has flushed.
* NOTE: this mode is intentionally exclusive – do not combine with crash/
* abort/etc. since those would terminate the process first. */
if (has_arg(argc, argv, "app-hang")) {
pthread_t t;
if (0 == pthread_create(&t, NULL, app_hang_demo_thread, NULL)) {
pthread_join(t, NULL);
}
sentry_close();
return EXIT_SUCCESS;
}
#endif

if (has_arg(argc, argv, "user-consent-revoke")) {
sentry_user_consent_revoke();
}
Expand Down
42 changes: 42 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -1699,6 +1699,48 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_attach_session_replay(
SENTRY_EXPERIMENTAL_API void sentry_options_set_session_replay_duration(
sentry_options_t *opts, uint32_t duration_ms);

/**
* Enable app-hang detection via the native crash backend.
*
* When enabled, the out-of-process daemon monitors the thread first emitting
* a heatbeat through `sentry_app_hang_heartbeat`.
* If the heartbeat goes stale for longer than the configured timeout, the
* daemon walks the thread's stack remotely and emits an `AppHang` event.
* The host process keeps running.
*
* Off by default. This setting only has an effect when using the `native`
* backend. In this initial release the feature is macOS-only; the call is a
* silent no-op on other platforms.
*/
SENTRY_EXPERIMENTAL_API void sentry_options_set_app_hang_enabled(
sentry_options_t *opts, int enabled);

/**
* Sets the heartbeat-staleness threshold (in milliseconds) used by the
* app-hang detector. Default 5000 ms.
*
* Read by the daemon once at startup; changes after `sentry_init` have no
* effect.
*/
SENTRY_EXPERIMENTAL_API void sentry_options_set_app_hang_timeout_ms(
sentry_options_t *opts, uint64_t timeout_ms);

/**
* Refresh the heartbeat.
*
* The first thread to call this becomes the monitored target for the lifetime
* of the SDK session (first caller wins, latched atomically). Call it from the
* thread you want monitored (typically the main / game thread) and ensure that
* thread issues the first heartbeat. Subsequent calls from any other thread are
* dropped.
*
* No-op if
* - app-hang detection is not enabled
* - the native backend is not active
* - the platform is not macOS
*/
SENTRY_EXPERIMENTAL_API void sentry_app_hang_heartbeat(void);

/**
* Sets the path to the crashpad handler if the crashpad backend is used.
*
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
sentry_target_sources_cwd(sentry
sentry_alloc.c
sentry_alloc.h
sentry_app_hang.c
sentry_app_hang.h
sentry_attachment.c
sentry_attachment.h
sentry_backend.c
Expand Down
14 changes: 14 additions & 0 deletions src/backends/native/sentry_crash_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,20 @@ typedef struct {
uint32_t module_count;
sentry_module_info_t modules[SENTRY_CRASH_MAX_MODULES];

/* App-hang detection.
*
* Sync model:
* - app_hang_enabled, app_hang_timeout_ms: written by host before daemon
* is signalled ready; read by daemon at startup. No further mutation.
* - app_hang_target_tid: latched once by host on first heartbeat.
* Daemon reads, never writes.
* - app_hang_last_heartbeat_ms: written on every heartbeat.
*/
bool app_hang_enabled;
uint64_t app_hang_timeout_ms;
volatile uint64_t app_hang_target_tid;
volatile uint64_t app_hang_last_heartbeat_ms;

} sentry_crash_context_t;

// Shared memory size: calculated at compile-time based on actual struct size
Expand Down
Loading
Loading