Skip to content

feat(hogql): opt flagged users into the native events cluster - #119468

Closed
vdekrijger wants to merge 56 commits into
codex/native-events-async-lifecyclefrom
vasco/native-events-user-flag
Closed

vdekrijger wants to merge 56 commits into
codex/native-events-async-lifecyclefrom
vasco/native-events-user-flag

Conversation

@vdekrijger

@vdekrijger vdekrijger commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  • The native events cluster can only be tried by sending each query to the API with explicit modifiers, so PostHog staff cannot click through the app on it.
  • The ClickHouse owner and the engineers driving the migration want to use the app on the new cluster, starting with themselves, and widen the group as checks pass.
  • Builds on #116838 (typed queries) and #115778 (async lifecycle), which made app queries able to run on the native cluster.

Changes

  • A user in the native-events-cluster-queries feature flag now runs their app queries on the native events cluster and the native JSON events table.
  • The opt-in applies only when all of these hold:
    • the project is in CLICKHOUSE_NATIVE_EVENTS_TEAMS and the native host is set;
    • the query type is one the native cluster supports;
    • neither the query nor a nested source sets useNewEventsCluster or useNewEventsSchema;
    • neither the query nor a nested source is raw HogQL;
    • the request does not come from a personal API key (OAuth clients such as MCP are included);
    • the project does not set its own useNewEventsSchema default.
  • Raw HogQL stays on its current route, because its workload depends on the tables it reads (logs and metrics are not analytics) and is only known after printing.
  • The opt-in keeps the query's other modifiers, such as personsOnEventsMode.
  • Every other query keeps its current route, so the flag never turns a working query into an error.
  • The flag does nothing until CLICKHOUSE_NATIVE_EVENTS_ASYNC is on. Dashboard tiles fill through an async run and read back from the cache, so both steps must agree on one route; with the gate off they would not.
  • Native results, flagged or explicit, skip the cache-warming freshness index, which tracks only legacy entries, so a native refresh never delays warming for everyone else.
  • The query API and the insights API mark their requests interactive, and process_query_model resolves the opt-in from the user they already pass, before the runner computes the cache key. Native and legacy results therefore keep separate cache entries, and a saved insight's query is parsed once.
  • Setting the modifier inside the runner's per-user defaults was rejected: the runner applies them after the cache lookup, so native results would land in the legacy cache entry.
  • Saved insights and dashboard tiles loaded through the insights API take the same opt-in, so once the async gate is on a flagged user sees native numbers on dashboards too.
  • The opt-in unwraps InsightVizNode and DataTableNode, which saved insights store, and checks the query that runs. Runnable wrappers such as FunnelsActorsQuery are checked as themselves, so the flag never opts in a query its runner would refuse.
  • Shared dashboards, exports, subscriptions, alerts, cache warming and server-side PostHog AI execution keep the legacy route. PostHog AI tiles a flagged user's browser renders through /query take the flag like any other request.
  • The flag reads the request's access method itself, so personal API key traffic stays legacy on both paths, and it evaluates through posthog_feature_flag_enabled like other server-side gates.

Warning

To roll back, turn the flag off first. Only remove the project from CLICKHOUSE_NATIVE_EVENTS_TEAMS or clear the native host after queued queries drain. Workers recheck eligibility and fail a queued native query rather than silently moving it to another cluster.

Note

The flag does not exist yet. Create native-events-cluster-queries in PostHog, targeted by email, before this has any effect.

How did you test this code?

Test rationale:

  • A parameterized API test in TestNewEventsClusterPilot runs one query with the flag off and on. It catches the opt-in not reaching the runner, and native results sharing the legacy cache key. It fails when the query API stops marking its requests interactive.
  • Unit tests check that the flag waits for the async gate, and that it targets the signed-in user's distinct ID and email with organization and project groups, evaluated locally.
  • A parameterized unit case in TestNewEventsClusterRouting catches the flag dropping a query's other modifiers, or forcing raw HogQL, unsupported query types, explicit schema choices or nested opt-outs onto the native route.
  • A parameterized API test loads a saved insight through the insights API with the flag off, on, and on with a personal API key. It fails when the insights API stops marking its requests interactive, when the opt-in stops looking through the wrapper node, when API key traffic takes the flag, or when a native run updates the freshness index.
  • A unit case covers a FunnelsActorsQuery that the flag must leave alone.
  • Not run: the ClickHouse-backed suites, because the local ClickHouse replicas were read-only.

Release status

  • This change is behind a feature flag and is not available to users

Docs update

None. No doc covers the native cluster opt-in yet.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Opus 5.5 (claude-opus-5-5)

  • Skills: /writing-tests, /writing-pr-descriptions.
  • A duplicate search for open native-cluster flag PRs found none.
  • The first idea, setting the modifier in create_default_modifiers_for_user, was dropped after reading the cache path (see Changes).

🤖 Generated with Claude Code

…uster

A user in the native-events-cluster-queries flag gets useNewEventsCluster and
useNewEventsSchema on their query API requests, for eligible projects and supported
query types, unless the query sets either modifier itself. The opt-in reaches the
runner as constructor modifiers, before the cache key is computed, so native results
keep their own cache entries.
@vdekrijger vdekrijger self-assigned this Oct 19, 2026

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 40bc8e0: 3 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

Comment thread posthog/api/services/query.py Resolved
Comment thread posthog/api/services/query.py Resolved
Comment thread posthog/api/services/query.py Resolved
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Trunk lane — backend Python lane

This PR is assigned to the backend Python lane. It runs backend Python tests and may merge in parallel with PRs in other lanes.

⚠️ Duplication (Python) — 33 new duplicated blocks (worst 187 tokens)

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

First copy Second copy Lines Tokens
posthog/api/services/query.py:597 posthog/api/services/query.py:621 22 187
products/warehouse_sources/backend/temporal/data_imports/sources/companycam/source.py:1 products/warehouse_sources/backend/temporal/data_imports/sources/kalshi/source.py:1 22 176
posthog/api/services/query.py:365 posthog/api/services/query.py:389 21 169
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_ads/source.py:68 products/warehouse_sources/backend/temporal/data_imports/sources/amazon_selling_partner/source.py:175 39 159
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_selling_partner/source.py:21 products/warehouse_sources/backend/temporal/data_imports/sources/appstack/source.py:18 19 146
products/warehouse_sources/backend/temporal/data_imports/sources/aws_budgets/source.py:17 products/warehouse_sources/backend/temporal/data_imports/sources/aws_cost_and_usage_report/source.py:22 17 146
products/warehouse_sources/backend/temporal/data_imports/sources/adyen/source.py:19 products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:25 17 143
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/dynamodb/source.py:81 39 141
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_selling_partner/source.py:27 products/warehouse_sources/backend/temporal/data_imports/sources/greenhouse/source.py:17 14 138
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_athena/source.py:102 38 136
products/warehouse_sources/backend/temporal/data_imports/sources/aws_athena/athena.py:62 products/warehouse_sources/backend/temporal/data_imports/sources/trino/trino.py:41 14 128
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_iam_access_analyzer/source.py:135 33 122
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/amazon_cloudwatch.py:361 products/warehouse_sources/backend/temporal/data_imports/sources/aws_athena/athena.py:176 14 115
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_cost_anomaly_detection/source.py:155 31 111
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_organizations/source.py:161 31 111
products/warehouse_sources/backend/temporal/data_imports/sources/azure_cost_management/azure_cost_management.py:314 products/warehouse_sources/backend/temporal/data_imports/sources/azure_log_analytics/azure_log_analytics.py:188 16 111
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_cost_and_usage_report/source.py:166 31 110
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_cost_explorer/source.py:153 30 106
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_sagemaker/source.py:153 28 101
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_config/source.py:143 27 100
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_glue_data_catalog/source.py:135 27 98
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_guardduty/source.py:126 27 98
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_inspector/source.py:146 27 98
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_cloudtrail/source.py:154 26 96
products/warehouse_sources/backend/temporal/data_imports/sources/azure_cost_management/source.py:85 products/warehouse_sources/backend/temporal/data_imports/sources/azure_log_analytics/source.py:43 27 95
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_budgets/source.py:150 26 94
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_macie/source.py:131 26 94
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/amazon_cloudwatch.py:333 products/warehouse_sources/backend/temporal/data_imports/sources/aws_athena/athena.py:202 15 93
products/warehouse_sources/backend/temporal/data_imports/sources/gcp_cloud_billing/gcp_cloud_billing.py:8 products/warehouse_sources/backend/temporal/data_imports/sources/kalshi/kalshi.py:11 11 91
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/amazon_cloudwatch.py:158 products/warehouse_sources/backend/temporal/data_imports/sources/aws_cost_and_usage_report/aws_cost_and_usage_report.py:116 13 90
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_batch/source.py:122 23 80
products/warehouse_sources/backend/temporal/data_imports/sources/amazon_cloudwatch/source.py:195 products/warehouse_sources/backend/temporal/data_imports/sources/aws_ses/source.py:167 22 76
products/warehouse_sources/backend/temporal/data_imports/sources/cloudflare_d1/source.py:68 products/warehouse_sources/backend/temporal/data_imports/sources/drip/source.py:110 20 71
⚠️ Duplication (TypeScript) — 10 new duplicated blocks (worst 1387 tokens)

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

First copy Second copy Lines Tokens
frontend/src/products.tsx:394 products/ai_observability/manifest.tsx:219 121 1387
frontend/src/products.tsx:429 products/ai_observability/manifest.tsx:257 87 1029
frontend/src/products.tsx:1332 products/ai_observability/manifest.tsx:355 83 630
frontend/src/products.tsx:75 products/ai_observability/manifest.tsx:180 38 237
products/signals/frontend/inbox/signalSourcesLogic.ts:688 products/signals/frontend/inbox/signalSourcesLogic.ts:753 64 140
frontend/src/products.tsx:2700 products/ai_observability/manifest.tsx:486 21 132
products/signals/frontend/inbox/signalSourcesLogic.ts:614 products/signals/frontend/inbox/signalSourcesLogic.ts:688 63 130
frontend/src/scenes/hog-functions/email-templater/EmailTemplater.tsx:240 frontend/src/scenes/hog-functions/email-templater/EmailTemplater.tsx:795 24 84
products/ai_observability/frontend/datasets/AIObservabilityDatasetsScene.tsx:76 products/ai_observability/frontend/datasets/DatasetItems.tsx:134 15 75
products/signals/frontend/inbox/signalSourcesLogic.ts:644 products/signals/frontend/inbox/signalSourcesLogic.ts:783 35 74
✅ MCP agent API — no agent API changes

This PR no longer changes the tools, params, scopes or annotations agents see.

⚠️ MCP snapshots — 4 updated (4 modified, 0 added, 0 deleted)

Snapshots: MCP unit test snapshots updated

Changes: 4 snapshots (4 modified, 0 added, 0 deleted)

What this means:

  • Snapshots have been automatically updated to match current output

Next steps:

  • Review the changes to ensure they're intentional
  • If unexpected, investigate what caused the output to change

Review snapshot changes →

The rollout opt-in now starts from the query's effective modifiers, including nested
sources, so settings such as personsOnEventsMode survive and a nested explicit opt-out
keeps its route. HogQL queries are no longer opted in automatically, because their
workload depends on the tables they read and is only known once they are printed.
@trunk-io

trunk-io Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

View Full Report ↗︎ ⋅ Docs

@vdekrijger
vdekrijger added this pull request to stack #116907 October 20, 2026 07:47
@vdekrijger vdekrijger added the posthog-review Run a Standard PostHog Review label Oct 20, 2026

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 388d983: no findings.

This Standard review started from the posthog-review label. Comment @posthog review --deep for a Deep review, or @posthog help to see all commands.

@posthog posthog Bot removed the posthog-review Run a Standard PostHog Review label Oct 20, 2026
@vdekrijger vdekrijger added the posthog-review Run a Standard PostHog Review label Oct 20, 2026

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 57c25da: no findings.

This Standard review started from the posthog-review label. Comment @posthog review --deep for a Deep review, or @posthog help to see all commands.

…events-user-flag

# Conflicts:
#	posthog/api/services/query.py
…nd dashboard tiles

The insights API now passes the user's native cluster opt-in into calculate_for_query_based_insight, and the opt-in looks through wrapper nodes such as InsightVizNode to the query they run. Shared views and background callers keep the legacy route.
@vdekrijger
vdekrijger removed this pull request from stack #120301 October 22, 2026 13:25
@vdekrijger
vdekrijger added this pull request to stack #122087 October 22, 2026 13:26
…cle' into pr119468

# Conflicts:
#	posthog/hogql/test/test_new_events_schema_routing.py
# Conflicts:
#	posthog/hogql_queries/query_runner.py
…users

The user flag unwrapped every source, so runnable actor wrappers such as FunnelsActorsQuery were opted in and then refused by their runner. It now unwraps only the UI wrappers process_query_model unwraps, and asks native_route_can_run whether the execution mode can use the native route.
…m the request user

Both entrypoints pass the request user instead of computing modifiers, so a saved insight's query is parsed once. The flag reads the request's access method itself, so personal API key traffic stays legacy on both the query and insights paths. The flag uses posthog_feature_flag_enabled like other server-side gates.
With the async gate off, cache-only reads took the native cache entry while the async fills they waited on wrote the legacy one, so a flagged user's dashboard tiles could come back empty. The flag now waits for CLICKHOUSE_NATIVE_EVENTS_ASYNC, so every mode agrees on one route. Native stores skip the warming freshness index, which only tracks legacy entries. Entrypoints pass interactive_request instead of the user twice, and the opt-in resolves next to the runner, cheap checks first.
@vdekrijger

Copy link
Copy Markdown
Contributor Author

@PostHog review

@posthog

posthog Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

@vdekrijger PostHog Review is reviewing this pull request.

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 4005153: no findings.

@vdekrijger

Copy link
Copy Markdown
Contributor Author

Superseded by #122489, which merged on Oct 26 and routes events_json reads to the events cluster with one rule instead of the per-query opt-in this PR carried. Closing; the branch stays.

@vdekrijger vdekrijger closed this Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant