Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
bbc0f54
fix: serialize non-finite event properties as null
marandaneto Oct 22, 2026
04d9f63
fix: bound recursive JSON serializer chains
marandaneto Oct 22, 2026
6a9282d
fix: preserve native object serialization and cycle detection
marandaneto Oct 22, 2026
bd515d6
merge: integrate release 4.17.0 from main
marandaneto Oct 22, 2026
25ad084
fix: preserve embedded-NUL keys and normalize serializable enums
marandaneto Oct 22, 2026
4059fad
fix: preserve property hooks on stdClass subclasses
marandaneto Oct 22, 2026
a3fd115
fix: preserve native JSON encoding for closures
marandaneto Oct 22, 2026
2155f5a
fix: preserve native getters and release JSON wrappers
marandaneto Oct 22, 2026
e414589
fix: preserve native flags and reduce plain-data encoding allocations
marandaneto Oct 23, 2026
446beec
fix: preserve live reference reads and safe native eligibility
marandaneto Oct 23, 2026
7e232ad
fix: defer declared property reads and bound JSON preparation
marandaneto Oct 23, 2026
c5d55ce
fix: reject invalid UTF-8 before expanding shared JSON graphs
marandaneto Oct 23, 2026
5a4a540
fix: emit JSON sequentially without wrapper graph allocation
marandaneto Oct 23, 2026
577a8b7
fix: preserve native object views and replaced array references
marandaneto Oct 23, 2026
d16a8b1
fix: restrict non-finite repair to bounded data-only payloads
marandaneto Oct 23, 2026
4df58ef
fix: compact retained array storage before non-finite repair
marandaneto Oct 23, 2026
9f690ed
refactor: keep JSON encoding internal and remove serialization docs
marandaneto Oct 23, 2026
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/seven-suns-strive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog-php": patch
---

Normalize NaN and infinity to null in bounded data-only JSON payloads, while preserving native encoding for objects, references and out-of-budget payloads.
3 changes: 2 additions & 1 deletion lib/Consumer/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Exception;
use PostHog\Consumer;
use PostHog\Internal\JsonEncoder;

/**
* Consumer that writes analytics messages to a local file.
Expand Down Expand Up @@ -101,7 +102,7 @@ private function write($body)
return false;
}

$content = json_encode($body);
$content = JsonEncoder::encode($body);
$content .= "\n";

return fwrite($this->file_handle, $content) == strlen($content);
Expand Down
98 changes: 98 additions & 0 deletions lib/Internal/JsonEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

namespace PostHog\Internal;

use JsonException;
use ReflectionReference;

/**
* Repairs non-finite numbers in bounded, object/reference-free data payloads.
*
* @internal
*/
final class JsonEncoder
{
private const MAX_DEPTH = 64;
private int $remainingValues = 10000;
private int $remainingBytes = 1048576;
private bool $hasNonFinite = false;

private function __construct()
{
}

public static function encode(mixed $value): string|false
{
// Opaque callbacks may retain ancestor arrays, so eligibility must precede every copy.
$inspection = new self();
if ($inspection->eligible($value, 0) && $inspection->hasNonFinite) {
$changed = false;
$value = self::normalize($value, $changed);
}
return json_encode($value);
}

private function eligible(mixed $value, int $depth): bool
{
if (--$this->remainingValues < 0) {
return false;
}
if (is_array($value)) {
if ($depth >= self::MAX_DEPTH) {
return false;
}
foreach ($value as $key => $item) {
if (
ReflectionReference::fromArrayElement($value, $key) !== null
|| (is_string($key) && !$this->eligibleString($key))
|| !$this->eligible($item, $depth + 1)
) {
return false;
}
}
return true;
}
if (is_string($value)) {
return $this->eligibleString($value);
}
if (is_float($value) && !is_finite($value)) {
$this->hasNonFinite = true;
}
return $value === null || is_scalar($value);
}

private function eligibleString(string $value): bool
{
$this->remainingBytes -= strlen($value);
if ($this->remainingBytes < 0) {
return false;
}
// Validation must not reset the JSON error state visible to subsequent native callbacks.
try {
json_encode($value, JSON_THROW_ON_ERROR);
return true;
} catch (JsonException) {
return false;
}
}

private static function normalize(mixed $value, bool &$changed): mixed
{
$changed = is_float($value) && !is_finite($value);
if ($changed) {
return null;
}
if (!is_array($value)) {
return $value;
}
// Compact retained storage first; array_map also preserves numeric-string keys without PHP key coercion.
$compact = array_slice($value, 0, null, true);
$result = array_map(static function (mixed $item) use (&$changed): mixed {
$childChanged = false;
$normalized = self::normalize($item, $childChanged);
$changed = $changed || $childChanged;
return $normalized;
}, $compact);
return $changed ? $result : $value;
}
}
3 changes: 2 additions & 1 deletion lib/QueueConsumer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PostHog;

use PostHog\Internal\JsonEncoder;
use Symfony\Component\Clock\Clock;
use Throwable;

Expand Down Expand Up @@ -213,7 +214,7 @@ private function now(): float
*/
protected function encodeBatchPayload($batch)
{
$payload = json_encode($this->payload($batch));
$payload = JsonEncoder::encode($this->payload($batch));
if (false === $payload) {
$this->handleError(json_last_error(), "Failed to encode batch payload: " . json_last_error_msg());
return false;
Expand Down
5 changes: 4 additions & 1 deletion scripts/PublicApiSnapshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ private static function buildPublicApi(string $sourceDir, string $namespacePrefi

$symbols = [];
foreach (array_merge(get_declared_interfaces(), get_declared_traits(), get_declared_classes()) as $name) {
if (!str_starts_with($name, $namespacePrefix)) {
if (
!str_starts_with($name, $namespacePrefix)
|| str_starts_with($name, $namespacePrefix . 'Internal\\')
) {
continue;
}

Expand Down
Loading
Loading