fix(runner): match the host-suffixed 'Failed to fetch' wording so the DEMOS-2X gate fires (DEV-2859) - #327
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 thefetchVersionsfailure path to a breadcrumb:App.tsx:1670gates onisOpaqueNetworkFailure(error)and files nothing when it matches.That gate has never fired in production. Evidence:
930f6a52ande32cdcdf— both well after the demotion landed. It has been filing events the whole time.fetchVersions(App.tsx:1654) and it is gated. So the gate is being reached and returning false.error.valueisFailed 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
The anchoring rationale in that file's header was correct and is preserved. An unanchored
/failed to fetch/ialso matchesFailed 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.Failed to fetch (demos.handsontable.com)— real production valueFailed to fetchFailed to fetch dynamically imported module: …— must not matchFailed to fetch (host) extra—$must still bindFirefox 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/11pnpm typecheck— clean across all four typechecked workspacespnpm test—1090 tests / 1088 pass / 0 fail / 2 todo(todos pre-existing)Two revert-checks, both reproduced independently during review:
/^failed to fetch$/i/failed to fetch/iThat second result is stronger than predicted: a loose pattern also matches
Failed to fetch (host) extraas 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
(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'sUNHANDLED_NOISEhas an unanchored/Failed to fetch/i. Deliberate and documented (navigate-away-mid-fetch shapes), gated onmechanism.handled === false, and its header explains why it must not move toignoreErrors. Out of scope. But note it would matchFailed to fetch dynamically imported module: …if such a rejection ever arrived unhandled — the same defect class this PR protects. Not reachable today because theasBabelretry 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
TypeErrormessages are oftenFailed to fetch (demos.handsontable.com), butisOpaqueNetworkFailureonly matched the bareFailed to fetchvia/^failed to fetch$/i, so thefetchVersions/ related gates inApp.tsxkept 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.