analytics: one ingest door — /v1/insights/e removed, its wire kept

A wire is a SHAPE, and a shape has never earned a path. /v1/insights/e existed
only because the PostHog wire spells fields differently; decodeIngest already
sniffs object-vs-array and bare-vs-envelope on one route, so sniffing one more
encoding is the mechanism that is already there, not a new one.

The wire does NOT go away — decodeEvent picks the decoder by sniffing keys, and
the ingress rewrite that fed the old door (insights-cloud-ingest-rewrite:
insights.hanzo.ai /e,/batch,/capture) now replacePaths onto /v1/event, so every
PostHog-wire caller keeps working. Six doors become five.

The sniff is on KEYS, not on 'did the first decoder return anything'. I wrote the
count-based fallback first and TestMount_HostCarve_IngestsForSiteOrg refuted it:
decodeIngest ACCEPTS a PostHog body as a bare canonical Event and returns ONE
event, which is then dropped whole downstream (canonicalType("") is "event",
not in publicKinds). The caller gets 200 and the event vanishes. A count of 1 is
not evidence the body was understood.

The wires are distinguishable exactly: canonical spells `distinctId` (camel) and
carries `type`; PostHog spells `distinct_id` (snake) and carries `api_key`.
Neither key appears in the other wire, so presence is proof, not a heuristic.
Batches are probed on elements because `batch` is shared.

Security properties re-proven on the one door, not assumed: key extraction from
body/query/x-api-key, presented-but-unresolvable => 403 fail-closed, keyless =>
the anonymous lane. Those tests now drive /v1/event and pass. Full package green.

$source says nothing real used the old door: 3584 rows via 'event', 15 via
'capture', 1 via 'posthog' — and that 1 is my own probe from this session.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
2026-07-30 20:39:36 -07:00
parent 02d7457bb3
commit 6fc2d88c7a
4 changed files with 68 additions and 12 deletions
+6 -6
View File
@@ -20,7 +20,7 @@ import (
// These tests cover the project-API-key → org resolution on the deprecated PostHog
// door, so keyed, bearer-less SDK traffic (posthog-js / insights-go batch) maps to a
// tenant. They drive the REAL /v1/insights/e handler through the injectable
// tenant. They drive the REAL /v1/event handler (PostHog wire) through the injectable
// resolveKeyOrg seam, so no IAM is needed. The observable proxy for "resolved to a
// tenant" is "passed the credential gate" — i.e. NOT 403; without a datastore the
// handler then returns 503, so any non-403 status means the request was admitted.
@@ -76,9 +76,9 @@ func TestKeyOrg_KeyExtractionReachesResolver(t *testing.T) {
hdr map[string]string
wantKey string
}{
{"body", "/v1/insights/e", `{"api_key":"hk-body","event":"e","distinct_id":"d"}`, nil, "hk-body"},
{"query", "/v1/insights/e?api_key=hk-query", `{"event":"e","distinct_id":"d"}`, nil, "hk-query"},
{"x-api-key", "/v1/insights/e", `{"event":"e","distinct_id":"d"}`, map[string]string{"x-api-key": "hk-hdr"}, "hk-hdr"},
{"body", "/v1/event", `{"api_key":"hk-body","event":"e","distinct_id":"d"}`, nil, "hk-body"},
{"query", "/v1/event?api_key=hk-query", `{"event":"e","distinct_id":"d"}`, nil, "hk-query"},
{"x-api-key", "/v1/event", `{"event":"e","distinct_id":"d"}`, map[string]string{"x-api-key": "hk-hdr"}, "hk-hdr"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -100,7 +100,7 @@ func TestKeyOrg_KeyExtractionReachesResolver(t *testing.T) {
func TestKeyOrg_UnresolvableKeyFailsClosed(t *testing.T) {
app := mountApp(t)
stubResolver(t, func(string) (string, bool) { return "", false }) // nothing resolves
code := postKeyed(t, app, "/v1/insights/e", "hanzo.ai",
code := postKeyed(t, app, "/v1/event", "hanzo.ai",
`{"api_key":"hk-bad","event":"e","distinct_id":"d"}`, nil)
if code != http.StatusForbidden {
t.Fatalf("presented-but-unresolvable key on a brand host must 403 (fail closed), got %d", code)
@@ -119,7 +119,7 @@ func TestKeyOrg_KeylessRequestNeverConsultsResolver(t *testing.T) {
t.Fatalf("resolver consulted for a keyless request (key=%q)", key)
return "", false
})
code := postKeyed(t, app, "/v1/insights/e", "hanzo.ai",
code := postKeyed(t, app, "/v1/event", "hanzo.ai",
`{"event":"e","distinct_id":"d"}`, nil)
if code != http.StatusOK {
t.Fatalf("keyless PostHog event want 200 (anonymous lane, kind dropped), got %d", code)
+8 -2
View File
@@ -44,9 +44,15 @@ import (
// decodeInsights and every canonical-wire beacon silently decodes to nothing, or
// relabel a door's source and the $source column — which is the sunset signal, and
// the only per-row record of which door a write came through — starts lying.
// /v1/insights/e was REMOVED as a door on 2026-07-31: a wire is a shape, and a shape
// does not earn a path. Its wire did not go away — decodeEvent tries the canonical
// decoder and falls back to decodeInsights when canonical yields nothing — and the
// ingress rewrite that fed it (insights-cloud-ingest-rewrite: insights.hanzo.ai
// /e,/batch,/capture) now replacePaths onto /v1/event, so every PostHog-wire caller
// keeps working through the one door. sourcePostHog therefore no longer appears here;
// its 1 lifetime row is a probe, not traffic.
var wantDoors = []door{
{path: "/v1/event", decode: decodeIngest, source: sourceEvent},
{path: "/v1/insights/e", decode: decodeInsights, source: sourcePostHog},
{path: "/v1/event", decode: decodeEvent, source: sourceEvent},
{path: "/v1/analytics", decode: decodeIngest, source: sourceCapture},
{path: "/v1/analytics/batch", decode: decodeIngest, source: sourceCapture},
{path: "/v1/tracker", decode: decodeIngest, source: sourceCapture},
+52 -2
View File
@@ -430,9 +430,59 @@ type door struct {
// The rule holds only because those callers name those paths themselves. It does NOT
// generalize to /v1/insights/e, whose callers arrive through an ingress rewrite — see
// its entry below before applying a $source count to any door.
// decodeEvent is the ONE door's decoder: the canonical wire, falling back to the
// PostHog wire only when canonical yields NOTHING from a non-empty body.
//
// /v1/insights/e used to be a second door for the second wire. A wire is a SHAPE,
// and a shape has never earned a path — decodeIngest already sniffs object-vs-array
// and bare-vs-envelope on the same route, so sniffing one more encoding is the
// mechanism it already is, not a new one. The two wires even share the `batch`
// envelope key and differ only in per-event field names.
//
// The wire is chosen by SNIFFING THE KEYS, never by "did the first decoder return
// anything". Trying canonical first and falling back on an empty result is WRONG and
// the host-carve test proves it: decodeIngest ACCEPTS a PostHog body as a bare
// canonical Event and returns ONE event, which is then dropped whole downstream
// (canonicalType("") is "event", not in publicKinds). A count of 1 therefore does not
// mean the body was understood, so a count-based fallback never fires and the event
// silently vanishes with a 200 receipt.
//
// The two wires are distinguishable exactly, with no heuristic: canonical spells the
// field `distinctId` (camel) and carries `type`; the PostHog wire spells it
// `distinct_id` (snake) and carries `api_key`. Neither key exists in the other wire,
// so presence is proof rather than a guess. Batches are probed on their elements
// because the envelope key `batch` is shared by both.
func isPostHogWire(body []byte) bool {
var probe struct {
DistinctID json.RawMessage `json:"distinct_id"`
APIKey json.RawMessage `json:"api_key"`
Batch []struct {
DistinctID json.RawMessage `json:"distinct_id"`
} `json:"batch"`
}
if json.Unmarshal(body, &probe) != nil {
return false
}
if probe.DistinctID != nil || probe.APIKey != nil {
return true
}
for _, e := range probe.Batch {
if e.DistinctID != nil {
return true
}
}
return false
}
func decodeEvent(body []byte) ([]CaptureEvent, error) {
if isPostHogWire(body) {
return decodeInsights(body)
}
return decodeIngest(body)
}
var doors = []door{
{path: "/v1/event", decode: decodeIngest, source: sourceEvent},
{path: "/v1/insights/e", decode: decodeInsights, source: sourcePostHog},
{path: "/v1/event", decode: decodeEvent, source: sourceEvent},
{path: "/v1/analytics", decode: decodeIngest, source: sourceCapture},
{path: "/v1/analytics/batch", decode: decodeIngest, source: sourceCapture},
{path: "/v1/tracker", decode: decodeIngest, source: sourceCapture},
+2 -2
View File
@@ -164,8 +164,8 @@ func TestMount_HostCarve_IngestsForSiteOrg(t *testing.T) {
}
}
// PostHog wire on /v1/insights/e.
code := postHost(t, app, "yadota.hanzo.app", "/v1/insights/e",
// PostHog wire on the ONE door /v1/event (decodeEvent falls back to the PostHog decoder).
code := postHost(t, app, "yadota.hanzo.app", "/v1/event",
`{"event":"$pageview","distinct_id":"d","properties":{"space":"attacker"}}`,
map[string]string{"X-Org-Id": "attacker"})
if code != http.StatusServiceUnavailable {