Skip to content

test(data): ClickHouse data-presence / dedup / freshness audit - #1335

Closed
SharedQA wants to merge 10 commits into
constructorfabric:mainfrom
SharedQA:claude/data-presence-audit
Closed

test(data): ClickHouse data-presence / dedup / freshness audit#1335
SharedQA wants to merge 10 commits into
constructorfabric:mainfrom
SharedQA:claude/data-presence-audit

Conversation

@SharedQA

@SharedQA SharedQA commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Adds scripts/ci/data_presence_audit.py — a warehouse-level check for the properties unit and PR tests cannot observe: whether the fetch wrote data, whether it is deduplicated, and whether it is fresh.

Checks

  • Presence — every connected bronze source DB has rows; every silver.class_* metric table is non-empty. Catches wired-but-empty sources and silver models that produced nothing (blank dashboards with no error).
  • Dedup — every silver ReplacingMergeTree table returns the same count() with and without FINAL. A mismatch means un-merged duplicates are present, which over-count for any reader without FINAL and drift as background merges run.
  • Freshness — newest row per populated table within --max-age-hours (default 48).

Transports

  • Local/kind: CH_EXEC_POD=insight-clickhouse-0 python3 scripts/ci/data_presence_audit.py (kubectl exec, no credentials).
  • CI / deployed: CH_HOST / CH_PORT / CH_USER / CH_PASSWORD over the ClickHouse HTTP interface.

--check exits non-zero on duplicates by default; --fail-on-empty and --fail-on-stale are opt-in; --waive-empty lists known-empty sources.

Live run against kind-insight (2026-06-14)

result
bronze sources with data 3 of 8 (m365, claude_team, bamboohr); cursor/slack/zoom/jira/bitbucket_cloud empty
silver metric tables empty 15 of 23
stale (>48h) 6
duplicated 0 (currently merged-clean)

Documents #1334; complements #1319 (empty connectors), #1330 (dedup), #1331 (freshness/time). Intended to run nightly against the deployed environment and as a gate on the production sync DAG.

Summary by CodeRabbit

  • Tests

    • Added comprehensive UI render contract tests to ensure dashboard tiles display values accurately, including formatting, rounding, and data ingestion validation.
  • Chores

    • Enhanced CI/CD pipeline with automated data quality gates to verify connector silver-class coverage and validate data presence and freshness in warehouses.

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@SharedQA, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 56 minutes and 12 seconds. Learn how PR review limits work.

To continue reviewing without waiting, enable usage-based billing in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e5754e8e-4632-4e19-887f-6a47be1b55e0

📥 Commits

Reviewing files that changed from the base of the PR and between cfd1545 and b239bf0.

📒 Files selected for processing (7)
  • .github/workflows/data-quality.yml
  • scripts/ci/data_presence_audit.py
  • scripts/ci/dbt_coverage.py
  • scripts/ci/nullable_key_audit.py
  • scripts/ci/test_nullable_key_audit.py
  • tests/ui_render_contract/README.md
  • tests/ui_render_contract/render_contract.py
📝 Walkthrough

Walkthrough

Adds two independent additions: (1) a new data-checks GitHub Actions workflow with static connector-to-silver-coverage scanning and a live ClickHouse data-presence audit script; (2) a tests/ui_render_contract suite with a pure display_value/round_half_up render-contract module, unit tests, and an env-gated Playwright e2e test validating dashboard tile text against API KPI values.

Changes

CI Data Quality Gates

Layer / File(s) Summary
Connector silver-coverage script and CI job
scripts/ci/connector_silver_coverage.py, .github/workflows/data-checks.yml
connector_silver_coverage.py statically scans dbt SQL config(tags=[...]) to detect connectors that reached bronze (__bronze_promoted.sql) but have no silver:class_* union tag, exiting 1 on unwaived "stranded" connectors. The workflow runs this check on PRs to main touching ingestion/CI paths, with a fixed --waive list for currently-accepted bronze-only connectors.
ClickHouse data-presence audit script and CI job
scripts/ci/data_presence_audit.py, .github/workflows/data-checks.yml
data_presence_audit.py queries ClickHouse via kubectl exec or HTTP to audit bronze row presence, silver dedup (ReplacingMergeTree FINAL), freshness via _version timestamp, and gold insight.* view resolution. CLI flags control which violation types gate the exit code; --skip-if-unreachable exits 0 cleanly on connectivity failure. The workflow runs this only on non-PR events (nightly/dispatch) with a ClickHouse service container.

UI Render Contract Test Suite

Layer / File(s) Summary
Render-contract module and unit tests
tests/ui_render_contract/render_contract.py, tests/ui_render_contract/conftest.py, tests/ui_render_contract/test_render_contract.py
render_contract.py defines NO_DATA/COMING_SOON constants, round_half_up (half-away-from-zero), and display_value (pure function applying ingestion gating, null handling, percent/integer/unit formatting). Unit tests assert rounding, not-ingested, null, unit-space, and real-zero display rules. conftest.py prepends the directory to sys.path for pytest collection.
Playwright e2e tests and README
tests/ui_render_contract/test_live_render_e2e.py, tests/ui_render_contract/README.md
Env-gated (INSIGHT_BASE_URL/INSIGHT_STORAGE_STATE) Playwright suite captures KPI API responses from POST /metrics/queries, then asserts each dashboard tile's rendered text contains the display_value()-computed expected string. A second test uses a regex to prevent numeric values glued to unit tokens. Known null/un-ingested failures are marked xfail. README documents auth setup and suite structure.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • constructorfabric/insight#1282: The new connector silver-coverage CI gate will statically scan the ChatGPT Team connector's dbt models (including chatgpt_team__bronze_promoted and associated silver:class_* usage) introduced in that PR.
  • constructorfabric/insight#1306: The new connector_silver_coverage.py will scan the Figma connector's figma__bronze_promoted.sql and its dbt models for required silver:class_* tags introduced in that PR.

Suggested reviewers

  • mitasovr
  • cyberantonz

Poem

🐇 Hoppin' through the pipeline, ears perked up with care,
A silver tag is missing? The rabbit finds it there!
ClickHouse rows are counted, the dashboard tiles align,
display_value rounds the numbers — half-up, every time!
No zero glued to "tasks", no null slips through unseen,
The contract holds its promise, and the CI gates stay green! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'test(data): ClickHouse data-presence / dedup / freshness audit' accurately describes the main additions of this PR—new data audit capabilities for ClickHouse that validate presence, deduplication, and freshness.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@SharedQA

Copy link
Copy Markdown
Contributor Author

Clarifying where this runs, since it reads like a per-PR test but isn't one — pushed as docstring + workflow comments in 48e528a.

It's a live-warehouse monitor. Every property here is true by construction on a freshly seeded e2e/CI database (data is always present, deduped, and "now"), so the checks only earn their keep against the real accumulated warehouse. Two distinct invocations:

  • PR CI (deterministic, blocking): resolution + dedup only (--check). These hold on any populated schema, so they catch gold↔silver drift and un-merged duplicates regardless of dataset — but they need a DB that has actually been dbt build-ed. Against a blank ClickHouse they pass vacuously.
  • Nightly against deployed + post-sync DAG gate: add --fail-on-empty --fail-on-stale. "A connected source wrote 0 rows" and "newest row older than the SLA" only mean "the real sync stopped" against live data, never in seeded CI. That's why those two flags are opt-in.

One thing to flag on the current data-checks.yml: the warehouse job runs against a blank clickhouse-server service with the dbt build step still a TODO stub, so on an empty DB it passes proving nothing, and the job is continue-on-error. It becomes a real gate only once dbt populates the service container — now called out inline in the workflow.

SharedQA added 3 commits June 15, 2026 18:45
Checks the warehouse properties unit and PR tests cannot see:
- presence: every connected bronze source has rows, every silver class_* metric
  is non-empty (catches wired-but-empty sources / silent blank dashboards);
- dedup: silver ReplacingMergeTree count() == count() FINAL (un-merged
  duplicates → over-count that drifts as merges run);
- freshness: newest row per populated table within --max-age-hours.

Stdlib only. Two transports: kubectl exec for a local/kind cluster (no creds),
or the ClickHouse HTTP interface via CH_HOST/CH_PORT/CH_USER/CH_PASSWORD for CI
against a deployed environment. --check gates on duplicates by default;
--fail-on-empty / --fail-on-stale opt-in; --waive-empty lists known-empty sources.

Live run against kind-insight (2026-06-14): 5 empty bronze sources, 15 empty
silver metric tables, 6 stale (>48h), 0 duplicated. See constructorfabric#1334.

Signed-off-by: Kenan Salim <kenan.salim@rolos.com>
…render-contract

- data_presence_audit.py: add RESOLUTION check — every insight.* gold view must
  resolve (SELECT … LIMIT 0) against the live silver schema. Catches gold↔silver
  drift where a view selects a silver column the deployed schema lacks and the
  dashboard section serves a 500 rendered as a blank "No data".
- connector_silver_coverage.py: fail when a connector reaches bronze but is tagged
  into no silver:class_* union (stranded — ingests data no dashboard can read).
  Currently flags task-tracking/youtrack and ai/openai.
- tests/ui_render_contract: the render contract (displayed == documented transform
  of the API value) as a pure spec + unit tests (rounding ownership, ComingSoon,
  null→no-data, unit spacing), plus an env-gated Playwright e2e asserting the live
  DOM matches it.

Signed-off-by: Kenan Salim <kenan.salim@rolos.com>
…eployed)

The presence/dedup/freshness/resolution checks only earn their keep against the
real accumulated warehouse — every one is true by construction on a seeded e2e/CI
DB. Make the two invocations explicit:
  - PR CI: resolution + dedup only (structural; hold on any populated schema),
    and only after dbt actually builds into the service container.
  - nightly vs deployed: add --fail-on-empty / --fail-on-stale (presence and
    freshness only mean 'the real sync stopped' against live data).

No behaviour change — docstring + workflow comment only. Addresses reviewer
questions on constructorfabric#1335.

Signed-off-by: Kenan Salim <kenan.salim@rolos.com>
@SharedQA
SharedQA force-pushed the claude/data-presence-audit branch from 48e528a to 1f75ff0 Compare June 15, 2026 15:47
@SharedQA
SharedQA marked this pull request as ready for review June 15, 2026 16:39
@SharedQA
SharedQA requested a review from a team as a code owner June 22, 2026 19:36
SharedQA and others added 2 commits June 22, 2026 22:41
connector-silver-coverage is blocking and correctly flags openai, youtrack,
and figma as stranded (bronze-only, no silver:class_* consumer). These are
known, accepted gaps, so waive them via the documented --waive ratchet: the
gate still blocks any NEW stranding while letting this land. Each waiver is
annotated; drop a name once its silver union exists.

Signed-off-by: SharedQA <122366558+SharedQA@users.noreply.github.com>
…unwired

The dbt-and-warehouse-gates job was red on every PR (ClickHouse 403 against an
empty service container) and merely tolerated via continue-on-error. A red check
we ship anyway is exactly the rot a QA gate should prevent.

These gates are meaningful only against a POPULATED warehouse, so:
- run them on the nightly schedule / manual dispatch, not on PRs
  (if: github.event_name != 'pull_request'); PRs no longer show the job;
- drop continue-on-error — the job now passes or fails honestly;
- add data_presence_audit.py --skip-if-unreachable: while the warehouse is
  unwired it exits 0 with a labelled SKIPPED line (not a fake pass, not an
  ignored red). Drop the flag once the dbt step builds into the container.

connector-silver-coverage (the static, day-one gate) is unchanged and still
blocks PRs.

Signed-off-by: Kenan Salim <ks@constructor.tech>

@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: 8

🧹 Nitpick comments (1)
tests/ui_render_contract/render_contract.py (1)

36-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove redundant int() casts in round_half_up.

math.floor() / math.ceil() already return integers, so the extra casts only add lint noise.

♻️ Suggested diff
 def round_half_up(value: float) -> int:
     """Round to nearest integer, halves away from zero (4.5→5, -4.5→-5, 4.4→4)."""
-    return int(math.floor(value + 0.5)) if value >= 0 else int(math.ceil(value - 0.5))
+    return math.floor(value + 0.5) if value >= 0 else math.ceil(value - 0.5)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ui_render_contract/render_contract.py` at line 36, The round_half_up
function contains redundant int() casts around the return values of math.floor()
and math.ceil(), which already return integers. Remove both int() casts from the
return statement so that math.floor(value + 0.5) and math.ceil(value - 0.5) are
returned directly without the unnecessary int() wrapper.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/data-checks.yml:
- Around line 1-20: The workflow file lacks explicit permission declarations and
relies on default GITHUB_TOKEN scopes. Add a top-level `permissions:` block
immediately after the `on:` section in the data-checks workflow. Start with the
least-privilege approach by setting `contents: read` as the base permission,
then review what each job actually requires (such as pull request access,
workflow triggering capabilities, or other specific scopes) and add only those
additional permissions where strictly necessary rather than granting broad
default permissions.
- Around line 56-84: The dbt build step in the "dbt build + tests + contract
coverage (changed models)" step is commented out as a TODO, causing the
subsequent "Gold-view resolution + data presence" audit to run against an empty
ClickHouse schema, which produces false-passing results. To fix this, uncomment
the dbt commands (dbt deps, dbt build --select state:modified+, and the
dbt_coverage.py script) in the first step, update the profiles.yml to point the
dbt profile at the local ClickHouse service on host 127.0.0.1:8123, and then
remove the --skip-if-unreachable flag from the data_presence_audit.py command in
the "Gold-view resolution + data presence" step since the container will now be
properly populated with dbt-built views and tables to validate.
- Around line 21-24: Replace the floating version tags with full commit SHAs for
security hardening. Update actions/checkout@v4 to use its full commit SHA and
actions/setup-python@v5 to use its full commit SHA. Apply this same change
pattern to the additional occurrences of these actions mentioned at lines 52-55
to ensure all GitHub Actions references are pinned to specific commit SHAs
rather than mutable version tags.

In `@scripts/ci/data_presence_audit.py`:
- Around line 171-179: The skip condition in the exception handler for the
ClickHouse connection check only skips when a connection exception occurs, but
does not handle the case where ClickHouse is reachable but the warehouse is
unwired (empty/no data). Modify the logic to also skip when
args.skip_if_unreachable is true AND the warehouse has no data populated, not
just when a connection exception is raised. This ensures that the script returns
SKIPPED for both unreachable and unwired warehouses, preventing false PASSED
results that mask an unwired gate. Apply the same fix to the similar logic
mentioned at lines 235-253.
- Around line 87-97: The code constructs HTTP URLs while sending credentials via
the X-ClickHouse-Key header, which exposes sensitive information over cleartext.
Modify the URL scheme construction in the urllib.request.Request call to use
HTTPS instead of HTTP, either by default or conditionally when CH_PASSWORD is
non-empty. Change the protocol in the f-string that builds the request URL from
http:// to https:// based on whether credentials are being used.

In `@tests/ui_render_contract/README.md`:
- Around line 15-17: The markdown file contains fenced code blocks that are
missing language identifiers, which violates markdownlint rule MD040. Add the
bash language identifier to both code blocks: the pytest command block at lines
15-17 and the playwright/pytest command block at lines 27-34. For each code
block, change the opening fence from triple backticks to triple backticks
followed by the word bash (e.g., ```bash instead of ```).

In `@tests/ui_render_contract/test_live_render_e2e.py`:
- Around line 84-86: The KPI data extraction in the loop iterating over
body.get("results", []) is accessing items from the wrong location. Instead of
checking r.get("status") and accessing r["items"] directly, the code should
access the items through the response field according to the batch API contract
structure. Change the condition and access pattern to look for r.get("response")
and extract items from r.get("response").get("items") to correctly parse the
nested KPI response data and avoid leaving captured empty.
- Around line 113-115: The pytest.xfail() call marking the known-bug case for
constructorfabric/insight#1337 lacks proper strict configuration, meaning if the
underlying bug is fixed, the test will unexpectedly pass (XPASS) without failing
the build. Add xfail_strict = true to the pytest configuration file (typically
pytest.ini, setup.cfg, pyproject.toml, or tox.ini in the repository root) to
ensure that unexpected passes from pytest.xfail() calls are treated as test
failures and properly alert the team when the referenced bug is actually
resolved.

---

Nitpick comments:
In `@tests/ui_render_contract/render_contract.py`:
- Line 36: The round_half_up function contains redundant int() casts around the
return values of math.floor() and math.ceil(), which already return integers.
Remove both int() casts from the return statement so that math.floor(value +
0.5) and math.ceil(value - 0.5) are returned directly without the unnecessary
int() wrapper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 59bcbf35-6e19-4978-8493-437b6aeaff73

📥 Commits

Reviewing files that changed from the base of the PR and between 059f8f5 and cfd1545.

📒 Files selected for processing (8)
  • .github/workflows/data-checks.yml
  • scripts/ci/connector_silver_coverage.py
  • scripts/ci/data_presence_audit.py
  • tests/ui_render_contract/README.md
  • tests/ui_render_contract/conftest.py
  • tests/ui_render_contract/render_contract.py
  • tests/ui_render_contract/test_live_render_e2e.py
  • tests/ui_render_contract/test_render_contract.py

Comment thread .github/workflows/data-checks.yml Outdated
Comment thread .github/workflows/data-checks.yml Outdated
Comment thread .github/workflows/data-checks.yml Outdated
Comment thread scripts/ci/data_presence_audit.py
Comment thread scripts/ci/data_presence_audit.py
Comment thread tests/ui_render_contract/README.md Outdated
Comment thread tests/ui_render_contract/test_live_render_e2e.py
Comment thread tests/ui_render_contract/test_live_render_e2e.py
Kenan Salim and others added 3 commits June 23, 2026 09:43
…s green

- data-checks.yml: add top-level 'permissions: contents: read' and pin
  actions/checkout + actions/setup-python to commit SHAs (matches the repo's
  other workflows; supply-chain hygiene).
- data_presence_audit.py: --skip-if-unreachable now also skips when ClickHouse
  is REACHABLE but unpopulated (no bronze_* / silver.class_* tables). A blank
  warehouse would otherwise pass every resolution/dedup check vacuously — a
  false green worse than a red. Now it skips honestly until dbt populates it.
- data_presence_audit.py: never send credentials over cleartext — default to
  https whenever CH_PASSWORD is set (plain http only for the password-less
  local/CI container); CH_SCHEME overrides.
- README: add bash language to fenced blocks (MD040).

Signed-off-by: Kenan Salim <ks@constructor.tech>
…g a TODO

The dbt-and-warehouse-gates job never did anything real: its dbt step was an
'echo' TODO and the audit ran against an empty throwaway ClickHouse, where every
resolution/dedup/presence check passes by construction. A gate that can't fail is
worse than no gate. If it's useless, it shouldn't exist — so remove it.

- data-checks.yml: drop the dbt-and-warehouse-gates job, the schedule trigger, and
  the TODO stub entirely. Keep connector-silver-coverage (a real, warehouse-free
  gate) and document where the warehouse audit actually belongs.
- data_presence_audit.py: drop the --skip-if-unreachable flag + reachable-but-empty
  skip (they only existed to make the vacuous gate 'honest'). The tool runs against
  a real populated warehouse (e2e rig / deployed), so an unreachable warehouse is a
  hard failure again. Keep the https-when-credentialed hardening. Docstring now
  points resolution/dedup at the e2e rig (which dbt-builds), not a standalone job.
- render_contract.py: drop redundant int() casts (math.floor/ceil already return int).

The test_live_render_e2e.py 'use response.items' review note is a false positive:
BatchQueryResult is #[serde(tag="status")] with #[serde(flatten)] response, so
status/items are top-level (verified in domain/query.rs) — the test is correct.

Signed-off-by: Kenan Salim <ks@constructor.tech>
…lity.yml

data-checks / data-integrity / data-contracts all triggered on the same
src/ingestion/** PRs — three workflows, three checkouts, three Python setups.
Fold them into one data-quality.yml with parallel jobs:
  - connector-silver-coverage (blocking)
  - nullable-key-guard        (blocking; absorbs constructorfabric#1348's nullable_key_audit + self-test)
  - dbt-coverage              (report-only; absorbs constructorfabric#1320's dbt_coverage)
Scripts moved into this PR; constructorfabric#1348 is closed as absorbed and constructorfabric#1320 slims to its
m365 type-mismatch fix. SHA-pinned actions, persist-credentials:false throughout.

Signed-off-by: SharedQA <122366558+SharedQA@users.noreply.github.com>
SharedQA added a commit to SharedQA/insight that referenced this pull request Jun 23, 2026
…constructorfabric#1335)

Drop data-contracts.yml + dbt_coverage.py from this PR — they now live in the
single data-quality.yml on constructorfabric#1335 (one data-gate workflow instead of three).
This PR keeps only its substance: the m365 collab_document type-mismatch fix
(constructorfabric#1318) + the silver collaboration contract.

Signed-off-by: SharedQA <122366558+SharedQA@users.noreply.github.com>
@ktursunov

Copy link
Copy Markdown
Contributor

#1744 - must reimplement

@ktursunov ktursunov closed this Jul 16, 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.

2 participants