From a6a591a982be08c3f63ad0c8971a1d4e8fe371b1 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 22 Oct 2026 13:43:28 +0200 Subject: [PATCH] fix: serialize non-finite event properties as null --- .changeset/tidy-numbers-persist.md | 5 + .../Runtime/Utilities/JsonSerializer.cs | 12 ++- tests/PostHog.Unity.Tests/EventQueueTests.cs | 93 +++++++++++++++++++ .../JsonSerializerTests.cs | 14 +++ 4 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 .changeset/tidy-numbers-persist.md diff --git a/.changeset/tidy-numbers-persist.md b/.changeset/tidy-numbers-persist.md new file mode 100644 index 0000000..063a1e8 --- /dev/null +++ b/.changeset/tidy-numbers-persist.md @@ -0,0 +1,5 @@ +--- +'com.posthog.unity': patch +--- + +Serialize numeric NaN and infinities as JSON null so queued analytics events retain neighboring properties and array positions and can flush safely. diff --git a/com.posthog.unity/Runtime/Utilities/JsonSerializer.cs b/com.posthog.unity/Runtime/Utilities/JsonSerializer.cs index 3abd83f..6bfb727 100644 --- a/com.posthog.unity/Runtime/Utilities/JsonSerializer.cs +++ b/com.posthog.unity/Runtime/Utilities/JsonSerializer.cs @@ -86,10 +86,18 @@ static void SerializeValue(object value, StringBuilder sb) sb.Append(l.ToString(CultureInfo.InvariantCulture)); break; case float f: - sb.Append(f.ToString(CultureInfo.InvariantCulture)); + sb.Append( + float.IsNaN(f) || float.IsInfinity(f) + ? "null" + : f.ToString(CultureInfo.InvariantCulture) + ); break; case double d: - sb.Append(d.ToString(CultureInfo.InvariantCulture)); + sb.Append( + double.IsNaN(d) || double.IsInfinity(d) + ? "null" + : d.ToString(CultureInfo.InvariantCulture) + ); break; case decimal dec: sb.Append(dec.ToString(CultureInfo.InvariantCulture)); diff --git a/tests/PostHog.Unity.Tests/EventQueueTests.cs b/tests/PostHog.Unity.Tests/EventQueueTests.cs index 228df61..163fdbe 100644 --- a/tests/PostHog.Unity.Tests/EventQueueTests.cs +++ b/tests/PostHog.Unity.Tests/EventQueueTests.cs @@ -65,6 +65,99 @@ public override string ToString() } } + [Theory] + [InlineData(float.NaN)] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + [InlineData(double.NaN)] + [InlineData(double.PositiveInfinity)] + [InlineData(double.NegativeInfinity)] + public void NonFinitePropertiesRoundTripAsNullWithoutLosingNeighbors(object value) + { + var harness = CreateHarness(); + var nested = new Dictionary + { + ["before"] = 1, + ["bad"] = value, + ["after"] = 2, + }; + var items = new object[] { 1, value, 2, nested }; + var properties = new Dictionary + { + ["before"] = 1, + ["bad"] = value, + ["nested"] = nested, + ["items"] = items, + ["float"] = 1.5f, + ["double"] = -2.5d, + ["zero"] = 0, + ["enabled"] = true, + ["disabled"] = false, + ["null"] = null, + ["literals"] = new[] { "NaN", "Infinity", "-Infinity" }, + ["after"] = 42, + }; + var evt = new PostHogEvent("non-finite", "distinct-id", properties); + var healthy = Event("healthy"); + healthy.Properties["answer"] = 7; + + harness.Queue.Enqueue(evt); + harness.Queue.Enqueue(healthy); + + var ids = harness.Storage.GetEventIds(); + Assert.Equal(2, ids.Count); + // Validate strict JSON before the custom parser can read any invalid array tokens. + using ( + var stored = System.Text.Json.JsonDocument.Parse(harness.Storage.LoadEvent(ids[0])) + ) + { + AssertProperties(stored.RootElement.GetProperty("properties")); + } + + harness.Results.Enqueue((true, 200)); + RunCoroutine(harness.Queue.FlushCoroutine()); + + var batch = Assert.Single(harness.Attempts); + Assert.Equal(new[] { "non-finite", "healthy" }, batch.Select(item => item.Event)); + Assert.Equal(evt.Uuid, batch[0].Uuid); + Assert.Equal(evt.DistinctId, batch[0].DistinctId); + Assert.Equal(evt.Timestamp, batch[0].Timestamp); + using var sent = System.Text.Json.JsonDocument.Parse( + JsonSerializer.SerializeBatch(new BatchPayload("test-api-key", batch)) + ); + var sentBatch = sent.RootElement.GetProperty("batch"); + Assert.Equal(2, sentBatch.GetArrayLength()); + AssertProperties(sentBatch[0].GetProperty("properties")); + Assert.Equal( + 7, + sentBatch[1].GetProperty("properties").GetProperty("answer").GetInt32() + ); + Assert.Empty(harness.Storage.GetEventIds()); + Assert.Same(value, properties["bad"]); + Assert.Same(nested, properties["nested"]); + Assert.Same(items, properties["items"]); + Assert.Same(value, nested["bad"]); + Assert.Same(value, items[1]); + Assert.Same(nested, items[3]); + + static void AssertProperties(System.Text.Json.JsonElement actual) + { + var expected = System.Text.Json.Nodes.JsonNode.Parse( + "{\"before\":1,\"bad\":null,\"nested\":{\"before\":1,\"bad\":null,\"after\":2}," + + "\"items\":[1,null,2,{\"before\":1,\"bad\":null,\"after\":2}]," + + "\"float\":1.5,\"double\":-2.5,\"zero\":0,\"enabled\":true,\"disabled\":false," + + "\"null\":null,\"literals\":[\"NaN\",\"Infinity\",\"-Infinity\"],\"after\":42}" + ); + Assert.True( + System.Text.Json.Nodes.JsonNode.DeepEquals( + expected, + System.Text.Json.Nodes.JsonNode.Parse(actual.GetRawText()) + ), + actual.GetRawText() + ); + } + } + [Fact] public void DuplicatePayloadUuidsUseDistinctQueueOwnedIds() { diff --git a/tests/PostHog.Unity.Tests/JsonSerializerTests.cs b/tests/PostHog.Unity.Tests/JsonSerializerTests.cs index ce389cd..96c6584 100644 --- a/tests/PostHog.Unity.Tests/JsonSerializerTests.cs +++ b/tests/PostHog.Unity.Tests/JsonSerializerTests.cs @@ -76,6 +76,20 @@ public void WithDouble_ReturnsDecimalNumber() Assert.Equal("3.14", result); } + [Theory] + [InlineData(float.NaN)] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + [InlineData(double.NaN)] + [InlineData(double.PositiveInfinity)] + [InlineData(double.NegativeInfinity)] + public void WithNonFiniteNumber_ReturnsNull(object value) + { + var result = JsonSerializer.Serialize(value); + + Assert.Equal("null", result); + } + [Fact] public void WithEmptyDictionary_ReturnsEmptyObject() {