Skip to content

fix(studio): an offline preview says GSAP could not load instead of an unhandled Event - #5111

Merged
miguel-heygen merged 4 commits into
mainfrom
fix/preview-gsap-fallback-report
Oct 6, 2026
Merged

miguel-heygen merged 4 commits into
mainfrom
fix/preview-gsap-fallback-report

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

What broke

When a project's GSAP script can't load from its CDN (offline, blocked, or a TLS-intercepting proxy), Studio's preview tries a fallback copy. When that also failed, the fallback's promise rejected with nobody listening. The page got an unhandled rejection whose reason is a bare Event (embedders such as Playwright log it as pageerror: Event), and the user was never told GSAP was missing. The only thing on screen was the knock-on Uncaught TypeError: Cannot read properties of undefined (reading 'timeline') in "Console errors in preview".

Repro: open a project made from the CLI blank template (GSAP from cdn.jsdelivr.net) with that host blocked.

Root cause

Two gaps on the same path:

  1. The fallback in studio-server/src/routes/preview.ts never handled the case where both loads fail. Also, the fallback's own failed <script> reached the same capture listener, which would have reported the failure a second time.
  2. Studio's console capture (useConsoleErrorCapture) attaches to the preview window on the iframe's load. Both GSAP loads fail before load, so anything the preview raised earlier never reached the modal. That includes the GSAP failure, and also any script error raised while the page loads.

Fix

  • The fallback catches its failed load and reports it with reportError(new Error("GSAP could not load from <authored URL> or <fallback URL>, so this preview's animations will not play.")). It skips its own fallback scripts, so each failure is reported once.
  • A Studio preview (not a capture) starts with a tiny script that keeps every error message the window raises in window.__hfPreviewErrors (the name lives in @hyperframes/core/studio-preview-mark). When Studio's capture attaches, right after its existing reset at load, it adds that list, then listens live as before. The list is read, not drained. The capture owns every reset: each attach starts from an empty findings list, adds its live listener, then reads the window's whole list (one array, no argument spread). So a capture that attaches mid-load, attaches again at load, or attaches twice to the same window (React StrictMode) shows each error once, and a preview that goes away clears its findings. App no longer resets the list itself, which removes a hidden dependence on the order of two load listeners.

Proof

Red first, on unchanged main (devbox):

  • preview.test.ts "reports a GSAP script that fails from the CDN and from the fallback, never rejecting unhandled": fails with vitest's Unhandled Rejection (the raw Event) and no report.
  • Same test with only the new self-skip removed: fails, because the failure is reported twice.
  • preview.test.ts "keeps every error a Studio preview raises from its first script, and leaves captures without it": fails, no list.
  • useConsoleErrorCapture.test.tsx "shows the errors a preview raised before its load, once each": fails, consoleErrors stays undefined.
  • useConsoleErrorCapture.test.tsx "shows a loaded preview's errors once when the capture attaches to it twice" (StrictMode): fails at the first head of this PR with the error shown twice; passes after the reset moved into attach.
  • useConsoleErrorCapture.test.tsx "keeps listening after reading a preview's very long error list" (200,000 entries): failed when the list was spread into arguments (expected undefined to be 200001).
  • useConsoleErrorCapture.test.tsx "clears a document's errors when its load replaces it, and when the preview goes away": failed on the null-preview case before the hook owned the reset.

With the fix: the full preview.test.ts passes 109/109 three runs in a row, and all four hook tests pass three runs in a row; tsc --noEmit passes for studio, studio-server and core; oxfmt, oxlint, comment ratchet and comment citations pass.

Real Studio (Vite dev server, Chrome for Testing 154, jsdelivr requests failed with InternetDisconnected, the repo's CLI blank template as the project):

  • Before: page errors Event and the timeline TypeError; the modal shows only the TypeError.
  • After: page errors are the GSAP message and the TypeError; the modal shows both, GSAP first.

Known behaviour change: after a save that fixes an error, the modal now closes when the reloaded preview loads, not the instant the save starts.

Not covered: the fallback still retries the same CDN host with a newer GSAP, so it can't help when that host is down. Serving a bundled GSAP is a separate change. Desktop's own surfaces were not exercised.

Before

Offline blank-template project on main: the GSAP failure is only an unhandled rejection with a bare Event (pageerror: Event), which never reaches the modal; the modal shows only the knock-on TypeError.

before-offline-preview

After

Same project at this head (bf50eff): the modal says GSAP could not load, and from where, before the TypeError.

after-offline-preview

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 2055 (base branch 2055), smooth 1493 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • crop-none-px-r0-root-z100: tracking 0.04, pressJump 0, drop 40.03, reload 40.03, render 39.98, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.06, reload 0.06, render 0.03, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.06, reload 0.06, render 0.03, renderKey -, undo true, teleport true

@miguel-heygen
miguel-heygen marked this pull request as ready for review October 6, 2026 15:33

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed #5111 at bf50effd9c6ad5475a7800dc8c2d6e52768d9276 against base/merge-base 6308727b85726433e6a1e148b608d8cb11085114. Approved on the six-file effective diff; no blocking issue found.

The preview fallback excludes its own injected script from the capture listener, catches the failed retry, and reports a contextual Error instead of leaving a bare unhandled Event. Non-capture Studio previews buffer errors before authored scripts execute; the Studio hook resets on attach, starts the live listener, and replays early entries, including on load and repeated attachment. The existing modal supplies dialog labelling and focus behavior. The actual Before/After captures show the previously lone timeline TypeError and, after this patch, the explicit GSAP failure above that TypeError; close and Copy to Agent remain visible.

Focused isolated suites passed 113/113 with NODE_ENV=test after building workspace dependencies. All 11 required GitHub checks, including Windows render, passed at this head. I did not run a separate live offline Studio/Electron E2E; the author’s real-Chrome offline repro and screenshots supply that evidence. Retrying GSAP from the same CDN host cannot recover a host outage, but that pre-existing limitation is disclosed and does not invalidate this error-reporting fix. No merge or deployment action from this review.

— Review by tai (pr-review)

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 5fad52f Oct 6, 2026
238 of 239 checks passed
@miguel-heygen
miguel-heygen deleted the fix/preview-gsap-fallback-report branch October 6, 2026 16:51
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.

2 participants