fix(event): prevent panic on non-string telemetry keys (#2502)

iceymoss created

Change summary

internal/event/event.go      |  6 +++++-
internal/event/event_test.go | 30 ++++++++++++++++++++++++++++++
2 files changed, 35 insertions(+), 1 deletion(-)

Detailed changes

internal/event/event.go 🔗

@@ -127,7 +127,11 @@ func pairsToProps(props ...any) posthog.Properties {
 	}
 
 	for i := 0; i < len(props); i += 2 {
-		key := props[i].(string)
+		key, ok := props[i].(string)
+		if !ok {
+			slog.Error("Event property key must be a string", "key", props[i], "index", i)
+			continue
+		}
 		value := props[i+1]
 		p = p.Set(key, value)
 	}

internal/event/event_test.go 🔗

@@ -4,7 +4,10 @@ package event
 // scenarios. These tests will not log anything.
 
 import (
+	"reflect"
 	"testing"
+
+	"github.com/posthog/posthog-go"
 )
 
 func TestError(t *testing.T) {
@@ -59,6 +62,33 @@ func TestError(t *testing.T) {
 	})
 }
 
+func TestPairsToProps(t *testing.T) {
+	t.Run("sets valid key value pairs", func(t *testing.T) {
+		got := pairsToProps("foo", "bar", "count", 3)
+		want := posthog.NewProperties().
+			Set("foo", "bar").
+			Set("count", 3)
+		if !reflect.DeepEqual(got, want) {
+			t.Fatalf("pairsToProps() = %#v, want %#v", got, want)
+		}
+	})
+
+	t.Run("returns empty properties for odd pairs", func(t *testing.T) {
+		got := pairsToProps("foo", "bar", "count")
+		if len(got) != 0 {
+			t.Fatalf("pairsToProps() should return empty properties, got %#v", got)
+		}
+	})
+
+	t.Run("ignores non-string key and continues", func(t *testing.T) {
+		got := pairsToProps(123, "bad", "ok", true)
+		want := posthog.NewProperties().Set("ok", true)
+		if !reflect.DeepEqual(got, want) {
+			t.Fatalf("pairsToProps() = %#v, want %#v", got, want)
+		}
+	})
+}
+
 // newDefaultTestError creates a test error that mimics runtime panic
 // errors. This helps us testing that the Error function can handle various
 // error types, including those that might be passed from a panic recovery