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
5 changes: 5 additions & 0 deletions .changeset/request-context-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog-server": minor
---

Add request-scoped context support for server-side captures, including PostHog tracing headers, session metadata, personless fallback events, and context-aware exception capture.
63 changes: 63 additions & 0 deletions posthog-server/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,69 @@ postHog.capture(

When `appendFeatureFlags` is `true`, the SDK will fetch feature flags for the user (or use locally evaluated flags if local evaluation is enabled) and include them in the event properties.

### Frontend-to-backend request context

If your server handles requests from a frontend that sends PostHog tracing headers, wrap request handling in a request context. Captures inside the scope can omit `distinctId`; the SDK uses the context distinct ID, falls back to a generated personless UUID when missing, and adds `$session_id` plus any request metadata unless explicitly overridden.

#### Kotlin

```kotlin
val context = PostHogRequestContext.fromHeaders(
headers = mapOf(
"X-PostHog-Distinct-Id" to requestDistinctIdHeader,
"X-PostHog-Session-Id" to requestSessionIdHeader,
),
properties = mapOf(
"\$current_url" to currentUrlWithoutSecrets,
"\$request_method" to method,
"\$request_path" to path,
"\$user_agent" to userAgent,
"\$ip" to clientIp,
),
)

PostHogRequestContext.beginScope(context, fresh = true).use {
postHog.capture("backend_event")
postHog.captureException(error)

// Uses context distinctId when omitted
val flags = postHog.evaluateFlags()

// Same, with request-scoped options
val scopedFlags = postHog.evaluateFlags(
PostHogEvaluateFlagsOptions.builder()
.flagKeys(listOf("new-checkout"))
.build()
)
}
```

#### Java

```java
Map<String, Object> properties = new HashMap<>();
properties.put("$request_path", path);
properties.put("$request_method", method);

PostHogRequestContextData context = PostHogRequestContext.fromHeaders(headers, true, properties);
try (PostHogRequestContext.Scope ignored = PostHogRequestContext.beginScope(context, true)) {
postHog.capture("backend_event");
postHog.captureException(error);

// Uses context distinctId when omitted
PostHogFeatureFlagEvaluations flags = postHog.evaluateFlags();

// Same, with request-scoped options
PostHogFeatureFlagEvaluations scopedFlags = postHog.evaluateFlags(
PostHogEvaluateFlagsOptions.builder()
.flagKeys(Arrays.asList("new-checkout"))
.build()
);
}
```

Use `PostHogRequestContext.fromHeaders(headers, false, properties)` to ignore client-supplied tracing headers while still attaching request metadata. Explicit `distinctId` and explicit `$session_id` values passed to `capture` always override request context. Header values are trimmed, control characters are removed, empty values are ignored, and long values are capped.

### Single-Call Flag Evaluation: `evaluateFlags()`

When you need to read several flags for the same user, call `evaluateFlags()` once and pass the resulting snapshot around. The snapshot answers `isEnabled` / `getFlag` / `getFlagPayload` from memory, dedups `$feature_flag_called` events per flag, and can be attached to a `capture()` call so the event is enriched without making another `/flags` request.
Expand Down
53 changes: 53 additions & 0 deletions posthog-server/api/posthog-server.api
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ public final class com/posthog/server/PostHog : com/posthog/PostHogStateless, co
public static final field Companion Lcom/posthog/server/PostHog$Companion;
public fun <init> ()V
public fun alias (Ljava/lang/String;Ljava/lang/String;)V
public fun capture (Ljava/lang/String;)V
public fun capture (Ljava/lang/String;Lcom/posthog/server/PostHogCaptureOptions;)V
public fun capture (Ljava/lang/String;Ljava/lang/String;)V
public fun capture (Ljava/lang/String;Ljava/lang/String;Lcom/posthog/server/PostHogCaptureOptions;)V
public fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;ZLcom/posthog/server/PostHogFeatureFlagEvaluations;)V
public synthetic fun capture (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;ZLcom/posthog/server/PostHogFeatureFlagEvaluations;)V
public fun captureException (Ljava/lang/Throwable;)V
public fun captureException (Ljava/lang/Throwable;Ljava/lang/String;)V
public fun captureException (Ljava/lang/Throwable;Ljava/lang/String;Ljava/util/Map;)V
public fun captureException (Ljava/lang/Throwable;Ljava/util/Map;)V
public fun close ()V
public fun debug (Z)V
public fun evaluateFlags ()Lcom/posthog/server/PostHogFeatureFlagEvaluations;
public fun evaluateFlags (Lcom/posthog/server/PostHogEvaluateFlagsOptions;)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
public fun evaluateFlags (Ljava/lang/String;)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
public fun evaluateFlags (Ljava/lang/String;Lcom/posthog/server/PostHogEvaluateFlagsOptions;)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
public fun evaluateFlags (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;ZZ)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
Expand Down Expand Up @@ -313,15 +318,20 @@ public final class com/posthog/server/PostHogFeatureFlagResultOptions$Companion

public abstract interface class com/posthog/server/PostHogInterface {
public abstract fun alias (Ljava/lang/String;Ljava/lang/String;)V
public abstract fun capture (Ljava/lang/String;)V
public abstract fun capture (Ljava/lang/String;Lcom/posthog/server/PostHogCaptureOptions;)V
public abstract fun capture (Ljava/lang/String;Ljava/lang/String;)V
public abstract fun capture (Ljava/lang/String;Ljava/lang/String;Lcom/posthog/server/PostHogCaptureOptions;)V
public abstract synthetic fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;ZLcom/posthog/server/PostHogFeatureFlagEvaluations;)V
public abstract synthetic fun capture (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;ZLcom/posthog/server/PostHogFeatureFlagEvaluations;)V
public abstract fun captureException (Ljava/lang/Throwable;)V
public abstract fun captureException (Ljava/lang/Throwable;Ljava/lang/String;)V
public abstract fun captureException (Ljava/lang/Throwable;Ljava/lang/String;Ljava/util/Map;)V
public abstract fun captureException (Ljava/lang/Throwable;Ljava/util/Map;)V
public abstract fun close ()V
public abstract fun debug (Z)V
public abstract fun evaluateFlags ()Lcom/posthog/server/PostHogFeatureFlagEvaluations;
public abstract fun evaluateFlags (Lcom/posthog/server/PostHogEvaluateFlagsOptions;)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
public abstract fun evaluateFlags (Ljava/lang/String;)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
public abstract fun evaluateFlags (Ljava/lang/String;Lcom/posthog/server/PostHogEvaluateFlagsOptions;)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
public abstract synthetic fun evaluateFlags (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;ZZ)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
Expand Down Expand Up @@ -351,13 +361,19 @@ public abstract interface class com/posthog/server/PostHogInterface {
}

public final class com/posthog/server/PostHogInterface$DefaultImpls {
public static fun capture (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;)V
public static fun capture (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Lcom/posthog/server/PostHogCaptureOptions;)V
public static fun capture (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/lang/String;)V
public static fun capture (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Lcom/posthog/server/PostHogCaptureOptions;)V
public static synthetic fun capture (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;ZLcom/posthog/server/PostHogFeatureFlagEvaluations;)V
public static synthetic fun capture$default (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;ZLcom/posthog/server/PostHogFeatureFlagEvaluations;ILjava/lang/Object;)V
public static synthetic fun capture$default (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;ZLcom/posthog/server/PostHogFeatureFlagEvaluations;ILjava/lang/Object;)V
public static fun captureException (Lcom/posthog/server/PostHogInterface;Ljava/lang/Throwable;)V
public static fun captureException (Lcom/posthog/server/PostHogInterface;Ljava/lang/Throwable;Ljava/lang/String;)V
public static fun captureException (Lcom/posthog/server/PostHogInterface;Ljava/lang/Throwable;Ljava/util/Map;)V
public static synthetic fun captureException$default (Lcom/posthog/server/PostHogInterface;Ljava/lang/Throwable;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V
public static fun evaluateFlags (Lcom/posthog/server/PostHogInterface;)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
public static fun evaluateFlags (Lcom/posthog/server/PostHogInterface;Lcom/posthog/server/PostHogEvaluateFlagsOptions;)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
public static fun evaluateFlags (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
public static fun evaluateFlags (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Lcom/posthog/server/PostHogEvaluateFlagsOptions;)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
public static synthetic fun evaluateFlags$default (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;ZZILjava/lang/Object;)Lcom/posthog/server/PostHogFeatureFlagEvaluations;
Expand All @@ -382,3 +398,40 @@ public final class com/posthog/server/PostHogInterface$DefaultImpls {
public static synthetic fun isFeatureEnabled$default (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Z
}

public final class com/posthog/server/PostHogRequestContext {
public static final field Companion Lcom/posthog/server/PostHogRequestContext$Companion;
public static final field DISTINCT_ID_HEADER Ljava/lang/String;
public static final field SESSION_ID_HEADER Ljava/lang/String;
public static final fun beginScope (Lcom/posthog/server/PostHogRequestContextData;)Lcom/posthog/server/PostHogRequestContext$Scope;
public static final fun beginScope (Lcom/posthog/server/PostHogRequestContextData;Z)Lcom/posthog/server/PostHogRequestContext$Scope;
public static final fun current ()Lcom/posthog/server/PostHogRequestContextData;
public static final fun fromHeaders (Ljava/util/Map;)Lcom/posthog/server/PostHogRequestContextData;
public static final fun fromHeaders (Ljava/util/Map;Z)Lcom/posthog/server/PostHogRequestContextData;
public static final fun fromHeaders (Ljava/util/Map;ZLjava/util/Map;)Lcom/posthog/server/PostHogRequestContextData;
}

public final class com/posthog/server/PostHogRequestContext$Companion {
public final fun beginScope (Lcom/posthog/server/PostHogRequestContextData;)Lcom/posthog/server/PostHogRequestContext$Scope;
public final fun beginScope (Lcom/posthog/server/PostHogRequestContextData;Z)Lcom/posthog/server/PostHogRequestContext$Scope;
public final fun current ()Lcom/posthog/server/PostHogRequestContextData;
public final fun fromHeaders (Ljava/util/Map;)Lcom/posthog/server/PostHogRequestContextData;
public final fun fromHeaders (Ljava/util/Map;Z)Lcom/posthog/server/PostHogRequestContextData;
public final fun fromHeaders (Ljava/util/Map;ZLjava/util/Map;)Lcom/posthog/server/PostHogRequestContextData;
public static synthetic fun fromHeaders$default (Lcom/posthog/server/PostHogRequestContext$Companion;Ljava/util/Map;ZLjava/util/Map;ILjava/lang/Object;)Lcom/posthog/server/PostHogRequestContextData;
public final fun withContext (Lcom/posthog/server/PostHogRequestContextData;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
public final fun withContext (Lcom/posthog/server/PostHogRequestContextData;ZLkotlin/jvm/functions/Function0;)Ljava/lang/Object;
}

public final class com/posthog/server/PostHogRequestContext$Scope : java/lang/AutoCloseable {
public fun close ()V
}

public final class com/posthog/server/PostHogRequestContextData {
public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getDistinctId ()Ljava/lang/String;
public final fun getProperties ()Ljava/util/Map;
public final fun getSessionId ()Ljava/lang/String;
}

44 changes: 30 additions & 14 deletions posthog-server/src/main/java/com/posthog/server/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class PostHog : PostHogStateless(), PostHogInterface {
appendFeatureFlags: Boolean,
flags: PostHogFeatureFlagEvaluations?,
) {
val captureContext = PostHogRequestContext.resolveCaptureContext(distinctId, properties)
val mergedProperties =
when {
flags != null -> {
Expand All @@ -73,7 +74,7 @@ public class PostHog : PostHogStateless(), PostHogInterface {
"using the supplied snapshot and skipping the redundant /flags fetch.",
)
}
mergeFeatureFlagPropertiesFromSnapshot(properties, flags)
mergeFeatureFlagPropertiesFromSnapshot(captureContext.properties, flags)
}
appendFeatureFlags -> {
getConfig<com.posthog.PostHogConfig>()?.logger?.log(
Expand All @@ -84,19 +85,19 @@ public class PostHog : PostHogStateless(), PostHogInterface {
"scope which flags to attach via flags.onlyAccessed() or flags.only(...).",
)
mergeFeatureFlagProperties(
distinctId = distinctId,
distinctId = captureContext.distinctId,
groups = groups,
userProperties = userProperties,
groupProperties = null,
properties = properties,
properties = captureContext.properties,
)
}
else -> properties
else -> captureContext.properties
}

super.captureStateless(
event,
distinctId,
captureContext.distinctId,
mergedProperties,
userProperties,
userPropertiesSetOnce,
Expand All @@ -116,8 +117,9 @@ public class PostHog : PostHogStateless(), PostHogInterface {
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): Boolean {
val resolvedDistinctId = PostHogRequestContext.resolveDistinctId(distinctId) ?: distinctId
return super.isFeatureEnabledStateless(
distinctId,
resolvedDistinctId,
key,
defaultValue,
groups,
Expand All @@ -137,8 +139,9 @@ public class PostHog : PostHogStateless(), PostHogInterface {
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): Any? {
val resolvedDistinctId = PostHogRequestContext.resolveDistinctId(distinctId) ?: distinctId
return super.getFeatureFlagStateless(
distinctId,
resolvedDistinctId,
key,
defaultValue,
groups,
Expand All @@ -158,8 +161,9 @@ public class PostHog : PostHogStateless(), PostHogInterface {
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): Any? {
val resolvedDistinctId = PostHogRequestContext.resolveDistinctId(distinctId) ?: distinctId
return super.getFeatureFlagPayloadStateless(
distinctId,
resolvedDistinctId,
key,
defaultValue,
groups,
Expand All @@ -181,8 +185,9 @@ public class PostHog : PostHogStateless(), PostHogInterface {
groupProperties: Map<String, Map<String, Any?>>?,
sendFeatureFlagEvent: Boolean?,
): FeatureFlagResult? {
val resolvedDistinctId = PostHogRequestContext.resolveDistinctId(distinctId) ?: distinctId
return super.getFeatureFlagResultStateless(
distinctId,
resolvedDistinctId,
key,
groups,
personProperties,
Expand Down Expand Up @@ -224,10 +229,20 @@ public class PostHog : PostHogStateless(), PostHogInterface {
distinctId: String?,
properties: Map<String, Any>?,
) {
if (!enabled) {
super.captureExceptionStateless(
exception,
distinctId = distinctId,
properties = properties,
)
return
}

val captureContext = PostHogRequestContext.resolveCaptureContext(distinctId, properties)
super.captureExceptionStateless(
exception,
distinctId = distinctId,
properties = properties,
distinctId = captureContext.distinctId,
properties = captureContext.properties,
)
}

Expand Down Expand Up @@ -293,7 +308,8 @@ public class PostHog : PostHogStateless(), PostHogInterface {
onlyEvaluateLocally: Boolean,
disableGeoip: Boolean,
): PostHogFeatureFlagEvaluations {
if (distinctId.isBlank()) {
val resolvedDistinctId = PostHogRequestContext.resolveDistinctId(distinctId)
if (resolvedDistinctId.isNullOrBlank()) {
return PostHogFeatureFlagEvaluations.empty(evaluationsHost)
}

Expand All @@ -303,7 +319,7 @@ public class PostHog : PostHogStateless(), PostHogInterface {

val result =
featureFlagsImpl.evaluateFlags(
distinctId = distinctId,
distinctId = resolvedDistinctId,
groups = groups,
personProperties = personProperties,
groupProperties = groupProperties,
Expand All @@ -313,7 +329,7 @@ public class PostHog : PostHogStateless(), PostHogInterface {
)

return PostHogFeatureFlagEvaluations(
distinctId = distinctId,
distinctId = resolvedDistinctId,
flagMap = result.flags,
locallyEvaluated = result.locallyEvaluated,
requestId = result.requestId,
Expand Down
Loading
Loading