Skip to content

feat(flags): mirror the events schema in flag_evaluations - #80994

Merged
trunk-io[bot] merged 1 commit into
masterfrom
posthog-code/flag-evaluations-events-mirror
Aug 11, 2026
Merged

trunk-io[bot] merged 1 commit into
masterfrom
posthog-code/flag-evaluations-events-mirror

Conversation

@haacked

@haacked haacked commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  • Moving flag evaluation events off the events table would break any customer query or integration built on their $feature_flag_called properties.
  • The table meant to hold them today (migration 0292) keeps a typed subset of columns and drops the raw properties JSON.
  • The family is empty everywhere and has no producer yet, so this fixes the schema before anything depends on it.

Changes

  • The table family now mirrors the events column set and keeps the full properties JSON as the source of truth, replacing the typed flag columns from 0292.
  • Nine columns are materialized out of properties: the events table's $group_0..$group_4 keys, copied verbatim, plus flag_key, response, session_id, and request_id.
  • A property earns a materialized column only when hot-path queries filter or group on it across many rows (flag_key, response), or a skip index needs a real column to sit on (session_id, request_id).
  • Everything else stays in the JSON, and the 90-day TTL keeps adding columns later cheap.
  • The schema extracts response with trim_quotes(JSONExtractRaw(...)), not JSONExtractString. The two agree on strings, booleans, and numbers. They differ only on JSON null, where this form stores the 'null' sentinel that HogQL maps back to SQL NULL, matching how the events table materializes its own columns.
  • The materialized columns sit on the sharded storage table, repeat plain on the Distributed read table, and are omitted from the writable and Kafka tables. This is the events table's own three-way split.
  • Migration 0296 drops and recreates the whole family. The family is empty, so recreating from the template cannot drift from a fresh install, and it avoids a DROP COLUMN mutation that can get stuck and block a release.

The physical design is the part to review closely: monthly PARTITION BY toYYYYMM(timestamp), ORDER BY (team_id, flag_key, toDate(timestamp), cityHash64(distinct_id)), a 90-day TTL with ttl_only_drop_parts, and sipHash64(distinct_id) sharding to match events so a later backfill runs shard-local. The rest of the diff is the SQL templates, the migration, the regenerated HCL and snapshot, and one test.

Note

This table stores the same person and group PII as events (properties, person_properties, person_id, group properties), but the property-deletion scanners are scoped to the events table (posthog/models/data_deletion_request.py:814). A deletion request would not sweep this table, so the 90-day TTL is the only erasure control. Nothing stores rows today. Tracked as #81002, which has to be resolved before this table gets a producer: either the deletion scanners cover it, or we document that the TTL is the accepted erasure window.

How did you test this code?

  • Applied the migration against a live ClickHouse 26.6.2 dev node. All ten drop-and-create statements succeeded on the real, empty family.
  • Produced a $feature_flag_called message to the clickhouse_flag_evaluations topic and read it back through the flag_evaluations proxy. It flowed Kafka table, materialized view, writable Distributed, and sharded storage, where the shard computed the materialized columns. A boolean $feature_flag_response stored as 'false'.
  • Confirmed the materialized flag_key works inside the sort key, both in ClickHouse and against repo precedent (logs34 and trace_spans sort on materialized columns).
  • Regenerated the HCL golden and sql. hcl/check.sh reports no differences for every env and role, and the generated DDL is byte-identical to sql.py.
  • Added test_flag_evaluations_read_table_declares_every_stored_column. It catches drift between the storage table's materialized columns and the read proxy's plain copies, which nothing else asserts. I mutation-tested it against a dropped column, a type change, and a broken parse anchor.
  • Not run: the multinode ClickHouse migration smoke test, which applies migrations against a real multi-node cluster. It needs infrastructure this session does not have.

Automatic notifications

  • Publish to changelog?

Docs update

None. No user-facing behavior or documented workflow changes, and the table has no query surface yet.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Tool: PostHog Code (Claude). Phil directed each step.
  • Skills invoked: /clickhouse-migrations, /writing-tests, /writing-code-comments, /simplify, /review-code, /writing-pr-descriptions.
  • The design changed mid-session. It started from a minimal typed schema (closed feat(flags): reshape flag_evaluations to minimal schema with person_id #78618) and switched to mirroring the events table after discussion with Ben White and Rory Shanks, so customer queries on event properties survive the routing switch.
  • I (actually Claude) corrected the response extraction from the original spec's JSONExtractString. Measured against a live ClickHouse node, the spec's assumption about boolean handling was wrong, and the real difference is only the JSON-null sentinel.
  • A /simplify pass hoisted duplicated column comments onto the shared abstract HCL block, removing 27 source lines with byte-identical generated output. A /review-code --fix pass then ran seven review dimensions and renamed one test helper.
  • Nothing in this PR carries material from the session that is not already public. No customer data was involved.

Created with PostHog Code

@haacked haacked self-assigned this Aug 11, 2026
@trunk-io

trunk-io Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

😎 Merged successfully - details.

@github-actions

github-actions Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

✅ Playwright — all passed

All tests passed.

View test results →

ℹ️ ClickHouse migration SQL — 1 migration(s)

ClickHouse migration SQL per cloud environment

  • unset
    • all
      DROP TABLE IF EXISTS flag_evaluations_mv
      DROP TABLE IF EXISTS kafka_flag_evaluations
      DROP TABLE IF EXISTS flag_evaluations
      DROP TABLE IF EXISTS writable_flag_evaluations
      DROP TABLE IF EXISTS sharded_flag_evaluations SYNC
      CREATE TABLE IF NOT EXISTS sharded_flag_evaluations
      (
          uuid UUID,
          event LowCardinality(String),
          properties String,
          timestamp DateTime64(6, 'UTC'),
          team_id Int64,
          distinct_id String,
          created_at DateTime64(6, 'UTC'),
          person_id UUID,
          person_properties String,
          group0_properties String,
          group1_properties String,
          group2_properties String,
          group3_properties String,
          group4_properties String,
          inserted_at DateTime64(6, 'UTC') DEFAULT timestamp
        
          , $group_0 String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_0'), '^"|"$', '') COMMENT 'column_materializer::$group_0'
          , $group_1 String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_1'), '^"|"$', '') COMMENT 'column_materializer::$group_1'
          , $group_2 String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_2'), '^"|"$', '') COMMENT 'column_materializer::$group_2'
          , $group_3 String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_3'), '^"|"$', '') COMMENT 'column_materializer::$group_3'
          , $group_4 String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_4'), '^"|"$', '') COMMENT 'column_materializer::$group_4'
          , flag_key String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag'), '^"|"$', '') COMMENT 'column_materializer::properties::$feature_flag'
          , response LowCardinality(String) MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') COMMENT 'column_materializer::properties::$feature_flag_response'
          , session_id String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$session_id'), '^"|"$', '') COMMENT 'column_materializer::properties::$session_id'
          , request_id String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_request_id'), '^"|"$', '') COMMENT 'column_materializer::properties::$feature_flag_request_id'
      
        
          , INDEX distinct_id_idx distinct_id TYPE bloom_filter(0.01) GRANULARITY 1
          , INDEX person_id_idx   person_id   TYPE bloom_filter(0.01) GRANULARITY 1
          , INDEX session_id_idx  session_id  TYPE bloom_filter(0.01) GRANULARITY 1
          , INDEX request_id_idx  request_id  TYPE bloom_filter(0.01) GRANULARITY 1
          , INDEX inserted_at_idx inserted_at TYPE minmax GRANULARITY 1
      
        
      , _timestamp DateTime
      , _offset UInt64
      , _partition UInt64
      
      )
      ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/posthog.flag_evaluations', '{replica}')
      -- Monthly, matching the events family. Daily partitions would put ~90 of them
      -- under a 90-day TTL, and enough parts across them to strain merges. The cost
      -- is coarser expiry: with ttl_only_drop_parts a part only drops once its newest
      -- row expires, so rows survive up to a month past the TTL.
      PARTITION BY toYYYYMM(timestamp)
      ORDER BY (team_id, flag_key, toDate(timestamp), cityHash64(distinct_id))
      TTL toDate(timestamp) + INTERVAL 90 DAY
      SETTINGS ttl_only_drop_parts = 1
      CREATE TABLE IF NOT EXISTS writable_flag_evaluations
      (
          uuid UUID,
          event LowCardinality(String),
          properties String,
          timestamp DateTime64(6, 'UTC'),
          team_id Int64,
          distinct_id String,
          created_at DateTime64(6, 'UTC'),
          person_id UUID,
          person_properties String,
          group0_properties String,
          group1_properties String,
          group2_properties String,
          group3_properties String,
          group4_properties String,
          inserted_at DateTime64(6, 'UTC') DEFAULT timestamp
        
        
      , _timestamp DateTime
      , _offset UInt64
      , _partition UInt64
      
      )
      ENGINE = Distributed('posthog', 'posthog_test', 'sharded_flag_evaluations', sipHash64(distinct_id))
      CREATE TABLE IF NOT EXISTS flag_evaluations
      (
          uuid UUID,
          event LowCardinality(String),
          properties String,
          timestamp DateTime64(6, 'UTC'),
          team_id Int64,
          distinct_id String,
          created_at DateTime64(6, 'UTC'),
          person_id UUID,
          person_properties String,
          group0_properties String,
          group1_properties String,
          group2_properties String,
          group3_properties String,
          group4_properties String,
          inserted_at DateTime64(6, 'UTC') DEFAULT timestamp
        
          , $group_0 String COMMENT 'column_materializer::$group_0'
          , $group_1 String COMMENT 'column_materializer::$group_1'
          , $group_2 String COMMENT 'column_materializer::$group_2'
          , $group_3 String COMMENT 'column_materializer::$group_3'
          , $group_4 String COMMENT 'column_materializer::$group_4'
          , flag_key String COMMENT 'column_materializer::properties::$feature_flag'
          , response LowCardinality(String) COMMENT 'column_materializer::properties::$feature_flag_response'
          , session_id String COMMENT 'column_materializer::properties::$session_id'
          , request_id String COMMENT 'column_materializer::properties::$feature_flag_request_id'
      
        
      , _timestamp DateTime
      , _offset UInt64
      , _partition UInt64
      
      )
      ENGINE = Distributed('posthog', 'posthog_test', 'sharded_flag_evaluations', sipHash64(distinct_id))
      CREATE TABLE IF NOT EXISTS kafka_flag_evaluations
      (
          uuid UUID,
          event LowCardinality(String),
          properties String,
          timestamp DateTime64(6, 'UTC'),
          team_id Int64,
          distinct_id String,
          created_at DateTime64(6, 'UTC'),
          person_id UUID,
          person_properties String,
          group0_properties String,
          group1_properties String,
          group2_properties String,
          group3_properties String,
          group4_properties String,
          inserted_at DateTime64(6, 'UTC')
      )
      ENGINE = Kafka(warpstream_ingestion, kafka_topic_list = 'clickhouse_flag_evaluations_test', kafka_group_name = 'clickhouse_flag_evaluations', kafka_format = 'JSONEachRow')
      SETTINGS kafka_skip_broken_messages = 100
      CREATE MATERIALIZED VIEW IF NOT EXISTS flag_evaluations_mv
      TO posthog_test.writable_flag_evaluations
      AS SELECT
          uuid,
          event,
          properties,
          timestamp,
          team_id,
          distinct_id,
          created_at,
          person_id,
          person_properties,
          group0_properties,
          group1_properties,
          group2_properties,
          group3_properties,
          group4_properties,
          -- Fall back to the Kafka message timestamp, which is stable across replays
          -- (inserted_at checkpoints the sync_feature_flag_last_called task, and an
          -- epoch-stamped row would stay invisible to it forever).
          if(inserted_at = toDateTime64('1970-01-01 00:00:00', 6, 'UTC'), _timestamp, inserted_at) AS inserted_at,
          _timestamp,
          _offset,
          _partition
      FROM posthog_test.kafka_flag_evaluations
  • US, EU, DEV
    • medium
      DROP TABLE IF EXISTS flag_evaluations_mv
      DROP TABLE IF EXISTS kafka_flag_evaluations
      DROP TABLE IF EXISTS writable_flag_evaluations
      CREATE TABLE IF NOT EXISTS writable_flag_evaluations
      (
          uuid UUID,
          event LowCardinality(String),
          properties String,
          timestamp DateTime64(6, 'UTC'),
          team_id Int64,
          distinct_id String,
          created_at DateTime64(6, 'UTC'),
          person_id UUID,
          person_properties String,
          group0_properties String,
          group1_properties String,
          group2_properties String,
          group3_properties String,
          group4_properties String,
          inserted_at DateTime64(6, 'UTC') DEFAULT timestamp
        
        
      , _timestamp DateTime
      , _offset UInt64
      , _partition UInt64
      
      )
      ENGINE = Distributed('posthog', 'posthog_test', 'sharded_flag_evaluations', sipHash64(distinct_id))
      CREATE TABLE IF NOT EXISTS kafka_flag_evaluations
      (
          uuid UUID,
          event LowCardinality(String),
          properties String,
          timestamp DateTime64(6, 'UTC'),
          team_id Int64,
          distinct_id String,
          created_at DateTime64(6, 'UTC'),
          person_id UUID,
          person_properties String,
          group0_properties String,
          group1_properties String,
          group2_properties String,
          group3_properties String,
          group4_properties String,
          inserted_at DateTime64(6, 'UTC')
      )
      ENGINE = Kafka(warpstream_ingestion, kafka_topic_list = 'clickhouse_flag_evaluations_test', kafka_group_name = 'clickhouse_flag_evaluations', kafka_format = 'JSONEachRow')
      SETTINGS kafka_skip_broken_messages = 100
      CREATE MATERIALIZED VIEW IF NOT EXISTS flag_evaluations_mv
      TO posthog_test.writable_flag_evaluations
      AS SELECT
          uuid,
          event,
          properties,
          timestamp,
          team_id,
          distinct_id,
          created_at,
          person_id,
          person_properties,
          group0_properties,
          group1_properties,
          group2_properties,
          group3_properties,
          group4_properties,
          -- Fall back to the Kafka message timestamp, which is stable across replays
          -- (inserted_at checkpoints the sync_feature_flag_last_called task, and an
          -- epoch-stamped row would stay invisible to it forever).
          if(inserted_at = toDateTime64('1970-01-01 00:00:00', 6, 'UTC'), _timestamp, inserted_at) AS inserted_at,
          _timestamp,
          _offset,
          _partition
      FROM posthog_test.kafka_flag_evaluations
    • data
      DROP TABLE IF EXISTS flag_evaluations
      DROP TABLE IF EXISTS sharded_flag_evaluations SYNC
      CREATE TABLE IF NOT EXISTS sharded_flag_evaluations
      (
          uuid UUID,
          event LowCardinality(String),
          properties String,
          timestamp DateTime64(6, 'UTC'),
          team_id Int64,
          distinct_id String,
          created_at DateTime64(6, 'UTC'),
          person_id UUID,
          person_properties String,
          group0_properties String,
          group1_properties String,
          group2_properties String,
          group3_properties String,
          group4_properties String,
          inserted_at DateTime64(6, 'UTC') DEFAULT timestamp
        
          , $group_0 String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_0'), '^"|"$', '') COMMENT 'column_materializer::$group_0'
          , $group_1 String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_1'), '^"|"$', '') COMMENT 'column_materializer::$group_1'
          , $group_2 String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_2'), '^"|"$', '') COMMENT 'column_materializer::$group_2'
          , $group_3 String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_3'), '^"|"$', '') COMMENT 'column_materializer::$group_3'
          , $group_4 String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_4'), '^"|"$', '') COMMENT 'column_materializer::$group_4'
          , flag_key String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag'), '^"|"$', '') COMMENT 'column_materializer::properties::$feature_flag'
          , response LowCardinality(String) MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') COMMENT 'column_materializer::properties::$feature_flag_response'
          , session_id String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$session_id'), '^"|"$', '') COMMENT 'column_materializer::properties::$session_id'
          , request_id String MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_request_id'), '^"|"$', '') COMMENT 'column_materializer::properties::$feature_flag_request_id'
      
        
          , INDEX distinct_id_idx distinct_id TYPE bloom_filter(0.01) GRANULARITY 1
          , INDEX person_id_idx   person_id   TYPE bloom_filter(0.01) GRANULARITY 1
          , INDEX session_id_idx  session_id  TYPE bloom_filter(0.01) GRANULARITY 1
          , INDEX request_id_idx  request_id  TYPE bloom_filter(0.01) GRANULARITY 1
          , INDEX inserted_at_idx inserted_at TYPE minmax GRANULARITY 1
      
        
      , _timestamp DateTime
      , _offset UInt64
      , _partition UInt64
      
      )
      ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/posthog.flag_evaluations', '{replica}')
      -- Monthly, matching the events family. Daily partitions would put ~90 of them
      -- under a 90-day TTL, and enough parts across them to strain merges. The cost
      -- is coarser expiry: with ttl_only_drop_parts a part only drops once its newest
      -- row expires, so rows survive up to a month past the TTL.
      PARTITION BY toYYYYMM(timestamp)
      ORDER BY (team_id, flag_key, toDate(timestamp), cityHash64(distinct_id))
      TTL toDate(timestamp) + INTERVAL 90 DAY
      SETTINGS ttl_only_drop_parts = 1
      CREATE TABLE IF NOT EXISTS flag_evaluations
      (
          uuid UUID,
          event LowCardinality(String),
          properties String,
          timestamp DateTime64(6, 'UTC'),
          team_id Int64,
          distinct_id String,
          created_at DateTime64(6, 'UTC'),
          person_id UUID,
          person_properties String,
          group0_properties String,
          group1_properties String,
          group2_properties String,
          group3_properties String,
          group4_properties String,
          inserted_at DateTime64(6, 'UTC') DEFAULT timestamp
        
          , $group_0 String COMMENT 'column_materializer::$group_0'
          , $group_1 String COMMENT 'column_materializer::$group_1'
          , $group_2 String COMMENT 'column_materializer::$group_2'
          , $group_3 String COMMENT 'column_materializer::$group_3'
          , $group_4 String COMMENT 'column_materializer::$group_4'
          , flag_key String COMMENT 'column_materializer::properties::$feature_flag'
          , response LowCardinality(String) COMMENT 'column_materializer::properties::$feature_flag_response'
          , session_id String COMMENT 'column_materializer::properties::$session_id'
          , request_id String COMMENT 'column_materializer::properties::$feature_flag_request_id'
      
        
      , _timestamp DateTime
      , _offset UInt64
      , _partition UInt64
      
      )
      ENGINE = Distributed('posthog', 'posthog_test', 'sharded_flag_evaluations', sipHash64(distinct_id))

@trunk-io

trunk-io Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

View Full Report ↗︎ ⋅ Docs

@haacked
haacked marked this pull request as ready for review August 11, 2026 00:41
@haacked
haacked requested a review from a team as a code owner August 11, 2026 00:41
Copilot AI lite review requested due to automatic review settings August 11, 2026 00:41
@posthog

posthog Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

🦔 ReviewHog reviewed this pull request

Nothing worth raising this time, so here's a calming picture instead:

A happy dog on a sunny path

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
### Issue 1
posthog/clickhouse/migrations/0294_flag_evaluations_events_mirror.py:43-54
**Replacement schema lacks compatibility window**

If old and new workers overlap during deployment, this migration removes the typed columns while installing their properties-based replacements, leaving no schema version compatible with both worker generations and causing old inserts or queries to reference missing columns.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(flags): mirror the events schema in..." | Re-trigger Greptile

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the ClickHouse flag_evaluations table family to mirror the core events schema (while keeping the full properties JSON), so existing queries/integrations built on $feature_flag_called properties will keep working when these events are routed off the events table.

Changes:

  • Replaces the previous typed/minimal flag_evaluations schema with an events-like column set including raw properties JSON, plus a 90-day TTL and events-aligned partitioning/sharding/sort key.
  • Adds 9 materialized columns derived from properties ($group_0..$group_4, flag_key, response, session_id, request_id) on the sharded storage table, and mirrors them as plain columns on the Distributed read table (omitted from Kafka + writable tables).
  • Adds/updates schema verification to assert the read proxy declares every stored column, preventing drift between sharded storage and Distributed read schemas.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
posthog/models/flag_evaluations/sql.py Reworks the ClickHouse DDL templates for the whole flag_evaluations family (events-like columns, materialized props, indexes, MV projection).
posthog/clickhouse/test/test_schema.py Adds a regression test that compares sharded vs Distributed-read declared columns/types (after stripping modifiers).
posthog/clickhouse/test/snapshots/test_schema.ambr Updates the golden snapshot for the new table definitions and MV projection.
posthog/clickhouse/migrations/max_migration.txt Advances the max ClickHouse migration pointer to 0294.
posthog/clickhouse/migrations/0294_flag_evaluations_events_mirror.py Drop-and-recreate migration for the entire table family to apply the new schema safely (given it’s empty).
posthog/clickhouse/hcl/sql/local-multi/data.sql Regenerates local-multi SQL output to reflect the updated schema.
posthog/clickhouse/hcl/roles/data/local/tables.hcl Updates the HCL source-of-truth for the flag_evaluations columns and materialized column patches.
posthog/clickhouse/hcl/golden/local-multi/data.hcl Updates the generated HCL golden file for local-multi.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

),
run_sql_with_exceptions(
FLAG_EVALUATIONS_TABLE_SQL(),
node_roles=[NodeRole.DATA],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put this table onto AUX cluster?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed offline. The scale warrants keeping this on main for now.

@github-project-automation github-project-automation Bot moved this to Approved in Feature Flags Aug 11, 2026
@haacked
haacked force-pushed the posthog-code/flag-evaluations-events-mirror branch 2 times, most recently from 382d189 to ecbfe59 Compare August 11, 2026 19:01
The table family now carries the events table's column set, narrowed to
what a flag evaluation actually has, with the full properties JSON as
the source of truth. Queries and integrations written against
$feature_flag_called properties keep working once evaluations move off
the events table. A 90-day TTL makes rows that wide affordable.

Nine materialized columns come out of properties: the events table's
$group_0..$group_4 keys, verbatim, plus flag_key, response, session_id
and request_id. A property earns one only when hot-path queries filter
or group on it across many rows, or a skip index needs a real column to
sit on. They are declared on the sharded table, which computes them,
and repeated plain on the Distributed read table, which computes
nothing. No column carries a codec.

Migration 0297 drops and recreates the family rather than altering it.
Nothing produces to the topic yet, so every table is empty; recreating
from the canonical template cannot drift from a fresh install, and it
avoids DROP COLUMN mutations that can get stuck and block a release.

Generated-By: PostHog Code
Task-Id: a95149f1-e894-43a4-a0ea-5f93010cf268
@haacked
haacked force-pushed the posthog-code/flag-evaluations-events-mirror branch from ecbfe59 to 94d0eb6 Compare August 11, 2026 20:07
@haacked

haacked commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

/trunk merge

@trunk-io
trunk-io Bot merged commit b38bae0 into master Aug 11, 2026
269 checks passed
@github-project-automation github-project-automation Bot moved this from Approved to Done in Feature Flags Aug 11, 2026
@trunk-io
trunk-io Bot deleted the posthog-code/flag-evaluations-events-mirror branch August 11, 2026 21:16
@deployment-status-posthog

deployment-status-posthog Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-08-11 21:47 UTC Run
prod-us ✅ Deployed 2026-08-11 22:06 UTC Run
prod-eu ✅ Deployed 2026-08-11 22:09 UTC Run

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

Labels

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

3 participants