Repository navigation
feat(functions-compiler): every build records a returned 5xx as one _b44_error line - #678
Conversation
`sourceMap: true` on `bundle`/`bundleApp` returns `map` beside `module`: an esbuild map of the final compile (not the per-function probes) with no sourcesContent and no sourceRoot. The module keeps the trailing `//# sourceMappingURL=_bundled.mjs.map` comment Cloudflare reads to remap stack traces, and otherwise matches the option-off build byte for byte. The map's sources name the request's files: per-app `fn_<index>/` is stripped and checked against that function's files, and a function's entry becomes its optional `entryPath` (its real stored path). Every other source stays in place, so the mappings' indices hold, behind a `base44-internal:` prefix. Off by default; the public CLI never sets it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`errorRecords: true` on `bundle`/`bundleApp` bakes a recorder into both
generated entries. It is inert unless the request carries
`x-b44-feed-context`. For a handler `Response` with status >= 500 it
returns that same response at once and, on a clone inside
ctx.waitUntil, logs one
`{ _b44_error: { v: 1, kind: "response", status, name?, message, stack? } }`
through the console patch.
The message comes from an Error logged during the request whose message
the JSON body repeats, else the body's first non-empty error / message /
detail / details / error.message (2 KB, 50 ms; the SSO
"Internal server error" placeholder skipped when details is present),
else the last logged Error. The stack comes only from that Error. Env and
runtime secrets of 8+ characters are scrubbed, longest first, before
name (64 chars), message (500 bytes) and stack (4 KB) are cut. Any
failure writes nothing; the crash path is unchanged.
Off by default; the public CLI never sets it, and flag-off entries and
banners keep their bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e_map Cloudflare remaps only the stacks of uncaught exceptions it captures; a stack the Worker logs itself (the per-app entry's console.error before rethrowing, or user logs) stays minified. Run a throwing function in workerd with a Tail Worker, for both entries, and map the exception's frames back to the app's files by hand — local workerd does no remapping — and pin that the logged per-app crash line stays minified. Say in the README what is remapped automatically and what a consumer must remap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It is the raw Error.stack read inside the Worker, and Cloudflare remaps only the exceptions it captures, never a logged string. Say so, and that a consumer remaps it with the same version's source_map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y recent logged Error
Two review findings on the error recorder:
- A JSON-valued binding with leading whitespace (`\n{"db":{...}}`) was
not parsed, so the secrets nested in it were logged unredacted.
Detect JSON after trimStart(); the original string stays a candidate.
- A single saved Error was overwritten on every log, so a later
unrelated warning lost the matching Error's name and stack. Keep the
last 8 Errors logged in the request, use the newest one whose message
the body repeats, and the newest one only as the fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The error recorder now replaces a matched secret with `[redacted]` instead of `[secret]`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop the `errorRecords` option. Both generated entries now always append the recorder, route the handler's response through `_b44RecordError` and seed `_b44ErrorStore(request)`, including the public CLI's own shard builds and bundleApp's probe compiles. An app is ready for error reporting the moment it opts in, with no rebuild. The off switch is the request: the recorder does nothing unless it carries `x-b44-feed-context`, which the backend sends only for production calls of opted-in apps while its tail switch is on. Runtime behaviour is unchanged, crash path included. The banner loses its `errorRecords` field; `compiler` says which builds carry the record. This changes every build's output once: about +3 KB raw (+1.2 KB gzipped) per module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cord
The record is logged through the console patch, not raw: a Tail Worker
gets one entry with level "log" whose single argument is the patch's
wrapper `{ _b44_env, _b44_function?, level: "error", message }`, and
`message` is the JSON string of `{ "_b44_error": {...} }`. apper's error
tail is the consumer. Pin that entry for both entries, so a change to
the console patch fails here instead of silently in production, and
state the shape in the README and the module doc. No change to what is
emitted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s-compiler-error-records
🚀 Package Preview Available!Install this PR's preview build with npm: npm i @base44-preview/cli@0.2.3-pr.678.97827aaPrefer not to change any import paths? Install using npm alias so your code still imports npm i "base44@npm:@base44-preview/cli@0.2.3-pr.678.97827aa"Or add it to your {
"dependencies": {
"base44": "npm:@base44-preview/cli@0.2.3-pr.678.97827aa"
}
}
Preview published to npm registry — try new features instantly! |
DescriptionEvery build of a backend function now carries a small recorder in its generated Worker entry. When a function catches its own error and returns a 5xx, the recorder logs one There is no build option. The off switch is the request: the recorder does nothing unless the request carries
Why it's in every build
Request and response changesNone. An earlier revision of this PR added an What a consumer sees (the contract)The recorder calls
A consumer keeps entries whose wrapper Invocation logs. The record is logged inside
Confirmed in workerd. It is still an ordinary stamped line, so views built on stamped Workers log lines (such as per-function log queries) show it as an error line whose message is the JSON. That applies only to production calls of opted-in apps, with secrets scrubbed. Behaviour (
|
…er-error-records # Conflicts: # packages/functions-compiler/README.md
Note
Description
A backend function that catches its own error and returns a 500 leaves Cloudflare nothing to record — no exception reaches the runtime, so the Tail Worker sees a status and no reason. This PR makes every compiled entry (single-function and per-app; not actors) carry a small recorder that, for a handler-returned 5xx, logs exactly one
_b44_errorline for the Production Agent to consume. There is no build flag: the switch is the request'sx-b44-feed-contextheader, which the backend sends only for production calls of apps that opted in, so an app is ready the moment it opts in with no rebuild.Related Issue
None
Type of Change
Changes Made
packages/functions-compiler/src/error-records.ts—ERROR_RECORDS_PATCH, a self-contained (import-free) JS prelude inlined into generated entries after the console patch. It adds ~3 KB per module (~1.2 KB gzipped).worker-entry.ts— both entry builders now route the handler through_b44RecordError(await handler(request, info)), inside telemetry and invocation logs so the recorder sees the handler's own response before it is re-wrapped, and both spread..._b44ErrorStore(request)into the request store. The single-function entry's three handler-expression branches collapse into one sharedHANDLER_CALL.Errorlogged during the request whose message the JSON body repeats (newest among the last 8, so a later unrelated warning cannot hide it); else the body (JSON ≤ 2,048 bytes read within 50 ms — first non-emptyerror/message/detail/details/error.message, skippingerrorwhen it is exactly"Internal server error"anddetailsis present); else the last loggedError. No message means no record.Errorthat supplied the message, as its raw (minified)stack— Cloudflare remaps only exceptions it captures, so a consumer remaps it with the same version'ssource_map.[redacted], longest first, before any truncation; thenname≤ 64 chars,message≤ 500 UTF-8 bytes (well-formed, cut on a code point),stack≤ 4 KB (cut at a line end).ctx.waitUntil. Any failure writes nothing and never reaches the request. The uncaught-throw path is untouched and stays Cloudflare's to record.compilerin the banner is what tells a consumer whether a build carries it.level: "log", one wrapper argument,JSON.parse(wrapper.message)._b44_error), plus updates to the banner and byte-stability notes.Testing
npm test)Checklist
docs/(AGENTS.md) if I made architectural changesAdditional Notes
New
test/error-records.e2e.test.ts(~550 lines) runs compiled bundles end to end: it pins the raw Tail entry shape for both entry kinds, body-message selection (including the SSO"Internal server error"placeholder case), the logged-Errorprecedence and per-request isolation, the no-record cases (body over 2 KB, a stream that misses the 50 ms deadline, anything throwing) while asserting the client still gets the whole body, scrubbing and every bound, that the response is the same response and is not held back, and that an uncaught throw is unchanged.worker-entry.test.tsadds a recorder suite, and the entry snapshots are regenerated — the generated bytes of every non-pull app change, which is why the snapshot diff is large.lint,typecheck,buildandknippass on CI; thefunctions-compilerand CLI test jobs were still running when this description was generated, so the two unchecked testing boxes reflect what could not be verified here rather than a known failure.🤖 Generated by Claude | 2026-10-26 15:00 UTC | f69de2a