fix(appkit): degrade typegen on build-time auth errors instead of failing the build - #561
fix(appkit): degrade typegen on build-time auth errors instead of failing the build#561atilafassina wants to merge 2 commits into
Conversation
…ling the build A deploy build runs `appkit generate-types --wait` (blocking mode). When a per-query or per-metric-view DESCRIBE rejected with anything the connectivity allowlist didn't recognize, it was recorded as a fatal error and thrown before the committed-types gate — so a build-time PERMISSION_DENIED failed the deploy even though generated types were committed. Build-time identity is not the app's runtime on-behalf-of identity, so a build-time permission gap should degrade to committed types, not block a deploy. Add `isAuthError` (HTTP 401/403 and Databricks error_code PERMISSION_DENIED / UNAUTHENTICATED, including a statusless error_code carried as a field or JSON body in the message) and route auth failures through the same degrade path as connectivity in both the query and metric-view DESCRIBE paths. Bad SQL, bad warehouse id (404), and malformed requests (400) stay fatal. A fresh checkout with no committed types still crashes via the has-types gate. Co-authored-by: Isaac <no-reply@databricks.com> Signed-off-by: Atila Fassina <atila@fassina.eu>
📦 Bundle size reportCompared against
|
| dist | raw | gzip |
|---|---|---|
| JS (runtime) | 1.0 MB (+1.7 KB) | 377 KB (+578 B) |
| Type declarations | 385 KB (+283 B) | 136 KB (+127 B) |
| Source maps | 2.1 MB (+3.7 KB) | 705 KB (+1.2 KB) |
| Other | 11 KB | 3.7 KB |
| Total | 3.5 MB (+5.7 KB) | 1.2 MB (+1.9 KB) |
Per-entry composition (own code — deps external (as shipped))
| Entry | Initial (gz) | Lazy (gz) | Total (gz) | node_modules (min) | Own code (min) |
|---|---|---|---|---|---|
. |
95 KB (+117 B) | 2.5 KB | 97 KB (+117 B) | external | 311 KB (+439 B) |
./beta |
75 KB | 457 B | 75 KB | external | 225 KB |
./testing |
17 KB | 0 B | 17 KB | external | 50 KB |
./tsdown |
520 B | 0 B | 520 B | external | 813 B |
./type-generator |
23 KB (+124 B) | 0 B | 23 KB (+124 B) | external | 65 KB (+439 B) |
Chunks:
| Entry | Chunk | Load | Size (gz) |
|---|---|---|---|
. |
index.js |
initial | 91 KB |
. |
utils.js |
initial | 4.0 KB |
. |
remote-tunnel-manager.js |
lazy | 2.5 KB |
./beta |
beta.js |
initial | 59 KB |
./beta |
stream-manager.js |
initial | 5.8 KB |
./beta |
wide-event-emitter.js |
initial | 3.2 KB |
./beta |
databricks.js |
initial | 3.2 KB |
./beta |
configuration.js |
initial | 2.1 KB |
./beta |
service-context.js |
initial | 1.3 KB |
./beta |
client.js |
initial | 434 B |
./beta |
client-options.js |
initial | 220 B |
./beta |
supervisor-api.js |
lazy | 192 B |
./beta |
databricks.js |
lazy | 142 B |
./beta |
index.js |
lazy | 123 B |
./testing |
index.js |
initial | 17 KB |
./tsdown |
index.js |
initial | 520 B |
./type-generator |
index.js |
initial | 23 KB |
@databricks/appkit-ui
npm tarball (packed): 350 KB (+5 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).
| dist | raw | gzip |
|---|---|---|
| JS (runtime) | 395 KB | 132 KB |
| Type declarations | 229 KB | 84 KB (+1 B) |
| Source maps | 766 KB | 253 KB |
| CSS | 16 KB | 3.2 KB |
| Total | 1.4 MB | 473 KB (+1 B) |
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
| Entry | Initial (gz) | Lazy (gz) | Total (gz) | node_modules (min) | Own code (min) |
|---|---|---|---|---|---|
./js |
5.3 KB | 49 KB | 55 KB | 208 KB | 14 KB |
./js/beta |
20 B | 0 B | 20 B | 0 B | 0 B |
./react |
432 KB | 49 KB | 481 KB | 1.3 MB | 177 KB |
./react/beta |
1.0 KB | 0 B | 1.0 KB | 0 B | 1.9 KB |
Chunks:
| Entry | Chunk | Load | Size (gz) |
|---|---|---|---|
./js |
index.js |
initial | 5.2 KB |
./js |
chunk |
initial | 120 B |
./js |
apache-arrow |
lazy | 49 KB |
./js/beta |
beta.js |
initial | 20 B |
./react |
index.js |
initial | 430 KB |
./react |
tslib |
initial | 2.1 KB |
./react |
apache-arrow |
lazy | 49 KB |
./react/beta |
beta.js |
initial | 1.0 KB |
🤖 AppKit PR bot🔬 Run evalsStart an eval for this PR from the evals-monitor app: Go to Evals Monitor → 📦 Try this PR's app templateScaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh run download 33528570528 -R databricks/appkit -n appkit-template-0.69.1-pr.2142851-typegen-issue-561 -D appkit-pr-561 \
&& unzip -o "appkit-pr-561/appkit-template-0.69.1-pr.2142851-typegen-issue-561.zip" -d "appkit-pr-561" \
&& databricks apps init --template "appkit-pr-561"The template pins |
…-list stays fatal Replace the per-class allowlist at the DESCRIBE reject path with a degrade-by-default policy plus an explicit deny-list. A statement that never ran (connectivity, auth/permission, SDK/config, or any unrecognized throw) now degrades so the has-types gate can reuse committed types; only the deny-list of deterministic client errors — HTTP 404 (bad warehouse id) and 400 (malformed request), via classifyBlockingFailure — stays fatal. This makes the reject path symmetric with preflight and stops the whack-a-mole of enumerating every error shape that lacks a status (the deploy PERMISSION_DENIED had no numeric status). Bad SQL remains a ran-and-failed statement and stays fatal via the syntax-error branch. Applies to both the query and metric-view DESCRIBE paths. Tests that asserted 'unrecognized/auth reject -> fatal' are updated to the new intent; fatal-path coverage switches to deny-list (404/400) errors. isAuthError is retained only to label the degrade cause (accurate 'auth blocked' warning for a statusless PERMISSION_DENIED). Co-authored-by: Isaac <no-reply@databricks.com> Signed-off-by: Atila Fassina <atila@fassina.eu>
There was a problem hiding this comment.
Pull request overview
This PR updates AppKit’s type generator to degrade-by-default on build-time DESCRIBE failures (so committed/generated types can still be used) and to keep only a small deny-list (HTTP 400/404) as build-blocking, aligning behavior across query and metric-view typegen.
Changes:
- Switch DESCRIBE rejection handling from allow-list “connectivity only” to deny-list “400/404 only”, degrading all other failures so the committed-types gate can decide.
- Add/adjust tests to cover auth/statusless auth-shaped errors degrading, and deny-list errors remaining fatal.
- Improve logging/cause reporting for degraded DESCRIBEs.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/appkit/src/type-generator/tests/mv-registry.test.ts | Updates metric-view sync transience expectations and adds deny-list/auth/statusless coverage. |
| packages/appkit/src/type-generator/tests/index.test.ts | Adjusts blocking-mode behavior test to use deny-list failure (400) for fatal path. |
| packages/appkit/src/type-generator/tests/generate-queries.test.ts | Updates query DESCRIBE tests to reflect degrade-by-default and deny-list fatal handling. |
| packages/appkit/src/type-generator/tests/errors.test.ts | Adds tests for auth detection and statusless error_code auth shapes. |
| packages/appkit/src/type-generator/query-registry.ts | Changes rejected DESCRIBE handling to degrade-by-default with deny-list fatal classification and improved logging. |
| packages/appkit/src/type-generator/mv-registry/types.ts | Updates MetricSyncFailure.transient doc to reflect “degrade vs fail-build” meaning. |
| packages/appkit/src/type-generator/mv-registry/sync.ts | Uses classifyBlockingFailure to implement deny-list logic for metric-view DESCRIBE failures. |
| packages/appkit/src/type-generator/errors.ts | Introduces isAuthError and refactors environmental cause labeling to recognize statusless auth error codes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // The DESCRIBE never ran (fetcher threw). Degrade by default (connectivity, | ||
| // auth/permission, SDK/config) so the has-types gate can reuse committed | ||
| // types; only the deny-list of deterministic client errors (bad warehouse | ||
| // id 404, malformed request 400) surfaces as fatal — the same split the | ||
| // query path and preflight make. (Truncated/multi-chunk and zero-column | ||
| // responses are ran-and-failed, handled below and kept non-transient.) |
| if (mode === "blocking") { | ||
| hadEnvironmentalFailure = true; | ||
| environmentalCause = environmentalCause ?? "unreachable"; | ||
| environmentalCause = | ||
| environmentalCause ?? classifyEnvironmentalCause(entry.reason); | ||
| } |
| // (connectivity, auth/permission, SDK/config, and unrecognized throws) — the | ||
| // has-types gate reuses committed types. Only the deny-list of deterministic | ||
| // client errors (bad warehouse id 404, malformed request 400) and ran-and-failed | ||
| // responses (FAILED, zero rows, unparseable payload, zero columns, the | ||
| // truncation guard) are non-transient and surface as a build failure — the same | ||
| // split the query path and preflight make. |
This PR shifts the behavior from "fail-first" to "degrade-first".
Before, we enumerated connection errors and would only allow the build if the error matched our allowlist, otherwise - even with available types, build would consider the warehouse was unreacheable and/or error was not transient.
This PR allows build to continues except for
404and400will cause a fatal error and thus block the build, other status codes will atempt the build if types are present on the source artifact.This makes the reject path symmetric with preflight and immune to the next statusless error class.
extra context:
Problem
When deploying an AppKit app, the build runs
appkit generate-types --wait(blocking mode) in aprebuildhook. If the build-time identity can't execute aDESCRIBEagainst the SQL warehouse (e.g.PERMISSION_DENIED), the failure was recorded as a fatal error and thrown before the committed-types gate — so the deploy build failed even though generated types were already committed to the repo.Root cause: the per-query (
query-registry.ts) and per-metric-view (mv-registry/sync.ts) DESCRIBE-reject paths only degraded on a hard-coded connectivity allowlist. Everything else — including auth — became afatalErrorsentry thrown atindex.ts, bypassing the committed-types fallback. This was asymmetric with the preflight path, which already routes non-404/400failures to degrade. The observed error also carried no numeric HTTP status ({ error_code: "PERMISSION_DENIED", message: <hash> }), so a status/allowlist-based classifier couldn't identify it at all.Fix: degrade-by-default, with a deny-list
Instead of enumerating which error classes are allowed to degrade (a losing game — the next SDK error shape without a status reopens the hole), the DESCRIBE reject path now degrades by default and keeps an explicit deny-list of what stays fatal.
The distinction that actually matters at DESCRIBE:
404(bad/typo'd warehouse id) and400(malformed request), viaclassifyBlockingFailure→ stay fatal, so real config errors surface.