From f1376ca633f8a4f23d97ba60d36e36ef2b5db9c6 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 19 Jun 2026 14:08:19 +0200 Subject: [PATCH 1/2] fix: validate top-level event uuids --- .changeset/clever-uuids-validate.md | 5 +++ lib/Client.php | 32 +++++++++++++-- lib/PostHog.php | 8 +++- test/PostHogTest.php | 63 +++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 .changeset/clever-uuids-validate.md diff --git a/.changeset/clever-uuids-validate.md b/.changeset/clever-uuids-validate.md new file mode 100644 index 0000000..1957285 --- /dev/null +++ b/.changeset/clever-uuids-validate.md @@ -0,0 +1,5 @@ +--- +"posthog-php": patch +--- + +Validate top-level event UUIDs and replace invalid values with generated UUIDs. diff --git a/lib/Client.php b/lib/Client.php index 7dccb58..e736f16 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -280,10 +280,13 @@ public function shutdown(): bool * properties?: array, * groups?: array, * timestamp?: mixed, + * uuid?: string, * flags?: FeatureFlagEvaluations, * send_feature_flags?: bool, * sendFeatureFlags?: bool - * } $message Event payload. `send_feature_flags` and `sendFeatureFlags` are deprecated; pass + * } $message Event payload. If a top-level `uuid` is supplied it must be a valid UUID; + * invalid values are replaced with a generated UUID v4. `send_feature_flags` and + * `sendFeatureFlags` are deprecated; pass * a `flags` snapshot from evaluateFlags() instead. Deprecated top-level batch metadata is * stripped before sending: use `event` instead of `type`, `properties['$lib']` instead of * `library`, `properties['$lib_version']` instead of `library_version`, and @@ -302,6 +305,7 @@ public function capture(array $message) $message = $this->applyCaptureContext($message, $usedGeneratedPersonlessDistinctId); } $message = $this->message($message); + $message = $this->normalizeMessageUuid($message); if (!array_key_exists('$groups', $message) && $hasGroups) { $message['$groups'] = $message['groups']; @@ -1726,12 +1730,13 @@ public function alias(array $message) /** * Queue a raw, already-prepared message. * - * @param array $message Prepared message payload. + * @param array $message Prepared message payload. If a top-level `uuid` is supplied + * it must be a valid UUID; invalid values are replaced with a generated UUID v4. * @return mixed Whether the underlying consumer accepted the message. */ public function raw(array $message) { - return $this->consumer->enqueue($message); + return $this->consumer->enqueue($this->normalizeMessageUuid($message)); } /** @@ -1930,6 +1935,27 @@ private function hasExplicitCaptureDistinctId(array &$msg): bool return false; } + private function normalizeMessageUuid(array $msg): array + { + if (array_key_exists('uuid', $msg) && !$this->isValidUuid($msg['uuid'])) { + $msg['uuid'] = Uuid::v4(); + } + + return $msg; + } + + private function isValidUuid(mixed $uuid): bool + { + if (!is_string($uuid)) { + return false; + } + + return preg_match( + '/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', + $uuid + ) === 1; + } + /** * Add common fields to the given `message` * diff --git a/lib/PostHog.php b/lib/PostHog.php index e7d2dc0..c7a00d8 100644 --- a/lib/PostHog.php +++ b/lib/PostHog.php @@ -128,10 +128,13 @@ public static function captureException( * properties?: array, * groups?: array, * timestamp?: mixed, + * uuid?: string, * flags?: FeatureFlagEvaluations, * send_feature_flags?: bool, * sendFeatureFlags?: bool - * } $message Event payload. `send_feature_flags` and `sendFeatureFlags` are deprecated; pass + * } $message Event payload. If a top-level `uuid` is supplied it must be a valid UUID; + * invalid values are replaced with a generated UUID v4. `send_feature_flags` and + * `sendFeatureFlags` are deprecated; pass * a `flags` snapshot from evaluateFlags() instead. Deprecated top-level batch metadata is * stripped before sending: use `event` instead of `type`, `properties['$lib']` instead of * `library`, `properties['$lib_version']` instead of `library_version`, and @@ -505,7 +508,8 @@ public static function contextFromHeaders(array $headers): array /** * Send a raw, already-prepared message to the underlying consumer queue. * - * @param array $message Prepared message payload. + * @param array $message Prepared message payload. If a top-level `uuid` is supplied + * it must be a valid UUID; invalid values are replaced with a generated UUID v4. * @return mixed Whether the underlying consumer accepted the message. */ public static function raw(array $message) diff --git a/test/PostHogTest.php b/test/PostHogTest.php index a36dd30..fad65c3 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -70,6 +70,14 @@ private function firstBatchEvent(): array self::fail("Expected a /batch/ call to have been made"); } + private function assertValidUuidV4(string $uuid): void + { + $this->assertMatchesRegularExpression( + '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', + $uuid + ); + } + private function withEnvApiKey(?string $apiKey, callable $callback): void { $previousApiKey = getenv(PostHog::ENV_API_KEY); @@ -690,6 +698,61 @@ public function testInvalidCaptureFlushIntervalDefaultsToFiveSeconds(mixed $flus } } + public function testCaptureKeepsValidTopLevelUuid(): void + { + $uuid = '01890f87-d7e7-7c75-8d35-8a1e16b6b0bf'; + + self::assertTrue( + PostHog::capture( + array( + "distinctId" => "john", + "event" => "Module PHP Event", + "uuid" => $uuid, + ) + ) + ); + PostHog::flush(); + + $event = $this->firstBatchEvent(); + + self::assertSame($uuid, $event['uuid']); + } + + public function testCaptureReplacesInvalidTopLevelUuid(): void + { + self::assertTrue( + PostHog::capture( + array( + "distinctId" => "john", + "event" => "Module PHP Event", + "uuid" => "not-a-uuid", + ) + ) + ); + PostHog::flush(); + + $event = $this->firstBatchEvent(); + + self::assertNotSame('not-a-uuid', $event['uuid']); + $this->assertValidUuidV4($event['uuid']); + } + + public function testRawReplacesInvalidTopLevelUuid(): void + { + $this->client->raw( + array( + "event" => "Raw Event", + "uuid" => false, + ) + ); + PostHog::flush(); + + $event = $this->firstBatchEvent(); + + self::assertNotSame(false, $event['uuid']); + $this->assertValidUuidV4($event['uuid']); + } + public function testCaptureIncludesIsServerProperty(): void { self::assertTrue( From 11e2f05e23e5fb7f2b951f7996f3bb7308dcad2c Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 19 Jun 2026 14:25:47 +0200 Subject: [PATCH 2/2] test: parameterize uuid validation coverage --- test/PostHogTest.php | 51 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/test/PostHogTest.php b/test/PostHogTest.php index fad65c3..da81bc1 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -141,6 +141,32 @@ public static function queuedBatchSizeCases(): array ]; } + public static function validTopLevelUuidCases(): array + { + return [ + 'v1 UUID' => ['01890f87-d7e7-1c75-8d35-8a1e16b6b0bf'], + 'v2 UUID' => ['01890f87-d7e7-2c75-8d35-8a1e16b6b0bf'], + 'v3 UUID' => ['01890f87-d7e7-3c75-8d35-8a1e16b6b0bf'], + 'v4 UUID' => ['01890f87-d7e7-4c75-8d35-8a1e16b6b0bf'], + 'v5 UUID' => ['01890f87-d7e7-5c75-8d35-8a1e16b6b0bf'], + 'v6 UUID' => ['01890f87-d7e7-6c75-8d35-8a1e16b6b0bf'], + 'v7 UUID' => ['01890f87-d7e7-7c75-8d35-8a1e16b6b0bf'], + 'v8 UUID' => ['01890f87-d7e7-8c75-8d35-8a1e16b6b0bf'], + ]; + } + + public static function invalidTopLevelUuidCases(): array + { + return [ + 'null' => [null], + 'empty string' => [''], + 'zero' => [0], + 'false' => [false], + 'non-UUID string' => ['not-a-uuid'], + 'nil UUID' => ['00000000-0000-0000-0000-000000000000'], + ]; + } + public static function facadeNoOpBeforeInitCases(): array { return [ @@ -698,10 +724,11 @@ public function testInvalidCaptureFlushIntervalDefaultsToFiveSeconds(mixed $flus } } - public function testCaptureKeepsValidTopLevelUuid(): void + /** + * @dataProvider validTopLevelUuidCases + */ + public function testCaptureKeepsValidTopLevelUuid(string $uuid): void { - $uuid = '01890f87-d7e7-7c75-8d35-8a1e16b6b0bf'; - self::assertTrue( PostHog::capture( array( @@ -718,14 +745,17 @@ public function testCaptureKeepsValidTopLevelUuid(): void self::assertSame($uuid, $event['uuid']); } - public function testCaptureReplacesInvalidTopLevelUuid(): void + /** + * @dataProvider invalidTopLevelUuidCases + */ + public function testCaptureReplacesInvalidTopLevelUuid(mixed $uuid): void { self::assertTrue( PostHog::capture( array( "distinctId" => "john", "event" => "Module PHP Event", - "uuid" => "not-a-uuid", + "uuid" => $uuid, ) ) ); @@ -733,23 +763,26 @@ public function testCaptureReplacesInvalidTopLevelUuid(): void $event = $this->firstBatchEvent(); - self::assertNotSame('not-a-uuid', $event['uuid']); + self::assertNotSame($uuid, $event['uuid']); $this->assertValidUuidV4($event['uuid']); } - public function testRawReplacesInvalidTopLevelUuid(): void + /** + * @dataProvider invalidTopLevelUuidCases + */ + public function testRawReplacesInvalidTopLevelUuid(mixed $uuid): void { $this->client->raw( array( "event" => "Raw Event", - "uuid" => false, + "uuid" => $uuid, ) ); PostHog::flush(); $event = $this->firstBatchEvent(); - self::assertNotSame(false, $event['uuid']); + self::assertNotSame($uuid, $event['uuid']); $this->assertValidUuidV4($event['uuid']); }