From f38d2c75d1c8a02beea3cf6924befa5bf5f77e3b Mon Sep 17 00:00:00 2001 From: santimillang Date: Fri, 2 Oct 2026 22:21:07 +0200 Subject: [PATCH] feat(server): allow a timestamp override on identify Adds identify(distinctId, userProperties, userPropertiesSetOnce, timestamp) to the server SDK, matching the timestamp override capture() already has. Closes #303. Co-Authored-By: Claude Opus 5.5 --- .changeset/brave-owls-identify.md | 6 +++++ posthog-server/api/posthog-server.api | 3 +++ .../main/java/com/posthog/server/PostHog.kt | 12 +++++++++- .../com/posthog/server/PostHogInterface.kt | 22 +++++++++++++++++++ .../java/com/posthog/server/PostHogTest.kt | 12 ++++++++++ posthog/api/posthog.api | 1 + .../main/java/com/posthog/PostHogStateless.kt | 14 ++++++++++++ 7 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 .changeset/brave-owls-identify.md diff --git a/.changeset/brave-owls-identify.md b/.changeset/brave-owls-identify.md new file mode 100644 index 000000000..fec777ea1 --- /dev/null +++ b/.changeset/brave-owls-identify.md @@ -0,0 +1,6 @@ +--- +"posthog-server": minor +"posthog": minor +--- + +feat(server): allow overriding the `$identify` event timestamp with `identify(distinctId, userProperties, userPropertiesSetOnce, timestamp)` diff --git a/posthog-server/api/posthog-server.api b/posthog-server/api/posthog-server.api index 0f4c373ab..0cef1fb21 100644 --- a/posthog-server/api/posthog-server.api +++ b/posthog-server/api/posthog-server.api @@ -38,6 +38,7 @@ public final class com/posthog/server/PostHog : com/posthog/PostHogStateless, co public fun identify (Ljava/lang/String;)V public fun identify (Ljava/lang/String;Ljava/util/Map;)V public fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V + public fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;)V public fun isFeatureEnabled (Ljava/lang/String;Ljava/lang/String;)Z public fun isFeatureEnabled (Ljava/lang/String;Ljava/lang/String;Lcom/posthog/server/PostHogFeatureFlagOptions;)Z public fun isFeatureEnabled (Ljava/lang/String;Ljava/lang/String;Z)Z @@ -419,6 +420,7 @@ public abstract interface class com/posthog/server/PostHogInterface { public abstract fun identify (Ljava/lang/String;)V public abstract fun identify (Ljava/lang/String;Ljava/util/Map;)V public abstract fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V + public abstract fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;)V public abstract fun isFeatureEnabled (Ljava/lang/String;Ljava/lang/String;)Z public abstract fun isFeatureEnabled (Ljava/lang/String;Ljava/lang/String;Lcom/posthog/server/PostHogFeatureFlagOptions;)Z public abstract fun isFeatureEnabled (Ljava/lang/String;Ljava/lang/String;Z)Z @@ -460,6 +462,7 @@ public final class com/posthog/server/PostHogInterface$DefaultImpls { public static synthetic fun group$default (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V public static fun identify (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;)V public static fun identify (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/util/Map;)V + public static fun identify (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V public static fun isFeatureEnabled (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/lang/String;)Z public static fun isFeatureEnabled (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Lcom/posthog/server/PostHogFeatureFlagOptions;)Z public static fun isFeatureEnabled (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Z)Z diff --git a/posthog-server/src/main/java/com/posthog/server/PostHog.kt b/posthog-server/src/main/java/com/posthog/server/PostHog.kt index b16c68cc7..6a656b914 100644 --- a/posthog-server/src/main/java/com/posthog/server/PostHog.kt +++ b/posthog-server/src/main/java/com/posthog/server/PostHog.kt @@ -80,10 +80,20 @@ public class PostHog : PostHogStateless(), PostHogInterface { userProperties: Map?, userPropertiesSetOnce: Map?, ) { - super.identify( + identify(distinctId, userProperties, userPropertiesSetOnce, null) + } + + override fun identify( + distinctId: String, + userProperties: Map?, + userPropertiesSetOnce: Map?, + timestamp: java.util.Date?, + ) { + super.identifyStateless( distinctId, userProperties, userPropertiesSetOnce, + timestamp, ) } diff --git a/posthog-server/src/main/java/com/posthog/server/PostHogInterface.kt b/posthog-server/src/main/java/com/posthog/server/PostHogInterface.kt index e82d13102..089a25f8c 100644 --- a/posthog-server/src/main/java/com/posthog/server/PostHogInterface.kt +++ b/posthog-server/src/main/java/com/posthog/server/PostHogInterface.kt @@ -29,6 +29,28 @@ public sealed interface PostHogInterface { distinctId: String, userProperties: Map?, userPropertiesSetOnce: Map?, + ) { + identify( + distinctId, + userProperties, + userPropertiesSetOnce, + null, + ) + } + + /** + * Identifies the user + * Docs https://posthog.com/docs/product-analytics/identify + * @param distinctId the distinctId + * @param userProperties the user properties, set as a "$set" property, Docs https://posthog.com/docs/product-analytics/user-properties + * @param userPropertiesSetOnce the user properties to set only once, set as a "$set_once" property, Docs https://posthog.com/docs/product-analytics/user-properties + * @param timestamp the `$identify` event timestamp override. UTC is preferred; the equivalent instant is serialized in UTC. If null, the current time is used. Docs https://posthog.com/docs/data/timestamps + */ + public fun identify( + distinctId: String, + userProperties: Map?, + userPropertiesSetOnce: Map?, + timestamp: Date?, ) /** diff --git a/posthog-server/src/test/java/com/posthog/server/PostHogTest.kt b/posthog-server/src/test/java/com/posthog/server/PostHogTest.kt index 9aa797733..8e48fe123 100644 --- a/posthog-server/src/test/java/com/posthog/server/PostHogTest.kt +++ b/posthog-server/src/test/java/com/posthog/server/PostHogTest.kt @@ -114,6 +114,18 @@ internal class PostHogTest { } } + @Test + fun `identify with timestamp passes timestamp through`() { + withClient { client, server -> + client.identify("user123", mapOf("name" to "Ada"), null, java.util.Date(1234567890L)) + client.flush() + val batch = assertNotNull(server.takeRequest(5, TimeUnit.SECONDS)).parseBatch() + assertEquals("\$identify", batch.firstEvent?.get("event")?.asString) + assertEquals("1970-01-15T06:56:07.890Z", batch.firstEvent?.get("timestamp")?.asString) + assertEquals(mapOf("name" to "Ada"), batch.firstEventProperties()["\$set"]) + } + } + @Test fun `capture with PostHogCaptureOptions works correctly`() { withClient { client, server -> diff --git a/posthog/api/posthog.api b/posthog/api/posthog.api index 71d451629..0434dde12 100644 --- a/posthog/api/posthog.api +++ b/posthog/api/posthog.api @@ -508,6 +508,7 @@ public class com/posthog/PostHogStateless : com/posthog/PostHogStatelessInterfac protected final fun getThrowableCoercer ()Lcom/posthog/internal/errortracking/ThrowableCoercer; public fun groupStateless (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V public fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V + protected final fun identifyStateless (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;)V protected final fun isEnabled ()Z public fun isFeatureEnabledStateless (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;)Z protected fun isMinimalFlagCalledEventsEnabled ()Z diff --git a/posthog/src/main/java/com/posthog/PostHogStateless.kt b/posthog/src/main/java/com/posthog/PostHogStateless.kt index 4eb589176..01d79db9a 100644 --- a/posthog/src/main/java/com/posthog/PostHogStateless.kt +++ b/posthog/src/main/java/com/posthog/PostHogStateless.kt @@ -386,6 +386,19 @@ public open class PostHogStateless protected constructor( distinctId: String, userProperties: Map?, userPropertiesSetOnce: Map?, + ) { + identifyStateless(distinctId, userProperties, userPropertiesSetOnce, timestamp = null) + } + + /** + * Identifies the user, optionally overriding the `$identify` event timestamp + * @param timestamp the event timestamp override, or null to use the current time + */ + protected fun identifyStateless( + distinctId: String, + userProperties: Map?, + userPropertiesSetOnce: Map?, + timestamp: Date?, ) { if (!isEnabled()) { return @@ -404,6 +417,7 @@ public open class PostHogStateless protected constructor( properties = props, userProperties = userProperties, userPropertiesSetOnce = userPropertiesSetOnce, + timestamp = timestamp, ) }