All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
[0.4.0] - 2026-06-04
Added
CDPEx.Page.navigate/3acceptsresponse: trueto return{:ok, page, %{status, url}}— the main document's HTTP status and final (post-redirect) URL, correlated to the navigation by itsloaderId. Lets callers detect a 403 wall / 404 / login-redirect instead of treating every navigation as success. Lazily enables theNetworkdomain; returns{:error, {:no_document_response, url}}when no document response arrives (#31).CDPEx.Page.wait_for_response/3blocks until a network response whose URL matches a function /Regex/ substring arrives, returning theNetwork.responseReceivedparams (HTTP status +requestIdfor a follow-upresponse_body/3) — for an XHR/fetchkicked off by aclick/3(#32).CDPEx.Page.wait_for_network_idle/2blocks until the network is idle (≤:max_inflightin-flight requests for:idle_timems continuously) — the Puppeteer "networkidle" primitive for waiting out SPA hydration (#32).:telemetryinstrumentation — CDPEx emits launch + navigate spans, page open/close events, and error events (Chrome exit, browser-connection down, WebSocket close). Silent by default (no handlers attached); seeCDPEx.Telemetryfor the event taxonomy. Adds a{:telemetry, "~> 1.2"}dependency (#33).
Breaking
CDPEx.Connection.await_event/4now returns{:ok, params}(the matched event's params) on a match, instead of a bare:ok(#32).- Request interception ↔
authenticate/4mutual exclusion is now enforced (it previously failed silently — returning a misleading:okwhile breaking the page, since both drive theFetchdomain). On the same page:enable_request_interception/2returns{:error, {:conflict, :authenticated}}when the page is authenticated,authenticate/4returns{:error, {:conflict, :intercepting}}when it's intercepting, and re-enabling interception returns{:error, :already_intercepting}(was:ok). Interception also now rejects a:session-transport page with{:error, {:unsupported_transport, :session}}, matchingauthenticate/4. Update any caller that only matched:okon these paths (#30).
Fixed
CDPEx.Pagerequest interception no longer bricks the page when its owner dies:CDPEx.Browsernow monitors the process that calledenable_request_interception/2and auto-Fetch.disables the page (off the Browser process, so a hung connection can't stall other pages) if that process exits without disabling — a crashed or forgetful caller can't leave every request paused with no resolver (#30).CDPEx.Page.authenticate/4no longer blocks theCDPEx.BrowserGenServer for the duration ofFetch.enable: the per-page Fetch handler now arms asynchronously and signals readiness, so the browser keeps serving other pages while a page authenticates.authenticate/4still returns only once interception is armed, preserving the "armed beforenavigate/3" guarantee (#36).- Compiles cleanly on Elixir 1.20 under
--warnings-as-errors(dropped an unusedrequire; pinned bitstring sizes in the test WebSocket parser). CI now covers Elixir 1.15 / 1.19 / 1.20.
0.3.0 - 2026-06-03
Added
CDPEx.Pool— a fixed-size pool of reusable browsers (checkout/2,checkin/2,with_browser/3,with_page/3) that keeps Chrome warm so a per-job fetch avoids a cold launch. Lazy launch up to:size, blocking checkout with timeout, automatic reclaim of a crashed caller's browser, and on-demand relaunch of a crashed one.CDPEx.Page.observe_network/2,stop_observing_network/2, andresponse_body/3— observe a page's network traffic (subscribe the caller toNetwork.requestWillBeSent/responseReceivedevents) and fetch a response body by requestId. Builds on the existing event-subscription machinery and the lazyNetwork.enable.CDPEx.Page.authenticate/4— answer proxy (--proxy-server) or HTTP Basic auth challenges with credentials, so authenticated proxies and Basic-auth-gated origins work. Backed by a per-pageCDPEx.Fetchhandler that enables theFetchdomain, auto-continues paused requests, and answersauthRequired(with a:sourcefilter and a bad-credentials loop guard). Supported on:dedicatedpages only — a:sessionpage returns{:error, {:unsupported_transport, :session}}(its handler would outlive the shared connection); an unknown:sourcereturns{:error, {:invalid_source, value}}. Authenticating a page from another browser (or an already-closed one) returns{:error, :unknown_page}, and re-arming an already-authenticated page returns{:error, :already_authenticated}.CDPEx.Pagerequest interception —enable_request_interception/2/disable_request_interception/2pause matching requests (deliveringFetch.requestPausedto the calling process), each resolved withcontinue_request/3(optionally rewriting url/method/headers/post-data),fulfill_request/3(a synthetic response), orfail_request/3(an error reason; unknown reasons return{:error, {:invalid_error_reason, value}}). Event-driven likeobserve_network/2; mutually exclusive withauthenticate/4per page (both drive theFetchdomain).CDPEx.error_reason/0— a documented (best-effort) type of the{:error, reason}shapes CDPEx returns, so consumers know which tagged kinds to match. Not closed/exhaustive — kinds like{:cdp_error, method, payload}wrap open data.
Breaking
- Error-reason shapes normalized toward tagged tuples (#20). A reason shape is a public contract — a matcher on the old bare atom silently falls through its catch-all with no compile/Dialyzer warning, so update any matchers:
CDPEx.Connection.await_event/4timeout:{:error, :timeout}→{:error, {:timeout, :await_event}}, sharing the{:timeout, _}shape withcall/5's{:timeout, method}.- A malformed
DevToolsActivePortat launch:{:error, :devtools_file_malformed}→{:error, {:devtools_file_malformed, excerpt}}, carrying the file's contents excerpt like its sibling{:debug_url_not_found, _}. - Base64 validation failures now carry the offending data:
CDPEx.Page.response_body/3returns{:error, {:invalid_response_body, excerpt}}(was:invalid_response_body),CDPEx.Page.pdf/2returns{:error, {:invalid_pdf_data, excerpt}}(was:invalid_pdf_data), andCDPEx.Page.screenshot/2returns{:error, {:invalid_screenshot_data, excerpt}}(was:invalid_screenshot_data) when Chrome sends a body / PDF / screenshot that isn't decodable base64. - Unchanged (still bare, intentional):
:noproc; the high-level "didn't happen in time":timeoutfromCDPEx.Pagewait_for_*andCDPEx.Pool.checkout/2; and the control-flow outcomes:unknown_page/:already_authenticated— self-describing states with no payload to carry.
Changed
CDPEx.Page.navigate/3andwait_for_navigation/2raiseArgumentErroron an unknown:wait_untilvalue instead of silently treating it as:network_almost_idle.
Fixed
CDPEx.Page.wait_for_navigation/2now waits via the same lifecycle machinery asnavigate/3(subscribe-before-wait, scoped to the page's session and thePage.lifecycleEventmethod) rather than a generic event matcher — so it can no longer be falsely resolved by another event method whose params happen to carry a matching"name".
0.2.2 - 2026-06-02
Added
- Documented
:launch_timeoutas a ceiling (not a fixed wait) onCDPEx.launch/1andCDPEx.with_page/3, plus a "Running in containers" README section (timeout tuning, the fresh-profile cold-start cost,/dev/shmsizing,--remote-allow-origins).
Changed
- Chrome readiness is now polled:
CDPEx.Chromechecks theDevToolsActivePortfile throughout the wait (not only at the deadline), so launch returns as soon as Chrome is reachable and:launch_timeoutacts as a ceiling rather than a fixed cost — robust to Chrome builds that don't print theDevTools listening on ws://…stderr line. - A launch that never exposes the DevTools endpoint now returns
{:error, {:debug_url_not_found, stderr_excerpt}}(was the bare atom:debug_url_not_found), carrying Chrome's captured stderr so the failure is self-diagnosing. Migration: code matching the bare:debug_url_not_foundatom (e.g. a retry classifier) must match{:debug_url_not_found, _}instead, or that error silently stops matching. CDPEx.with_page/3, given launch options, now contains a throwaway-browser crash: it returns{:error, reason}instead of letting the browser's linked exit propagate to and kill the caller. It briefly traps exits in the calling process for the duration of the call (see thewith_page/3docs for the foreign-EXIT caveat and escape hatch).
Fixed
CDPEx.Connectionno longer ignores an owning-process exit that arrives during the WebSocket upgrade — it aborts the connect at once instead of lingering until the upgrade timeout.CDPEx.Connectionnow stops when a WebSocket pong write fails (mirroring a failed command write), rather than continuing on a dead socket until the next command notices.CDPEx.Page.navigate/3prefers a just-arrived connection:DOWNover a best-effort readiness timeout when the two tie at the deadline, so a connection death surfaces as an error rather than a stale{:ok, page}.CDPEx.Browserno longer leaks Chrome if the browser connection drops in the window between connecting and its first subscribe — the init-time exit is caught and Chrome is reaped.CDPEx.Chromelaunch-failure cleanup waits for the OS process to exit before removing the temp profile (matchingstop/1), closing akill/rmrace.CDPEx.Connection.call/5andawait_event/4return the documented timeout tuple instead of crashing the caller if the outerGenServer.calldeadline fires first under scheduler starvation.CDPEx.Connectionaccumulates WebSocket upgrade headers across response chunks instead of replacing them (defensive; dormant for single-response101upgrades).
0.2.1 - 2026-06-02
Changed
CDPEx.Browsersetsshutdown: 10_000inchild_spec/1so a supervisor givesterminate/2enough time to reap Chrome.
Fixed
CDPEx.Protocol.parse_ws_url/1parses IPv6 hosts and raises a clearArgumentErroron a malformed URL (previously aMatchError).CDPEx.Connectionno longer crashes whencall/5/await_event/4is given a negative timeout (it fires immediately).CDPEx.Connectionteardown fails in-flight callers with{:error, {:ws_closed, _}}instead of{:error, :noproc}onclose/1.CDPEx.Page.navigate/3subscribes to lifecycle events before issuing the navigate, so a fast readiness event (e.g.loadon a cached/local page) can no longer be dropped — a register-after-navigate race.
0.2.0 - 2026-06-02
Added
- Opt-in
sessionIdmultiplexing:CDPEx.new_page(browser, transport: :session)drives many pages over the one browser WebSocket (default:dedicated= one socket per page).CDPEx.Connection.call/5andawait_event/4gain a:session_idoption. CDPEx.Page.wait_for_navigation/2— await a navigation lifecycle milestone without issuing a navigation (e.g. after a click that navigates).CDPEx.Page.wait_for_function/3— poll a JavaScript expression until it is truthy.CDPEx.Page.text/3,attribute/4,visible?/3— element text / attribute / visibility helpers.CDPEx.Page.cookies/2,set_cookies/3,clear_cookies/2— cookie get / set / clear (lazily enables theNetworkdomain).CDPEx.Page.set_extra_headers/3,set_user_agent/3— extra HTTP headers and User-Agent override.CDPEx.Page.set_viewport/4— viewport / device-metrics emulation.CDPEx.Page.pdf/2— render the page to PDF (Page.printToPDF); returns bytes or writes to:path.CDPEx.Page.call_function/4— call a JS function with JSON-serialized arguments.
Changed
- A clean browser-connection close (its socket dropping, e.g. Chrome exiting) now stops
CDPEx.Browserwith a:shutdownreason rather than a crash reason — no spurious error report on expected teardown. Abnormal connection failures still surface loudly.
Fixed
CDPEx.Connection.call/5andawait_event/4no longer crash the connection when given an:infinitytimeout (which is valid per thetimeout()spec).CDPEx.Connectionnow stops when its owning process exits, closing the socket even if the owner skipped its own teardown (e.g. a:brutal_kill).
0.1.0 - 2026-06-01
Added
- Initial OTP-native Chrome DevTools Protocol client.
CDPEx.launch/1andCDPEx.stop/1— supervised headless Chrome lifecycle.CDPEx.new_page/2,CDPEx.close_page/2,CDPEx.with_page/3.CDPEx.Page:navigate/3,wait_for_selector/3,evaluate/3,click/3,html/2,screenshot/2.