From dff726d80cb388a58676148c691bc137b4b0fd5b Mon Sep 17 00:00:00 2001 From: Catalin Irimie Date: Mon, 3 Aug 2026 20:42:28 +0300 Subject: [PATCH 1/2] docs: add Java error tracking installation and library docs --- .../docs/error-tracking/installation/java.mdx | 303 ++++++++++++++++++ contents/docs/libraries/java/index.mdx | 21 ++ src/navs/index.js | 4 + 3 files changed, 328 insertions(+) create mode 100644 contents/docs/error-tracking/installation/java.mdx diff --git a/contents/docs/error-tracking/installation/java.mdx b/contents/docs/error-tracking/installation/java.mdx new file mode 100644 index 000000000000..bcda663ec1d9 --- /dev/null +++ b/contents/docs/error-tracking/installation/java.mdx @@ -0,0 +1,303 @@ +--- +title: Java error tracking installation +platformLogo: java +showStepsToc: true +--- + +import { Steps, Step } from 'components/Docs/Steps' +import { CalloutBox } from 'components/Docs/CalloutBox' + +> This guide covers the server-side Java SDK (`posthog-server`), for JVM server applications. For Android apps, see the [Android error tracking installation guide](/docs/error-tracking/installation/android). + + + + + +Install the [PostHog Java SDK](/docs/libraries/java) with Gradle or Maven. + +#### Gradle + +```gradle_kotlin file=build.gradle +dependencies { + implementation 'com.posthog:posthog-server:2.+' +} +``` + +#### Maven + +```xml file=pom.xml + + com.posthog + posthog-server + LATEST + +``` + + + + + +The Java SDK captures stack traces with file names, line numbers, and function names, but does not yet support source context (displaying the surrounding lines of code in the error tracking UI). Symbolication of obfuscated builds is supported through ProGuard/R8 mappings — see [Deobfuscate stack traces](#deobfuscate-stack-traces) below. + + + + + + + +Build a client once during application startup and reuse it. It uses an internal queue to send events asynchronously. + +```java +import com.posthog.server.PostHog; +import com.posthog.server.PostHogConfig; +import com.posthog.server.PostHogInterface; + +PostHogConfig config = PostHogConfig + .builder("") + .host("") + .build(); + +PostHogInterface posthog = PostHog.with(config); +``` + +You can find your project token and instance address in your [project settings](https://app.posthog.com/settings/project). + + + + + +Use `captureException` to send an exception to PostHog as an `$exception` event. The exception type, message, and full stack trace are captured, along with the cause chain and any suppressed exceptions. + +```java +try { + // Your code that might throw + riskyOperation(); +} catch (Throwable e) { + posthog.captureException(e); +} +``` + +By default, exceptions are captured personlessly: the SDK generates a UUID and sets `$process_person_profile` to `false`, so no [person profile](/docs/data/persons) is created. + +### Associate a person + +Pass a distinct ID to link the exception to a person: + +```java +try { + processOrder(orderId); +} catch (Throwable e) { + posthog.captureException(e, "user_distinct_id"); +} +``` + +### Add properties + +Pass extra properties to include with the event. You can also override reserved exception properties such as `$exception_level` (severity) and `$exception_fingerprint` ([issue grouping](/docs/error-tracking/grouping-issues)) here: + +```java +import java.util.HashMap; +import java.util.Map; + +try { + processOrder(orderId); +} catch (Throwable e) { + Map properties = new HashMap<>(); + properties.put("order_id", orderId); + properties.put("$exception_level", "warning"); + + posthog.captureException(e, "user_distinct_id", properties); +} +``` + +For finer control over groups, feature flags, or the timestamp, pass [`PostHogCaptureOptions`](/docs/libraries/java) instead of a plain map: + +```java +import com.posthog.server.PostHogCaptureOptions; + +posthog.captureException( + e, + "user_distinct_id", + PostHogCaptureOptions + .builder() + .property("order_id", orderId) + .group("company", "company_id_in_your_db") + .build() +); +``` + + + + + +Inside a web request, you usually don't want to pass a distinct ID to every `captureException` call. Use [request context](/docs/libraries/java#request-context) to set the distinct ID (and session ID) once per request, so every capture on that thread — including exceptions — is attributed to the right person and associated with session replay. + +```java +try (PostHogRequestContext.Scope ignored = PostHogRequestContext.beginScope(context, true)) { + // No distinct ID needed — the request context supplies it + posthog.captureException(error); +} +``` + +See the [request context documentation](/docs/libraries/java#request-context) for how to build the context from incoming headers. + + + + + +PostHog classifies each stack frame as either in-app (your code) or library code, which controls how issues are grouped and displayed. This is configured on the client through `inAppExcludes` and `inAppIncludes`. + +Out of the box, the SDK ships a sensible default `inAppExcludes` list that marks common JVM and framework packages as library code, so it works with zero configuration. The defaults are: + +``` +java. javax. jakarta. kotlin. kotlinx. scala. sun. com.sun. jdk. +org.springframework. io.netty. org.apache. org.eclipse.jetty. io.undertow. +okhttp3. okio. com.posthog. +``` + +Any frame not matched by `inAppExcludes` is considered in-app. To narrow this down to just your own packages, set `inAppIncludes`: + +```java +import java.util.Arrays; + +PostHogConfig config = PostHogConfig + .builder("") + .host("") + .inAppIncludes(Arrays.asList("com.yourcompany")) + .build(); +``` + +Excludes always win over includes. Assigning your own `inAppExcludes` list **replaces** the defaults rather than adding to them, so start from the default set above if you only want to add entries. + + + + + +To automatically capture exceptions that crash a thread, opt in with `captureUncaughtExceptions`. On setup, the SDK installs a JVM-wide [`Thread.defaultUncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setDefaultUncaughtExceptionHandler-java.lang.Thread.UncaughtExceptionHandler-) that captures the crashing exception (marked as fatal, unhandled), flushes, and then delegates to any handler that was previously registered. The handler is removed again when you call `posthog.close()`. + +```java +PostHogConfig config = PostHogConfig + .builder("") + .host("") + .captureUncaughtExceptions(true) + .flushAt(1) + .build(); +``` + + + +The handler captures the exception and then calls `flush()`, which drains the queue synchronously on the crashing thread. The capture itself is enqueued asynchronously, though, so it may not have landed in the queue by the time `flush()` runs. The background flush doesn't reliably cover this either: it only sends once `flushAt` events (default `100`) have accumulated, or on the flush interval (default every `5` seconds), neither of which is likely to fire before the process exits. If you rely on capturing the crash, set `flushAt(1)` so the enqueue itself triggers a send — at the cost of batching for every other event on that client. Delivery under an immediate hard exit is best-effort. + + + + + + + +If your application uses [Logback](https://logback.qos.ch/), you can automatically capture logged errors as exceptions with the PostHog appender, without adding `captureException` calls throughout your code. + +Add the appender module: + +```gradle_kotlin file=build.gradle +dependencies { + implementation 'com.posthog:posthog-server-logback:0.+' +} +``` + +```xml file=pom.xml + + com.posthog + posthog-server-logback + LATEST + +``` + + + +Register the appender in your `logback.xml`: + +```xml file=logback.xml + + + ERROR + + + + + + +``` + +Then register your PostHog client with the appender once during startup, before the logging you want to capture. Registration is process-wide, so events logged before a client is registered are dropped. + +```java +PostHogInterface posthog = PostHog.with(config); +PostHogAppender.setPostHog(posthog); +``` + +Alternatively, let the appender create and own its own client from `logback.xml` by setting `apiKey` (and optionally `host`) instead of calling `setPostHog`: + +```xml file=logback.xml + + ${POSTHOG_API_KEY} + ERROR + +``` + +A few things to know about what the appender captures: + +- Only log events at or above `minimumCaptureLevel` (default `ERROR`) that carry a `Throwable` are captured. Message-only logs are skipped. +- The log level is mapped onto `$exception_level` (`ERROR` → `error`, `WARN` → `warning`). +- Events from the SDK's own loggers (`com.posthog.*`) are always skipped, so the SDK can't recursively report its own errors. +- Because capture runs through the SDK's `captureException`, request-context distinct-id resolution and in-app frame configuration apply automatically. +- If the same exception is both logged and reaches the uncaught-exception handler, it's captured only once. + + + + + +If you obfuscate your JVM builds with ProGuard or R8, upload the mapping file to PostHog so stack traces are deobfuscated in the error tracking UI. + +Set `releaseIdentifier` on the client. This stamps every captured stack frame with a `map_id` that ties it to the uploaded mapping: + +```java +PostHogConfig config = PostHogConfig + .builder("") + .host("") + .releaseIdentifier("my-service@1.2.3") + .build(); +``` + +Then upload the mapping file with the [PostHog CLI](/docs/cli), using the **same** identifier for `--map-id`: + +```bash +posthog-cli proguard upload --path "mapping.txt" --map-id "my-service@1.2.3" +``` + +The `--map-id` value must match the `releaseIdentifier` set at runtime for that build. This is only needed for obfuscated builds — non-obfuscated JVM apps produce readable stack traces without any upload. + + + + + +Trigger a test exception to confirm events are being sent to PostHog. You should see it appear in the [error tracking issues view](https://app.posthog.com/error_tracking). + +```java +try { + throw new RuntimeException("This is a test exception from Java"); +} catch (Throwable e) { + posthog.captureException(e, "test_user"); +} + +// Flush before the process exits so the event is sent +posthog.flush(); +``` + + + + + +## What's not supported yet + +- **Source context.** Captured frames include file names, line numbers, and function names, but the surrounding lines of source code are not shown in the UI. +- **Framework middleware.** There is no built-in Spring (or other framework) middleware for automatic capture yet. Use the Logback appender, the uncaught-exception handler, and manual `captureException` calls, wiring request context in your framework integration. diff --git a/contents/docs/libraries/java/index.mdx b/contents/docs/libraries/java/index.mdx index c25d2f982810..c320f27cfa05 100644 --- a/contents/docs/libraries/java/index.mdx +++ b/contents/docs/libraries/java/index.mdx @@ -12,6 +12,7 @@ features: groupAnalytics: true surveys: false aiObservability: false + errorTracking: true --- This is an optional library you can install if you're working with server-side Java applications. It uses an internal queue to make calls fast and non-blocking. It also batches requests and flushes asynchronously, making it perfect to use in any part of your web app or other server side application that needs performance. @@ -338,6 +339,26 @@ if ("neural_network".equals(variant)) { It's also possible to [run experiments without using feature flags](/docs/experiments/running-experiments-without-feature-flags). +## Error tracking + +You can capture exceptions with the Java SDK. `captureException` sends a `Throwable` to PostHog as an `$exception` event with a full stack trace, cause chain, and any suppressed exceptions: + +```java +try { + riskyOperation(); +} catch (Throwable e) { + // Captured personlessly + posthog.captureException(e); + + // Or associated with a person, with optional properties + posthog.captureException(e, "user_distinct_id"); +} +``` + +Inside a request scope, exceptions are automatically attributed using the [request context](#request-context) distinct ID, so you don't need to pass one. You can also capture logged errors automatically with the Logback appender, and opt in to capturing uncaught JVM exceptions. + +For the full setup guide — including request context, in-app frame configuration, the Logback appender, uncaught-exception capture, and deobfuscating ProGuard/R8 builds — see the [Java error tracking installation docs](/docs/error-tracking/installation/java). + ## GeoIP properties The `posthog-server` library disregards the server IP, does not add the GeoIP properties, and does not use the values for feature flag evaluations. diff --git a/src/navs/index.js b/src/navs/index.js index 1a64cf778c0a..3265c07c5c2b 100644 --- a/src/navs/index.js +++ b/src/navs/index.js @@ -5604,6 +5604,10 @@ export const docsMenu = { name: 'Rust', url: '/docs/error-tracking/installation/rust', }, + { + name: 'Java', + url: '/docs/error-tracking/installation/java', + }, { name: 'iOS', url: '/docs/error-tracking/installation/ios', From 964a719d28c078fa1e9fc71d193547603da2c409 Mon Sep 17 00:00:00 2001 From: Catalin Irimie Date: Fri, 11 Sep 2026 02:34:08 +0300 Subject: [PATCH 2/2] docs(error-tracking): drop the Logback appender from the Java guide and match the shipped uncaught-capture behavior The posthog-server-logback module was not shipped (PR closed), so its step and mentions go. Uncaught capture shipped in posthog-server 2.16.0 with a bounded fatal-crash flush and per-thread levels, so the flushAt(1) caveat is replaced with the actual semantics and the minimum version is set. --- .../docs/error-tracking/installation/java.mdx | 75 ++----------------- contents/docs/libraries/java/index.mdx | 4 +- 2 files changed, 9 insertions(+), 70 deletions(-) diff --git a/contents/docs/error-tracking/installation/java.mdx b/contents/docs/error-tracking/installation/java.mdx index bcda663ec1d9..4d2dd50044c3 100644 --- a/contents/docs/error-tracking/installation/java.mdx +++ b/contents/docs/error-tracking/installation/java.mdx @@ -33,7 +33,7 @@ dependencies { ``` - +Error tracking on this page requires `posthog-server` 2.16.0 or later. @@ -173,87 +173,26 @@ Excludes always win over includes. Assigning your own `inAppExcludes` list **rep -To automatically capture exceptions that crash a thread, opt in with `captureUncaughtExceptions`. On setup, the SDK installs a JVM-wide [`Thread.defaultUncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setDefaultUncaughtExceptionHandler-java.lang.Thread.UncaughtExceptionHandler-) that captures the crashing exception (marked as fatal, unhandled), flushes, and then delegates to any handler that was previously registered. The handler is removed again when you call `posthog.close()`. +To automatically capture exceptions that crash a thread, opt in with `captureUncaughtExceptions`. On setup, the SDK installs a JVM-wide [`Thread.defaultUncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setDefaultUncaughtExceptionHandler-java.lang.Thread.UncaughtExceptionHandler-) that captures the crashing exception as unhandled, then delegates to any handler that was previously registered. If there was none, it still prints the JVM's usual `Exception in thread ...` output to stderr, so crashes never disappear from your logs. The handler is removed again when you call `posthog.close()`. + +An exception on the main thread is captured with level `fatal`, since it is expected to end the process. An exception on any other thread only kills that thread, so it is captured with level `error` and sent through the normal queue. ```java PostHogConfig config = PostHogConfig .builder("") .host("") .captureUncaughtExceptions(true) - .flushAt(1) .build(); ``` - + -The handler captures the exception and then calls `flush()`, which drains the queue synchronously on the crashing thread. The capture itself is enqueued asynchronously, though, so it may not have landed in the queue by the time `flush()` runs. The background flush doesn't reliably cover this either: it only sends once `flushAt` events (default `100`) have accumulated, or on the flush interval (default every `5` seconds), neither of which is likely to fire before the process exits. If you rely on capturing the crash, set `flushAt(1)` so the enqueue itself triggers a send — at the cost of batching for every other event on that client. Delivery under an immediate hard exit is best-effort. +For a main-thread crash, the SDK sends the event (and anything else still queued) before the JVM exits, blocking the crashing thread for at most 2 seconds. On a slow or unreachable network, or if something calls `Runtime.halt()` first, the event can still be lost. - - -If your application uses [Logback](https://logback.qos.ch/), you can automatically capture logged errors as exceptions with the PostHog appender, without adding `captureException` calls throughout your code. - -Add the appender module: - -```gradle_kotlin file=build.gradle -dependencies { - implementation 'com.posthog:posthog-server-logback:0.+' -} -``` - -```xml file=pom.xml - - com.posthog - posthog-server-logback - LATEST - -``` - - - -Register the appender in your `logback.xml`: - -```xml file=logback.xml - - - ERROR - - - - - - -``` - -Then register your PostHog client with the appender once during startup, before the logging you want to capture. Registration is process-wide, so events logged before a client is registered are dropped. - -```java -PostHogInterface posthog = PostHog.with(config); -PostHogAppender.setPostHog(posthog); -``` - -Alternatively, let the appender create and own its own client from `logback.xml` by setting `apiKey` (and optionally `host`) instead of calling `setPostHog`: - -```xml file=logback.xml - - ${POSTHOG_API_KEY} - ERROR - -``` - -A few things to know about what the appender captures: - -- Only log events at or above `minimumCaptureLevel` (default `ERROR`) that carry a `Throwable` are captured. Message-only logs are skipped. -- The log level is mapped onto `$exception_level` (`ERROR` → `error`, `WARN` → `warning`). -- Events from the SDK's own loggers (`com.posthog.*`) are always skipped, so the SDK can't recursively report its own errors. -- Because capture runs through the SDK's `captureException`, request-context distinct-id resolution and in-app frame configuration apply automatically. -- If the same exception is both logged and reaches the uncaught-exception handler, it's captured only once. - - - If you obfuscate your JVM builds with ProGuard or R8, upload the mapping file to PostHog so stack traces are deobfuscated in the error tracking UI. @@ -300,4 +239,4 @@ posthog.flush(); ## What's not supported yet - **Source context.** Captured frames include file names, line numbers, and function names, but the surrounding lines of source code are not shown in the UI. -- **Framework middleware.** There is no built-in Spring (or other framework) middleware for automatic capture yet. Use the Logback appender, the uncaught-exception handler, and manual `captureException` calls, wiring request context in your framework integration. +- **Framework middleware.** There is no built-in Spring (or other framework) middleware for automatic capture yet. Use the uncaught-exception handler and manual `captureException` calls, wiring request context in your framework integration. diff --git a/contents/docs/libraries/java/index.mdx b/contents/docs/libraries/java/index.mdx index c320f27cfa05..18d0b3800acf 100644 --- a/contents/docs/libraries/java/index.mdx +++ b/contents/docs/libraries/java/index.mdx @@ -355,9 +355,9 @@ try { } ``` -Inside a request scope, exceptions are automatically attributed using the [request context](#request-context) distinct ID, so you don't need to pass one. You can also capture logged errors automatically with the Logback appender, and opt in to capturing uncaught JVM exceptions. +Inside a request scope, exceptions are automatically attributed using the [request context](#request-context) distinct ID, so you don't need to pass one. You can also opt in to capturing uncaught JVM exceptions. -For the full setup guide — including request context, in-app frame configuration, the Logback appender, uncaught-exception capture, and deobfuscating ProGuard/R8 builds — see the [Java error tracking installation docs](/docs/error-tracking/installation/java). +For the full setup guide — including request context, in-app frame configuration, uncaught-exception capture, and deobfuscating ProGuard/R8 builds — see the [Java error tracking installation docs](/docs/error-tracking/installation/java). ## GeoIP properties