You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Track the work required before enabling native JSON event reads and, later, retiring the legacy events table.
Merging compatibility code is separate from enabling it. For #91506, batch exports remain on legacy storage and no teams are enabled as part of the merge. The export follow-up below is deferred from that PR, not waived for cutover. This issue records the intended rollout boundary; it does not assert that deployment settings have been inspected.
Before enabling native reads
Land the base feature-flag compatibility layer:feat(hogql): flag compatibility for native events and a $false sentinel #91506. Verify direct flag reads, active-flag comparisons, presence checks, restrictions, and the "false" variant sentinel. Legacy-table compilation must remain unchanged. Since 7932d0a the layer runs only on native reads, and the PR description records a legacy-SQL comparison against master whose one difference is that HogQL now accepts mapExists. The flag API also rejects $false as a variant key for every team. Both approvals date from Sep 1 and Sep 10, before the sentinel and the native-only change.
Restore sent types for declared String paths:posthog/clickhouse/events_json.py declares 382 property paths as String, and that column is the only stored copy. Numbers, booleans and objects sent to those keys come back as text when the whole document is rebuilt (false becomes "false", 1.5 becomes "1.5"). Single properties.<key> reads still match because HogQL casts by property type. On those keys JSONExtractBool returns false for a sent true, JSONExtractRaw and toJSONString(properties) return quoted values, and JSONType reports String. What breaks: the replay inspector shows an LLM event that sent $ai_is_error: false as an error, because "false" is truthy; the AI trace runners' events-table fallback returns the text (test_trace_query_runner.py pins it); native exports would write strings to destinations. Choose the storage for keys that carry non-string values (for example Dynamic) and re-backfill alongside the "false"-variant reinsertion, or explicitly accept and document the text form. Test whole-document reads, raw JSON functions and exports.
Resolve flag-type ambiguities and old data: deploy the feat(hogql): flag compatibility for native events and a $false sentinel #91506 cleaner before reinserting historical rows whose variant was literally "false"; the old map representation cannot distinguish that variant from boolean false. The reinsertion reads the legacy table, so it must finish before retirement. Decide how JSON functions distinguish a string variant named "true" from boolean true, which fix(hogql): read rebuilt flags through every native JSON function #106152 identifies as still ambiguous. Verify sentinel rejection and reader/writer compatibility during rollout. The map-derived $active_feature_flags also differs from what some senders wrote: posthog-go before 1.13.2 listed off flags as active, and a $feature/<key> set by hand to the string "false" reads as active. Add both to the expected-differences list.
Fix batch-export source/column mismatches before moving exports: custom export columns are persisted as compiled ClickHouse expressions. Runtime recompilation in resolve_batch_exports_model follows the team setting, but scheduled runs and recent backfills can still read legacy storage. Saving an export after enablement also persists native expressions. Schemas saved with property restrictions omit their original HogQL, so merely moving recompilation to runtime does not handle them. Cover exports created before and after enablement, restricted and unrestricted schemas, scheduled runs, recent and historical backfills, stable aliases, and parameter bindings. Also cover $active_feature_flags fields/filters and the exported whole-properties contract. Native exports serialize the declared array paths as [] on every event, adding keys the legacy export never had (test_http_batch_export_workflow.py skips its insert test on native for this). The native field rewrite relocates $feature/<key> paths but not JSON-function calls such as JSONExtractString(properties, '$feature/<key>'). Until this is solved, enforce legacy export routing when any other reads are enabled. Sources: feat(hogql): flag compatibility for native events and a $false sentinel #91506 and fix(hogql): read rebuilt flags through every native JSON function #106152.
Choose and enforce who can override the rollout:feat(hogql): add a useNewEventsSchema query modifier #106274's useNewEventsSchema overrides the instance setting and team allowlist. Per-query input and persisted team modifiers are separate routes; a restriction on team-setting writes alone does not control query input. Decide the authorization policy before shipping the override, and test explicit true/false, unset, team defaults, saved queries, and cache separation. Do not enable teams as part of merging feat(hogql): flag compatibility for native events and a $false sentinel #91506.
Deploy and validate the executable-UDF reliability fix:fix(clickhouse): close inherited descriptors in udf wrapper scripts #105651. Verify fresh/reloaded UDF pools and repeated client requests on the deployed ClickHouse version. Track the longer-term ClickHouse fix separately from the wrapper workaround.
Decide remaining datetime compatibility:fix(hogql): run CI on the native-JSON events table and fix what broke #104783 fixes zoned scalar datetime reads, but naive datetime strings inferred as UTC can differ from legacy team-local interpretation, and datetimes nested inside objects still lack a zone. Separately, the JSON column infers Date and DateTime types for date-looking strings in undeclared paths at insert: offsets are converted to UTC and dropped, formats such as Jan 15 2024 are rewritten, and whether a value converts depends on the other rows in the same insert. Turning off input_format_try_infer_dates and input_format_try_infer_datetimes for ingestion and the backfill kept strings as sent in a local ClickHouse 26.6 test. Fix ingestion/reading or explicitly accept and document the differences, with tests for non-UTC teams and nested values.
Restore person_id squash coverage:chore(clickhouse): drop events_json from the person_id squash targets #105148 removed sharded_events_json from the weekly person_id squash. The squash rewrites person_id for merged persons on the legacy events and flag-evaluation tables, then deletes the overrides that recorded the mapping, so native rows keep the absorbed person_id and nothing corrects them at query time. 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. That repair needs the legacy table. chore(clickhouse): make squash coverage a checked invariant #98865 (open) records the exclusion as an explicit exemption. See the deletion-coverage document.
Verify the deployed schema and full ingestion/erasure path:feat(clickhouse): update native JSON events schema #91515 requires a manual rollout for existing tables. Confirm the deployed table definitions, ingestion views, UDF versions, temporary-property TTL, and distributed aliases agree. Complete Kafka-to-native ingestion and end-to-end erasure validation, which that PR did not run. Recheck its previously reported nested-null payload and equal-timestamp attribution cases against the final code; do not assume the historical findings remain open without reproducing them.
Record correctness and performance acceptance: compare representative query shapes against both tables using synthetic fixtures and authorized validation data. Keep an explicit expected-differences list for empty/null values, JSON scalar types, number formatting (1.50 is stored as 1.5), flag arrays/order, omitted properties, datetime normalization, and deduplication. HogQL reads every native path through the generic Dynamic expression, even declared typed paths such as $browser (resolve_json_subcolumn_source); measure that before signing off on performance. feat(hogql): add a useNewEventsSchema query modifier #106274's modifier is the intended way to run one query against each table. Record rollback criteria and the decision to start a limited rollout here. Do not put customer queries or private operational data in this public issue.
Before retiring the legacy events table
Remove the historical-person-property dependency: on master, posthog/models/person/point_in_time_properties.py reads $set from legacy events with raw SQL. fix(persons): build properties_at_time via HogQL instead of raw SQL #91452 ports it to HogQL and intentionally pins properties_at_time and historical flag test evaluation to legacy events; its $set/$set_once test runs on both tables and fails on native without the pin. Native $set/$set_once live in temporary_properties with a 60-day TTL, which cannot serve the longer replay window. Choose a durable source or an explicitly changed product contract before dropping legacy storage. The mutation log in feat(clickhouse): retain event person mutation payloads on aux #97215 has a 30-day retention window and is not automatically a replacement.
Audit direct SQL and deliberate legacy pins: inventory readers outside the HogQL schema switch and either migrate or retire them. Starting points include products/feature_flags/backend/flag_analytics.py and products/growth/backend/temporal/health_checks/sdk_outdated.py, plus the historical-person and export paths above. The MCP analytics metrics in posthog/tasks/usage_report.py read events directly because their dedup expression matches the legacy sort key. A semgrep rule against new raw events SQL outside the switch was proposed and not started. Native-only result regression coverage is required before the first production trial, as tracked above; the complete reader inventory and replacement of deliberate legacy dependencies must still finish before retirement.
Close legacy fallbacks and verify rollback/retention: do not stop legacy writes or drop storage until exports, historical property replay, deletion catch-up, and remaining readers have an approved replacement, and until the repairs that read legacy rows are done: the "false"-variant reinsertion, the person_id repair, and any typed-value re-backfill. Record completion evidence for each dependency here.
When working on native JSON events, consult this issue first. Add newly confirmed cutover blockers with a public code/PR reference and a concrete completion condition. Check an item off only when its implementation and required rollout/validation are complete; a merged PR alone may not satisfy both. Link follow-up PRs back here. Keep no-rollout merge decisions distinct from permission to enable teams or retire storage.
Track the work required before enabling native JSON event reads and, later, retiring the legacy events table.
Merging compatibility code is separate from enabling it. For #91506, batch exports remain on legacy storage and no teams are enabled as part of the merge. The export follow-up below is deferred from that PR, not waived for cutover. This issue records the intended rollout boundary; it does not assert that deployment settings have been inspected.
Before enabling native reads
"false"variant sentinel. Legacy-table compilation must remain unchanged. Since 7932d0a the layer runs only on native reads, and the PR description records a legacy-SQL comparison against master whose one difference is that HogQL now acceptsmapExists. The flag API also rejects$falseas a variant key for every team. Both approvals date from Sep 1 and Sep 10, before the sentinel and the native-only change.arrayJoin(JSONExtractArrayRaw(properties, '$active_feature_flags')),JSONLength,JSONType, boolean extraction, andtoString(properties)wrappers. The base PR alone does not cover these query shapes.propertiespassed through a subquery still miss reconstructed flag properties after fix(hogql): read rebuilt flags through every native JSON function #106152. Decide whether whole-document reads reconstruct the legacy shape or expose the native map, then implement and test the chosen contract. Reconstruction also changes whatSELECT propertiesand the event properties panel return on native, where flags now appear as one$feature_flagsobject, so it is a schema-contract decision, not only a HogQL fix. Include property restrictions and generated column aliases. Sources: feat(hogql): flag compatibility for native events and a $false sentinel #91506 and fix(hogql): read rebuilt flags through every native JSON function #106152.posthog/clickhouse/events_json.pydeclares 382 property paths as String, and that column is the only stored copy. Numbers, booleans and objects sent to those keys come back as text when the whole document is rebuilt (falsebecomes"false",1.5becomes"1.5"). Singleproperties.<key>reads still match because HogQL casts by property type. On those keysJSONExtractBoolreturns false for a senttrue,JSONExtractRawandtoJSONString(properties)return quoted values, andJSONTypereports String. What breaks: the replay inspector shows an LLM event that sent$ai_is_error: falseas an error, because"false"is truthy; the AI trace runners' events-table fallback returns the text (test_trace_query_runner.pypins it); native exports would write strings to destinations. Choose the storage for keys that carry non-string values (for exampleDynamic) and re-backfill alongside the"false"-variant reinsertion, or explicitly accept and document the text form. Test whole-document reads, raw JSON functions and exports."false"; the old map representation cannot distinguish that variant from boolean false. The reinsertion reads the legacy table, so it must finish before retirement. Decide how JSON functions distinguish a string variant named"true"from boolean true, which fix(hogql): read rebuilt flags through every native JSON function #106152 identifies as still ambiguous. Verify sentinel rejection and reader/writer compatibility during rollout. The map-derived$active_feature_flagsalso differs from what some senders wrote: posthog-go before 1.13.2 listed off flags as active, and a$feature/<key>set by hand to the string"false"reads as active. Add both to the expected-differences list.resolve_batch_exports_modelfollows the team setting, but scheduled runs and recent backfills can still read legacy storage. Saving an export after enablement also persists native expressions. Schemas saved with property restrictions omit their original HogQL, so merely moving recompilation to runtime does not handle them. Cover exports created before and after enablement, restricted and unrestricted schemas, scheduled runs, recent and historical backfills, stable aliases, and parameter bindings. Also cover$active_feature_flagsfields/filters and the exported whole-properties contract. Native exports serialize the declared array paths as[]on every event, adding keys the legacy export never had (test_http_batch_export_workflow.pyskips its insert test on native for this). The native field rewrite relocates$feature/<key>paths but not JSON-function calls such asJSONExtractString(properties, '$feature/<key>'). Until this is solved, enforce legacy export routing when any other reads are enabled. Sources: feat(hogql): flag compatibility for native events and a $false sentinel #91506 and fix(hogql): read rebuilt flags through every native JSON function #106152.useNewEventsSchemaoverrides the instance setting and team allowlist. Per-query input and persisted team modifiers are separate routes; a restriction on team-setting writes alone does not control query input. Decide the authorization policy before shipping the override, and test explicit true/false, unset, team defaults, saved queries, and cache separation. Do not enable teams as part of merging feat(hogql): flag compatibility for native events and a $false sentinel #91506.test-new-events-schemalabel remains available for broader validation. The agreed approach is the hand-maintained list, not a new periodic full run or mandatory drift-guard plugin. Sources: fix(hogql): run CI on the native-JSON events table and fix what broke #104783, fix(ci): gate native-json tests by label #105879, chore(ci): test native JSON events without doubling the suite #106280.Jan 15 2024are rewritten, and whether a value converts depends on the other rows in the same insert. Turning offinput_format_try_infer_datesandinput_format_try_infer_datetimesfor ingestion and the backfill kept strings as sent in a local ClickHouse 26.6 test. Fix ingestion/reading or explicitly accept and document the differences, with tests for non-UTC teams and nested values.$feature_flag_calledtransition with Stop advertising$feature_flag_calledin taxonomy, autocomplete, and product surfaces #88126; hiding new references in taxonomy/autocomplete does not migrate existing insights, cohorts, actions, or destination filters. Sources: feat(clickhouse): update native JSON events schema #91515, the event cleaner, and Stop advertising$feature_flag_calledin taxonomy, autocomplete, and product surfaces #88126.sharded_events_jsonindeletes_job. Re-enabling the target does not revisit deletion requests already marked verified, so plan and verify a backfill sweep as well. Confirm all native storage copies and clusters are reached; chore(clickhouse): test adhoc deletes across events clusters #99416 (draft) adds a two-cluster test for the job. See deletion coverage.sharded_events_jsonfrom the weekly person_id squash. The squash rewritesperson_idfor merged persons on the legacy events and flag-evaluation tables, then deletes the overrides that recorded the mapping, so native rows keep the absorbedperson_idand nothing corrects them at query time. Add the table back once its cluster resolves reliably, and repair rows stranded in the meantime, for example by copyingperson_idfrom the squashed legacy rows by uuid. That repair needs the legacy table. chore(clickhouse): make squash coverage a checked invariant #98865 (open) records the exclusion as an explicit exemption. See the deletion-coverage document.1.50is stored as1.5), flag arrays/order, omitted properties, datetime normalization, and deduplication. HogQL reads every native path through the genericDynamicexpression, even declared typed paths such as$browser(resolve_json_subcolumn_source); measure that before signing off on performance. feat(hogql): add a useNewEventsSchema query modifier #106274's modifier is the intended way to run one query against each table. Record rollback criteria and the decision to start a limited rollout here. Do not put customer queries or private operational data in this public issue.Before retiring the legacy events table
posthog/models/person/point_in_time_properties.pyreads$setfrom legacy events with raw SQL. fix(persons): build properties_at_time via HogQL instead of raw SQL #91452 ports it to HogQL and intentionally pinsproperties_at_timeand historical flag test evaluation to legacy events; its$set/$set_oncetest runs on both tables and fails on native without the pin. Native$set/$set_oncelive intemporary_propertieswith a 60-day TTL, which cannot serve the longer replay window. Choose a durable source or an explicitly changed product contract before dropping legacy storage. The mutation log in feat(clickhouse): retain event person mutation payloads on aux #97215 has a 30-day retention window and is not automatically a replacement.products/feature_flags/backend/flag_analytics.pyandproducts/growth/backend/temporal/health_checks/sdk_outdated.py, plus the historical-person and export paths above. The MCP analytics metrics inposthog/tasks/usage_report.pyreadeventsdirectly because their dedup expression matches the legacy sort key. A semgrep rule against new raw events SQL outside the switch was proposed and not started. Native-only result regression coverage is required before the first production trial, as tracked above; the complete reader inventory and replacement of deliberate legacy dependencies must still finish before retirement."false"-variant reinsertion, the person_id repair, and any typed-value re-backfill. Record completion evidence for each dependency here.Completed foundations
Keeping this tracker current
When working on native JSON events, consult this issue first. Add newly confirmed cutover blockers with a public code/PR reference and a concrete completion condition. Check an item off only when its implementation and required rollout/validation are complete; a merged PR alone may not satisfy both. Link follow-up PRs back here. Keep no-rollout merge decisions distinct from permission to enable teams or retire storage.
Initial inventory: #91506, #91515, #104783, #105879, #106152, #106274, #106280, #105651, #91452, #97215, #97217, and #79128. The deletion dependency comes from #102195 and the repository's deletion-coverage documentation. No production settings were changed while assembling this tracker.