diff --git a/.changeset/uuid-v7-personless-ids.md b/.changeset/uuid-v7-personless-ids.md new file mode 100644 index 00000000..60c654fe --- /dev/null +++ b/.changeset/uuid-v7-personless-ids.md @@ -0,0 +1,7 @@ +--- +"posthog": patch +"posthog-android": patch +"posthog-server": patch +--- + +Generate UUID v7 values for SDK-created personless distinct IDs. diff --git a/posthog-server/src/main/java/com/posthog/server/PostHogRequestContext.kt b/posthog-server/src/main/java/com/posthog/server/PostHogRequestContext.kt index edcc5fd0..1d853d5d 100644 --- a/posthog-server/src/main/java/com/posthog/server/PostHogRequestContext.kt +++ b/posthog-server/src/main/java/com/posthog/server/PostHogRequestContext.kt @@ -1,6 +1,6 @@ package com.posthog.server -import java.util.UUID +import com.posthog.vendor.uuid.TimeBasedEpochGenerator /** * Request-scoped PostHog analytics context. @@ -181,7 +181,7 @@ public class PostHogRequestContext private constructor() { val resolvedDistinctId = resolveDistinctId(distinctId) val isPersonless = resolvedDistinctId.isNullOrBlank() val mergedProperties = properties?.toMutableMap() ?: mutableMapOf() - val captureDistinctId = resolvedDistinctId ?: UUID.randomUUID().toString() + val captureDistinctId = resolvedDistinctId ?: TimeBasedEpochGenerator.generate().toString() if (isPersonless) { mergedProperties.putIfAbsent(PROCESS_PERSON_PROFILE_PROPERTY, false) diff --git a/posthog-server/src/test/java/com/posthog/server/PostHogRequestContextTest.kt b/posthog-server/src/test/java/com/posthog/server/PostHogRequestContextTest.kt index 1274e84f..2c5a252d 100644 --- a/posthog-server/src/test/java/com/posthog/server/PostHogRequestContextTest.kt +++ b/posthog-server/src/test/java/com/posthog/server/PostHogRequestContextTest.kt @@ -155,7 +155,8 @@ internal class PostHogRequestContextTest { val distinctId = batch.firstEvent?.get("distinct_id")?.asString assertNotNull(distinctId) assertNotEquals("header-user", distinctId) - assertTrue(runCatching { java.util.UUID.fromString(distinctId) }.isSuccess) + val parsedDistinctId = java.util.UUID.fromString(distinctId) + assertEquals(7, parsedDistinctId.version()) val properties = batch.firstEventProperties() assertEquals(false, properties["\$process_person_profile"]) assertFalse(properties.containsKey("\$session_id")) @@ -176,7 +177,8 @@ internal class PostHogRequestContextTest { val batch = request.parseBatch() val distinctId = batch.firstEvent?.get("distinct_id")?.asString assertNotNull(distinctId) - assertTrue(runCatching { java.util.UUID.fromString(distinctId) }.isSuccess) + val parsedDistinctId = java.util.UUID.fromString(distinctId) + assertEquals(7, parsedDistinctId.version()) val properties = batch.firstEventProperties() assertEquals(false, properties["\$process_person_profile"]) assertEquals("present", properties["request"]) diff --git a/posthog/src/main/java/com/posthog/PostHogEvent.kt b/posthog/src/main/java/com/posthog/PostHogEvent.kt index 27f8d0b8..d09c02f4 100644 --- a/posthog/src/main/java/com/posthog/PostHogEvent.kt +++ b/posthog/src/main/java/com/posthog/PostHogEvent.kt @@ -16,7 +16,7 @@ import java.util.UUID * @property distinctId The distinct Id * @property properties All the event properties * @property timestamp The timestamp is automatically generated - * @property uuid the UUID v4 is automatically generated and used for deduplication + * @property uuid the UUID v7 is automatically generated and used for deduplication */ public data class PostHogEvent( val event: String, diff --git a/posthog/src/main/java/com/posthog/PostHogStateless.kt b/posthog/src/main/java/com/posthog/PostHogStateless.kt index 7b487580..ff043f81 100644 --- a/posthog/src/main/java/com/posthog/PostHogStateless.kt +++ b/posthog/src/main/java/com/posthog/PostHogStateless.kt @@ -13,8 +13,8 @@ import com.posthog.internal.PostHogPrintLogger import com.posthog.internal.PostHogQueueInterface import com.posthog.internal.PostHogThreadFactory import com.posthog.internal.errortracking.ThrowableCoercer +import com.posthog.vendor.uuid.TimeBasedEpochGenerator import java.util.Date -import java.util.UUID import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -599,7 +599,7 @@ public open class PostHogStateless protected constructor( var id = distinctId if (id.isNullOrBlank()) { exceptionProperties.set("\$process_person_profile", false) - id = UUID.randomUUID().toString() + id = TimeBasedEpochGenerator.generate().toString() } captureStateless(PostHogEventName.EXCEPTION.event, distinctId = id, properties = exceptionProperties) diff --git a/posthog/src/test/java/com/posthog/PostHogStatelessTest.kt b/posthog/src/test/java/com/posthog/PostHogStatelessTest.kt index ebcc9ac3..2e18118d 100644 --- a/posthog/src/test/java/com/posthog/PostHogStatelessTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogStatelessTest.kt @@ -13,6 +13,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import java.io.File import java.util.Date +import java.util.UUID import java.util.concurrent.Executors import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -1349,7 +1350,7 @@ internal class PostHogStatelessTest { } @Test - fun `captureExceptionStateless without distinctId generates random UUID`() { + fun `captureExceptionStateless without distinctId generates UUID v7`() { val mockQueue = MockQueue() sut = createStatelessInstance() config = createConfig() @@ -1365,6 +1366,7 @@ internal class PostHogStatelessTest { val event = mockQueue.events.first() assertTrue(event.distinctId.isNotBlank()) assertTrue(event.distinctId.matches(("[0-9a-fA-F-]{36}".toRegex()))) + assertEquals(7, UUID.fromString(event.distinctId).version()) assertEquals(false, event.properties!!["\$process_person_profile"]) }