Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions context/commandments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ commandments:
- Call `IdentifyAsync` for known users and put PII such as email in person properties, not in event properties
- Use `CaptureException(exception, distinctId, properties, groups, flags)` for handled exceptions; automatic exception capture is not available in the .NET SDK yet

# 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].
java-server:
- 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`
- 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
- Read the project token and host from the environment or configuration; never hardcode PostHog secrets
- 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
- 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
- Identify users by attaching person properties to a capture via `PostHogCaptureOptions.builder().userProperty(...)` / `.userPropertySetOnce(...)`; keep PII in person properties, not event properties
- For feature flags, call `posthog.evaluateFlags(distinctId)` once per request, then read values from the snapshot with `isEnabled("flag-key")` / `getFlagPayload("flag-key")`
- The Java server SDK has no automatic error tracking, surveys, or session replay; do not promise or scaffold those features

elixir:
- posthog-elixir is installed as the `posthog` Hex package; add `{:posthog, "~> 2.0"}` to `mix.exs` and run `mix deps.get`
- 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
Expand Down
9 changes: 9 additions & 0 deletions context/skills/integration/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,15 @@ variants:
docs_urls:
- https://posthog.com/docs/libraries/elixir.md

- id: java
framework: java
example_paths: example-apps/java-spring-boot
display_name: Java (Spring Boot)
description: PostHog integration for Java and Spring Boot applications using the posthog-server SDK
tags: [java, java-server]
docs_urls:
- https://posthog.com/docs/libraries/java.md

- id: go
template: description-go-docs-only.md
display_name: Go
Expand Down
2 changes: 2 additions & 0 deletions example-apps/java-spring-boot/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
POSTHOG_PROJECT_TOKEN=
POSTHOG_HOST=https://us.i.posthog.com
6 changes: 6 additions & 0 deletions example-apps/java-spring-boot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.env
build/
.gradle/
*.class
.idea/
*.iml
162 changes: 162 additions & 0 deletions example-apps/java-spring-boot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# PostHog Spring Boot example

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.

## Features

- **Product analytics**: Track user events and behaviors
- **User identification**: Associate events with authenticated users via person properties
- **Feature flags**: Control feature rollouts with PostHog feature flags
- **Server-side tracking**: All tracking happens server-side with the `posthog-server` SDK
- **Single client bean**: One PostHog client for the whole app, flushed on shutdown

> **Note on error tracking:** the PostHog Java server SDK does not have automatic
> exception capture. This example reports failures as an explicit `error_occurred`
> event instead. Do not expect session replay or surveys from the server SDK either.

## Getting started

### 1. Configure environment variables

Copy `.env.example` to `.env` and fill in your values, then export them:

```bash
export POSTHOG_PROJECT_TOKEN=your_posthog_project_token
export POSTHOG_HOST=https://us.i.posthog.com
```

Get your PostHog project token from your [PostHog project settings](https://app.posthog.com/project/settings).

### 2. Run the app

```bash
./gradlew bootRun
```

Open [http://localhost:8000](http://localhost:8000) with your browser to see the app.

## Project structure

```
java-spring-boot/
├── build.gradle # Gradle build with the posthog-server dependency
├── settings.gradle
├── .env.example # Environment variable template
├── .gitignore
└── src/main/
├── resources/
│ ├── application.properties # Binds POSTHOG_* env vars into config
│ └── templates/ # Thymeleaf pages
│ ├── home.html # Home / login page
│ ├── burrito.html # Event tracking
│ ├── dashboard.html # Feature flag example
│ └── profile.html # Error reporting
└── java/com/posthog/example/
├── Application.java # Spring Boot entry point
├── config/
│ └── PostHogConfiguration.java # The single PostHog client bean
└── controller/
└── BurritoController.java # Events, identify, flags, errors
```

## Key integration points

### PostHog client bean (config/PostHogConfiguration.java)

Create **one client per process** and expose it as a singleton bean. `destroyMethod = "close"` makes Spring flush queued events when the app shuts down.

```java
@Bean(destroyMethod = "close")
public PostHogInterface posthog(
@Value("${posthog.project-token:}") String projectToken,
@Value("${posthog.host:https://us.i.posthog.com}") String host) {

PostHogConfig config = PostHogConfig
.builder(projectToken)
.host(host)
.build();

return PostHog.with(config);
}
```

### Configuration from the environment (application.properties)

Read the token and host from the environment so secrets never live in source:

```properties
posthog.project-token=${POSTHOG_PROJECT_TOKEN:}
posthog.host=${POSTHOG_HOST:https://us.i.posthog.com}
```

### User identification (controller/BurritoController.java)

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.

```java
posthog.capture(
userId,
"user_logged_in",
PostHogCaptureOptions
.builder()
.userProperty("email", email)
.property("login_method", "email")
.build());
```

### Event tracking (controller/BurritoController.java)

```java
posthog.capture(
userId,
"burrito_considered",
PostHogCaptureOptions
.builder()
.property("total_considerations", count)
.build());
```

### Feature flags (controller/BurritoController.java)

Evaluate flags once per request, then read individual flags off the snapshot. Avoid the deprecated per-flag helpers.

```java
PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags(userId);
boolean showNewFeature = flags.isEnabled("new-dashboard-feature");
```

### Error reporting (controller/BurritoController.java)

There is no automatic exception capture in the Java server SDK, so report failures as an explicit event:

```java
try {
riskyOperation();
} catch (RuntimeException e) {
posthog.capture(
userId,
"error_occurred",
PostHogCaptureOptions
.builder()
.property("error_type", e.getClass().getSimpleName())
.property("error_message", e.getMessage())
.build());
}
```

### Flush and shutdown

`destroyMethod = "close"` on the bean handles this for you at shutdown. If you ever
manage the client yourself, flush and close explicitly:

```java
posthog.flush(); // send any remaining events
posthog.close(); // shut down the client
```

## Learn more

- [PostHog Java integration](https://posthog.com/docs/libraries/java)
- [PostHog feature flags](https://posthog.com/docs/feature-flags)
- [PostHog documentation](https://posthog.com/docs)
- [Spring Boot documentation](https://spring.io/projects/spring-boot)
24 changes: 24 additions & 0 deletions example-apps/java-spring-boot/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
id 'java'
}

group = 'com.posthog'
version = '0.0.1'

java {
sourceCompatibility = '17'
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// PostHog server-side Java SDK
implementation 'com.posthog:posthog-server:2.+'
}
1 change: 1 addition & 0 deletions example-apps/java-spring-boot/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'posthog-java-spring-boot-example'
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.posthog.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* Entry point for the PostHog Spring Boot example.
*
* <p>The interesting integration code lives in:
* <ul>
* <li>{@link com.posthog.example.config.PostHogConfiguration} &mdash; the single
* PostHog client, exposed as a Spring bean and flushed on shutdown.</li>
* <li>{@link com.posthog.example.controller.BurritoController} &mdash; where events,
* user identification, feature flags, and error reporting happen.</li>
* </ul>
*/
@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.posthog.example.config;

import com.posthog.server.PostHog;
import com.posthog.server.PostHogConfig;
import com.posthog.server.PostHogInterface;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* Wires a single PostHog client into the Spring context.
*
* <p>Key integration points demonstrated here:
* <ul>
* <li><b>One client per process.</b> The client is a {@code @Bean}, so Spring
* creates exactly one instance and injects it everywhere. Never construct a
* new client per request.</li>
* <li><b>Config from the environment.</b> The project token and host are read
* from configuration (backed by env vars) &mdash; secrets are never hardcoded.</li>
* <li><b>Flush on shutdown.</b> {@code destroyMethod = "close"} makes Spring call
* {@link PostHogInterface#close()} when the context shuts down, so queued
* events are flushed before the JVM exits.</li>
* <li><b>Fail loudly, but never break the app.</b> A missing token logs a clear
* warning instead of throwing, so a misconfigured environment is obvious in
* dev without taking the whole service down.</li>
* </ul>
*/
@Configuration
public class PostHogConfiguration {

private static final Logger log = LoggerFactory.getLogger(PostHogConfiguration.class);

@Bean(destroyMethod = "close")
public PostHogInterface posthog(
@Value("${posthog.project-token:}") String projectToken,
@Value("${posthog.host:https://us.i.posthog.com}") String host) {

if (projectToken == null || projectToken.isBlank()) {
log.warn("POSTHOG_PROJECT_TOKEN is not set. PostHog events will not be delivered. "
+ "Set it in your environment (see .env.example) to enable analytics.");
}

PostHogConfig config = PostHogConfig
.builder(projectToken)
.host(host)
.build();

return PostHog.with(config);
}
}
Loading
Loading