Skip to content

fix(env): put runtime config on <html> so client reads can't outrun it - #6923

Merged
icecrasher321 merged 3 commits into
stagingfrom
fix/public-env-race
Aug 21, 2026
Merged

fix(env): put runtime config on <html> so client reads can't outrun it#6923
icecrasher321 merged 3 commits into
stagingfrom
fix/public-env-race

Conversation

@icecrasher321

@icecrasher321 icecrasher321 commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Runtime NEXT_PUBLIC_* config now rides on the <html> element as well as the inline script. <html> is the document's first tag, so the values are readable by any client code that can run at all.
  • getEnv reads that attribute when window.__ENV has not been assigned yet. window.__ENV stays the public global and the preferred read; both transports are built from one function so they cannot drift.
  • The script assigning window.__ENV lands ~13KB after the <script async> bootstrap tags, and appBootstrap hydrates synchronously whenever self.__next_s is empty — always, now that the script is a plain tag rather than a beforeInteractive one. Reads that fell in that window resolved to nothing: the socket URL fell back to the page origin for the life of the document, getBaseUrl() threw, and every module-scope flag in env-flags froze wrong.
  • The workflow error boundary reports what it caught. It implemented only getDerivedStateFromError, so a canvas crash left no trace anywhere.
  • Enabled PostHog's native capture_exceptions for unhandled errors and rejections. Error boundaries only see their own subtree, so chunk-load failures, rejected promises and throws from event or socket callbacks reached nothing. capture_console_errors stays off — it would capture the hydration warnings we already filter as noise.
  • Added realtime_connection_failing, once per socket after 3 consecutive failures, carrying the resolved origin. A socket that never connects raises no exception, so this class was previously invisible; the origin separates "realtime is unreachable" from "this client resolved the wrong host". Counted in connect_error only — a failed reconnect also emits the manager's reconnect_error, so counting both advanced twice per attempt.
  • The webhook-URL card row is left throwing on an unresolvable base URL, with a comment recording why. That value gets copied into a third-party provider, so a guessed origin or a blank row hands the user a URL that provider accepts and never delivers to; with the race fixed, reaching that path means the deployment has no application base URL at all.

Type of Change

  • Bug fix

Testing

Tested manually. Added 6 tests for the browser env resolution order (attribute fallback, window.__ENV precedence, per-key fall-through, re-read on attribute change, malformed attribute, absent key) and 3 pinning that the <html> payload matches the script's. Tests pass across lib/core/config, app/_shell, app/workspace/providers, lib/posthog and the workflow editor tree (409 in the editor tree alone). type-check, lint, the block-registry audit and all 32 check:audits pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

The inline script that assigns `window.__ENV` is rendered from the component
tree, so it lands ~13KB after the `<script async>` bootstrap tags React emits
in the preamble. `appBootstrap` calls `hydrate()` synchronously whenever
`self.__next_s` is empty — which it always is now that the script is a plain
tag rather than a `beforeInteractive` one, that queue having been the only
thing sequencing the assignment ahead of hydration. So module bodies and the
first commit could both read env before the assignment landed: the socket URL
fell back to the page origin for the life of the document, `getBaseUrl()`
threw, and every module-scope flag in `env-flags` froze on nothing.

Carry the same snapshot on `<html>`, the document's first tag, and read it in
`getEnv` when `window.__ENV` is not yet assigned. Parsing is memoized against
the raw attribute rather than against having run once, so the cache can never
serve a value the document no longer carries. `window.__ENV` stays the public
global and the preferred read, and both transports are built from one function
so they cannot drift.

Alongside: guard the read-only webhook-URL field so a base URL it cannot
resolve is a blank field rather than a dead canvas; report what the workflow
error boundary catches, which it previously swallowed entirely; and enable
PostHog's native exception capture, since error boundaries only ever see their
own subtree and chunk-load failures, rejected promises and throws from event
or socket callbacks reached nothing.
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 21, 2026 4:57am

Request Review

@cursor

cursor Bot commented Aug 21, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes how every client getEnv read resolves during bootstrap, so a bug here can freeze flags and URLs for the session. Observability additions are additive and low-impact.

Overview
Puts the same NEXT_PUBLIC_* snapshot on <html> as data-public-env so getEnv can resolve it before the late window.__ENV script runs. That race previously left socket URLs, getBaseUrl(), and module-scope flags empty for the life of the document.

getEnv still prefers window.__ENV; the attribute is a fallback with a memoized parse that treats malformed JSON as absent. Both transports are built from one readPublicEnv() so they cannot drift.

Separately, previously silent failures now report: PostHog capture_exceptions for unhandled errors/rejections (not console.error), workflow_canvas_crashed from the canvas error boundary, and realtime_connection_failing after three connect_errors with the resolved socket origin. The unused NextError helper is removed.

Reviewed by Cursor Bugbot for commit 9e7772e. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR makes runtime public configuration available from the initial <html> tag, adds client exception and workflow-canvas reporting, and instruments persistent realtime connection failures.

  • Adds an HTML-attribute fallback for runtime NEXT_PUBLIC_* configuration and tests its resolution order.
  • Enables PostHog handling for unhandled errors and promise rejections.
  • Reports workflow canvas crashes and persistent socket connection failures.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/core/config/env.ts Adds a cached, malformed-input-tolerant HTML-attribute fallback before the existing process environment fallback.
apps/sim/app/_shell/public-env-script.tsx Generates the runtime public-environment payload shared by the initial HTML attribute and global assignment.
apps/sim/app/layout.tsx Places the public runtime-environment payload on the root HTML element.
apps/sim/app/workspace/providers/socket-provider.tsx Counts connection failures only from connect_error and emits one persistent-failure analytics event per successful-connection cycle.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx Reports workflow canvas errors through structured logging and PostHog.
apps/sim/app/_shell/providers/posthog-provider.tsx Enables PostHog capture for unhandled browser errors and rejected promises while excluding console errors.

Reviews (3): Last reviewed commit: "fix(workflow): let an unresolvable webho..." | Re-trigger Greptile

Comment thread apps/sim/app/workspace/providers/socket-provider.tsx
Comment thread apps/sim/app/workspace/providers/socket-provider.tsx
`manager.reconnect()` calls `open()`, whose error path emits `error` — which
the socket re-emits as `connect_error` — and then emits `reconnect_error`
itself. A failed reconnect therefore reached both handlers and advanced the
counter twice, so the outage report tripped on the second real attempt while
claiming three.

Count in `connect_error` alone: it is the only handler that fires exactly once
for both the initial failure and every retry. `reconnect_error` keeps its log
line and states why it deliberately does not count.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 99fee82. Configure here.

Reverts the guard added earlier in this PR. A blank row titled "Webhook URL"
is a worse outcome than a crash: it explains nothing, and the value is one a
user copies into a third-party provider, so any substitute — a guessed page
origin, an empty string — is a URL that provider accepts and then never
delivers to.

The read can no longer come back empty from the hydration race this PR fixes,
so reaching it at all means the deployment has no application base URL, which
breaks webhook registration and callbacks regardless. The error boundary now
reports what it caught, so the throw names its own cause instead of surfacing
as an unexplained fallback.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 9e7772e. Configure here.

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321
icecrasher321 merged commit aca152c into staging Aug 21, 2026
30 of 31 checks passed
@icecrasher321
icecrasher321 deleted the fix/public-env-race branch August 21, 2026 05:12
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