diff --git a/contents/docs/error-tracking/installation/java.mdx b/contents/docs/error-tracking/installation/java.mdx new file mode 100644 index 000000000000..4d2dd50044c3 --- /dev/null +++ b/contents/docs/error-tracking/installation/java.mdx @@ -0,0 +1,242 @@ +--- +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 + +``` + +Error tracking on this page requires `posthog-server` 2.16.0 or later. + + + +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 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) + .build(); +``` + + + +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 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 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..18d0b3800acf 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 opt in to capturing uncaught JVM exceptions. + +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 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',