Skip to content

feat(server): opt-in uncaught exception capture - #671

Draft
cat-ph wants to merge 1 commit into
cat/java-et-server-configfrom
cat/java-et-uncaught
Draft

feat(server): opt-in uncaught exception capture#671
cat-ph wants to merge 1 commit into
cat/java-et-server-configfrom
cat/java-et-uncaught

Conversation

@cat-ph

@cat-ph cat-ph commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Third PR in the 4-PR JVM error-tracking stack: opt-in capture of uncaught JVM exceptions for the server SDK.

  • New PostHogConfig.captureUncaughtExceptions (also on the Builder), default off. When enabled, PostHog.setup installs a global Thread.defaultUncaughtExceptionHandler that captures the crashing exception as a fatal, unhandled $exception event (mechanism UncaughtExceptionHandler), flushes, then delegates to the previously registered handler. close() removes it again.
  • Unlike the Android SDK, this is gated purely on the local flag: the server SDK never fetches remote config, so the remote autocapture toggle can never fire. To support that, the shared PostHogErrorTrackingAutoCaptureIntegration takes an optional caller-supplied enabledGate instead of always using the built-in gate (local errorTrackingConfig.autoCapture + remote config as a kill-switch, i.e. the behavior from Bug: error tracking autocapture misses crashes on first launch before remote config resolves #648 is preserved for Android).
  • The handler now delivers captures through an internal CaptureTarget seam (installWith), because the core PostHogInterface and the stateless server client share no capture supertype.
  • With no previous default handler present, the handler reproduces the JVM's own Exception in thread "..." stderr output, so opting into capture never hides a crash from stderr log collection.
  • New internal PostHogCapturedThrowables guard: a weak, identity-keyed, process-wide set letting independent capture paths avoid double-reporting the same Throwable instance. It is deliberately directional — the uncaught handler only marks (a crash is always captured as the authoritative fatal/unhandled record, even if the same instance was logged first), while log-mirror paths (the appender in PR 4) consult it and skip instances already reported. It never keeps a throwable or its stack alive.

Delivery caveat documented in the KDoc and changeset: flush() drains the queue synchronously on the crashing thread, but the preceding capture enqueues asynchronously, so under an immediate hard exit the final exception is best-effort — the same guarantee the Android SDK gives. Services that care should keep flushAt low.

💚 How did you test it?

  • 15 new tests: 7 in the core PostHogErrorTrackingAutoCaptureIntegrationTest (local-only gate installs/refuses, CaptureTarget capture+flush, capture-not-suppressed-by-dedup, marking for later log mirrors, JVM stderr fallback, non-owning instance cannot tear down the installed handler), 2 in PostHogCapturedThrowablesTest (identity keying, value-equal throwables both captured), and 6 in the new server PostHogUncaughtExceptionTest (off by default, install+chain+restore on close, fatal/unhandled/mechanism assertions over a real /batch request, repeated setup keeps ownership, second client does not stack a handler, works with no remote config).
  • ./gradlew :posthog:test :posthog-server:test pass (PostHogErrorTrackingAutoCaptureIntegrationTest 33 tests, PostHogCapturedThrowablesTest 2, PostHogUncaughtExceptionTest 6), plus :posthog:apiCheck / :posthog-server:apiCheck and spotlessCheck. API dumps regenerated; additive only.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file
  • Added the "release" label to the PR to indicate we're publishing new versions for the affected packages

🔗 Stacked PR

Position 3 of 4. Base: cat/java-et-server-config (PR #670).

  1. PR feat(error-tracking): complete exception chain metadata and in-app classification #669 — core exception chain metadata + in-app classification
  2. PR feat(server): expose error tracking config and captureException options #670 — server error-tracking config and captureException options
  3. this PR — opt-in server uncaught-exception capture
  4. cat/java-et-logback — new posthog-server-logback appender module

Adds captureUncaughtExceptions (default false) to the server PostHogConfig.
When enabled, the core PostHogErrorTrackingAutoCaptureIntegration installs a
Thread.defaultUncaughtExceptionHandler that captures the throwable as a fatal,
unhandled $exception (mechanism UncaughtExceptionHandler), flushes, then
delegates to the previously installed handler.

Core changes (all additive; Android behavior and the released
install(PostHogInterface) path unchanged):
- Gate strategy seam on the integration so the server can install with a
  local-only gate (no remote config, which the server SDK never fetches);
  Android keeps the remote errorTracking.autocaptureExceptions gate.
- Captures flow through an internal CaptureTarget seam so the server's
  stateless client can drive the integration.
- Handler-install ownership is tracked per integration instance, so closing a
  second opted-in client (whose install was a process-wide no-op) does not
  tear down the handler a still-open first client owns.
- New @PostHogInternal PostHogCapturedThrowables identity marker (weak,
  ReferenceQueue-pruned). The guard is directional: log mirrors consult it,
  the uncaught handler only marks — a crash is always captured as the
  authoritative fatal/unhandled record even if the same instance was logged
  first (logger.error(..., e); throw e), and marking keeps post-crash log
  mirrors from re-reporting it.
- Repeated setup() cannot replace the owning integration with a non-owning
  one, which would leave the global handler installed after close().
- With no previous default handler to chain to, the handler reproduces the
  JVM's built-in stderr crash output so enabling capture never hides crashes
  from stderr log collection.
- Server config KDoc documents the flushAt implication for the crash path.

# Conflicts:
#	posthog/api/posthog.api
#	posthog/src/main/java/com/posthog/errortracking/PostHogErrorTrackingAutoCaptureIntegration.kt
#	posthog/src/test/java/com/posthog/errortracking/PostHogErrorTrackingAutoCaptureIntegrationTest.kt
@cat-ph
cat-ph force-pushed the cat/java-et-server-config branch from b4e866a to ffe3132 Compare August 3, 2026 23:03
@cat-ph
cat-ph force-pushed the cat/java-et-uncaught branch from 29a1276 to c9db1af Compare August 3, 2026 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant