Repository navigation
fix: capture network on Safari and Appium sessions without BiDi - #437
Open
vishnuv688 wants to merge 17 commits into
Open
vishnuv688 wants to merge 17 commits into
vishnuv688 wants to merge 17 commits into
Conversation
selenium-webdriver's NetworkInspector hands every network event to every subscriber's callback, so the request handler also received responses and the response handler also received requests. Every request in a Selenium Chrome trace was listed twice, a pending row beside the completed one, and 57 of 135 rows in a Nightwatch trace. Each handler now ignores the other's events by their shape. The inspector also models neither a redirect hop, which re-sends the request under the same id, nor a failed fetch, for which it has no hook. Rows are kept by id for the session so a hop continues its row under the first hop's clock, and network.fetchError is read off the inspector's own connection so a request the browser gives up on closes as a failure instead of staying pending. A browser that refuses that subscription keeps the inspector and loses only the failure rows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Network capture rode on WebDriver BiDi, or on Chrome's performance log in Nightwatch, so Safari and every Appium session showed an empty Network tab. The injected page collector now also reads the document's Resource Timing buffer, which every browser keeps: each load with its timing phases and size, on Chromium its status, never headers or bodies. It reads the buffer at install so loads made before the collector existed are included, then observes. The collector's fetch/XHR patch claims each call it sees, so a request both saw is listed once with the patch's headers and body, and an entry that arrives before the call has answered is held until the patch records or drops that call. A driver-side source wins once it has delivered. The gate is evidence, not a subscription: on Appium 3.7's XCUITest driver the BiDi inspectors attach and then send nothing, so each adapter latches on the first BiDi network event it receives and gates the page's rows on that. Nightwatch's performance log, read after the drain, lays its headers and status over the page's row for the same load instead of listing it again, and a load the log listed first is dropped from the next drain. The Python adapter also stops treating safaridriver's echoed `webSocketUrl: true` as a BiDi endpoint, which crashed selenium's connection thread. What Resource Timing cannot see is stated rather than left blank: the row carries its source, the dashboard colours a status-less row as unknown rather than pending and names the limit on the detail card, and the HAR entry carries `_source` and `_resourceType` so a reopened trace renders the same row rather than a failed request. Verified on desktop Safari 26.5 and on iOS 26.5 Safari through Appium 3.7 with all four adapters; Chrome sessions are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The login example on desktop Safari fails to leave /login after the submit click in most runs, with and without the network changes, while network capture works in every run. The suspect is the adapter's per-command work after a click on Safari; it is unresolved, and the Nightwatch BDD example's assertions are not Safari-proof either. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… demo The demo credential trips Chrome's password-leak check, after which Chrome drops every WebDriver-synthesized input to the tab and the logout click silently does nothing. The other examples that submit it already map the check's endpoint to localhost; this one did not. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
[Medium impact] The PR appears safe to merge, with a non-blocking slowdown in network capture on long runs. Findings
SummaryAdds page-based network capture for Safari and Appium sessions without working BiDi events.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Page requests] --> B[Fetch and XHR patch]
A --> C[Resource Timing]
B --> D[Match claims and keep one row]
C --> D
D --> E[Adapter drain]
F[Delivered BiDi events] --> E
E --> G[Dashboard and saved trace]
Reviews (4) · Last reviewed commit: "fix(selenium): fold only a wait()'s own ..." · Reviewed by Greptile |
…earby request The drain filter dropped a drained Resource Timing row whenever any row the session held without that source shared its url within 2 s. That set includes the collector's own patched fetch rows and rows the log had already upgraded, so a second request to the same url inside the window was read as a second listing of the first and lost its only row: a fetch answered with a content-type-less 204 after one answered with JSON never reached the Network tab or the trace. The session now keeps the loads the performance log listed before the page's drain could carry them, and the next drain spends each on one page row, the nearest in time under its url. Patched rows and upgraded rows are never candidates. The ledger is spent at the next drain that reaches the driver, a collector-less document included, and emptied at a session replacement, because the page observed the load before the log could list it: a row absent from that drain is never coming, and a load kept longer would take the row of the next request to the same url. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…lback The fallback was described as seeing no headers or bodies, and on Safari no status, as if that held for the whole injected path. Only the Resource Timing rows have those limits; a fetch or XMLHttpRequest call the collector patched carries its method, headers, status and body. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Core's injection gate and the page collector's request filter each kept their own list of schemes no server answers, and they had drifted: the collector missed chrome-native:, the new-tab page Chrome on Android opens a session on, and listed it as a request. Both now read shared/browser-schemes.ts, which also parses the scheme properly so a relative path that merely starts like one is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Safari and every Appium session the collector is appended as a <script> after each navigation, and four things went wrong there: - It waited for an animation frame before attaching its listeners, which Safari's automation window does not fire until something forces a paint, so a live Safari run captured no typing and every row replayed the same DOM. It now starts at once when the page body already exists. - Two injections racing past one presence probe replaced the collector and dropped its buffers; on Android Chrome the trace had no network rows. A second evaluation now keeps the collector it finds. - The DOM anchor read attributes only, so fields typed into before the collector arrived replayed empty. It now records each field's live value and checked state, except a password's. - A reused XMLHttpRequest re-ran every earlier send's listeners, and a claim pushed out of the 500-request window lost the entry it held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
selenium-webdriver's NetworkInspector has a fetchError hook, so failed requests now subscribe through it and fall back to reading the socket only for an inspector without one. The rows kept for a session now evict finished requests first, so a long-running request still open past 5000 others keeps a row for its answer to land in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
selenium-webdriver's wait() runs its first poll inside the test's call and the rest from a timer, which carries no call site, so the retry fold never matched them; and because onCommand is not awaited, a poll could be captured before the previous one had an id. Captures now run one at a time per capturer, and a repeat without a call site folds into the row the user's line produced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Safari delivers synthesized input only to its frontmost window and reports success either way, so once the live dashboard took the front every click reached nothing and a login never left /login. The WDIO service, Nightwatch and Python adapters now run open -a Safari after session bring-up in live mode, for a local desktop Safari only: never an iOS session through Appium or one served from another host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nput Without document-start injection the collector was appended from the unawaited onCommand after get() returned, so typing that followed raced it. A navigation now resolves to the test only once the destination is instrumented. Before each element click, sendKeys, clear or submit on a Safari session the plugin brings the session's own window forward when it has lost focus (open -a Safari can surface the user's other Safari window instead), leaving a test inside a frame alone, and the dashboard opens behind Safari on macOS. Measured: 3 of 3 live Safari logins, against 0 of 2 before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The adapter subscribed to request and response events only, so a blocked beacon or refused connection stayed pending for the session. It now subscribes network.fetchError and closes the row with status 0 and the browser's reason, separately from the pair so an endpoint that lacks the event keeps the rest of network capture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md, the changeset, the Python release note and the Selenium README describe the collector, focus and wait() changes above, and replace the earlier note that blamed Safari's lost clicks on adapter flakiness. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A claim pushed out of the 500-request window listed its held Resource Timing row at once; when the page's own call then completed, the patch listed the same request again. The call's row now replaces the early one, or takes its id when that row was already drained so the dashboard replaces it in place, and the adapter-side list replaces a re-sent id the same way. The record methods return the row they pushed rather than the array's last entry, which a concurrent call could own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An appended script runs synchronously, so the injection now returns the readiness check itself and polls only when the collector is not up yet; pollUntilReady gains checkFirst for the same reason. On Safari and Appium, where every navigation injects, this removes at least 200 ms per navigation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A command with no call site of the test's own was folded into the previous row whenever it repeated it, which also swallowed a helper library's call. The patched wait() now runs its condition inside an AsyncLocalStorage, so only the commands that condition issues are marked as polls, per driver and per async chain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines
+502
to
+504
| const at = req.id | ||
| ? this.networkRequests.findIndex((r) => r.id === req.id) | ||
| : -1 |
Contributor
There was a problem hiding this comment.
Network capture slows over time
Every incoming request now runs findIndex over all requests saved so far, including new ids. Capturing N requests therefore takes roughly N²/2 comparisons instead of appending them directly. Busy pages and long runs spend increasing time draining network data. Use an id lookup so replacing a released row does not require scanning the whole recording.
This branch has not been deployed
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.
Summary
Reported on the launch post: network calls are not available for Safari and Appium. Network capture rode on WebDriver BiDi, or on Chrome's performance log in Nightwatch, and neither exists on Safari or on a session Appium serves, so those sessions showed an empty Network tab.
packages/script/src/collectors/resourceTiming.ts). Every browser keeps it: each load with its timing phases and size, on Chromium its status, never headers or bodies. The buffer is read at install, so loads made before the collector existed are included, then observed. The collector's existingfetch/XMLHttpRequestpatch claims each call it sees, so a request both saw is listed once with the patch's headers and body.webSocketUrl: trueas a BiDi endpoint. It is a boolean, not a socket address, and selenium's connection thread crashed on it.source: 'resource-timing', the Network tab colours a status-less row as unknown rather than pending, the detail card names what the browser does not expose, and the HAR entry carries_sourceand_resourceTypeso a reopened trace renders the same row rather than a failed request.Found on the way and fixed in its own commit: Selenium and Nightwatch traces on Chrome listed every BiDi request twice, a pending row beside the completed one. selenium-webdriver's
NetworkInspectorhands every network event to every subscriber's callback, so the request handler also received responses and the response handler received requests.packages/core/src/bidi.tsnow ignores the other handler's events by shape, keeps rows by request id so a redirect hop continues its row, and readsnetwork.fetchErroroff the inspector's own connection so a request the browser gives up on closes as a failure instead of staying pending.Native app sessions have no document and still have no network capture; only a device- or proxy-level capture sees that traffic.
Verification
pnpm test: 2599 tests across 196 files, green.pnpm lint: 0 errors.tsc --noEmit: clean.pnpm build: clean, and the workspace-package leak grep over every dist is empty.pnpm test:ui: 31 specs green.main.Follow-ups, not in this PR
examples/MOBILE.mdhere.CLAUDE.md. Network capture worked in every run.🤖 Generated with Claude Code