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.12.0 - 2026-10-22
Changed
- Return values change for callers that matched the old failure shapes (details under Fixed):
with_page/3raisesArgumentErrorfor an invalid launch option instead of returning{:error, {%ArgumentError{}, _}}(#127).- Under a
:deadline,with_page/3raises forlaunch_timeout: nilor:infinity, which the deadline used to replace (#127). - A
:sessionpage's waits on a closed socket return{:error, {:ws_closed, _}}instead of{:error, :noproc}(#124). - A
:sessionpage whose target closed or detached fails its calls and waits with{:error, {:session_closed, session_id}}instead of running to{:error, {:timeout, _}}(#123). - Calls that used to exit or raise on a gone or dying process now return
{:error, _}or:ok, so arescue/catch :exitaround them no longer fires (#124).
Fixed
- Runtime failures now come back as
{:error, reason}instead of a raise or an exit in the caller, sowith_page/3no longer needs arescue/catch :exitaround it. Invalid arguments still raise, and a raise, throw or exit fromwith_page/3's ownfunstill propagates. The guarantee covers return values, not links: a browser started withlaunch/1orconnect/2stays linked to the process that started it, so a browser that stops on its own (Chrome died) still sends that process an exit signal (#124).- A failed
CDPEx.launch/1,connect/2orConnection.start_link/2returns its error to a caller that doesn't trap exits instead of killing it, and on OTP 24/25 no longer leaves a stray{:EXIT, pid, reason}in the caller's mailbox (it did after #120). A Chrome that can't be spawned, or whose profile dir can't be created, returns the new
{:error, {:launch_failed, {:spawn | :user_data_dir, reason}}}(classified:unknown).new_page/2,close_page/2andPage.authenticate/4on a browser that is gone or dies mid-call return{:error, :noproc}, and a request it can't serve in time returns{:error, {:timeout, :new_page | :close_page | :authenticate}}. An abandonedauthenticate/4still releases its half-armed handler.with_page/3andPool.with_page/3on such a browser return the same.Connection.call/5params that JSON can't encode (e.g. a non-UTF-8 binary) return{:error, {:invalid_args, _}}instead of crashing the connection and every page on it.Connection.subscribe/4returns{:error, :noproc},{:error, {:timeout, :subscribe}}or{:error, {:ws_closed, reason}}instead of exiting, so a:sessionpage'sobserve_network/2and similar waits on a closed socket now return{:error, {:ws_closed, reason}}where they returned{:error, :noproc}(still:transient);unsubscribe/2is:okon a gone connection;call/5,await_event/4andPool.checkout/2return{:error, :noproc}for a process killed mid-call.CDPEx.stop/1,Pool.stop/1andConnection.close/1are:okon a process that is already gone, and log a warning when it stopped with anything other than a clean or already-gone reason.- The relay behind
Page.authenticate/4is labelled{:cdp_authenticate, target_id}.
- A failed
- An invalid launch option (e.g.
extra_args: [123],window_size: {0, 600},headless: nil) raisesArgumentErrorin the caller ofCDPEx.launch/1,with_page/3,Browser.start_link/1,Pool.start_link/1andChrome.launch/1. It used to raise inside the browser process during startup, which killed a caller that doesn't trap exits;with_page/3returned it as{:error, {%ArgumentError{}, stacktrace}}instead, so match on the raise now. Under a:deadline,launch_timeout: nilor:infinityused to be replaced by the deadline and now raises too (#127). - A
transport: :sessionpage whose target closed or detached no longer leaves its calls and waits running to their own timeouts. Calls fail with the new{:error, {:session_closed, session_id}}(classified:transient, like a dedicated page's:noproc): one in flight when the session detaches fails at once, and a later one fails on Chrome's "session not found" reply, which used to be dropped because it carries nosessionId.navigate/3,wait_for_navigation/2,wait_for_network_idle/2,wait_for_response/3and session-scopedConnection.await_event/4waiters end with the same error on the detach, andwait_for_selector/3,wait_for_function/3andclick/3 :waitstop retrying.with_page/3's:deadline(andCDPEx.Pool.with_page/3's) now returns near the deadline on session pages too. AConnection.subscribe/4to"Target.detachedFromTarget"(or:all) scoped to a session now also receives that session's own detach (#123).
0.11.0 - 2026-10-21
Added
CDPEx.Page.click/3:waitoption: a total budget (ms) for the element to appear and get a clickable box. The whole locate + click is retried, 100ms apart, while the element is missing, has no box, or the page has no execution context mid-navigation; any other error returns at once, and nothing is retried after the mouse events are dispatched, so a click never fires twice. Defaults to0(one attempt, as before);:timeoutis still the per-call timeout (#119).CDPEx.Page.click_text/3: click an element by its text content (textContent, whitespace-normalized like XPathnormalize-space(), exact match). Clicks the innermost match, resolving a boxless one (an SVG<title>, adisplay: contentsspan) to its clickable HTML ancestor, and prefers a visible target so a hidden duplicate (display: none,visibility: hidden,opacity: 0) doesn't win. Takes:selector,:waitand:trusted. Returns the new{:error, {:text_not_found, text}}(:terminal) when nothing matches (#119).CDPEx.Page.scroll/3: scroll the window to:top,:bottomor{x, y}, instantly even underscroll-behavior: smooth(#119).CDPEx.with_page/3takes a:deadline(ms) bounding the whole call, launch included. Iffunhasn't returned by then, the browser resource is torn down underneath it (a throwaway browser is stopped; with a browser pid only the page is closed) andwith_pagereturns{:error, :deadline_exceeded}, a new:transientreason.funkeeps running in the calling process. Emits[:cdp_ex, :with_page, :deadline_exceeded]. See thewith_page/3docs for what it can't interrupt (#120).
Fixed
CDPEx.with_page/3with launch options no longer kills a caller that doesn't trap exits when the launch fails; it returns{:error, reason}. On OTP 24/25 the failed start's{:EXIT, pid, reason}is left in the caller's mailbox instead (#120).
Changed
- CDPEx's own lockfile now uses
mint1.11.0 (andhpax1.1.0), which fix EEF-CVE-2026-91043 (HPACK-indexed cookie fields bypassmax_header_list_size), EEF-CVE-2026-92103 (HTTP/2 oversized frames buffered before themax_frame_sizecheck), and EEF-CVE-2026-94194 (HTTP/1 chunked framing applied when chunked is not the final transfer coding). This release doesn't raise the version floor, so runmix deps.update mint hpaxin your project to pick up the fixes (#112).
0.10.0 - 2026-09-21
Added
Process labels on every GenServer and task CDPEx starts, so a pid in a crash report,
:observer, or Phoenix LiveDashboard identifies itself:{:cdp_browser, os_pid}(the Chrome OS pid;:startingbefore it is known,:connectedin connect-mode,:launchedwhen the OS pid is unavailable),{:cdp_connection, target}(the DevTools path, which distinguishes the browser socket from a dedicated page socket; the query string is stripped since it can carry an auth token),{:cdp_page_helper, :idle_wait | :capture, target_id}for the helpers behindwait_for_network_idle/2andnavigate/3withresponse: true,{:cdp_fetch_auth, source},{:cdp_fetch_disable, conn}, and{:cdp_pool, size}/{:cdp_pool_launch, pool}. TheTask.SupervisorinsideCDPEx.Poolstays unlabelled — OTP offers no way to label a supervisor from outside it. Labels are set on Elixir 1.17+ and compile to a no-op on 1.15/1.16; see the README's "Process labels" section (#103).[:cdp_ex, :network_idle, :watching]telemetry event — fires whenCDPEx.Page.wait_for_network_idle/2's helper has finished setting up (subscribed,Networkenabled, events that arrived during enable discarded) and starts counting in-flight requests. Everything before it is setup, so a request that starts earlier is deliberately not counted. Measurements%{system_time}, metadata%{session_id}(nilfor a dedicated page connection). Silent by default like every other CDPEx event; seeCDPEx.Telemetry(#101).
Changed
- Bumped
mintto 1.10.1 for EEF-CVE-2026-82672 (HTTP/1 chunk-size tail response smuggling), whichmix hex.audithad started failing CI on (#107).
Fixed
CDPEx.Page.navigate/3no longer ends its readiness wait on a lifecycle milestone that belongs to a previous document. A fresh page'sabout:blankemitsnetworkAlmostIdle~500ms after the target is created (with the oldloaderId), and Chrome replays lifecycle events onPage.setLifecycleEventsEnabled; either could land mid-navigation and be matched on name alone. Withresponse: true(#31) that closed the capture window before the main-document response and surfaced as an intermittent{:error, {:no_document_response, url}}instead of the real status (seen in CI on a loaded runner); on the default path it could return{:ok, page}before the real document had loaded. The milestone is now pinned to the navigation'sloaderId(+frameId) on both paths, and re-pinned to a successor document when one replaces ours after it commits (a<meta http-equiv="refresh">, a JSlocationchange, a waiting-room bounce — the original document never reaches network-quiet, so without the re-pin such pages would only return at the deadline; Puppeteer'sLifecycleWatcherreaches the same outcome by accepting any loader other than the pre-navigation one). Withresponse: truethe reported%{status, url}stays that of the document you navigated to, as before. Correlation only rejects when both sides carry an id and they differ, so an absentloaderId(optional on thePage.navigateresult) never makes the milestone unmatchable.wait_for_navigation/2has noloaderIdto pin to (the caller triggered the navigation) and keeps its name-only match (#106).
0.9.0 - 2026-06-14
Breaking
CDPEx.Page.click/3now dispatches a trustedInputmouse event by default (realevent.isTrusted, hit-tested at the element's center after scroll-into-view) instead of a syntheticel.click(). Passtrusted: falsefor the old synthetic behavior. A trusted click can newly fail with{:error, {:not_clickable, css}}for an element with no usable box (zero-size / off-screen even after scroll) (#72).CDPEx.Protocol.parse_ws_url/1now returns a 4-tuple{scheme, host, port, target}(was{host, port, path}) and acceptswss://again (reversing the 0.8.0 rejection), enabling TLS DevTools endpoints / cloud browser providers behindconnect/2. The added leadingschemeelement is a breaking change for any caller destructuring the old 3-tuple — update{host, port, path} = parse_ws_url(url)to{_scheme, host, port, target} = parse_ws_url(url). The fourth element is now the WebSocket request target (path plus any?query), so a token-bearing cloud endpoint (wss://host/cdp?token=…) keeps its query through the upgrade (#73).
Added
CDPEx.connect/2— drive an already-running Chrome instead of launching one. Accepts aws:///wss://browser URL or anhttp:///https://base URL (discovered viaGET /json/version). Returns the same handle aslaunch/1;stop/1closes only the pages cdp_ex opened and never reaps the remote Chrome. Pages default to:sessiontransport (:dedicatedover a connected browser is not yet supported — returns{:error, {:unsupported_transport, :dedicated}}).wss://is verified against the OS trust store (:public_key.cacerts_get()), with:insecure/:cacertfile/:cacertsescape hatches;:discovery_timeoutbounds a slow remote/json/version. A failed endpoint discovery returns{:error, {:connect_discovery_failed, reason}}(classified:unknown). Combining:proxywith:connectis rejected with{:error, {:unsupported_with_connect, :proxy}}(a--proxy-serverflag can't apply to a Chrome cdp_ex didn't launch).with_page([connect: endpoint], fun)is the one-shot form (#73).CDPEx.Page.type/4— focus an element and enter text viaInput.insertText(#72).CDPEx.Page.press/4— press a named key (Enter,Tab,Escape,Backspace,Delete, the arrows,Home,End) with realkeyDown/keyUpevents; anilselector targets the currently-focused element. An unsupported key returns{:error, {:unknown_key, key}}(#72).
Fixed
- The page WebSocket URL now brackets an IPv6 host (
ws://[::1]:9222/…) instead of producing a malformedws://::1:9222/…, so a:dedicatedpage can be opened against a Chrome bound to an IPv6 address (#73). CDPEx.Page.observe_network/2now scopes its subscription to the page's session, so on a:session-transport connection a caller receives only that page'sNetworkevents instead of every session's on the shared socket.wait_for_network_idle/2— which already filtered other sessions out in its own receive loop — is now scoped at the source too, so it no longer receives siblings' events at all.CDPEx.Connection.subscribe/4gains asession_id:option (defaultnil= all sessions, the prior behaviour) (#27).
0.8.0 - 2026-06-14
Breaking
CDPEx.Page.evaluate/3(andcall_function/3, which delegates to it) now return{:error, {:unserializable_value, uv}}for a result Chrome can only express as anunserializableValue(NaN/Infinity/-0/BigInt), carrying the raw string. These previously fell through to{:error, {:unexpected_evaluate, _}}— which now means only an unrecognized result envelope. Update any matcher; the new reason classifies as:terminal. This lets callers tell a recoverable unserializable value from a malformed result (#75).
Changed
CDPEx.Protocol.parse_ws_url/1now rejectswss://(and any non-ws://scheme) with anArgumentError. CDPEx launches a local Chrome and speaks plaintextws://to itsDevToolsActivePort, and there is no public connect-to-remote API, so the connect path was always plaintext — acceptingwss://only to fail at connect was a latent contract mismatch. Connecting to a remote/TLS DevTools endpoint is tracked in #73.CDPEx.Page.click/3doc now states it dispatches a synthetic DOM.click()(not a trusted OS-level input event); realInput-domain dispatch is tracked in #72.CDPEx.Page.evaluate/3/CDPEx.Protocol.evaluate_result/1docs now describe the realreturnByValueoutcomes (verified against live Chrome): a DOM node or function serializes lossily to{:ok, %{}}; an unserializable number (NaN/Infinity/-0/BigInt) surfaces as{:error, {:unserializable_value, _}}(see Breaking, above); a value Chrome can't serialize (window, a circular object, aSymbol) fails the call as{:error, {:cdp_error, "Runtime.evaluate", _}}. Covered by unit, doctest, and integration tests.
0.7.0 - 2026-06-06
Added
CDPEx.launch/1(andwith_page/3,CDPEx.Pool) accept a:proxyoption — a URL ("http://user:pass@host:8080") or keyword list ([server:, scheme:, username:, password:]). It sets Chrome's--proxy-serverand, for an authenticated proxy, automatically answers the auth challenge on each:dedicatedpage, so callers justnew_page+navigate(no manualauthenticate/4). A credentialed proxy requires:dedicatedtransport (:sessionreturns{:error, {:unsupported_transport, :session}}) and can't combine withenable_request_interception/2on the same page (both driveFetch); a malformed value — or combining:proxywith a full:argsoverride (which would discard the injected--proxy-server) — fails the launch with{:error, {:invalid_proxy, _}}. Parsing lives in the newCDPEx.Proxymodule (#62).CDPEx.Page.set_user_agent/3accepts:user_agent_metadata(a CDPEmulation.UserAgentMetadatamap) and:accept_language, passed through toEmulation.setUserAgentOverride. Overriding only the UA string leaves the UA Client Hints surface (navigator.userAgentData, theSec-CH-UA*headers) at Chrome's defaults — a visiblenavigator.userAgent↔ Client-Hints mismatch;:user_agent_metadatakeeps them consistent (#34).
0.6.0 - 2026-06-04
Added
CDPEx.classify_error/1andCDPEx.transient?/1— classify any{:error, reason}CDPEx returns as:transient(a fresh attempt may succeed — connection dropped or couldn't be established, timeout, Chrome died or was slow to start, an internal helper crashed, or a connection-layernet::ERR_*navigation failure such asERR_CONNECTION_REFUSED/ERR_TIMED_OUT),:terminal(deterministic — selector miss, JS exception, usage/validation error, missing Chrome binary), or:unknown(payload- or timing-dependent — an ambiguousnet::ERR_*navigation error such as DNS / blocked / aborted, a CDP error code, or a no-document navigation — you decide). The library owns the transient/terminal decision and tracks the error surface as it evolves, so retry logic isn't reimplemented (and re-drifted) in every caller. The return type is the namedCDPEx.error_classification/0. See the "Error handling" section of theCDPExdocs (#56). Consumer note:transient?/1treats:unknownas not transient (conservative); retries remain the caller's responsibility (bound attempts, back off, and re-establish the resource — don't retry a dead handle).CDPEx.error_reason/0reconciled with what the public ops actually return: added{:capture_failed, _}/{:idle_wait_failed, _}(helper crashes fromnavigate/3response: trueandwait_for_network_idle/2, returned since 0.5.0) and{:ws_connect, _}/{:ws_upgrade, _}(browser-socket connect/upgrade failures fromlaunch/1andnew_page/2) — all previously missing from the aggregate type. No runtime change. A compile-time coverage test now fails if anyerror_reason/0member lacks aclassify_error/1exemplar, so a documented member can't be added to the type without being classified (the reverse direction — an error the code produces but never adds to the type — stays a review responsibility) (#56).
0.5.0 - 2026-06-04
Changed
CDPEx.Poolnow launches browsers asynchronously. A cold Chrome start used to run synchronously inside the pool process, blocking every other checkout, checkin, and timeout for its duration (a few seconds), so a short:checkout_timeoutwas unreliable while the pool grew to:size. Each launch now runs in its own task: the pool stays responsive during warmup, launches for simultaneous waiters run concurrently, and:checkout_timeoutis honored even mid-launch. Behavior is otherwise unchanged —count + in-flight launchesnever exceeds:size, a launch failure surfaces to the caller, and a launched browser is adopted (re-linked) by the pool so its crash handling andterminatereaping are preserved (#22).
Fixed
CDPEx.Page.navigate/3withresponse: trueno longer disturbs a same-processobserve_network/2subscription. The document-response capture now runs in a short-lived, isolated helper process with its ownNetwork.responseReceivedsubscription and mailbox, so a caller that is also observing the network on the same page keeps its subscription and its buffered events intact (previously the navigation unsubscribed the caller and drained those events). Cross-process observation was already unaffected (#42).CDPEx.Page.wait_for_network_idle/2no longer disturbs a same-processobserve_network/2subscription — the same fix as #42 applied to the idle wait. The in-flight tracking now runs in an isolated helper process, so a caller observing the network on the same page keeps its subscription and buffered events intact (previously the idle wait tore down the overlapping request-lifecycle subscriptions and drained those events). An abnormal crash of the internal helper surfaces as{:error, {:idle_wait_failed, reason}}(#48).CDPEx.Page.authenticate/4no longer leaves a page stuck on{:error, :already_authenticated}when its caller times out. If the 15s call deadline elapses during a slow arm (severe scheduler starvation), the caller used to depart while the page stayed armed with no way to recover but closing it. TheCDPEx.Browsernow monitors the authenticating caller and, on its departure, cancels the orphanedFetchhandler — which disablesFetch(releasing any paused request) and stops, clearing the page's auth entry so it can be authenticated again (#40).CDPEx.Page.navigate/3andwait_for_navigation/2now treat:timeoutas a true overall ceiling. Previously the lazyNetwork.enableand the event subscription each drew from a fresh budget on top of the navigation wait, so a slow connection could block well past:timeout(worst case ~2×). One absolute deadline now spans enable + subscribe + navigate + the readiness/capture wait — matchingwait_for_response/3andwait_for_network_idle/2.CDPEx.Connection.subscribe/3gained an optionaltimeoutso the subscription can be folded into that deadline, and the internal capture/idle helper enforces the deadline with a force-kill backstop (#49).
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.