Skip to content

feat(livestream): Return geo data in the livestream service#44720

Closed
jordanm-posthog wants to merge 2 commits intomasterfrom
jordanm-posthog/addGeoPropertiesToLivestream
Closed

feat(livestream): Return geo data in the livestream service#44720
jordanm-posthog wants to merge 2 commits intomasterfrom
jordanm-posthog/addGeoPropertiesToLivestream

Conversation

@jordanm-posthog
Copy link
Copy Markdown
Contributor

Problem

The Livestream client currently doesn't expose a way to get geo data alongside the event data. Additionally, the geo data only included lat / lng.

Changes

  • Adds latitude, longitude, country code to livestream event responses via a new geo field
  • Refactors GeoLocator to return a GeoResult struct instead of separate lat/lng values, making it easier to extend with additional geo fields in the future
  • Changes geo coordinate fields from float64 to *float64 to handle cases where geo lookup fails or returns no data
  • Upgrades maxminddb-golang from v1 to v2 for improved performance and so we can use the Found() method.

How did you test this code?

👉 Stay up-to-date with PostHog coding conventions for a smoother review.

Publish to changelog?

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jan 10, 2026

Greptile Summary

Adds geographic data (latitude, longitude, country code) to livestream event responses through a new geo field. Refactors GeoLocator to return a GeoResult struct with pointer fields for nullable geo data, replacing the previous separate lat/lng return values. Upgrades maxminddb-golang from v1 to v2 for improved performance and access to the Found() method for detecting missing geo records.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

Comment thread livestream/geo/geoip.go
Comment on lines +64 to +68
return GeoResult{
Latitude: &record.Location.Latitude,
Longitude: &record.Location.Longitude,
CountryCode: &record.Country.ISOCode,
}, nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code always returns a pointer to CountryCode even when the field is empty. MaxMind may return an empty string for iso_code when country data is unavailable. To maintain consistency with the nil-pointer pattern used for missing data, check if ISOCode is empty before returning a pointer:

Suggested change
return GeoResult{
Latitude: &record.Location.Latitude,
Longitude: &record.Location.Longitude,
CountryCode: &record.Country.ISOCode,
}, nil
result := GeoResult{
Latitude: &record.Location.Latitude,
Longitude: &record.Location.Longitude,
}
if record.Country.ISOCode != "" {
result.CountryCode = &record.Country.ISOCode
}
return result, nil
Prompt To Fix With AI
This is a comment left during a code review.
Path: livestream/geo/geoip.go
Line: 64:68

Comment:
The code always returns a pointer to `CountryCode` even when the field is empty. MaxMind may return an empty string for `iso_code` when country data is unavailable. To maintain consistency with the nil-pointer pattern used for missing data, check if `ISOCode` is empty before returning a pointer:

```suggestion
	result := GeoResult{
		Latitude:  &record.Location.Latitude,
		Longitude: &record.Location.Longitude,
	}
	if record.Country.ISOCode != "" {
		result.CountryCode = &record.Country.ISOCode
	}
	return result, nil
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread livestream/geo/geoip.go
Comment on lines +64 to +68
return GeoResult{
Latitude: &record.Location.Latitude,
Longitude: &record.Location.Longitude,
CountryCode: &record.Country.ISOCode,
}, nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, consider checking if latitude/longitude values are zero (0, 0), which typically indicates missing or invalid geo coordinates. While less critical than empty country codes, returning nil for (0, 0) coordinates would prevent false positives:

Suggested change
return GeoResult{
Latitude: &record.Location.Latitude,
Longitude: &record.Location.Longitude,
CountryCode: &record.Country.ISOCode,
}, nil
result := GeoResult{}
if record.Location.Latitude != 0 || record.Location.Longitude != 0 {
result.Latitude = &record.Location.Latitude
result.Longitude = &record.Location.Longitude
}
if record.Country.ISOCode != "" {
result.CountryCode = &record.Country.ISOCode
}
return result, nil

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: livestream/geo/geoip.go
Line: 64:68

Comment:
Similarly, consider checking if latitude/longitude values are zero (0, 0), which typically indicates missing or invalid geo coordinates. While less critical than empty country codes, returning nil for (0, 0) coordinates would prevent false positives:

```suggestion
	result := GeoResult{}
	if record.Location.Latitude != 0 || record.Location.Longitude != 0 {
		result.Latitude = &record.Location.Latitude
		result.Longitude = &record.Location.Longitude
	}
	if record.Country.ISOCode != "" {
		result.CountryCode = &record.Country.ISOCode
	}
	return result, nil
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 73 to +78
func convertToResponsePostHogEvent(event PostHogEvent, teamId int) *ResponsePostHogEvent {
geo := &GeoData{
Lat: event.Lat,
Lng: event.Lng,
CountryCode: event.CountryCode,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The geo field is always created with a GeoData struct even when all fields are nil. This adds unnecessary data to the JSON response. Consider returning nil when no geo data is available:

Suggested change
func convertToResponsePostHogEvent(event PostHogEvent, teamId int) *ResponsePostHogEvent {
geo := &GeoData{
Lat: event.Lat,
Lng: event.Lng,
CountryCode: event.CountryCode,
}
var geo *GeoData
if event.Lat != nil || event.Lng != nil || event.CountryCode != nil {
geo = &GeoData{
Lat: event.Lat,
Lng: event.Lng,
CountryCode: event.CountryCode,
}
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: livestream/events/filter.go
Line: 73:78

Comment:
The `geo` field is always created with a `GeoData` struct even when all fields are nil. This adds unnecessary data to the JSON response. Consider returning nil when no geo data is available:

```suggestion
	var geo *GeoData
	if event.Lat != nil || event.Lng != nil || event.CountryCode != nil {
		geo = &GeoData{
			Lat:         event.Lat,
			Lng:         event.Lng,
			CountryCode: event.CountryCode,
		}
	}
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 109 to 134
@@ -122,8 +127,9 @@ func TestParse(t *testing.T) {
"message_count": 0.,
"message_kind": "event",
},
Lat: 10,
Lng: 20,
Lat: ptrFloat64(10),
Lng: ptrFloat64(20),
CountryCode: ptrString("US"),
}, got)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test already expects CountryCode in the result but doesn't explicitly assert it was set. Add an explicit assertion to verify the country code is correctly parsed.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: livestream/events/kafka_test.go
Line: 109:134

Comment:
Test already expects CountryCode in the result but doesn't explicitly assert it was set. Add an explicit assertion to verify the country code is correctly parsed.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 56 to 76
@@ -64,8 +69,8 @@ func TestPostHogKafkaConsumer_Consume(t *testing.T) {
assert.Equal(t, "test-distinct-id", event.DistinctId)
assert.Equal(t, "test-event", event.Event)
assert.Equal(t, "test-token", event.Token)
assert.Equal(t, 37.7749, event.Lat)
assert.Equal(t, -122.4194, event.Lng)
assert.Equal(t, 37.7749, *event.Lat)
assert.Equal(t, -122.4194, *event.Lng)
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test verifies lat/lng but doesn't check CountryCode. Add assertion for the country code field to ensure complete geo data validation:

Suggested change
// Mock GeoLocator Lookup
mockGeoLocator.On("Lookup", "192.0.2.1").Return(geo.GeoResult{
Latitude: ptrFloat64(37.7749),
Longitude: ptrFloat64(-122.4194),
CountryCode: ptrString("US"),
}, nil)
// Run Consume in a goroutine
go consumer.Consume()
// Wait for the message to be processed
select {
case event := <-outgoingChan:
assert.Equal(t, "test-uuid", event.Uuid)
assert.Equal(t, "test-distinct-id", event.DistinctId)
assert.Equal(t, "test-event", event.Event)
assert.Equal(t, "test-token", event.Token)
assert.Equal(t, 37.7749, *event.Lat)
assert.Equal(t, -122.4194, *event.Lng)
assert.Equal(t, "US", *event.CountryCode)
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: livestream/events/kafka_test.go
Line: 56:76

Comment:
Test verifies lat/lng but doesn't check CountryCode. Add assertion for the country code field to ensure complete geo data validation:

```suggestion
	// Mock GeoLocator Lookup
	mockGeoLocator.On("Lookup", "192.0.2.1").Return(geo.GeoResult{
		Latitude:    ptrFloat64(37.7749),
		Longitude:   ptrFloat64(-122.4194),
		CountryCode: ptrString("US"),
	}, nil)

	// Run Consume in a goroutine
	go consumer.Consume()

	// Wait for the message to be processed
	select {
	case event := <-outgoingChan:
		assert.Equal(t, "test-uuid", event.Uuid)
		assert.Equal(t, "test-distinct-id", event.DistinctId)
		assert.Equal(t, "test-event", event.Event)
		assert.Equal(t, "test-token", event.Token)
		assert.Equal(t, 37.7749, *event.Lat)
		assert.Equal(t, -122.4194, *event.Lng)
		assert.Equal(t, "US", *event.CountryCode)
	case <-time.After(time.Second):
		t.Fatal("Timed out waiting for message")
	}
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jan 10, 2026

Additional Comments (1)

livestream/events/filter_test.go
Test doesn't verify that geo data is properly included in the response. Add assertions for the Geo field:

func TestConvertToResponsePostHogEvent(t *testing.T) {
	timestamp := "2023-01-01T00:00:00Z"
	event := PostHogEvent{
		Uuid:        "123",
		Timestamp:   timestamp,
		DistinctId:  "user1",
		Event:       "pageview",
		Properties:  map[string]interface{}{"url": "https://example.com"},
		Lat:         ptrFloat64(40.7128),
		Lng:         ptrFloat64(-74.0060),
		CountryCode: ptrString("US"),
	}

	result := convertToResponsePostHogEvent(event, 1)

	assert.Equal(t, "123", result.Uuid)
	assert.Equal(t, "2023-01-01T00:00:00Z", result.Timestamp)
	assert.Equal(t, "user1", result.DistinctId)
	assert.NotEmpty(t, result.PersonId)
	assert.Equal(t, "pageview", result.Event)
	assert.Equal(t, "https://example.com", result.Properties["url"])
	assert.NotNil(t, result.Geo)
	assert.Equal(t, 40.7128, *result.Geo.Lat)
	assert.Equal(t, -74.0060, *result.Geo.Lng)
	assert.Equal(t, "US", *result.Geo.CountryCode)
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: livestream/events/filter_test.go
Line: 70:88

Comment:
Test doesn't verify that geo data is properly included in the response. Add assertions for the `Geo` field:

```suggestion
func TestConvertToResponsePostHogEvent(t *testing.T) {
	timestamp := "2023-01-01T00:00:00Z"
	event := PostHogEvent{
		Uuid:        "123",
		Timestamp:   timestamp,
		DistinctId:  "user1",
		Event:       "pageview",
		Properties:  map[string]interface{}{"url": "https://example.com"},
		Lat:         ptrFloat64(40.7128),
		Lng:         ptrFloat64(-74.0060),
		CountryCode: ptrString("US"),
	}

	result := convertToResponsePostHogEvent(event, 1)

	assert.Equal(t, "123", result.Uuid)
	assert.Equal(t, "2023-01-01T00:00:00Z", result.Timestamp)
	assert.Equal(t, "user1", result.DistinctId)
	assert.NotEmpty(t, result.PersonId)
	assert.Equal(t, "pageview", result.Event)
	assert.Equal(t, "https://example.com", result.Properties["url"])
	assert.NotNil(t, result.Geo)
	assert.Equal(t, 40.7128, *result.Geo.Lat)
	assert.Equal(t, -74.0060, *result.Geo.Lng)
	assert.Equal(t, "US", *result.Geo.CountryCode)
}
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 12 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="livestream/events/kafka_test.go">

<violation number="1" location="livestream/events/kafka_test.go:73">
P2: Add assertion to verify CountryCode field is populated correctly from the geo lookup result.</violation>
</file>

<file name="livestream/geo/geoip.go">

<violation number="1" location="livestream/geo/geoip.go:67">
P2: Consider checking if `ISOCode` is empty before returning a pointer. MaxMind may return an empty string when country data is unavailable. Returning a pointer to an empty string breaks the nil-pointer pattern used elsewhere for missing data.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

assert.Equal(t, 37.7749, event.Lat)
assert.Equal(t, -122.4194, event.Lng)
assert.Equal(t, 37.7749, *event.Lat)
assert.Equal(t, -122.4194, *event.Lng)
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Add assertion to verify CountryCode field is populated correctly from the geo lookup result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At livestream/events/kafka_test.go, line 73:

<comment>Add assertion to verify CountryCode field is populated correctly from the geo lookup result.</comment>

<file context>
@@ -64,8 +69,8 @@ func TestPostHogKafkaConsumer_Consume(t *testing.T) {
-		assert.Equal(t, 37.7749, event.Lat)
-		assert.Equal(t, -122.4194, event.Lng)
+		assert.Equal(t, 37.7749, *event.Lat)
+		assert.Equal(t, -122.4194, *event.Lng)
 	case <-time.After(time.Second):
 		t.Fatal("Timed out waiting for message")
</file context>
Suggested change
assert.Equal(t, -122.4194, *event.Lng)
assert.Equal(t, -122.4194, *event.Lng)
assert.NotNil(t, event.CountryCode)
assert.Equal(t, "US", *event.CountryCode)
Fix with Cubic

Comment thread livestream/geo/geoip.go
Comment on lines +67 to 69
CountryCode: &record.Country.ISOCode,
}, nil
}
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Consider checking if ISOCode is empty before returning a pointer. MaxMind may return an empty string when country data is unavailable. Returning a pointer to an empty string breaks the nil-pointer pattern used elsewhere for missing data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At livestream/geo/geoip.go, line 67:

<comment>Consider checking if `ISOCode` is empty before returning a pointer. MaxMind may return an empty string when country data is unavailable. Returning a pointer to an empty string breaks the nil-pointer pattern used elsewhere for missing data.</comment>

<file context>
@@ -26,22 +32,38 @@ func NewMaxMindGeoLocator(dbPath string) (*MaxMindLocator, error) {
+	return GeoResult{
+		Latitude:    &record.Location.Latitude,
+		Longitude:   &record.Location.Longitude,
+		CountryCode: &record.Country.ISOCode,
+	}, nil
 }
</file context>
Fix with Cubic

@jordanm-posthog jordanm-posthog changed the title feat(web-analytics): Return geo data in the livestream service feat(livestream): Return geo data in the livestream service Jan 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, post a comment or remove the stale label – otherwise this will be closed in another week. If you want to permanentely keep it open, use the waiting label.

@github-actions github-actions Bot added the stale label Jan 19, 2026
Comment on lines +64 to 71
var v1 interface{}
if m, ok := v1.(easyjson.Unmarshaler); ok {
m.UnmarshalEasyJSON(in)
} else if m, ok := v1.(json.Unmarshaler); ok {
_ = m.UnmarshalJSON(in.Raw())
} else {
v1 = in.Interface()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical bug in property unmarshaling logic. The variable v1 is declared as interface{} (nil) before type assertions, so the type checks will always fail and always execute the else branch. This breaks custom unmarshaling for properties that implement easyjson.Unmarshaler or json.Unmarshaler.

What breaks: Properties with custom unmarshaling logic will fail to unmarshal correctly, potentially causing data corruption or runtime panics.

Fix:

v1 := in.Interface()
if m, ok := v1.(easyjson.Unmarshaler); ok {
    m.UnmarshalEasyJSON(in)
} else if m, ok := v1.(json.Unmarshaler); ok {
    _ = m.UnmarshalJSON(in.Raw())
}
(out.Properties)[key] = v1

The original pattern should be restored - assign v1 first, then perform type assertions on the actual value.

Suggested change
var v1 interface{}
if m, ok := v1.(easyjson.Unmarshaler); ok {
m.UnmarshalEasyJSON(in)
} else if m, ok := v1.(json.Unmarshaler); ok {
_ = m.UnmarshalJSON(in.Raw())
} else {
v1 = in.Interface()
}
v1 := in.Interface()
if m, ok := v1.(easyjson.Unmarshaler); ok {
m.UnmarshalEasyJSON(in)
} else if m, ok := v1.(json.Unmarshaler); ok {
_ = m.UnmarshalJSON(in.Raw())
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant