Skip to content

fix(integrations): coerce ClickHouse UInt64 counts in Cloudflare usage (fixes BYO-CH 500)#168

Open
Makisuo wants to merge 1 commit into
mainfrom
fix/cloudflare-usage-uint64-coercion
Open

fix(integrations): coerce ClickHouse UInt64 counts in Cloudflare usage (fixes BYO-CH 500)#168
Makisuo wants to merge 1 commit into
mainfrom
fix/cloudflare-usage-uint64-coercion

Conversation

@Makisuo

@Makisuo Makisuo commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Problem

GET /api/integrations/cloudflare/usage returned a bare, message-less 500 for BYO-ClickHouse orgs (the integration sparklines silently degraded to the poller-only view — "works for Tinybird, empty for BYO").

Root cause

Raw ClickHouse FORMAT JSON serializes 64-bit ints as JSON strings. The usage query's datapoints field is count() (a UInt64), so ClickHouse returns "2", while Tinybird returns a number. Once readiness-aware routing began sending write-ready BYO-CH orgs to their own ClickHouse, getUsage received datapoints: "2" and passed it straight into the CloudflareUsageBucket Schema.Class (datapoints: Schema.Number). The ParseError was thrown as an unhandled defect, so the HTTP layer surfaced it as a 500 with no body.

Diagnosed from the CloudflareAnalyticsService.getUsage span StatusMessage:

Expected number, got "2"
  at ["datapoints"]

The span also confirms the CH query returned rows — so those BYO-CH orgs are ready, their metrics are in ClickHouse, and the decode was the only thing left breaking.

Fix

Coerce datapoints / requests with Number(...) at the handler edge (the established CH-UInt64-as-string convention), so a raw-CH response can't throw inside the response Schema.Class. Adds a regression test that feeds string-encoded counts exactly as raw ClickHouse does (fails without the fix, passes with it).

Testing

  • CloudflareAnalyticsService suite: 20 passed (incl. the new coercion guard)
  • bun typecheck: clean

Builds on the readiness-aware routing already merged to main.

🤖 Generated with Claude Code


Open in Devin Review

The Cloudflare integration usage endpoint (`GET /api/integrations/cloudflare/usage`)
returned a bare, message-less 500 for BYO-ClickHouse orgs.

Raw ClickHouse `FORMAT JSON` serializes 64-bit ints (here `count()` -> the
`datapoints` field, a UInt64) as JSON strings, whereas Tinybird returns them as
numbers. Once readiness-aware routing began sending write-ready BYO-CH orgs to
their own ClickHouse, `getUsage` received `datapoints: "2"` and passed it straight
into the `CloudflareUsageBucket` Schema.Class, whose `datapoints` field is
`Schema.Number`. The resulting ParseError was thrown as an unhandled defect, so
the HTTP layer surfaced it as a 500 with no body (diagnosed via the
`CloudflareAnalyticsService.getUsage` span StatusMessage:
`Expected number, got "2" at ["datapoints"]`).

Coerce `datapoints`/`requests` with `Number(...)` at the handler edge (the
established CH-UInt64-as-string convention) so a raw-CH response can't throw
inside the response Schema.Class. Adds a regression test feeding string-encoded
counts exactly as raw ClickHouse does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pullfrog

pullfrog Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Your LLM provider API key was rejected. Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.

Update repo secret → · Model settings → · Setup docs → · Ask in Discord →

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +1483 to +1484
const requests = Math.round(Number(row.requests))
const datapoints = Number(row.datapoints)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Compiled query outputs lack runtime schema validation — string coercion is a point fix

The compileCH function at lib/clickhouse-builder/src/ch/compile.ts:234 calls makeCompiledQuery<Output>(sql) without passing a rowSchema, so decodeRows at lib/clickhouse-builder/src/ch/compile.ts:76 just casts rows as Output without any runtime validation or coercion. This means the CloudflareUsageOutput interface (packages/query-engine/src/ch/queries/cloudflare-usage.ts:25-33) declaring requests: number and datapoints: number is purely a compile-time contract — at runtime, ClickHouse FORMAT JSON can return UInt64/count() values as strings. This PR correctly adds Number() coercion at the handler edge for this specific query, but any other compiledQuery consumer reading from a BYO-CH org could hit the same string-vs-number mismatch silently. A systematic fix (e.g., passing a row schema to compileCH or adding coercion in the decode layer) would prevent future occurrences.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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