Skip to content

feat: verify ClickHouse events table and query customer event stats - #8116

Open
Zaimwa9 wants to merge 3 commits into
mainfrom
feat/clickhouse-verify-table-and-stats
Open

feat: verify ClickHouse events table and query customer event stats#8116
Zaimwa9 wants to merge 3 commits into
mainfrom
feat/clickhouse-verify-table-and-stats

Conversation

@Zaimwa9

@Zaimwa9 Zaimwa9 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Thanks for submitting a PR! Please check the boxes below:

  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature.
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

  • Connection verification now checks the events table exists; a missing table errors with a "run the setup SQL" detail.
  • Event stats are queried from the customer's instance for ClickHouse connections, so the setup-SQL section can collapse once events arrive.
  • Default ClickHouse database is now flagsmith_exp.

How did you test this code?

Unit tests, lint, mypy. Manually: tested a connection without the table (errors with the hint), created it, re-tested, checked stats populate.

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Jul 28, 2026 8:32am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
flagsmith-frontend-preview Ignored Ignored Preview Jul 28, 2026 8:32am
flagsmith-frontend-staging Ignored Ignored Preview Jul 28, 2026 8:32am

Request Review

@Zaimwa9

Zaimwa9 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@themis-blindfold review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ff9c4af2-379d-4c54-907e-20fceae92922

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added api Issue related to the REST API docs Documentation updates feature New feature or request and removed docs Documentation updates labels Jul 27, 2026
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.70%. Comparing base (96d20b5) to head (e96947d).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #8116   +/-   ##
=======================================
  Coverage   98.69%   98.70%           
=======================================
  Files        1525     1525           
  Lines       60607    60661   +54     
=======================================
+ Hits        59819    59873   +54     
  Misses        788      788           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread api/experimentation/types.py
Comment thread api/experimentation/services.py
Comment thread api/experimentation/services.py
Comment thread api/experimentation/services.py Outdated
Comment thread api/experimentation/services.py Outdated
@themis-blindfold

Copy link
Copy Markdown
Contributor

⚖️ Themis judgement: 🟠 Fix before merge

Verifying the events table and reading stats from the customer's own ClickHouse is the right way to make BYO-warehouse onboarding self-diagnosing, and the service-level tests cover the new branches well. Two things need attention before merge: the default-database change stops at the backend (the UI still sends flagsmith, so users never get flagsmith_exp), and stats collection now performs a blocking outbound connect to a customer-controlled host on the unthrottled list/retrieve endpoints — the same outbound connect that the write path deliberately rate-limits via warehouse_connection_write.

One API Unit Tests (3.11) run reported a failure; a second 3.11 run and every 3.12/3.13 run reported success, so I have no evidence tying it to this diff. The API dependencies aren't installed in this sandbox, so I could not execute the suite locally.

🎯 Correctness 3/5
🧪 Test coverage 3/5
📐 Code quality 3/5
🚀 Product impact 3/5

🟠 Majors

  • api/experimentation/types.py:60 — the default database moved to flagsmith_exp on the backend only; the UI still prefills and sends flagsmith.
  • api/experimentation/services.py:920GET list/retrieve now block on an outbound connect to the customer's ClickHouse with no throttle.

🧹 Nits

  • api/experimentation/services.py:842 — the "run the setup SQL" hint points at an artefact that isn't in the UI or docs yet.
  • api/experimentation/services.py:925 — stats failures are swallowed with no log line.
  • api/experimentation/services.py:947 — the stats query and row parsing are a verbatim copy of get_warehouse_event_stats.
  • api/tests/unit/experimentation/test_views.py:1541 — this GET test now exercises the real socket path unmocked.
📝 Walkthrough
  • _connect_customer_clickhouse — client construction and the internal-address re-check extracted from verify_clickhouse_connection so both verification and stats reuse one code path.
  • Verification — adds EXISTS TABLE events after SELECT 1, with a new MissingEventsTableError mapped to a "run the setup SQL" detail string.
  • annotate_warehouse_event_stats — new early branch for clickhouse connections that queries counts from the customer's instance instead of the managed warehouse.
  • CLICKHOUSE_DEFAULTS — backend default database changed from flagsmith to flagsmith_exp; view/serializer tests updated to match.
  • Events catalogue — line-number references regenerated after the services.py shuffle.
🧪 How to verify
  • Point a connection at a reachable ClickHouse whose configured database has no events table, hit test-warehouse-connection, and confirm the detail is the missing-table message rather than a generic failure.
  • Create a ClickHouse connection through the environment settings UI without editing the database field, then read the persisted config.database — it should be flagsmith_exp, not flagsmith.
  • Stop (or firewall) the customer ClickHouse host and time GET /api/v1/environments/<key>/experimentation/warehouse-connections/; measure how long the request holds the worker.
  • Run pytest tests/unit/experimentation/test_views.py -k clickhouse with outbound DNS/sockets blocked and confirm nothing hangs or slows down.
  • Automate: add a view test for the clickhouse list/retrieve path that patches experimentation.services.Client and asserts the stats query is issued.

Product take: Makes BYO-ClickHouse onboarding self-diagnosing — a missing events table now produces an actionable message instead of a bare "verification failed", and event counts confirm data is actually landing. Solid improvement to a feature that is otherwise hard to debug from the UI, though the frontend half of the default-database change still needs to land for it to be coherent.

Big picture: annotate_warehouse_event_stats is becoming the place where read-path serialization owns outbound third-party I/O: one branch queries Flagsmith's own pooled ClickHouse, the other opens a fresh connection to a customer-controlled host per request. Every future warehouse type adds another blocking dialect here, and GET latency for environment settings becomes a function of customer infrastructure. The smallest useful boundary is to keep stats collection out of the request/serialization path — populate them where verification already runs, or cache per connection — so the view reads state rather than fetching it.

🧭 Assumptions & unverified claims
  • clickhouse_driver is not installed in this environment, so I could not execute the suite or empirically confirm that EXISTS TABLE events returns a single row whose first column is a 0/1 flag; the rows[0][0] indexing assumes that shape.
  • I assume the "setup SQL" referenced in the new error message is intended to create flagsmith_exp.events and lands in a follow-up change; grep found no SQL, docs page, or UI snippet providing it in this checkout.
  • The blocking-GET consequence is reasoned from the configured timeouts (connect_timeout=5, send_receive_timeout=5) and the absence of a throttle scope on the read actions; I did not reproduce a slow-host request.

Verifying the table exists before trusting it is good manners; now the frontend just needs to agree on which database to look in. · reviewed at 361572d

1 finding(s) anchored outside the diff (not posted inline)
  • api/tests/unit/experimentation/test_views.py:1541 🧹 Nit · ⚡ Quick win

This GET test now runs the live-connection path unmocked. Observed: it does not patch experimentation.services.Client, so list reaches _connect_customer_clickhouse and attempts a real DNS lookup plus socket connect to ch.acme-corp.example:9440; the autouse restrict_http_requests guard (tests/conftest.py:241) only wraps urllib3's HTTPConnectionPool.urlopen, so a raw-socket driver slips past it. Patching Client here keeps the suite off the network.

@github-actions github-actions Bot added docs Documentation updates feature New feature or request and removed feature New feature or request docs Documentation updates labels Jul 28, 2026
@themis-blindfold

Copy link
Copy Markdown
Contributor

Review was cancelled before completing (worker timeout or shutdown). Mention @themis-blindfold with review to retry.

@Zaimwa9
Zaimwa9 marked this pull request as ready for review July 28, 2026 08:49
@Zaimwa9
Zaimwa9 requested review from a team as code owners July 28, 2026 08:49
@Zaimwa9
Zaimwa9 requested review from emyller and removed request for a team July 28, 2026 08:49
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Docker builds report

Image Build Status Security report
ghcr.io/flagsmith/flagsmith-e2e:pr-8116 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-api-test:pr-8116 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-frontend:pr-8116 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-api:pr-8116 Finished ✅ Results
ghcr.io/flagsmith/flagsmith:pr-8116 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-private-cloud:pr-8116 Finished ✅ Results

@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request docs Documentation updates labels Jul 28, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: db174986-e9ac-462c-8943-b581082c35b1

📥 Commits

Reviewing files that changed from the base of the PR and between 4d5fff8 and e96947d.

📒 Files selected for processing (8)
  • api/app/settings/common.py
  • api/app/settings/test.py
  • api/experimentation/services.py
  • api/experimentation/types.py
  • api/experimentation/views.py
  • api/tests/unit/experimentation/test_services.py
  • api/tests/unit/experimentation/test_views.py
  • docs/docs/deployment-self-hosting/observability/_events-catalogue.md

Comment on lines +814 to +832
def _connect_customer_clickhouse(connection: WarehouseConnection) -> Client:
"""Build a client for the customer's ClickHouse instance from stored
connection details, re-checking the host against internal address ranges
first: DNS may resolve differently than it did at validation time, and
rows may predate host validation."""
config = typing.cast(ClickHouseConfig, connection.config or {})
credentials = typing.cast(ClickHouseCredentials, connection.credentials or {})
if is_internal_address(config["host"], include_shared=True):
raise InternalAddressError(config["host"])
return Client(
config["host"],
port=config["port"],
user=config["username"],
password=credentials["password"],
database=config["database"],
secure=config["secure"],
connect_timeout=CLICKHOUSE_CONNECT_TIMEOUT_SECONDS,
send_receive_timeout=CLICKHOUSE_VERIFY_TIMEOUT_SECONDS,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect is_internal_address to see whether it resolves DNS and whether resolution is reused for the connection.
rg -n -B2 -A25 'def is_internal_address' api/experimentation/services.py

Repository: Flagsmith/flagsmith

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find files mentioning is_internal_address/ClickHouse/client:"
rg -n "is_internal_address|ClickHouse.*Config|def _connect_customer_clickhouse|Client\(" . || true

echo
echo "List likely files under api/experimentation:"
fd -a . api/experimentation 2>/dev/null | sed 's#^\./##' | head -100

Repository: Flagsmith/flagsmith

Length of output: 18599


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect core/network.py:"
cat -n api/core/network.py

echo
echo "Inspect experimentation/services.py near Client helper and customer connect code:"
sed -n '1,180p' api/experimentation/services.py | cat -n
echo "---"
sed -n '780,850p' api/experimentation/services.py | cat -n

echo
echo "Search for dnslib/resolve/Client import in experimentation services:"
rg -n "dns|resolve|Client|Warehouse|warehouse|connect_timeout|secure" api/experimentation/services.py api/core/network.py api/experimentation/warehouse_validation.py

Repository: Flagsmith/flagsmith

Length of output: 18526


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import ast
from pathlib import Path

services_path = Path("api/experimentation/services.py")
network_path = Path("api/core/network.py")

services_tree = ast.parse(services_path.read_text())
network_tree = ast.parse(network_path.read_text())

for node in services_tree.body:
    if isinstance(node, ast.FunctionDef) and node.name == "_connect_customer_clickhouse":
        print("services._connect_customer_clickhouse:")
        print(ast.unparse(node))
        print("--- source line numbers:", node.lineno, "-", node.end_lineno)

print()

for node in network_tree.body:
    if isinstance(node, ast.FunctionDef) and node.name == "is_internal_address":
        print("core.network.is_internal_address:")
        print(ast.unparse(node))
        print("--- source line numbers:", node.lineno, "-", node.end_lineno)

print
print("Imports at api/experimentation/services.py line 1-12:")
for node in services_tree.body[:15]:
    if isinstance(node, ast.ImportFrom):
        line = ast.get_source_segment(services_path.read_text(), node)
        print(f"{node.lineno}: {line}")
PY

echo
echo "Relevant static call-chain:"
rg -n "def _get_customer_warehouse_event_stats_cached|def .*event_stats.*customer|WarehouseConnection|warehouse_type" api/experimentation/services.py | sed -n '1,120p'

Repository: Flagsmith/flagsmith

Length of output: 3779


Resolve and pin the ClickHouse host address before connecting.

is_internal_address() is only one independent host-name lookup; Client() resolves the hostname again during connection. A customer-controlled, low-TTL hostname can resolve to a public address through the first check and then to a private/internal/metadata address when the client connects, bypassing this guard entirely. Resolve the hostname once, reject internal addresses among the resolved IPs, and connect using the pinned IP while preserving the original hostname for TLS SNI/hostname verification when secure=True.

Comment on lines +823 to +832
return Client(
config["host"],
port=config["port"],
user=config["username"],
password=credentials["password"],
database=config["database"],
secure=config["secure"],
connect_timeout=CLICKHOUSE_CONNECT_TIMEOUT_SECONDS,
send_receive_timeout=CLICKHOUSE_VERIFY_TIMEOUT_SECONDS,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the actual values assigned to these constants.
rg -n 'CLICKHOUSE_VERIFY_TIMEOUT_SECONDS|CLICKHOUSE_QUERY_TIMEOUT_SECONDS|CLICKHOUSE_CONNECT_TIMEOUT_SECONDS' api/experimentation/services.py

Repository: Flagsmith/flagsmith

Length of output: 578


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== outline =="
ast-grep outline api/experimentation/services.py --match '_connect_customer_clickhouse' --view expanded || true
ast-grep outline api/experimentation/services.py --match '_get_customer_warehouse_event_stats_cached' --view expanded || true
ast-grep outline api/experimentation/services.py --match 'verify_clickhouse_connection' --view expanded || true

echo "== relevant lines 800-990 =="
sed -n '800,990p' api/experimentation/services.py | nl -ba -v800

echo "== customer clickhouse usages =="
rg -n '_connect_customer_clickhouse|_query_event_stats|CLICKHOUSE_QUERY_TIMEOUT_SECONDS|CLICKHOUSE_VERIFY_TIMEOUT_SECONDS|verify_clickhouse_connection' api/experimentation/services.py

Repository: Flagsmith/flagsmith

Length of output: 511


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant lines 800-990 =="
python3 - <<'PY'
from pathlib import Path
p = Path('api/experimentation/services.py')
text = p.read_text()
lines = text.splitlines()
for i, line in enumerate(lines[799:990], start=800):
    print(f"{i:04d}: {line}")
PY

echo "== customer clickhouse usages =="
rg -n '_connect_customer_clickhouse|_query_event_stats|CLICKHOUSE_QUERY_TIMEOUT_SECONDS|CLICKHOUSE_VERIFY_TIMEOUT_SECONDS|verify_clickhouse_connection' api/experimentation/services.py

Repository: Flagsmith/flagsmith

Length of output: 8967


Use the query timeout for customer event stats.

_connect_customer_clickhouse() shares the 5s CLICKHOUSE_VERIFY_TIMEOUT_SECONDS with verify_clickhouse_connection(), but _get_customer_warehouse_event_stats_cached() uses the same client for full-table count()/uniqExact(event) aggregations. This can make the stats path time out prematurely and return cached unavailable results for 60s; give the stats path the existing 30s CLICKHOUSE_QUERY_TIMEOUT_SECONDS.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
✅ private-cloud · depot-ubuntu-latest-arm-16 — run #18771 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  4 passed

Details

stats  4 tests across 4 suites
duration  38.7 seconds
commit  e96947d
info  🔄 Run: #18771 (attempt 1)

🗂️ Previous results
✅ private-cloud · depot-ubuntu-latest-16 — run #18771 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  32.4 seconds
commit  e96947d
info  🔄 Run: #18771 (attempt 1)

✅ oss · depot-ubuntu-latest-16 — run #18771 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  42.3 seconds
commit  e96947d
info  🔄 Run: #18771 (attempt 1)

@github-actions

Copy link
Copy Markdown
Contributor
✅ oss · depot-ubuntu-latest-arm-16 — run #18771 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  43.9 seconds
commit  e96947d
info  🔄 Run: #18771 (attempt 1)

@github-actions

Copy link
Copy Markdown
Contributor

Visual Regression

19 screenshots compared. See report for details.
View full report

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

Labels

api Issue related to the REST API feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant