Repository navigation
chore(ingestion): extract the $os_name alias into normalizeOsAlias - #90615
Conversation
personInitialAndUTMProperties did two unrelated jobs: it lifts taxonomy and campaign properties onto the person, and it filled the event's own $os from the $os_name some SDKs send instead. The event-level fill was reachable only by going through the person-level lifting. The fill is now normalizeOsAlias, which normalizeEvent calls before personInitialAndUTMProperties. Behavior is unchanged: the function keeps the $is_server skip and stays inside the $snapshot and $performance_event exclusion, so every event resolves the alias exactly as before. The $is_server skip keeps a server's host OS out of the event's own $os. The person side is guarded separately: personInitialAndUTMProperties skips $os and $os_version while lifting, and skips the whole $os_name block for a server event. A server's host OS still reaches the person under the $os_name keys, which is unchanged here and noted in the test. Verified against master over 810 property combinations spanning five event names, $os, $os_name and $is_server, and six property shapes: identical values throughout. Only JSON key order moves, because $os is now written before the lifting loop rather than after. Generated-By: PostHog Desktop Task-Id: 3d076e7d-0063-41b6-8c52-559f198bb165
|
😎 Merged successfully - details. |
🤖 CI report✅ Trunk lane — non-backend laneThis PR is assigned to the non-backend lane. It does not run backend Python tests and may merge in parallel with PRs in other lanes. |
👀 Auto-assigned reviewersThese soft owners were skipped because they only have minor changes here. Nothing blocks merge, so self-assign if you'd like a look:
Soft owners come from each directory's |
There was a problem hiding this comment.
Pull request overview
Refactors Node.js ingestion event normalization to extract $os_name → $os aliasing into a dedicated normalizeOsAlias helper, and updates tests/docs to reflect the new call order. This is intended to preserve existing behavior while allowing $os normalization to be invoked independently from person property lifting.
Changes:
- Add
normalizeOsAlias(properties)and call it fromnormalizeEventbeforepersonInitialAndUTMProperties(still skipping$snapshot/$performance_event). - Adjust
personInitialAndUTMPropertiesto stop writing the event-level$osfrom$os_name, keeping only the person-property behavior. - Update Jest expectations and add coverage for
$snapshotand$is_servercases; fix a Rust doc comment path reference.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| rust/feature-flags/src/flags/flag_matching_utils.rs | Updates a doc comment to reference the moved Node.js file path and clarifies normalization scope. |
| nodejs/tests/utils/db/utils.test.ts | Updates expectations/comments for personInitialAndUTMProperties now that event-level $os is filled upstream. |
| nodejs/src/common/utils/event.ts | Introduces normalizeOsAlias and adjusts normalizeEvent ordering to fill $os earlier in normalization. |
| nodejs/src/common/utils/event.test.ts | Adds focused tests for $os_name aliasing, including $snapshot and $is_server behaviors. |
| nodejs/src/common/utils/db/utils.ts | Removes the event-level $os fill from personInitialAndUTMProperties, leaving only person $set/$set_once behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Reviews (1): Last reviewed commit: "chore(ingestion): extract the $os_name a..." | Re-trigger Greptile |
The doc comment claimed personInitialAndUTMProperties skips the $os_name block for $is_server events, implying the person side is fully protected. The loop still lifts $os_name onto the person as $os_name and $initial_os_name; only the $os_name-to-$os normalization block is skipped. The comment now states that behavior, matching the test in event.test.ts. Generated-By: PostHog Desktop Task-Id: 609b2f33-52ce-4161-a4a0-6c3b4e244fe0
| if (!('$os' in $set)) { | ||
| $set.$os = osName | ||
| } |
There was a problem hiding this comment.
The $set.$os and $set_once.$initial_os writes inside this block are now unreachable in production. Once normalizeOsAlias has filled $os, the lifting loop always populates both keys first, so both guards are always false; only the two deletes still do work. Their sole remaining coverage is the standalone unit test, which now exercises a path the pipeline never takes.
| /// (`personInitialAndUTMProperties` in nodejs/src/common/utils/db/utils.ts), but | ||
| /// that only covers newly-written rows, and it excludes `$is_server` events; |
There was a problem hiding this comment.
The Rust doc comment fix rides along in an otherwise Node.js-only PR. It is also now slightly stale in a new way: it attributes $os_name -> $os normalization to personInitialAndUTMProperties, but the event-level half moved to normalizeOsAlias in event.ts. Correct for person rows, misleading for anyone chasing the event path.
| test.each([ | ||
| { | ||
| name: 'fills a missing $os', | ||
| event: 'some_event', | ||
| properties: { $os_name: 'Windows' }, | ||
| expected: 'Windows', | ||
| }, | ||
| { | ||
| name: 'leaves an existing $os alone', | ||
| event: 'some_event', | ||
| properties: { $os: 'Mac OS X', $os_name: 'Windows' }, | ||
| expected: 'Mac OS X', | ||
| }, | ||
| { | ||
| name: 'skips a $snapshot event, which skips person property lifting too', | ||
| event: '$snapshot', | ||
| properties: { $os_name: 'Windows' }, | ||
| expected: undefined, | ||
| }, | ||
| ])('$name', ({ event, properties, expected }) => { |
There was a problem hiding this comment.
The new test.each block covers $snapshot but not $performance_event, and does not cover a null $os_name. Both are cheap additions to the existing parameterized cases.
eli-r-ph
left a comment
There was a problem hiding this comment.
Couple of small nits worth a revisit but nothing blocking, good change! 🚀
normalizeOsAlias fills the event's own $os before personInitialAndUTMProperties runs, so the lifting loop always writes $set.$os first. The $initial_os fallback stays: the loop skips it for a null $os, leaving $os_name as the only first-touch value. Point the populate_os_aliases rustdoc at normalizeOsAlias for the event path. Generated-By: PostHog Desktop Task-Id: 3d076e7d-0063-41b6-8c52-559f198bb165

Problem
personInitialAndUTMPropertiesdid two unrelated jobs.$osfrom the$os_namethat some SDKs send instead (posthog-js-lite#244).The event-level fill ran only as a side effect of the person-level lifting, so nothing could call it on its own.
Changes
Important
No user-visible changes here. Every event resolves
$osexactly as it did before.normalizeOsAliasnow owns the event-level fill, andnormalizeEventcalls it beforepersonInitialAndUTMProperties.$snapshotand$performance_eventexclusion still wraps both calls, and the$is_serverskip still applies, so the same events are covered as before.$os, so filling$osbefore transformations would stop it deriving$browserand$device.rust/feature-flags/src/flags/flag_matching_utils.rsgets a corrected file path. No Rust logic changes.The riskier part is the reordering:
$osnow enters the person-property lifting loop instead of being written after it. Only key insertion order in$setmoves, because the$os_nameblock's!('$os' in $set)guards already skip a key the loop wrote.How did you test this code?
nodejs/src/common/utils/event.test.tsgains a$os_name aliasblock. It catches a regression no existing test did:normalizeOsAliasfilling$oson a$snapshotevent, or on a server event, neither of which the old inline block did.nodejs/tests/utils/db/utils.test.tsdrops$osfrom one expectation, becausepersonInitialAndUTMPropertiesno longer writes it.npx jest src/common/utils/event.test.ts.Automatic notifications
Docs update
None. No user-facing behavior, API, or documented workflow changes.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Written with Claude Code in PostHog Desktop. Skills invoked:
/review-code,/explain-open,/squash,/writing-pr-descriptions.A review across five lenses (correctness, security, testing, architecture, maintainability) found no blocking issues. It changed one thing: the
normalizeOsAliasdocstring had attributed the$is_serverguard to keeping a host OS off the sticky$initial_os.personInitialAndUTMPropertiesalready guards that on its own, so the guard here only keeps a server's host OS out of the event's own$os. The docstring now says that.The review also noted that a server's host OS still reaches the person under the
$os_nameand$initial_os_namekeys. That is unchanged from master and is now stated in the test, so the next reader does not take the test as proof the skip closed it.Nothing in this PR draws on material from the agent session that is not already in this repository.