Skip to content

Native JSON events: cutover readiness tracker #106296

Description

@aspicer

Track the work left before enabling native JSON event reads and, later, retiring the legacy events table. Every item names work someone has to do and what done looks like. Do not put customer queries or private operational data here.

Where things stand (Sep 29)

Merged: the native schema and base property reads (#91515), correctness fixes (#104783), the retained mutation log (#97215), flag compatibility with the $false sentinel (#91506), the native CI lane (#106280), the HogQL port of point-in-time person properties (#91452), and the UDF wrapper fix (#105651).

In the merge queue: #107043 (moved event properties read from temporary_properties). #106380 (native-only regression tests) is approved and joins the queue when its checks pass. #106274 (the useNewEventsSchema modifier, the rollout switch) left the queue on Sep 29 when a cancelled MCP run failed its batch, and needs to be queued again.

Open: #107546 moves the table to dynamic JSON paths, adds the null-key columns, turns date inference off, and stores dotted keys flat. The production table is re-backfilled after it lands. It conflicts with master, so its CI does not run until master is merged in. #106152 is open and listed below.

Rollout mechanism: staff set useNewEventsSchema in a project's team modifiers in Django admin. An explicit modifier is part of the query cache key, so the switch recomputes on native and clearing it restores the legacy cache entries. The final global flip uses the instance setting. Customers cannot set the key on their project (#106274); the per-query override stays available to API callers during the trial.

Before the backfill starts

Before enabling native reads for any project

  • Read-side setting as a profile default on the events cluster. ClickHouse restores %2E to a dot only when the reading query sets json_type_escape_dots_in_keys. HogQL and the export templates set it per query (feat(clickhouse): use dynamic JSON paths in the native events schema #107546, 0a9745a), but mutations run with server settings, and ad-hoc readers set nothing. Add it to the default profile on the events cluster. Done when a clickhouse-client session on that cluster prints a dotted key with a dot.
  • Rebuild nulls in whole-document reads. The JSON column cannot store null, so the cleaner drops those fields and feat(clickhouse): use dynamic JSON paths in the native events schema #107546 records their paths in properties_null_keys, temporary_properties_null_keys and person_properties_null_keys. Nothing reads those arrays yet. Batch exports and the events API put the nulls back. Done when a native export and the events API return null for a fixture that sent one.
  • Land fix(hogql): read rebuilt flags through every native JSON function #106152, flag reads through JSON functions. JSONExtractRaw(properties, '$feature/x'), arrayJoin(JSONExtractArrayRaw(properties, '$active_feature_flags')), JSONLength, JSONType and toString(properties) do not see the rebuilt flags on native. It also has to decide how these functions tell a string variant named "true" from boolean true. Rebase onto master, drop the inherited legacy-table rewrites, land.
  • Decide the whole-document flag shape. On native, SELECT properties, the event properties panel and exports show flags as one $feature_flags object; the legacy table shows many $feature/<key> properties. Decide whether whole-document reads rebuild the legacy shape or expose the map, then implement it, including property restrictions and generated column aliases.
  • Batch exports. Pin exports to the legacy table now: the export source builds its query with the project's default modifiers, so switching a project would move its exports too. Before exports move: custom export columns are persisted as compiled ClickHouse expressions and schemas saved with property restrictions omit their original HogQL, so a saved export must recompile against the table it reads at run time; native exports serialize declared array paths as [] on every event; the native field rewrite relocates $feature/<key> paths but not JSON-function calls on them. Done when an export created before enablement, one created after, a restricted schema and a historical backfill all produce the same rows from both tables apart from the recorded expected differences.
  • Land chore(tests): prove event queries read native-only fixtures #106380, native-only regression tests. Fixtures that exist only in the native table, with the legacy table asserted empty, so a query that fell back to legacy fails. Approved and submitted to the merge queue on Sep 29, covering the events-list boolean filters and daily trends. Done when it merges, before the first project switches.
  • Deletions cover the native table. feat(deletes): skip sharded_events_json, configurable per run #102195 skips sharded_events_json in deletes_job on purpose. Re-enable it, then run a catch-up over deletion requests already marked verified, because re-enabling does not revisit them. chore(clickhouse): test adhoc deletes across events clusters #99416 (draft) adds a two-cluster test. See deletion coverage.
  • person_id squash covers the native table. chore(clickhouse): drop events_json from the person_id squash targets #105148 removed sharded_events_json from the weekly squash, so native rows keep the absorbed person_id and merged users count twice. Add the table back once its cluster resolves reliably, and repair rows stranded in the meantime, for example by copying person_id from the squashed legacy rows by uuid; the repair needs the legacy table. chore(clickhouse): make squash coverage a checked invariant #98865 records the exclusion.
  • Property removal on the native table. Removing one property from all of a project's events is not implemented for the JSON columns. Implement it for properties, temporary_properties and person_properties, including $unparseable_properties. The deletion job reads a dot in a requested name as nesting on both tables; with dotted keys stored flat, the predicate, the verify step and the JSONDropKeys key list need the same %2E encoding to reach a flat dotted key (Sep 29 comment).
  • Compare and measure. Run the warm insights of the first trial project against both tables with the feat(hogql): add a useNewEventsSchema query modifier #106274 modifier and record the drift here, classified against the expected differences: empty and null values, JSON scalar types, number formatting (1.50 is stored as 1.5), flag arrays and order, omitted properties, deduplication; nulls and empty strings inside $set are dropped; object keys come back sorted; numbers above the signed 64-bit range inside an object read come back as JSON strings; an empty $set or $unset reads as missing; a key sent with a literal %2E reads back with a dot; an event carrying both a.b and a%2Eb keeps the first value; posthog-go before 1.13.2 listed off flags as active. Measure insert and merge cost with up to 1024 dynamic paths per part, and reads of wide objects past a column's dynamic-path budget (200k local synthetic events: a whole-object read of a wide $set took 20 s native against 0.8 s legacy, a $set.email filter read 9 MB against 177 MB). HogQL reads every native path through the generic Dynamic expression, even declared typed paths such as $browser; measure that too. Done when the numbers and the rollback criteria are written here.

Before retiring the legacy events table

Legacy writes stop and storage is dropped only after the items above and these are done.

Done

Keeping this tracker current

Consult this issue first when working on native JSON events. Add an item only when it names work someone has to do, with a code or PR reference and what done looks like. Check an item off when the work is in production, not when a PR merges. Keep merge decisions distinct from permission to enable a project or retire storage.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions