Skip to content

fix(runner): match the host-suffixed 'Failed to fetch' wording so the DEMOS-2X gate fires (DEV-2859) - #327

Merged
demtario merged 2 commits into
masterfrom
fix/DEV-2859-opaque-fetch-host-suffix
Sep 9, 2026
Merged

fix(runner): match the host-suffixed 'Failed to fetch' wording so the DEMOS-2X gate fires (DEV-2859)#327
demtario merged 2 commits into
masterfrom
fix/DEV-2859-opaque-fetch-host-suffix

Conversation

@demtario

@demtario demtario commented Sep 9, 2026

Copy link
Copy Markdown
Member

A one-row regex fix. It is small, but it means a shipped fix has not been working for nine days.

What happened

Commit 590cb58b2 ("stop misreporting a dropped visitor network as a host defect", PR #274, 2026-08-31) demoted the fetchVersions failure path to a breadcrumb: App.tsx:1670 gates on isOpaqueNetworkFailure(error) and files nothing when it matches.

That gate has never fired in production. Evidence:

  • Sentry DEMOS-2X's most recent events (2026-09-08) carry releases 930f6a52 and e32cdcdf — both well after the demotion landed. It has been filing events the whole time.
  • There is exactly one caller of fetchVersions (App.tsx:1654) and it is gated. So the gate is being reached and returning false.
  • The reason is in the data. Sentry's error.value is Failed to fetch (demos.handsontable.com) — with a host suffix. The Chromium row was anchored /^failed to fetch$/i, and an anchored pattern cannot match a string carrying a suffix.

The fix

/^failed to fetch$/i          →  /^failed to fetch(?: \([^)]*\))?$/i

The anchoring rationale in that file's header was correct and is preserved. An unanchored /failed to fetch/i also matches Failed to fetch dynamically imported module: <url>, which is the rotated-chunk host defect (DEMOS-15 / DEV-2569) — a real defect class this pattern has no business silencing. The anchor was simply too tight for the wording that actually arrives.

wording old loose this PR
Failed to fetch (demos.handsontable.com) — real production value
Failed to fetch
Failed to fetch dynamically imported module: …must not match matches
Failed to fetch (host) extra$ must still bind matches

Firefox and Safari rows are untouched: Firefox's real wording carries a trailing period, so $-anchoring it would break the one wording it exists to match.

This is not a reversal of PR #274

Worth being explicit, because it looks like one. PR #274's intent was right and its demotion stays exactly as written — this PR makes its gate actually match. Nothing is re-promoted.

Verification

  • pipeline/fetch-failure.test.mjs — 11/11
  • pnpm typecheck — clean across all four typechecked workspaces
  • pnpm test1090 tests / 1088 pass / 0 fail / 2 todo (todos pre-existing)

Two revert-checks, both reproduced independently during review:

mutation expected red result
restore /^failed to fetch$/i the fix-prover only 10 pass / 1 fail ✓
substitute loose /failed to fetch/i the DEV-2569 guard 9 pass / 2 fail — the guard and the anchor near-miss

That second result is stronger than predicted: a loose pattern also matches Failed to fetch (host) extra as a substring, so the anchor guard catches it too. The fix-prover uses the verbatim production string; the other two are labelled in-file as guards, not fix-provers.

Also corrected in here

The file's header comment asserted the message "carries nothing about our host in it" — the sentence that justified the demotion, and precisely what the production data disproves — and described the match as being "on a substring" when it was anchored. Both are now accurate. That stale comment is arguably how this survived: it documented a belief about the wording that was never checked against a real event.

Two notes for follow-up, not changed here

  • The suffix's origin is unverified. Nothing in our source appends (demos.handsontable.com); it is either the browser or the Sentry SDK's fetch instrumentation. The comment says so honestly rather than guessing.
  • sentry.ts's UNHANDLED_NOISE has an unanchored /Failed to fetch/i. Deliberate and documented (navigate-away-mid-fetch shapes), gated on mechanism.handled === false, and its header explains why it must not move to ignoreErrors. Out of scope. But note it would match Failed to fetch dynamically imported module: … if such a rejection ever arrived unhandled — the same defect class this PR protects. Not reachable today because the asBabel retry path catches it (handled: true), so it is one uncaught dynamic import away from mattering.

After deploy

DEMOS-2X's events become breadcrumbs and the issue goes quiet — which re-opens the question the triage wanted answered ("visitor blip or our availability dip?"). That is what the sibling branch's retry + attempt-count instrumentation is for; a blip fails once, a dip fails twice.

🤖 Generated with Claude Code


Note

Low Risk
Narrows error-message matching for Sentry demotion only; keeps exclusions for host defects and adds regression tests.

Overview
Fixes a nine-day gap where PR #274’s visitor-network demotion never ran: production TypeError messages are often Failed to fetch (demos.handsontable.com), but isOpaqueNetworkFailure only matched the bare Failed to fetch via /^failed to fetch$/i, so the fetchVersions / related gates in App.tsx kept filing Sentry (DEMOS-2X) instead of treating them as opaque transport failures.

The Chromium pattern is now /^failed to fetch(?: \([^)]*\))?$/i — still end-anchored so dynamic import failures (Failed to fetch dynamically imported module: …) stay out of this bucket. Header comments are corrected to match real Sentry wording.

Tests: a fix-prover for the suffixed production string, a regression for the bare Chrome wording, and an anchor guard so (host) must be the message suffix.

Reviewed by Cursor Bugbot for commit c27aaee. Bugbot is set up for automated code reviews on this repo. Configure here.

demtario and others added 2 commits September 9, 2026 11:46
…ures (DEV-2859)

The Chromium row in isOpaqueNetworkFailure was anchored /^failed to fetch$/i,
but the real production message carries a host suffix: Sentry DEMOS-2X's
latest events (2026-09-08, releases 930f6a5 and e32cdcd, both after
590cb58 / PR #274 landed) are `Failed to fetch (demos.handsontable.com)`,
which the anchored pattern can never match.

This is not a reversal of 590cb58. That commit demoted the fetchVersions
failure path behind isOpaqueNetworkFailure, but the gate has never actually
fired in production — fetchVersions has exactly one caller and it is gated,
so DEMOS-2X kept filing for nine days after the demotion shipped because the
gate's pattern didn't match the message's real shape. This change makes the
gate reach the population it was written for; it does not change what gets
filed versus suppressed once the gate does match.

The fix adds one optional non-capturing group for a parenthesised suffix
immediately before the trailing $, so the anchor still excludes
"Failed to fetch dynamically imported module: <url>" (the deploy-rotated
chunk defect, Sentry DEMOS-15 / DEV-2569, packages/runtime/src/transpile.ts)
that an unanchored /failed to fetch/i would wrongly swallow. Verified by
reverting to the old anchored pattern (fix-prover test goes red, 10/11) and
by substituting a loose /failed to fetch/i (over-widening guard plus the new
anchor near-miss test both go red, 9/11); the shipped pattern passes all 11.

Extends pipeline/fetch-failure.test.mjs with the verbatim DEMOS-2X wording,
and a near-miss case proving the suffix must be the end of the message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to cbc38c2: the module's opening contract statement still said
the message carries "nothing about our host in it," which the fix itself
disproves (the real wording is `Failed to fetch (demos.handsontable.com)`).
Also updated the stale "matched on a message substring" line — the row is
anchored, not a substring match. No behavior change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@demtario
demtario merged commit be2676d into master Sep 9, 2026
6 checks passed
@demtario
demtario deleted the fix/DEV-2859-opaque-fetch-host-suffix branch September 9, 2026 10:00
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