Skip to content
Merged
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/clever-uuids-validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog-php": patch
---

Validate top-level event UUIDs and replace invalid values with generated UUIDs.
32 changes: 29 additions & 3 deletions lib/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,13 @@ public function shutdown(): bool
* properties?: array<string, mixed>,
* groups?: array<string, mixed>,
* 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
Expand All @@ -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'];
Expand Down Expand Up @@ -1726,12 +1730,13 @@ public function alias(array $message)
/**
* Queue a raw, already-prepared message.
*
* @param array<string, mixed> $message Prepared message payload.
* @param array<string, mixed> $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));
}

/**
Expand Down Expand Up @@ -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();
Comment thread
marandaneto marked this conversation as resolved.
}

return $msg;
Comment thread
marandaneto marked this conversation as resolved.
}

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',
Comment thread
marandaneto marked this conversation as resolved.
$uuid
) === 1;
}

/**
* Add common fields to the given `message`
*
Expand Down
8 changes: 6 additions & 2 deletions lib/PostHog.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,13 @@ public static function captureException(
* properties?: array<string, mixed>,
* groups?: array<string, mixed>,
* 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
Expand Down Expand Up @@ -505,7 +508,8 @@ public static function contextFromHeaders(array $headers): array
/**
* Send a raw, already-prepared message to the underlying consumer queue.
*
* @param array<string, mixed> $message Prepared message payload.
* @param array<string, mixed> $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)
Expand Down
96 changes: 96 additions & 0 deletions test/PostHogTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -133,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 [
Expand Down Expand Up @@ -690,6 +724,68 @@ public function testInvalidCaptureFlushIntervalDefaultsToFiveSeconds(mixed $flus
}
}

/**
* @dataProvider validTopLevelUuidCases
*/
public function testCaptureKeepsValidTopLevelUuid(string $uuid): void
{
self::assertTrue(
PostHog::capture(
array(
"distinctId" => "john",
"event" => "Module PHP Event",
"uuid" => $uuid,
)
)
);
PostHog::flush();

$event = $this->firstBatchEvent();

self::assertSame($uuid, $event['uuid']);
}

/**
* @dataProvider invalidTopLevelUuidCases
*/
public function testCaptureReplacesInvalidTopLevelUuid(mixed $uuid): void
{
self::assertTrue(
PostHog::capture(
array(
"distinctId" => "john",
"event" => "Module PHP Event",
"uuid" => $uuid,
)
)
);
PostHog::flush();

$event = $this->firstBatchEvent();

self::assertNotSame($uuid, $event['uuid']);
$this->assertValidUuidV4($event['uuid']);
}

/**
* @dataProvider invalidTopLevelUuidCases
*/
public function testRawReplacesInvalidTopLevelUuid(mixed $uuid): void
{
$this->client->raw(
array(
"event" => "Raw Event",
"uuid" => $uuid,
)
);
PostHog::flush();

$event = $this->firstBatchEvent();

self::assertNotSame($uuid, $event['uuid']);
$this->assertValidUuidV4($event['uuid']);
}

public function testCaptureIncludesIsServerProperty(): void
{
self::assertTrue(
Comment thread
marandaneto marked this conversation as resolved.
Expand Down
Loading