From 3381edbbecd8c7673d5bbea3d791f9f6c284193b Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Fri, 24 Oct 2025 12:07:02 -0400 Subject: [PATCH] feat: Add a Spring sample project This demonstrates the usage of a PostHog client as a Spring-managed bean with dependency injection into a controller. --- .../posthog-spring-sample/.gitignore | 37 +++++ .../posthog-spring-sample/README.md | 139 ++++++++++++++++++ .../posthog-spring-sample/build.gradle.kts | 24 +++ .../posthog-spring-sample/settings.gradle.kts | 1 + .../spring/sample/ActionController.java | 45 ++++++ .../spring/sample/PostHogConfiguration.java | 30 ++++ .../PostHogSpringSampleApplication.java | 12 ++ .../src/main/resources/application.properties | 7 + settings.gradle.kts | 1 + 9 files changed, 296 insertions(+) create mode 100644 posthog-samples/posthog-spring-sample/.gitignore create mode 100644 posthog-samples/posthog-spring-sample/README.md create mode 100644 posthog-samples/posthog-spring-sample/build.gradle.kts create mode 100644 posthog-samples/posthog-spring-sample/settings.gradle.kts create mode 100644 posthog-samples/posthog-spring-sample/src/main/java/com/posthog/spring/sample/ActionController.java create mode 100644 posthog-samples/posthog-spring-sample/src/main/java/com/posthog/spring/sample/PostHogConfiguration.java create mode 100644 posthog-samples/posthog-spring-sample/src/main/java/com/posthog/spring/sample/PostHogSpringSampleApplication.java create mode 100644 posthog-samples/posthog-spring-sample/src/main/resources/application.properties diff --git a/posthog-samples/posthog-spring-sample/.gitignore b/posthog-samples/posthog-spring-sample/.gitignore new file mode 100644 index 00000000..b2c5beaa --- /dev/null +++ b/posthog-samples/posthog-spring-sample/.gitignore @@ -0,0 +1,37 @@ +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# Gradle +.gradle/ +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +gradle-app.setting +.gradletasknamecache + +# IDE +.idea/ +*.iml +*.iws +*.ipr +.vscode/ +.classpath +.project +.settings/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/posthog-samples/posthog-spring-sample/README.md b/posthog-samples/posthog-spring-sample/README.md new file mode 100644 index 00000000..285f30c6 --- /dev/null +++ b/posthog-samples/posthog-spring-sample/README.md @@ -0,0 +1,139 @@ +# PostHog Spring Sample + +A sample Spring Boot application demonstrating how to integrate the PostHog server-side SDK as a Spring-managed bean. + +## Overview + +This sample shows how to: + +- Configure PostHog as a Spring Bean using dependency injection +- Inject the PostHog client into Spring controllers +- Capture events from REST API endpoints +- Configure PostHog via Spring properties + +## Project Structure + +``` +src/main/java/com/posthog/spring/sample/ +├── PostHogSpringSampleApplication.java # Spring Boot application entry point +├── PostHogConfiguration.java # PostHog bean configuration +└── ActionController.java # REST controller that captures events +``` + +## Configuration + +### PostHogConfiguration.java + +The `PostHogConfiguration` class creates a Spring-managed PostHog bean: + +```java +@Configuration +public class PostHogConfiguration { + @Value("${posthog.api.key:phc_YOUR_API_KEY_HERE}") + private String apiKey; + + @Value("${posthog.host:https://us.i.posthog.com}") + private String host; + + @Bean(destroyMethod = "close") + public PostHogInterface posthog() { + PostHogConfig config = PostHogConfig + .builder(this.apiKey) + .host(this.host) + .debug(true) + .build(); + + return PostHog.with(config); + } +} +``` + +Key points: + +- The `@Bean` annotation makes PostHog available for dependency injection +- `destroyMethod = "close"` ensures proper cleanup when the application shuts down +- Configuration values are read from `application.properties` + +### application.properties + +Configure PostHog settings in `src/main/resources/application.properties`: + +```properties +# PostHog Configuration +posthog.api.key=phc_YOUR_API_KEY_HERE +posthog.host=https://us.i.posthog.com + +# Spring Boot Configuration +spring.application.name=posthog-spring-sample +logging.level.com.posthog=DEBUG +``` + +## Setup + +1. Update `application.properties` with your PostHog API key: + + ```properties + posthog.api.key=phc_your_actual_api_key + ``` + +2. (Optional) Change the host if using a self-hosted instance: + + ```properties + posthog.host=https://your-posthog-instance.com + ``` + +3. Build and run the application: + ```bash + ./gradlew :posthog-samples:posthog-spring-sample:bootRun + ``` + +The application will start on port 8080 by default. + +## Usage + +### Capturing Events + +The sample provides a REST endpoint that captures a PostHog event: + +```bash +curl -X POST http://localhost:8080/api/action +``` + +This will: + +1. Generate a distinct user ID (in production, use a real user identifier) +2. Capture an event named `action_performed` with properties `{ "source": "api" }` +3. Return a 200 OK response + +### Controller Implementation + +The `ActionController` demonstrates dependency injection of the PostHog client: + +```java +@RestController +@RequestMapping("/api") +public class ActionController { + + private final PostHogInterface postHog; + + public ActionController(PostHogInterface postHog) { + this.postHog = postHog; + } + + @PostMapping("/action") + public ResponseEntity performAction() { + String distinctId = "user-" + System.currentTimeMillis(); + + postHog.capture( + distinctId, + "action_performed", + PostHogCaptureOptions + .builder() + .property("source", "api") + .build() + ); + + return ResponseEntity.ok().build(); + } +} +``` diff --git a/posthog-samples/posthog-spring-sample/build.gradle.kts b/posthog-samples/posthog-spring-sample/build.gradle.kts new file mode 100644 index 00000000..a0717c84 --- /dev/null +++ b/posthog-samples/posthog-spring-sample/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + java + id("org.springframework.boot") version "3.2.0" + id("io.spring.dependency-management") version "1.1.4" +} + +group = "com.posthog" +version = "1.0.0" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation(project(":posthog-server")) + testImplementation("org.springframework.boot:spring-boot-starter-test") +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/posthog-samples/posthog-spring-sample/settings.gradle.kts b/posthog-samples/posthog-spring-sample/settings.gradle.kts new file mode 100644 index 00000000..c12bc3d4 --- /dev/null +++ b/posthog-samples/posthog-spring-sample/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "posthog-spring-sample" diff --git a/posthog-samples/posthog-spring-sample/src/main/java/com/posthog/spring/sample/ActionController.java b/posthog-samples/posthog-spring-sample/src/main/java/com/posthog/spring/sample/ActionController.java new file mode 100644 index 00000000..10447a33 --- /dev/null +++ b/posthog-samples/posthog-spring-sample/src/main/java/com/posthog/spring/sample/ActionController.java @@ -0,0 +1,45 @@ +package com.posthog.spring.sample; + +import com.posthog.server.PostHogInterface; +import com.posthog.server.PostHogCaptureOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api") +public class ActionController { + + private static final Logger logger = LoggerFactory.getLogger(ActionController.class); + + private final PostHogInterface postHog; + + public ActionController(PostHogInterface postHog) { + this.postHog = postHog; + } + + @PostMapping("/action") + public ResponseEntity performAction() { + // In practice, use a real, stable distinct ID + // String distinctId = currentUser.getId(); + String distinctId = "user-" + System.currentTimeMillis(); + + postHog.capture( + distinctId, + "action_performed", + PostHogCaptureOptions + .builder() + .property("source", "api") + .build() + ); + + return ResponseEntity.ok().build(); + } +} diff --git a/posthog-samples/posthog-spring-sample/src/main/java/com/posthog/spring/sample/PostHogConfiguration.java b/posthog-samples/posthog-spring-sample/src/main/java/com/posthog/spring/sample/PostHogConfiguration.java new file mode 100644 index 00000000..4f5bcd77 --- /dev/null +++ b/posthog-samples/posthog-spring-sample/src/main/java/com/posthog/spring/sample/PostHogConfiguration.java @@ -0,0 +1,30 @@ +package com.posthog.spring.sample; + +import com.posthog.server.PostHogConfig; +import com.posthog.server.PostHogInterface; +import com.posthog.server.PostHog; +import jakarta.annotation.PreDestroy; +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; + +@Configuration +public class PostHogConfiguration { + @Value("${posthog.api.key:phc_YOUR_API_KEY_HERE}") + private String apiKey; + + @Value("${posthog.host:https://us.i.posthog.com}") + private String host; + + @Bean(destroyMethod = "close") + public PostHogInterface posthog() { + PostHogConfig config = PostHogConfig + .builder(this.apiKey) + .host(this.host) + .build(); + + return PostHog.with(config); + } +} diff --git a/posthog-samples/posthog-spring-sample/src/main/java/com/posthog/spring/sample/PostHogSpringSampleApplication.java b/posthog-samples/posthog-spring-sample/src/main/java/com/posthog/spring/sample/PostHogSpringSampleApplication.java new file mode 100644 index 00000000..99f245f6 --- /dev/null +++ b/posthog-samples/posthog-spring-sample/src/main/java/com/posthog/spring/sample/PostHogSpringSampleApplication.java @@ -0,0 +1,12 @@ +package com.posthog.spring.sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PostHogSpringSampleApplication { + + public static void main(String[] args) { + SpringApplication.run(PostHogSpringSampleApplication.class, args); + } +} diff --git a/posthog-samples/posthog-spring-sample/src/main/resources/application.properties b/posthog-samples/posthog-spring-sample/src/main/resources/application.properties new file mode 100644 index 00000000..cd1662d5 --- /dev/null +++ b/posthog-samples/posthog-spring-sample/src/main/resources/application.properties @@ -0,0 +1,7 @@ +# PostHog Configuration +posthog.api.key=phc_YOUR_API_KEY_HERE +posthog.host=https://us.i.posthog.com + +# Spring Boot Configuration +spring.application.name=posthog-spring-sample +logging.level.com.posthog=DEBUG diff --git a/settings.gradle.kts b/settings.gradle.kts index d3cbbd02..8a7128a6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,3 +22,4 @@ include(":posthog-server") // samples include(":posthog-samples:posthog-android-sample") include(":posthog-samples:posthog-java-sample") +include(":posthog-samples:posthog-spring-sample")