From c861ac4c0e04df1cf118cfc844839828b0abfdb0 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:49:45 +0200 Subject: [PATCH 1/2] fix: generate personless ids with UUID v7 --- .changeset/gold-cats-jump.md | 5 ++++ api/public-api.json | 7 +++++ lib/Client.php | 2 +- lib/ExceptionCapture.php | 2 +- lib/Uuid.php | 31 ++++++++++++++++++++ test/ExceptionCaptureTest.php | 2 +- test/ExceptionPayloadBuilderTest.php | 4 +-- test/RequestContextTest.php | 5 +++- test/UuidTest.php | 42 ++++++++++++++++++++++++++++ 9 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 .changeset/gold-cats-jump.md create mode 100644 test/UuidTest.php diff --git a/.changeset/gold-cats-jump.md b/.changeset/gold-cats-jump.md new file mode 100644 index 0000000..54fb092 --- /dev/null +++ b/.changeset/gold-cats-jump.md @@ -0,0 +1,5 @@ +--- +"posthog-php": patch +--- + +Generate personless distinct IDs with UUID v7. diff --git a/api/public-api.json b/api/public-api.json index 15811be..1aae968 100644 --- a/api/public-api.json +++ b/api/public-api.json @@ -4473,6 +4473,13 @@ "final": false, "returnType": "string", "parameters": [] + }, + "v7": { + "static": true, + "abstract": false, + "final": false, + "returnType": "string", + "parameters": [] } } } diff --git a/lib/Client.php b/lib/Client.php index 7dccb58..41489ba 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -1899,7 +1899,7 @@ private function applyCaptureContext(array $msg, bool &$usedGeneratedPersonlessD } if (!$explicitDistinctId) { - $msg["distinct_id"] = Uuid::v4(); + $msg["distinct_id"] = Uuid::v7(); $usedGeneratedPersonlessDistinctId = true; if (!array_key_exists('$process_person_profile', $msg["properties"])) { $msg["properties"]['$process_person_profile'] = false; diff --git a/lib/ExceptionCapture.php b/lib/ExceptionCapture.php index 32c18c6..5f4ee4b 100644 --- a/lib/ExceptionCapture.php +++ b/lib/ExceptionCapture.php @@ -433,7 +433,7 @@ private static function sendExceptionEvent( $distinctId = $providerContext['distinctId']; if ($distinctId === null) { - $distinctId = Uuid::v4(); + $distinctId = Uuid::v7(); $properties['$process_person_profile'] = false; } diff --git a/lib/Uuid.php b/lib/Uuid.php index 064a6a4..eddc070 100644 --- a/lib/Uuid.php +++ b/lib/Uuid.php @@ -29,4 +29,35 @@ public static function v4(): string random_int(0, 0xffff) ); } + + /** + * Generate a random UUID v7 string. + * + * @return string UUID v7. + * @throws \Random\RandomException When random_bytes() cannot gather sufficient entropy. + */ + public static function v7(): string + { + $bytes = random_bytes(16); + $timestamp = (int) floor(microtime(true) * 1000); + + for ($i = 5; $i >= 0; --$i) { + $bytes[$i] = chr($timestamp & 0xff); + $timestamp >>= 8; + } + + $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x70); + $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80); + + $hex = bin2hex($bytes); + + return sprintf( + '%s-%s-%s-%s-%s', + substr($hex, 0, 8), + substr($hex, 8, 4), + substr($hex, 12, 4), + substr($hex, 16, 4), + substr($hex, 20, 12) + ); + } } diff --git a/test/ExceptionCaptureTest.php b/test/ExceptionCaptureTest.php index bae5752..7908d22 100644 --- a/test/ExceptionCaptureTest.php +++ b/test/ExceptionCaptureTest.php @@ -169,7 +169,7 @@ public function testExceptionHandlerCapturesFlushesAndChainsPreviousHandler(): v $this->assertSame('RuntimeException', $event['properties']['$exception_list'][0]['type']); $this->assertFalse($event['properties']['$process_person_profile']); $this->assertMatchesRegularExpression( - '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', + '/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $event['distinct_id'] ); } finally { diff --git a/test/ExceptionPayloadBuilderTest.php b/test/ExceptionPayloadBuilderTest.php index 4718ba0..edcb13f 100644 --- a/test/ExceptionPayloadBuilderTest.php +++ b/test/ExceptionPayloadBuilderTest.php @@ -396,9 +396,9 @@ public function testCaptureExceptionWithoutDistinctIdGeneratesUuidAndSetsNoProfi $payload = json_decode($batchCall['payload'], true); $event = $payload['batch'][0]; - // distinct_id should look like a UUID + // distinct_id should look like a UUID v7 $this->assertMatchesRegularExpression( - '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', + '/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $event['distinct_id'] ); $this->assertFalse($event['properties']['$process_person_profile']); diff --git a/test/RequestContextTest.php b/test/RequestContextTest.php index 7b025f3..ef49269 100644 --- a/test/RequestContextTest.php +++ b/test/RequestContextTest.php @@ -113,7 +113,10 @@ public function testMissingDistinctIdCreatesPersonlessEvent(): void $event = $this->flushAndGetEvents()[0]; $this->assertIsString($event['distinct_id']); - $this->assertNotSame('', $event['distinct_id']); + $this->assertMatchesRegularExpression( + '/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', + $event['distinct_id'] + ); $this->assertFalse($event['properties']['$process_person_profile']); $this->assertSame('free', $event['properties']['plan']); } diff --git a/test/UuidTest.php b/test/UuidTest.php new file mode 100644 index 0000000..f136d5a --- /dev/null +++ b/test/UuidTest.php @@ -0,0 +1,42 @@ +assertMatchesRegularExpression( + '/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', + $uuid + ); + } + + public function testV7EmbedsCurrentTimestamp(): void + { + $before = (int) floor(microtime(true) * 1000); + $uuid = Uuid::v7(); + $after = (int) floor(microtime(true) * 1000); + + $timestamp = hexdec(substr(str_replace('-', '', $uuid), 0, 12)); + + $this->assertGreaterThanOrEqual($before, $timestamp); + $this->assertLessThanOrEqual($after, $timestamp); + } + + public function testV7GeneratesUniqueValues(): void + { + $uuids = []; + + for ($i = 0; $i < 100; ++$i) { + $uuid = Uuid::v7(); + $this->assertArrayNotHasKey($uuid, $uuids); + $uuids[$uuid] = true; + } + } +} From 488bc9e84c5cd1bbb5ac9c602b55aedeaad27974 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 19 Jun 2026 14:03:20 +0200 Subject: [PATCH 2/2] address uuid v7 review feedback --- api/public-api.json | 7 ----- composer.json | 5 +++- composer.lock | 2 +- lib/Client.php | 2 +- lib/ExceptionCapture.php | 2 +- lib/Uuid.php | 56 +++++++++++++++++++++------------------- test/UuidTest.php | 9 ++++--- 7 files changed, 42 insertions(+), 41 deletions(-) diff --git a/api/public-api.json b/api/public-api.json index 1aae968..15811be 100644 --- a/api/public-api.json +++ b/api/public-api.json @@ -4473,13 +4473,6 @@ "final": false, "returnType": "string", "parameters": [] - }, - "v7": { - "static": true, - "abstract": false, - "final": false, - "returnType": "string", - "parameters": [] } } } diff --git a/composer.json b/composer.json index f1115d6..c9e6d00 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,10 @@ "autoload": { "psr-4": { "PostHog\\": "lib/" - } + }, + "files": [ + "lib/Uuid.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/composer.lock b/composer.lock index e4ade16..c17e233 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c11900659348ffada66c82406a3d8c6c", + "content-hash": "14cfceee7677b783774cabf54cf62c97", "packages": [ { "name": "psr/clock", diff --git a/lib/Client.php b/lib/Client.php index 41489ba..f6b3404 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -1899,7 +1899,7 @@ private function applyCaptureContext(array $msg, bool &$usedGeneratedPersonlessD } if (!$explicitDistinctId) { - $msg["distinct_id"] = Uuid::v7(); + $msg["distinct_id"] = uuidV7(); $usedGeneratedPersonlessDistinctId = true; if (!array_key_exists('$process_person_profile', $msg["properties"])) { $msg["properties"]['$process_person_profile'] = false; diff --git a/lib/ExceptionCapture.php b/lib/ExceptionCapture.php index 5f4ee4b..2cb6f58 100644 --- a/lib/ExceptionCapture.php +++ b/lib/ExceptionCapture.php @@ -433,7 +433,7 @@ private static function sendExceptionEvent( $distinctId = $providerContext['distinctId']; if ($distinctId === null) { - $distinctId = Uuid::v7(); + $distinctId = uuidV7(); $properties['$process_person_profile'] = false; } diff --git a/lib/Uuid.php b/lib/Uuid.php index eddc070..4531559 100644 --- a/lib/Uuid.php +++ b/lib/Uuid.php @@ -29,35 +29,39 @@ public static function v4(): string random_int(0, 0xffff) ); } +} - /** - * Generate a random UUID v7 string. - * - * @return string UUID v7. - * @throws \Random\RandomException When random_bytes() cannot gather sufficient entropy. - */ - public static function v7(): string - { - $bytes = random_bytes(16); - $timestamp = (int) floor(microtime(true) * 1000); +/** + * Generate a UUID v7 string using the RFC 9562 Unix timestamp layout. + * + * PHP does not provide a built-in UUID API in the SDK's supported runtimes, so + * this internal helper keeps UUID v7 generation local without adding a runtime dependency. + * + * @internal + * @return string UUID v7. + * @throws \Random\RandomException When random_bytes() cannot gather sufficient entropy. + */ +function uuidV7(): string +{ + $bytes = random_bytes(16); + $timestamp = (int) floor(microtime(true) * 1000); - for ($i = 5; $i >= 0; --$i) { - $bytes[$i] = chr($timestamp & 0xff); - $timestamp >>= 8; - } + for ($i = 5; $i >= 0; --$i) { + $bytes[$i] = chr($timestamp & 0xff); + $timestamp >>= 8; + } - $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x70); - $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80); + $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x70); + $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80); - $hex = bin2hex($bytes); + $hex = bin2hex($bytes); - return sprintf( - '%s-%s-%s-%s-%s', - substr($hex, 0, 8), - substr($hex, 8, 4), - substr($hex, 12, 4), - substr($hex, 16, 4), - substr($hex, 20, 12) - ); - } + return sprintf( + '%s-%s-%s-%s-%s', + substr($hex, 0, 8), + substr($hex, 8, 4), + substr($hex, 12, 4), + substr($hex, 16, 4), + substr($hex, 20, 12) + ); } diff --git a/test/UuidTest.php b/test/UuidTest.php index f136d5a..7594479 100644 --- a/test/UuidTest.php +++ b/test/UuidTest.php @@ -3,13 +3,14 @@ namespace PostHog\Test; use PHPUnit\Framework\TestCase; -use PostHog\Uuid; + +use function PostHog\uuidV7; class UuidTest extends TestCase { public function testV7GeneratesValidVersionAndVariant(): void { - $uuid = Uuid::v7(); + $uuid = uuidV7(); $this->assertMatchesRegularExpression( '/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', @@ -20,7 +21,7 @@ public function testV7GeneratesValidVersionAndVariant(): void public function testV7EmbedsCurrentTimestamp(): void { $before = (int) floor(microtime(true) * 1000); - $uuid = Uuid::v7(); + $uuid = uuidV7(); $after = (int) floor(microtime(true) * 1000); $timestamp = hexdec(substr(str_replace('-', '', $uuid), 0, 12)); @@ -34,7 +35,7 @@ public function testV7GeneratesUniqueValues(): void $uuids = []; for ($i = 0; $i < 100; ++$i) { - $uuid = Uuid::v7(); + $uuid = uuidV7(); $this->assertArrayNotHasKey($uuid, $uuids); $uuids[$uuid] = true; }