Skip to content

Commit 7dfb504

Browse files
sarahxsandersclaude
andcommitted
feat(integration): add java (spring boot) as a full example-based variant
Add a Java integration variant anchored on a real Spring Boot example app (example-apps/java-spring-boot): the posthog-server client is exposed as a singleton @bean configured from the environment, flushed and closed on shutdown via destroyMethod="close". A controller identifies users, captures events, evaluates feature flags with evaluateFlags, and reports errors as an explicit event (the Java server SDK has no automatic exception capture, documented honestly rather than faked). Ships a full Spring Boot example rather than docs-only so the wizard has a concrete shape to match for the most common Java backend framework. Adds a java-server commandments block, keyed java-server (not java) so these backend rules never fold into the Android skill, which carries the java tag; the variant is tagged [java, java-server]. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 73b2584 commit 7dfb504

15 files changed

Lines changed: 499 additions & 0 deletions

File tree

context/commandments.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,17 @@ commandments:
9797
- Call `IdentifyAsync` for known users and put PII such as email in person properties, not in event properties
9898
- Use `CaptureException(exception, distinctId, properties, groups, flags)` for handled exceptions; automatic exception capture is not available in the .NET SDK yet
9999

100+
# Keyed `java-server` (not `java`) so these backend rules never fold into the Android skill, which carries the `java` tag. The Java variant is tagged [java, java-server].
101+
java-server:
102+
- posthog-server is the PostHog Java server SDK; add the `com.posthog:posthog-server` dependency via Gradle or Maven and import from `com.posthog.server`
103+
- Build one client per process with `PostHogConfig.builder(token).host(host).build()` then `PostHog.with(config)`; share the returned `PostHogInterface` (for example as a singleton Spring `@Bean`), never per request
104+
- Read the project token and host from the environment or configuration; never hardcode PostHog secrets
105+
- Flush and close on shutdown with `posthog.flush()` and `posthog.close()` (for a Spring `@Bean`, use `destroyMethod = "close"`) so queued events are delivered before exit
106+
- Server-side captures must pass a stable `distinctId` as the first argument to `capture(...)` that matches frontend identify calls; avoid anonymous or literal IDs for business events
107+
- Identify users by attaching person properties to a capture via `PostHogCaptureOptions.builder().userProperty(...)` / `.userPropertySetOnce(...)`; keep PII in person properties, not event properties
108+
- For feature flags, call `posthog.evaluateFlags(distinctId)` once per request, then read values from the snapshot with `isEnabled("flag-key")` / `getFlagPayload("flag-key")`
109+
- The Java server SDK has no automatic error tracking, surveys, or session replay; do not promise or scaffold those features
110+
100111
elixir:
101112
- posthog-elixir is installed as the `posthog` Hex package; add `{:posthog, "~> 2.0"}` to `mix.exs` and run `mix deps.get`
102113
- Configure PostHog in application config using `api_host`, `api_key`, and `in_app_otp_apps`; read secrets from environment or runtime config, never hardcode them

context/skills/integration/config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,15 @@ variants:
236236
docs_urls:
237237
- https://posthog.com/docs/libraries/elixir.md
238238

239+
- id: java
240+
framework: java
241+
example_paths: example-apps/java-spring-boot
242+
display_name: Java (Spring Boot)
243+
description: PostHog integration for Java and Spring Boot applications using the posthog-server SDK
244+
tags: [java, java-server]
245+
docs_urls:
246+
- https://posthog.com/docs/libraries/java.md
247+
239248
- id: go
240249
template: description-go-docs-only.md
241250
display_name: Go
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
POSTHOG_PROJECT_TOKEN=
2+
POSTHOG_HOST=https://us.i.posthog.com
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.env
2+
build/
3+
.gradle/
4+
*.class
5+
.idea/
6+
*.iml
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# PostHog Spring Boot example
2+
3+
This is a [Spring Boot](https://spring.io/projects/spring-boot) example demonstrating PostHog integration with product analytics, feature flags, and user identification using the server-side Java SDK.
4+
5+
## Features
6+
7+
- **Product analytics**: Track user events and behaviors
8+
- **User identification**: Associate events with authenticated users via person properties
9+
- **Feature flags**: Control feature rollouts with PostHog feature flags
10+
- **Server-side tracking**: All tracking happens server-side with the `posthog-server` SDK
11+
- **Single client bean**: One PostHog client for the whole app, flushed on shutdown
12+
13+
> **Note on error tracking:** the PostHog Java server SDK does not have automatic
14+
> exception capture. This example reports failures as an explicit `error_occurred`
15+
> event instead. Do not expect session replay or surveys from the server SDK either.
16+
17+
## Getting started
18+
19+
### 1. Configure environment variables
20+
21+
Copy `.env.example` to `.env` and fill in your values, then export them:
22+
23+
```bash
24+
export POSTHOG_PROJECT_TOKEN=your_posthog_project_token
25+
export POSTHOG_HOST=https://us.i.posthog.com
26+
```
27+
28+
Get your PostHog project token from your [PostHog project settings](https://app.posthog.com/project/settings).
29+
30+
### 2. Run the app
31+
32+
```bash
33+
./gradlew bootRun
34+
```
35+
36+
Open [http://localhost:8000](http://localhost:8000) with your browser to see the app.
37+
38+
## Project structure
39+
40+
```
41+
java-spring-boot/
42+
├── build.gradle # Gradle build with the posthog-server dependency
43+
├── settings.gradle
44+
├── .env.example # Environment variable template
45+
├── .gitignore
46+
└── src/main/
47+
├── resources/
48+
│ ├── application.properties # Binds POSTHOG_* env vars into config
49+
│ └── templates/ # Thymeleaf pages
50+
│ ├── home.html # Home / login page
51+
│ ├── burrito.html # Event tracking
52+
│ ├── dashboard.html # Feature flag example
53+
│ └── profile.html # Error reporting
54+
└── java/com/posthog/example/
55+
├── Application.java # Spring Boot entry point
56+
├── config/
57+
│ └── PostHogConfiguration.java # The single PostHog client bean
58+
└── controller/
59+
└── BurritoController.java # Events, identify, flags, errors
60+
```
61+
62+
## Key integration points
63+
64+
### PostHog client bean (config/PostHogConfiguration.java)
65+
66+
Create **one client per process** and expose it as a singleton bean. `destroyMethod = "close"` makes Spring flush queued events when the app shuts down.
67+
68+
```java
69+
@Bean(destroyMethod = "close")
70+
public PostHogInterface posthog(
71+
@Value("${posthog.project-token:}") String projectToken,
72+
@Value("${posthog.host:https://us.i.posthog.com}") String host) {
73+
74+
PostHogConfig config = PostHogConfig
75+
.builder(projectToken)
76+
.host(host)
77+
.build();
78+
79+
return PostHog.with(config);
80+
}
81+
```
82+
83+
### Configuration from the environment (application.properties)
84+
85+
Read the token and host from the environment so secrets never live in source:
86+
87+
```properties
88+
posthog.project-token=${POSTHOG_PROJECT_TOKEN:}
89+
posthog.host=${POSTHOG_HOST:https://us.i.posthog.com}
90+
```
91+
92+
### User identification (controller/BurritoController.java)
93+
94+
The server SDK identifies a user by attaching person properties to a capture with `userProperty(...)`. The `distinctId` (first argument) must match the id your frontend `identify` call uses.
95+
96+
```java
97+
posthog.capture(
98+
userId,
99+
"user_logged_in",
100+
PostHogCaptureOptions
101+
.builder()
102+
.userProperty("email", email)
103+
.property("login_method", "email")
104+
.build());
105+
```
106+
107+
### Event tracking (controller/BurritoController.java)
108+
109+
```java
110+
posthog.capture(
111+
userId,
112+
"burrito_considered",
113+
PostHogCaptureOptions
114+
.builder()
115+
.property("total_considerations", count)
116+
.build());
117+
```
118+
119+
### Feature flags (controller/BurritoController.java)
120+
121+
Evaluate flags once per request, then read individual flags off the snapshot. Avoid the deprecated per-flag helpers.
122+
123+
```java
124+
PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags(userId);
125+
boolean showNewFeature = flags.isEnabled("new-dashboard-feature");
126+
```
127+
128+
### Error reporting (controller/BurritoController.java)
129+
130+
There is no automatic exception capture in the Java server SDK, so report failures as an explicit event:
131+
132+
```java
133+
try {
134+
riskyOperation();
135+
} catch (RuntimeException e) {
136+
posthog.capture(
137+
userId,
138+
"error_occurred",
139+
PostHogCaptureOptions
140+
.builder()
141+
.property("error_type", e.getClass().getSimpleName())
142+
.property("error_message", e.getMessage())
143+
.build());
144+
}
145+
```
146+
147+
### Flush and shutdown
148+
149+
`destroyMethod = "close"` on the bean handles this for you at shutdown. If you ever
150+
manage the client yourself, flush and close explicitly:
151+
152+
```java
153+
posthog.flush(); // send any remaining events
154+
posthog.close(); // shut down the client
155+
```
156+
157+
## Learn more
158+
159+
- [PostHog Java integration](https://posthog.com/docs/libraries/java)
160+
- [PostHog feature flags](https://posthog.com/docs/feature-flags)
161+
- [PostHog documentation](https://posthog.com/docs)
162+
- [Spring Boot documentation](https://spring.io/projects/spring-boot)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
plugins {
2+
id 'org.springframework.boot' version '3.3.4'
3+
id 'io.spring.dependency-management' version '1.1.6'
4+
id 'java'
5+
}
6+
7+
group = 'com.posthog'
8+
version = '0.0.1'
9+
10+
java {
11+
sourceCompatibility = '17'
12+
}
13+
14+
repositories {
15+
mavenCentral()
16+
}
17+
18+
dependencies {
19+
implementation 'org.springframework.boot:spring-boot-starter-web'
20+
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
21+
22+
// PostHog server-side Java SDK
23+
implementation 'com.posthog:posthog-server:2.+'
24+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'posthog-java-spring-boot-example'
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.posthog.example;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
/**
7+
* Entry point for the PostHog Spring Boot example.
8+
*
9+
* <p>The interesting integration code lives in:
10+
* <ul>
11+
* <li>{@link com.posthog.example.config.PostHogConfiguration} &mdash; the single
12+
* PostHog client, exposed as a Spring bean and flushed on shutdown.</li>
13+
* <li>{@link com.posthog.example.controller.BurritoController} &mdash; where events,
14+
* user identification, feature flags, and error reporting happen.</li>
15+
* </ul>
16+
*/
17+
@SpringBootApplication
18+
public class Application {
19+
20+
public static void main(String[] args) {
21+
SpringApplication.run(Application.class, args);
22+
}
23+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.posthog.example.config;
2+
3+
import com.posthog.server.PostHog;
4+
import com.posthog.server.PostHogConfig;
5+
import com.posthog.server.PostHogInterface;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.context.annotation.Bean;
10+
import org.springframework.context.annotation.Configuration;
11+
12+
/**
13+
* Wires a single PostHog client into the Spring context.
14+
*
15+
* <p>Key integration points demonstrated here:
16+
* <ul>
17+
* <li><b>One client per process.</b> The client is a {@code @Bean}, so Spring
18+
* creates exactly one instance and injects it everywhere. Never construct a
19+
* new client per request.</li>
20+
* <li><b>Config from the environment.</b> The project token and host are read
21+
* from configuration (backed by env vars) &mdash; secrets are never hardcoded.</li>
22+
* <li><b>Flush on shutdown.</b> {@code destroyMethod = "close"} makes Spring call
23+
* {@link PostHogInterface#close()} when the context shuts down, so queued
24+
* events are flushed before the JVM exits.</li>
25+
* <li><b>Fail loudly, but never break the app.</b> A missing token logs a clear
26+
* warning instead of throwing, so a misconfigured environment is obvious in
27+
* dev without taking the whole service down.</li>
28+
* </ul>
29+
*/
30+
@Configuration
31+
public class PostHogConfiguration {
32+
33+
private static final Logger log = LoggerFactory.getLogger(PostHogConfiguration.class);
34+
35+
@Bean(destroyMethod = "close")
36+
public PostHogInterface posthog(
37+
@Value("${posthog.project-token:}") String projectToken,
38+
@Value("${posthog.host:https://us.i.posthog.com}") String host) {
39+
40+
if (projectToken == null || projectToken.isBlank()) {
41+
log.warn("POSTHOG_PROJECT_TOKEN is not set. PostHog events will not be delivered. "
42+
+ "Set it in your environment (see .env.example) to enable analytics.");
43+
}
44+
45+
PostHogConfig config = PostHogConfig
46+
.builder(projectToken)
47+
.host(host)
48+
.build();
49+
50+
return PostHog.with(config);
51+
}
52+
}

0 commit comments

Comments
 (0)