fix(aio): stop dropping posthog_ custom properties on otel paths - #73578
Conversation
Prompt To Fix All With AIFix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
nodejs/src/ingestion/pipelines/ai/otel/middleware/custom-metadata.ts:14-16
**Inherited names block promotion**
For metadata names such as `posthog_constructor` or `posthog_toString`, this lookup resolves the inherited `Object.prototype` member and treats it as an existing property. The middleware then strips the namespaced source key, silently discarding that custom dimension; check for an existing own property instead.
Reviews (1): Last reviewed commit: "fix: otel passthrough custom metadata" | Re-trigger Greptile |
|
Note 🤖 stamphog reviewed This modifies core AI-observability event ingestion middleware (property promotion run on every OTel-based event before stripping), which is risky ingestion-path territory; the author is not on the owning ingestion team and the only review present is Greptile's, left on an older commit — the fix (own-property check + proto guard) looks correct on inspection, but there is no current-head approval/comment confirming it.
Gate mechanics and policy version
Updated in place — this replaces 1 earlier stamphog review(s) on this PR. |
|
The bug is real. On a plain object, The fix is straightforward — replace the This correctly allows promotion only when it('promotes posthog_constructor metadata (not blocked by prototype chain)', () => {
const props: Record<string, unknown> = { 'ns.posthog_constructor': 'my-value' }
promotePosthogCustomMetadata(props, 'ns.')
expect(props['constructor']).toBe('my-value')
}) |
Problem
Custom properties sent as
posthog_-prefixed OTel metadata were silently dropped on two AI observability ingestion paths.The Vercel AI SDK path strips the whole
ai.telemetry.metadata.*namespace, and the Traceloop/OpenLLMetry path stripstraceloop.association.properties.*.Both strips ran with only a narrow allowlist, so anyone attaching custom dimensions (the mechanism people use to filter generations, e.g.
tagsorenvironment) lost them.The generic OTel and Pydantic AI paths already forward custom attributes as-is, so this was an inconsistency rather than intended behavior:
posthog_-namespaced metadata is meant to survive.Origin: internal thread.
Changes
promotePosthogCustomMetadata(props, namespacePrefix).It promotes
<namespace>posthog_<name>to a top-level<name>property (prefix stripped), only when<name>is unset, skipping$-prefixed keys anddistinct_idso custom metadata can't clobber reserved properties or the event distinct id.ai.telemetry.metadata.(Vercel) andtraceloop.association.properties.(Traceloop).posthog_distinct_idkeeps its existing identity treatment.Because the promotion runs per event, custom properties land on each
$ai_generation, so generations are individually filterable.How did you test this code?
Automated only. I (Claude) ran these; no manual or end-to-end testing was done.
posthog_keys ignored,$-prefixed skipped,distinct_idskipped, only-if-undefined.ai.telemetry.metadata.posthog_tags→tags, Tracelooptraceloop.association.properties.posthog_env→env.nodejs/src/ingestion/pipelines/ai/otel/suite: 208 passing (6 suites). Prettier clean.tsc --noEmitclean for the changed files (only pre-existing, unrelated@posthog/replay-anonymizermodule-resolution errors remain in this environment).The changed lines are covered by the tests above.
👉 Stay up-to-date with PostHog coding conventions for a smoother review.
Automatic notifications
Docs update
No user-facing doc change. This restores expected behavior (
posthog_metadata passing through), so there is nothing new to document. Safe to addskip-inkeep-docs.🤖 Agent context
Autonomy: Human-driven (agent-assisted) — I directed the work, so assign me as the DRI.
Built with Claude Code (Opus). Skills invoked:
/writing-tests(to gate the test additions) and/pr-link(for this PR link).The work started scoped to the Vercel path only, matching the incoming request. While verifying I checked all three middlewares plus the generic mapper and found Traceloop blanket-strips its own custom-metadata namespace (
traceloop.association.properties.*) the same way Vercel does, while the generic OTel and Pydantic paths already forward custom attributes. So the proper fix generalizes to both stripping paths, which is why the promotion moved into a shared helper wired into each. I deliberately left the pass-through paths alone rather than forcing a uniformposthog_rename that would only break existing property names.I first framed this as a feature and added an onboarding-doc note, then reverted it:
posthog_properties surviving the strip is expected behavior, so this is a bug fix, not a documented feature.