fix(integrations): coerce ClickHouse UInt64 counts in Cloudflare usage (fixes BYO-CH 500)#168
fix(integrations): coerce ClickHouse UInt64 counts in Cloudflare usage (fixes BYO-CH 500)#168Makisuo wants to merge 1 commit into
Conversation
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>
|
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 →
|
| const requests = Math.round(Number(row.requests)) | ||
| const datapoints = Number(row.datapoints) |
There was a problem hiding this comment.
🚩 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.
Was this helpful? React with 👍 or 👎 to provide feedback.

Problem
GET /api/integrations/cloudflare/usagereturned 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 JSONserializes 64-bit ints as JSON strings. The usage query'sdatapointsfield iscount()(aUInt64), so ClickHouse returns"2", while Tinybird returns a number. Once readiness-aware routing began sending write-ready BYO-CH orgs to their own ClickHouse,getUsagereceiveddatapoints: "2"and passed it straight into theCloudflareUsageBucketSchema.Class(datapoints: Schema.Number). TheParseErrorwas thrown as an unhandled defect, so the HTTP layer surfaced it as a 500 with no body.Diagnosed from the
CloudflareAnalyticsService.getUsagespanStatusMessage: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/requestswithNumber(...)at the handler edge (the established CH-UInt64-as-string convention), so a raw-CH response can't throw inside the responseSchema.Class. Adds a regression test that feeds string-encoded counts exactly as raw ClickHouse does (fails without the fix, passes with it).Testing
CloudflareAnalyticsServicesuite: 20 passed (incl. the new coercion guard)bun typecheck: cleanBuilds on the readiness-aware routing already merged to
main.🤖 Generated with Claude Code