There is an official Java client for PostHog (here), however it unfortunately has some shortcomings:
- No support for feature flag local evalution
- There is a PR for this here, however it is not a priority for PostHog and has been left since June 2024.
- Shading all of its dependencies, this notably includes okhttp3, which brings along the entire Kotlin stdlib (a second copy for those already using Kotlin).
- Various other strange implementation details, such as duplicating the exact same network request to test a feature flag just to check for null (link).
- No support for new features (such as error tracking).
There has been some discussion/planning around extracting the relevant parts of the Android SDK to act as a standalone Java SDK, however this has yet to materialize.
posthog-java
is available
on maven central.
repositories {
mavenCentral()
}
dependencies {
implementation 'dev.hollowcube:posthog-java:<see releases>'
}
You will need to initialize the client with your PostHog project API key.
import net.hollowcube.posthog.PostHog;
class Sample {
private static final String POSTHOG_API_KEY = "phc_mK0jji1aC3hvMBGLOLjuVARqolDGPS9AiuNUOhMwVyA";
private static final String POSTHOG_HOST = "https://us.i.posthog.com";
public static void main(String[] args) {
PostHog.init("phc_<your project api key>", config -> config.endpoint(POSTHOG_HOST));
// Run application
PostHog.shutdown(); // Send remaining events in queue
}
}
Usage with non-static client
It is also possible to use the client in a non-static/singleton pattern.
import net.hollowcube.posthog.PostHog;
class Sample {
private static final String POSTHOG_API_KEY = "phc_mK0jji1aC3hvMBGLOLjuVARqolDGPS9AiuNUOhMwVyA";
private static final String POSTHOG_HOST = "https://us.i.posthog.com";
public static void main(String[] args) {
PostHogClient posthog = PostHogClient.newBuilder(POSTHOG_API_KEY)
.endpoint(POSTHOG_HOST)
.build();
// Run application
posthog.shutdown(Duration.ofSeconds(10));
}
}
You can send custom events using capture
:
PostHog.capture("distinct_id_of_user","user_signed_up");
Tip
PostHog recommends using an [object] [verb]
format for your event names,
where [object]
is the entity that the behavior relates to, and [verb]
is the behavior itself. For example, project created
, user signed up
,
or invite sent
.
Optionally, you can also include additional information in the event by setting the properties value:
public static void handleUserSignup() {
// Logic...
PostHog.capture("distinct_id_of_the_user", "user_signed_up", Map.of(
"login_type", "email",
"is_free_trial", true
));
}
Any Gson
serializable (to object) type may be used as event properties.
Event properties from struct
public static void handleUserSignup() {
// Logic...
record UserSignedUpEvent(String loginType, boolean isFreeTrial) {
}
PostHog.capture("distinct_id_of_the_user", "user_signed_up", new UserSignedUpEvent("email", true));
}
By default, captured events will be associated with the distinct_id
to create a
person profile. It is possible to set
person properties in these events
using $set
or $set_once
(
see here
for more details).
public void sample() {
PostHog.capture("distinct_id", "event_name", Map.of(
"$set", Map.of("name", "Max Hedgehog"),
"$set_once", Map.of("initial_url", "/blog")
));
}
Defining person properties without an associated event cal also be
done using identify
, the following is equivalent.
public void sample() {
PostHog.identify("distinct_id",
Map.of("name", "Max Hedgehog"), // $set
Map.of("initial_url", "/blog") // $set_once
);
}
To capture an anonymous event,
set $process_person_profile
to false
.
public void sample() {
PostHog.capture("distinct_id", "event_name", Map.of(
"$process_person_profile", false
));
}
Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend.
In this case, you can use alias to assign another distinct ID to the same user.
public void sample() {
PostHog.alias("distinct_id", "new_distinct_id");
}
See the alias docs for more information.
Group analytics allows you to associate an event with a group (e.g. teams, organizations, etc.). Read the Group Analytics guide for more information.
Events can be associated with a group by providing the $group
property, for example:
public void sample() {
PostHog.capture("distinct_id_of_user", "event_name", Map.of(
"$groups", Map.of("company", "company_id_in_your_db")
));
}
You can update properties for a group using groupIdentify
.
public void sample() {
PostHog.groupIdentify("company", "company_id_in_your_db", Map.of(
"name", "Hollow Cube",
"github_url", "https://github.com/hollow-cube"
));
}
The name
property on a group is used as a display name in the PostHog UI. If not present, the ID will be used instead.
Feature flags allow you to enable or disable features dynamically at runtime.
public void sample() {
boolean isMyFlagEnabled = PostHog.isFeatureEnabled("my-flag", "distinct_id_of_user");
if (isMyFlagEnabled) {
// Special logic
}
}
public void sample() {
FeatureFlagState myFlag = PostHog.getFeatureFlag("my_flag", "distinct_id_of_user");
if ("variant-key".equals(myFlag.getVariant())) { // replace 'variant-key' with the key of your variant
// Do something special
}
}
public void sample() {
FeatureFlagStates features = PostHog.getAllFeatureFlags("distinct_id_of_user");
if (features.isEnabled("my_flag")) {
// Special logic
}
if ("variant-key".equals(features.getVariant("my_flag"))) {
// Do something special
}
}
Sometimes, you may want to evaluate feature flags using person properties, groups, or group properties that haven't been ingested yet, or were set incorrectly earlier (or you want to evaluate locally, see below).
You can provide properties to evaluate the flag with by setting the person properties
, groups
, and
group properties
options on the FeatureFlagContext
. PostHog will then use these values to evaluate the flag, instead
of any properties currently stored on your PostHog server.
public void sample() {
FeatureFlagState result = PostHog.getFeatureFlag("my_flag", "distinct_id", FeatureFlagContext.newBuilder()
.personProperties(Map.of("name", "Max Hedgehog"))
.groups(Map.of("your_group_type", "your_group_id"))
.groupProperties(Map.of("your_group_type", Map.of(
"group_property_name", "value"
)))
.build());
if (result.isEnabled()) {
// Special logic...
}
}
Capturing $feature_flag_called events enable PostHog to know when a flag was accessed by a user and thus provide
analytics and insights on the flag. These are never sent by default, but can be enabled by setting
sendFeatureFlagEvents
on the client or the specific FeatureFlagContext
.
public void sample() {
PostHog.getFeatureFlag("my_flag", "distinct_id", new FeatureFlagContext.Builder()
.sendFeatureFlagEvents(true)
.build());
}
Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, we will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests.
It is best practice to use local evaluation flags when possible, since this enables you to resolve flags faster and with fewer API calls.
There are 3 steps to enable local evaluation:
1. Create a personal API key (link).
When you initialize PostHog with your personal API key, PostHog will use your the key to automatically fetch feature flag definitions. These definitions are then used to evaluate feature flags locally.
By default, PostHog fetches these definitions every 5 minutes. However, you can change this frequency by specifying a different value in the polling interval argument in the client config.
Note
For billing purposes, PostHog counts the request to fetch the feature flag definitions as being equivalent to 10 decide requests.
public static void main(String[] args) {
PostHog.init("<your project api key>", config -> config
.endpoint("https://us.i.posthog.com")
.personalApiKey("<your personal api key>"));
}
To evaluate the feature flag, call any of the flag related methods, like getFeatureFlag
or getAllFlags
, as you normally would. The only difference is that you must provide any
person properties
, groups
or group properties
used to evaluate the release conditions
of the flag.
public void sample() {
FeatureFlagState result = PostHog.getFeatureFlag("my_flag", "distinct_id", FeatureFlagContext.newBuilder()
.personProperties(Map.of("name", "Max Hedgehog"))
.groups(Map.of("your_group_type", "your_group_id"))
.groupProperties(Map.of("your_group_type", Map.of(
"group_property_name", "value"
)))
.build());
if (result.isEnabled()) {
// Special logic...
}
}
If the client is unable to evaluate the flag locally, it will make a request to the server
to fetch the value. This behavior can be disabled by setting allowRemoteFeatureFlagEvaluation
to false
either on the client or the specific FeatureFlagContext
.
public void sample() {
FeatureFlagState result = PostHog.getFeatureFlag("my_flag", "distinct_id", new FeatureFlagContext.Builder()
.setAllowRemoteEvaluation(false)
.build());
if (result.isEnabled()) {
// Special logic...
}
}
A flag which is only evaluated locally may create an indeterminate result if we do not have all required
context to evaluate the flag. Indeterminate results are treated as disabled, but can be checked explicitly
using FeatureFlagState#isIndeterminate
.
It is not possible to evaluate flags that:
- Have experience continuity enabled, which is set when you check 'persist flag across authentication steps' on your feature flag.
- Are linked to an early access feature.
- Depend on static cohorts.
- Use
is_not_set
as an evaluation condition. - Contain a condition that uses a property which was not passed as context.
The client periodically refreshes feature flag definitions, however you can also trigger an immediate
refresh using PostHog#reloadFeatureFlags
.
Warning
Error tracking is currently in beta and may change significantly in the future. It must be enabled from within your PostHog account for use. See here for official docs.
Error tracking enables tracking, investigating, and resolving exceptions in your app. Exceptions can be captured
using PostHog#captureException
.
public void sample() {
try {
int a = 1 / 0; // Some bad logic...
} catch (Exception e) {
PostHog.captureException(e);
// A distinct id may also be provided to associate the event with a user
PostHog.captureException(e, "distinct_id");
// Extra properties may also be provided with the event
PostHog.captureException(e, "distinct_id", Map.of("key", "value"));
}
}
posthog-java
does not have any automatic exception capturing, all exceptions must be reported to the client
manually. It is recommended to set up a global uncaught exception handler to capture any missed exceptions.
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler((ignored, e) -> {
PostHog.captureException(e);
});
}
Contributions via PRs and issues are always welcome.
This project is licensed under the MIT License.