fix(plausible): register a team so v3 ingests events; widen post-restore health wait
continuous-integration/drone/push Build is failing

plausible v3 (community-edition) only ingests events for a site that belongs to
a TEAM. The custom tier registered a site row and nothing else, which was enough
for v2 — under v3 the POST still acks 202 and the row still exists in postgres,
but every event is discarded. ClickHouse records the reason itself in
ingest_counters as dropped_not_found, and events_v2 stays empty, so it presents
as a silent ingestion stall.

Verified on cc-ci against v3.2.1: identical site row with no team ->
dropped_not_found and 0 rows; with a team linked -> buffered and the rows land.

_register_site now provisions a team and links the site, guarded on the schema
actually having teams so it stays a no-op on v2 (the upgrade tier deploys the
older base first).

Separately, the custom health check waited 60s for /api/health. That tier runs
after backup/restore, which disrupts postgres under the app and restarts it, and
v3 boots through sleep 10 + createdb + migrate + cache warmers before health
flips to 200. Widened to 300s, still far inside the recipe HTTP_TIMEOUT of 1200.
The assertion is unchanged: a hard 200 from the real readiness endpoint.

Neither change weakens a test - the event tests still require the row to arrive
in ClickHouse and match what was sent.
This commit is contained in:
cc-ci
2026-08-11 05:18:18 +00:00
parent 0a229ac016
commit eb1d6d9161
2 changed files with 42 additions and 4 deletions
+35 -3
View File
@@ -14,7 +14,10 @@ Both assert real app state (the event reached the analytics store), not just the
plausible only ingests events for *known* sites — the in-memory `sites_cache` gates ingestion and plausible only ingests events for *known* sites — the in-memory `sites_cache` gates ingestion and
drops events for unregistered domains (empirically confirmed: an event for an unregistered domain drops events for unregistered domains (empirically confirmed: an event for an unregistered domain
never appears in events_v2). So each test first registers a site row in the metadata postgres, then never appears in events_v2). From v3 (community-edition) a site is only "known" once it belongs to a
TEAM; a teamless site is dropped as `dropped_not_found` while the POST still acks 202, so the failure
looks like a silent ingestion stall. `_register_site` therefore provisions a team as well when the
schema has one. So each test first registers a site row in the metadata postgres, then
POSTs repeatedly while polling ClickHouse: the sites_cache must refresh to admit the new site and the POSTs repeatedly while polling ClickHouse: the sites_cache must refresh to admit the new site and the
event write-buffer must flush to ClickHouse, so the first landing is not instantaneous. Re-POSTing the event write-buffer must flush to ClickHouse, so the first landing is not instantaneous. Re-POSTing the
same event is safe — we assert the row count is >= 1. same event is safe — we assert the row count is >= 1.
@@ -51,11 +54,40 @@ def _ch(domain: str, sql: str) -> str:
def _register_site(domain: str, site: str) -> None: def _register_site(domain: str, site: str) -> None:
"""Insert a site row into the metadata postgres (`db` service) so plausible will ingest events for """Register `site` in the metadata postgres so plausible will ingest events for it.
it. Idempotent (ON CONFLICT DO NOTHING)."""
Idempotent. Works against BOTH schema generations, because the upgrade tier deploys an older
base version before upgrading:
* v2 (`plausible/analytics`) — a row in `sites` is sufficient.
* v3 (`ghcr.io/plausible/community-edition`) — sites belong to a TEAM, and ingestion drops
events for a site whose team is missing. The POST still acks 202 and the row still exists in
postgres, so the only visible symptom is that nothing ever reaches ClickHouse; the reason is
recorded in ClickHouse's own `ingest_counters` as `dropped_not_found`. Verified on cc-ci
against v3.2.1: identical site row, no team → `dropped_not_found`; with a team linked →
`buffered` and the row appears in `events_v2`.
The team block is guarded on the schema actually having teams, so this stays a no-op on v2
rather than branching on a version string.
"""
sql = ( sql = (
"INSERT INTO sites (domain, timezone, inserted_at, updated_at, native_stats_start_at) " "INSERT INTO sites (domain, timezone, inserted_at, updated_at, native_stats_start_at) "
f"VALUES ('{site}','UTC', now(), now(), now()) ON CONFLICT (domain) DO NOTHING; " f"VALUES ('{site}','UTC', now(), now(), now()) ON CONFLICT (domain) DO NOTHING; "
"DO $ccci$ "
"BEGIN "
" IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'teams') "
" AND EXISTS (SELECT 1 FROM information_schema.columns "
" WHERE table_name = 'sites' AND column_name = 'team_id') THEN "
" INSERT INTO teams (name, inserted_at, updated_at, accept_traffic_until, setup_complete) "
" SELECT 'cc-ci', now(), now(), now() + interval '365 days', true "
" WHERE NOT EXISTS (SELECT 1 FROM teams WHERE name = 'cc-ci'); "
" UPDATE sites "
" SET team_id = COALESCE(team_id, (SELECT id FROM teams WHERE name = 'cc-ci' LIMIT 1)), "
" accept_traffic_until = COALESCE(accept_traffic_until, now() + interval '365 days') "
f" WHERE domain = '{site}'; "
" END IF; "
"END "
"$ccci$; "
f"SELECT domain FROM sites WHERE domain = '{site}';" f"SELECT domain FROM sites WHERE domain = '{site}';"
) )
out = lifecycle.exec_in_app( out = lifecycle.exec_in_app(
+7 -1
View File
@@ -17,6 +17,12 @@ def test_plausible_root_serves(live_app):
62-char SECRET_KEY_BASE, see recipe_meta.EXTRA_ENV); the dedicated 62-char SECRET_KEY_BASE, see recipe_meta.EXTRA_ENV); the dedicated
/api/health endpoint is. /api/health endpoint is.
""" """
# The custom tier runs AFTER the backup/restore tier, which disrupts postgres under the app and
# restarts it. v3 (community-edition) then boots through `sleep 10` + `db createdb` + `db migrate`
# + cache warmers before /api/health flips to 200, which does not fit in 60s — that is what put
# this recipe RED on build 1224 while install/upgrade/backup/restore all passed. The assertion is
# unchanged (still a hard 200 from the real readiness endpoint); only the wait matches the boot
# profile the recipe already declares via recipe_meta.HTTP_TIMEOUT (1200).
url = f"https://{live_app}/api/health" url = f"https://{live_app}/api/health"
status, _ = harness_http.retry_http_get(url, expect_status=(200,), max_wait=60, interval=3) status, _ = harness_http.retry_http_get(url, expect_status=(200,), max_wait=300, interval=5)
assert status == 200, f"GET {url} HTTP {status}" assert status == 200, f"GET {url} HTTP {status}"