Skip to content

feat(sdk): send the app's build commit as X-Base44-Build from the browser - #324

Open
egidijusj wants to merge 3 commits into
base44:mainfrom
egidijusj:sdk-build-header
Open

egidijusj wants to merge 3 commits into
base44:mainfrom
egidijusj:sdk-build-header

Conversation

@egidijusj

@egidijusj egidijusj commented Oct 12, 2026 •

Copy link
Copy Markdown

Why

Base44 groups runtime errors from published apps into issues, and treats an issue as fixed once a new version of the app ships without the error. Browsers don't reload when an app is republished, though. A user who opened the app before the fix keeps running the old bundle, and keeps hitting and reporting the old error. Those reports look identical to new ones, so a fixed issue appears to come straight back.

To tell the two apart, the backend needs to know which build of the frontend sent each request. Only the build knows that. The publish pipeline knows the app's git commit when it builds the bundle, but nothing in the bundle carries it to the API afterwards.

This PR has the SDK send that commit as an X-Base44-Build header on its API calls. The backend can then attribute each browser-side error to the build that produced it, and ignore errors from bundles older than the fix. The SDK is the right place for it:

  • Every request goes through it. Entity calls and function invocations, where these errors are recorded, both use the SDK.
  • It reaches every app. The platform updates the SDK on every build, while each app's own source is never rewritten by a build. So no app code has to change.

The publish pipeline provides the commit at build time as VITE_BASE44_BUILD_COMMIT. The API accepts X-Base44-Build only as an exact 40-character lowercase hex commit and ignores anything else. The header is optional: requests without it, from older SDK versions or builds that don't set a commit, behave exactly as before.

Route: the SDK reads import.meta.env itself

App source is never rewritten at build time, so the SDK reads import.meta.env.VITE_BASE44_BUILD_COMMIT directly. Vite replaces that expression statically inside node_modules:

  • vite build: the commit is inlined as a string literal, with no import.meta left in the bundle. When the var is unset, it becomes void 0.
  • dev server: Vite prepends an import.meta.env = {…} literal to the prebundled .vite/deps/@base44_sdk.js, so the value is present there too.

This covers web apps and game apps, which have no @base44/vite-plugin. A plugin define route would have missed game apps, so I didn't use it.

Keeping import.meta away from Metro, Node and Deno

import.meta breaks Metro on both platforms. Expo 54's Hermes build fails on any import.meta in a dependency ("import.meta is not supported in Hermes"). On Expo web, Metro ships a classic <script>, where import.meta is a SyntaxError and the page doesn't render. Every mobile build bumps the SDK, so a bare read would break them all. The reader is therefore reached only through a package import:

"imports": {
  "#build-commit-source": {
    "module": "./dist/utils/build-commit-source.js",
    "default": "./dist/utils/build-commit-source.fallback.js"
  }
}
file reached by
utils/build-commit-source.js: the import.meta.env read, in a try module condition (Vite, webpack, esbuild)
utils/build-commit-source.fallback.js: returns undefined everything else: Metro (native and web), Node, Deno

A tsconfig paths entry and a Vitest alias point the specifier at the source file. The emitted .d.ts files don't reference it.

An earlier revision of this PR used the browser/react-native fields instead. Metro's web resolver follows browser and ignores react-native, so it bundled the reader and the Expo web page failed to parse. This revision fixes that, and the verifier now checks for it.

Behaviour

  • createClient() in a browser (window and document present) adds X-Base44-Build to the shared axios headers. That covers entity CRUD, functions.invoke, and every other axios call. It is also added to functions.fetch(). A caller-supplied X-Base44-Build in createClient({ headers }), in any casing, replaces the baked-in commit on every call path, so one client never reports two builds.
  • The header is omitted unless the value is exactly 40 lowercase hex characters. An empty header is never sent.
  • It is never sent from createClientFromRequest(), which opts out explicitly whatever runtime it is in, or from Node/Deno (no window) or React Native (no document).
  • fetchWithAuth, sockets and the AI gateway are unchanged.
  • No public API or type changes. The only change to createClient is that its body moved into a private buildClient(config, { sendBuildCommit }).

Side effects checked

  • Preflights: axios calls already send custom headers (X-App-Id, X-Origin-URL), so they already preflight and the new header adds none. The one new preflight is an anonymous, header-less functions.fetch() GET, which used to be a simple request. The server allows *, and the SDK has no allowlist that would strip the header.
  • Caching: the SDK has no request cache or dedupe keyed on headers.

Tests

  • tests/unit/build-commit-header.test.ts (22 tests) checks that:
    • the header is set on entities, functions.invoke and functions.fetch
    • it is omitted when the value is absent or malformed (empty, uppercase, 39 or 41 chars, padded, trailing newline, non-hex, non-string)
    • it is omitted for createClientFromRequest, Node and React Native
    • a caller override wins on entities, functions.invoke and functions.fetch alike, in any casing
    • the reader returns import.meta.env's value, it is the only source file that uses import.meta, the fallback returns nothing, and the imports map is correct
  • npm run test:build-commit (scripts/verify-build-commit.sh) packs the SDK, installs the tarball into fresh template-like apps (not linked), then:
    • runs vite build with Vite 6.4 and Vite 7.3 and greps the bundle for a known commit (also checks the var-unset case)
    • starts the dev server and greps the prebundled SDK
    • with --expo, runs Expo 54 Android (Hermes) and web exports, checks the SDK is bundled without the reader, and parses the web bundle as a classic script (this check fails against the earlier browser-field routing)
  • I also checked by hand in a real browser, against both the dev server and vite preview of the production build: entities.list/filter/create and functions.invoke all send X-Base44-Build: <commit>.
  • npm test: 353/353 pass. tsc and the type tests are clean, and lint is clean.

Risk

A tool that doesn't support package imports can't resolve #build-commit-source, and the build fails. Examples are webpack 4, Jest before 28 and very old Metro. Expo 54 and 58, Vite 6 and 7, Node and webpack 5 all support it.

Not covered

  • Mobile (Expo/Metro) apps. Metro doesn't read VITE_* vars and can't take import.meta, so they deliberately send nothing. Covering them would need the platform to expose the commit in a form Metro inlines, e.g. EXPO_PUBLIC_BASE44_BUILD_COMMIT read through process.env in a react-native-mapped source file. That is a follow-up if wanted.

Release

No version bump here. A maintainer runs manual-publish with patch, which should give @base44/sdk@0.8.54. No template or plugin change is needed.

🤖 Generated with Claude Code

egidijusj and others added 3 commits October 12, 2026 22:44
…wser

The Production Agent groups runtime errors from published apps into issues,
and needs to know which frontend build produced each browser-side error so a
tab still running an old bundle can't reopen a fixed issue.

The publish pipeline sets VITE_BASE44_BUILD_COMMIT for every build. The SDK
now reads it itself via `import.meta.env.VITE_BASE44_BUILD_COMMIT`, which
Vite statically replaces inside node_modules in both `vite build` and the dev
server's prebundled deps, so no app code changes. Every API call from a
browser client (entities, functions.invoke, functions.fetch, and the rest of
the axios path) carries the header when the value is exactly 40 lowercase hex
chars; otherwise it is omitted.

`import.meta` lives only in build-commit-source.browser.js, reached through
the package's `browser` field. The default file is a stub, so Node, Deno and
SSR never see it, and the `react-native` field maps Metro back to the stub,
since Hermes (Expo 54) rejects `import.meta` outright.

Never sent from createClientFromRequest, Node/Deno, or React Native.

scripts/verify-build-commit.sh (npm run test:build-commit) proves it against
the packed tarball: Vite 6 and 7 build + dev, and Expo 54 with --expo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…port

The `browser` field broke Expo web: Metro's web resolver ignores the
`react-native` field and follows `browser`, so it bundled the
`import.meta` reader into a classic <script>, and the page failed to parse
("Cannot use 'import.meta' outside a module").

Now `#build-commit-source` in the package's `imports` field gives the reader
only to the `module` condition (Vite, webpack, esbuild), and everything
else, including Metro on native and web, Node and Deno, gets the fallback.
tsconfig `paths` and a Vitest alias point the specifier at the source file.

verify-build-commit.sh --expo now also exports Expo 54 web and parses the
bundle as a classic script; that check fails on the previous routing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
functions.fetch() copied the baked-in build commit directly, so
createClient({ headers: { "X-Base44-Build": override } }) sent the override
on entity calls and functions.invoke() but the baked-in commit on
functions.fetch(), attributing one client's requests to two builds. A
lowercase override also landed next to the baked-in key instead of
replacing it.

The effective header is now resolved once: the caller's value, matched
case-insensitively, else the baked-in commit. Regression tests cover each
casing, with and without a baked-in commit, across entities,
functions.invoke() and functions.fetch().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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