Skip to content

2.0.8 batch: five Sentry-evidenced fixes (F-509, F-783, F-816, F-817, F-818) - #64

Open
AminDhouib wants to merge 6 commits into
fix/sentry-2.0.7-batchfrom
fix/sentry-2.0.8-batch
Open

2.0.8 batch: five Sentry-evidenced fixes (F-509, F-783, F-816, F-817, F-818)#64
AminDhouib wants to merge 6 commits into
fix/sentry-2.0.7-batchfrom
fix/sentry-2.0.8-batch

Conversation

@AminDhouib

Copy link
Copy Markdown
Member

Stacked on #63 (it builds on the F-815 Sentry filter there). Retarget to main after #63 merges.

Five fixes, each driven by a live Sentry stacktrace, each TDD'd RED-first against the exact production error shape. Full hermetic lane: 1636 passed (+42 new tests), budget gate green, all LOC caps exact.

The fixes

F-817 — nodriver handler-table race (element_resolution.py) — Fixes STEALTH-CHROME-DEVTOOLS-MCP-2R, -2S
nodriver 0.47's remove_handler ends in a bare del self.handlers[evt_dom] that deletes the whole key; two overlapping Tab.wait()s race and the loser crashes with KeyError: <class 'nodriver.cdp.page.FrameStoppedLoading'>. The existing recovery wrapper now retries it like the -32000 stale-node case (one classifier, no parallel path); if it persists, a readable ToolError replaces the class-repr leak. Unrelated KeyErrors propagate untouched (two negative-control pins).

F-816 — CDP param-name folding (cdp_function_executor.py, 1010/1012) — Fixes -22
F-813 forgave command-name spelling; params still crashed on the CDP docs' own casing (awaitPromise). build_cdp_call folds incoming keys onto the resolved method's real signature; unknown params raise ToolError listing the valid names, through the method's KEEP contract.

F-818 — target-less browser + typed instance-not-found (browser_manager.py, exactly 1532/1532, line-neutral) — Fixes -2K, -12
Browser.get()'s bare next(filter(...)) turns into RuntimeError: coroutine raised StopIteration when no page target remains; the one exposed call site converts exactly that shape into an actionable ToolError. navigate's unknown-instance raise becomes InstanceNotFoundError, its timeout ToolError. Census of all 7 bare raises included in the commit; 4 left deliberately (F-811 operator-hint contract, internal invariants).

F-783 closed — error-convention sweep (dom_handler.py, server.py exactly 3411/3411) — Fixes -2H, -Y, -2F, -2P, -2V, -2Q, -2M, -2N
All 21 bare raise Exception sites in dom_handler + _with_cdp_timeout's timeout raise become ToolError (messages byte-identical). The F-815 filter now drops the family — which also dissolves the per-instance-UUID splitting of CDP-timeout issues. Deliberately kept shipping: cancellation (still CancelledError) and spawn failures (server.py:454 stays bare so the F-811 exhaustion class remains visible in Sentry). Audit finding doc annotated closed.

F-509 — reserved-port fallback (singleton.py exactly 1000/1000, proxy_forwarder.py) — Fixes -2J
Windows excluded-port ranges forbid binding 19222 (WinError 10013) but have no listener, so the connect-based foreign-held probe read the port as free and uvicorn died in the child on every start — seen on an external user's machine. New bind-and-close probe disqualifies a port only on PermissionError: treating any bind failure as disqualifying scattered the cold-start herd onto private ports (herd test 240s timeout vs 3.9s — the A/B that found it is pinned in TestOccupiedIsNotForbidden). Selection policy, restart_backend's contract, and identity/adoption gates unchanged.

Known residuals (flagged, not fixed here)

  • F-509 A2 (DESIGN.md): a fleet cold-starting against a genuinely reserved port still scatters; needs port choice serialized under the cold-start lock.
  • nodriver upstream: remove_handler's unguarded dels remain — we work around, not vendor-patch.
  • dom_handler.py:859 ValueError for invalid scroll direction still ships to Sentry (chain contains a non-ToolError).
  • DESIGN.md/RUNBOOK.md port-selection prose doesn't yet mention the permission dimension (no doc pin falsified).

Integration lane runs on this PR's CI (E2E across 3 OSes), same as #63.

AminDhouib and others added 6 commits August 7, 2026 04:38
nodriver 0.47's Connection.remove_handler ends in a bare
`del self.handlers[evt_dom]` that deletes the whole key, so two
overlapping Tab.wait()s on one tab race and the loser crashes with
KeyError(<class 'nodriver.cdp.page.FrameStoppedLoading'>). The race is
transient by construction, so _resolve_with_recovery now treats a
KeyError naming a nodriver.cdp event class as recoverable alongside the
-32000 stale-node case (same retry bound, same backoff, one classifier).
If it persists, the caller gets a readable ToolError naming the
condition instead of a bare class repr leaking into error messages.

Unrelated KeyErrors (string keys, non-nodriver classes) propagate
untouched, pinned by two negative-control tests.

Fixes STEALTH-CHROME-DEVTOOLS-MCP-2R
Fixes STEALTH-CHROME-DEVTOOLS-MCP-2S

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F-813 forgave command-NAME spelling; params still required nodriver's
snake_case, so the CDP docs' own spelling ('awaitPromise') crashed with
TypeError. build_cdp_call now maps each incoming key onto the resolved
method's real signature with the same underscore-stripped fold, so
'returnByValue' and 'return_by_value' are the same argument. A key that
matches nothing raises ToolError listing the command's valid params,
honored through the method's KEEP {"success": False} contract exactly
like the F-813 unknown-command path.

Fixes STEALTH-CHROME-DEVTOOLS-MCP-22

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nodriver's Browser.get() locates the new page target with a bare
next(filter(...)); with no page target left (browser mid-teardown, last
tab closed, targets degraded to raw Connections) the StopIteration
becomes "RuntimeError: coroutine raised StopIteration" out of
_replace_main_tab. The one exposed call site now converts exactly that
shape (isinstance(e.__cause__, StopIteration)) into an actionable
ToolError; unrelated RuntimeErrors propagate untouched.

navigate's unknown-instance raise becomes InstanceNotFoundError and its
timeout raise becomes ToolError (convention 2). Line-neutral at the
file's exact 1532 cap; the four remaining bare raises are deliberate
(spawn-failure operator-hint contract F-811, internal invariants).

Fixes STEALTH-CHROME-DEVTOOLS-MCP-2K
Fixes STEALTH-CHROME-DEVTOOLS-MCP-12

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All 21 bare `raise Exception` sites in dom_handler.py (element lookups,
argument guards, and their re-wraps) and _with_cdp_timeout's timeout
raise become ToolError, message text byte-identical. The F-815 Sentry
filter now drops the whole family as expected tool failures — which also
dissolves the per-instance-UUID issue splitting the timeout messages
caused. Cancellation still propagates CancelledError untouched, and
spawn_browser's inner bare Exception is left deliberately so genuine
spawn failures (the F-811 exhaustion class) keep shipping.

query_elements now passes an already-ToolError from the resolution layer
through unchanged instead of double-wrapping it, and the
execute_cdp_command docstring stops claiming params must be snake_case
(F-816 made both spellings resolve). server.py stays at its exact 3411
cap.

Fixes STEALTH-CHROME-DEVTOOLS-MCP-2H
Fixes STEALTH-CHROME-DEVTOOLS-MCP-Y
Fixes STEALTH-CHROME-DEVTOOLS-MCP-2F
Fixes STEALTH-CHROME-DEVTOOLS-MCP-2P
Fixes STEALTH-CHROME-DEVTOOLS-MCP-2V
Fixes STEALTH-CHROME-DEVTOOLS-MCP-2Q
Fixes STEALTH-CHROME-DEVTOOLS-MCP-2M
Fixes STEALTH-CHROME-DEVTOOLS-MCP-2N

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Windows excluded/reserved port range (Hyper-V/WinNAT) forbids binding
19222 with WinError 10013 but has no listener, so the connect-based
foreign-held probe read it as free, selection kept it, and uvicorn died
in the child on every start — the backend never came up at all.

bindable_port/_port_is_forbidden join _free_port in proxy_forwarder (the
one port-primitive home): a bind-and-close probe that disqualifies a
port ONLY on PermissionError. Occupied-but-permitted ports are
deliberately NOT disqualified — a herd of cold-starting proxies probes
concurrently, and treating any bind failure as disqualifying scattered
11 of 12 sessions onto private ports nothing would ever serve (herd test
240s timeout vs 3.9s). singleton passes its existing conflict verdict
down as force_new; selection policy, restart_backend's contract, and the
identity/adoption gates are unchanged. singleton.py stays at its exact
1000 cap.

Fixes STEALTH-CHROME-DEVTOOLS-MCP-2J

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AminDhouib

Copy link
Copy Markdown
Member Author

CI arbitration — the reds are pre-existing environmental findings, not this PR

Two runs, byte-identical trees (26fb2ec and empty-commit re-run 06356f7):

Cell Run 1 Run 2 Verdict
integration (Windows/X64) FAIL — pytest: null, no junit (the documented pre-2.0.3 herd-hang signature, evidence_F509_windows_herd_hang_2026-08-01.md) PASS flake, arbitrated green on an identical tree
integration (Linux/X64) PASS PASS
integration (macOS/ARM64) FAIL — 3 nodes FAIL — 2 nodes F-806, see below
everything else (unit ×3, transport ×2, coverage, smoke ×6, aggregates' inputs) PASS PASS

The macOS failures are F-806 with new, conclusive evidence. Run 1's junit:

test_autodiscovery_and_cdp_match_image_chrome
AssertionError: ('Chrome/151.0.7922.76', '150.0.7871.187')  — assert '151' == '150'

The version probed early in the run was Chrome 150; the CDP-launched browser was Chrome 151 — Chrome's updater upgraded the binary mid-run on the macOS runner. That is exactly F-806's documented mechanism (pre-launch version probe memoized → masked UA contradicts the live browser's own client hints), which is why both stealth gates (ua_major_matches_client_hints, f770_product_masks_ua) fail in both runs. The "Event loop is closed" teardown noise is the separately-documented F-779.

This PR's diff has zero overlap with UA masking / platform_utils / Chrome discovery; any branch run on the current macOS image (including #63, if re-run today) inherits the same red until Chrome's updater and the image version converge — or until the preserved F-806 fix branch (fix/ua-version-skew @ b863878: memo re-keyed on binary identity + post-launch Browser.getVersion reconciliation) is finished and landed.

Local evidence for this PR: hermetic lane 1636 passed ×3 (twice pre-push hook, once direct), budget gate exit 0, all LOC caps exact.

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