Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use Chrome DevTools Protocol (CDP) Network events from Selenium, enabled before navigation. Create one record per CDP requestId, use responseReceived for HTTP status and resource type, loadingFinished for completion, and loadingFailed for browser-level failures. Filter for the protocol resource types Script and Stylesheet rather than relying only on .js and .css filenames.

The implementation below captures initial loads, cache information, timing and failure text, then applies a policy that fails only when required first-party assets are broken.

What you need

  • Python 3 and Selenium 4 installed with pip install selenium.
  • Google Chrome (or Chromium) and a compatible Selenium driver. Selenium Manager can usually obtain the driver automatically.
  • A test page and a definition of which JavaScript and CSS resources are required. Analytics, advertising, chat and other optional third-party requests should normally be allowlisted rather than treated as test failures.

Chrome DevTools Network events are documented in the CDP Network specification. Selenium exposes the command bridge through driver.execute_cdp_cmd(); its Python WebDriver API is documented at selenium.webdriver.remote.webdriver.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Capture JavaScript and CSS requests in Selenium

Complete Python example using performance logs

This compatibility-oriented example enables the Network domain before calling get(), polls Chrome’s performance log, correlates events by request ID, and prints a report. The adapter deliberately keeps CDP parsing in one place so it can later be replaced with a WebDriver BiDi implementation.

import json
import time
from urllib.parse import urlsplit
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

TARGET = "https://example.com"

options = Options()
options.set_capability("goog:loggingPrefs", {"performance": "ALL"})
driver = webdriver.Chrome(options=options)

# Must happen before navigation, otherwise early requests can be missed.
driver.execute_cdp_cmd("Network.enable", {})
started = time.monotonic()
records = {}

try:
    driver.get(TARGET)
    # Allow late styles/scripts (for example, lazy modules) to emit events.
    time.sleep(2)

    for entry in driver.get_log("performance"):
        message = json.loads(entry["message"])["message"]
        method = message.get("method")
        params = message.get("params", {})
        if method not in {
            "Network.requestWillBeSent",
            "Network.responseReceived",
            "Network.loadingFinished",
            "Network.loadingFailed",
            "Network.requestServedFromCache",
        }:
            continue

        request_id = params.get("requestId")
        if not request_id:
            continue
        record = records.setdefault(request_id, {
            "request_id": request_id,
            "url": None,
            "resource_type": None,
            "status": None,
            "mime_type": None,
            "cache": False,
            "finished": False,
            "failed": False,
            "error_text": None,
            "encoded_bytes": None,
            "initiator": None,
        })

        if method == "Network.requestWillBeSent":
            request = params.get("request", {})
            record["url"] = request.get("url")
            record["resource_type"] = params.get("type")
            record["initiator"] = params.get("initiator")
        elif method == "Network.responseReceived":
            response = params.get("response", {})
            record["url"] = response.get("url") or record["url"]
            record["status"] = response.get("status")
            record["mime_type"] = response.get("mimeType")
            record["resource_type"] = params.get("type") or record["resource_type"]
        elif method == "Network.loadingFinished":
            record["finished"] = True
            record["encoded_bytes"] = params.get("encodedDataLength")
        elif method == "Network.loadingFailed":
            record["failed"] = True
            record["error_text"] = params.get("errorText")
            record["blocked_reason"] = params.get("blockedReason")
            record["canceled"] = params.get("canceled", False)
        elif method == "Network.requestServedFromCache":
            record["cache"] = True

    def is_script_or_stylesheet(item):
        if item["resource_type"] in {"Script", "Stylesheet"}:
            return True
        # Fallback for browsers/pages that omit the protocol type.
        url = (item["url"] or "").split("?", 1)[0].lower()
        return url.endswith(".js") or url.endswith(".css")

    elapsed = time.monotonic() - started
    print(f"Captured in {elapsed:.2f}s")
    for item in records.values():
        if is_script_or_stylesheet(item):
            print(json.dumps(item, indent=2, sort_keys=True))
finally:
    driver.quit()

The protocol type is preferable: bundlers frequently serve JavaScript from extensionless URLs, and query strings or redirects make suffix checks incomplete. MIME type (text/javascript, application/javascript or text/css) is another useful fallback when the type is unavailable.

Understand the event sequence

Create the request record

Network.requestWillBeSent is the first useful event. Store the request ID, URL, protocol resource type and initiator. The initiator helps distinguish a document’s own bundle from a dynamically inserted script or a stylesheet requested by another frame.

Read the HTTP result

Network.responseReceived supplies response.status, final URL, MIME type and headers. A 200, 304 or another HTTP response is still a response; decide whether it is acceptable according to your test policy. Redirects can produce multiple events, so retain the latest response data and inspect redirect details when needed.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Confirm completion

Network.loadingFinished means the browser finished receiving the resource. Record encodedDataLength when present. A response status alone does not prove that the transfer completed successfully.

Handle browser-level failures

Network.loadingFailed reports failures such as DNS errors, connection resets, blocked requests and cancellations. Capture errorText, blockedReason and the cancellation flag. Some canceled requests are expected during navigation or SPA route changes; do not automatically classify every cancellation as a defect.

Keep cache state

Network.requestServedFromCache marks a cache hit. Preserve that flag in reports instead of discarding the request. A cached asset can have different transfer characteristics from a fresh network load, and cache behavior is often the reason a failure is difficult to reproduce.

Turn observations into a useful test verdict

Fail only required assets

Build an explicit allowlist or predicate for required first-party JavaScript and CSS. For each matching record, fail when there is a loading failure, no response/completion within your collection window, or an HTTP status outside the statuses your application accepts. Keep optional analytics, advertisements, chat widgets and third-party embeds separate so their outages do not make the page test meaningless.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from urllib.parse import urlsplit

REQUIRED_HOSTS = {"example.com", "static.example.com"}

def required_asset(item):
    if item["resource_type"] not in {"Script", "Stylesheet"}:
        return False
    host = urlsplit(item["url"] or "").hostname
    return host in REQUIRED_HOSTS

failures = []
for item in records.values():
    if not required_asset(item):
        continue
    status = item["status"]
    if item["failed"]:
        failures.append((item["url"], "browser failure: " + str(item["error_text"])))
    elif status is None:
        failures.append((item["url"], "no response event"))
    elif not (200 <= status < 400):
        failures.append((item["url"], f"HTTP {status}"))
    elif not item["finished"]:
        failures.append((item["url"], "response received but loading did not finish"))

if failures:
    raise AssertionError("Required asset failures:n" + "n".join(f"{u}: {r}" for u, r in failures))

Use URL host/path rules that match your deployment, and make the policy visible in source control. The CDP API does not define what your application considers a pass; that is a test decision.

Capture requests beyond the first page load

The initial get() call is only one lifecycle. Clicks, form submissions, redirects, iframe navigations, XHR/fetch calls and single-page-app route changes can create later script and stylesheet events. Drain the performance log after each action, or run a polling loop until an application-specific condition is met.

def drain_network_events(driver, seconds=3):
    end = time.monotonic() + seconds
    while time.monotonic() < end:
        for entry in driver.get_log("performance"):
            yield json.loads(entry["message"])["message"]
        time.sleep(0.1)

# Example:
driver.find_element("css selector", "a.next").click()
for event in drain_network_events(driver, 5):
    if event.get("method") == "Network.loadingFailed":
        print("Later failure:", event["params"])

Choose an end condition tied to the page (for example, a known selector becoming visible) rather than an arbitrary sleep when test reliability matters. A short grace period is still useful for late modules and styles.

Performance, reliability and compatibility

  • Enable early: call Network.enable before navigation and before any click that triggers the requests you care about.
  • Bound collection: performance logs accumulate. Drain them regularly on long-running tests and retain only fields needed for your report.
  • Use stable identifiers: correlate every event with requestId; URL-only matching breaks when the same asset is requested more than once.
  • Separate browser failure from HTTP failure: an HTTP 404 is a server response, while loadingFailed represents a browser/network failure. Report both distinctly.
  • Account for frames and workers: include frame or initiator information when debugging resources that do not belong to the top document.
  • Expect version differences: event fields and Selenium adapters can vary with Chrome and Selenium versions. Log unknown fields rather than assuming they exist.

Selenium documents that direct CDP methods are transitional: “While Selenium 4 provides direct access to the Chrome DevTools Protocol, these methods will eventually be removed when WebDriver BiDi implemented.” Keep the event-processing contract isolated so you can move to Selenium’s BiDi session APIs as support matures. See Selenium’s CDP network features documentation and the Python CDP module reference at selenium.webdriver.common.bidi.cdp.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting common results

No performance entries appear

Verify the goog:loggingPrefs capability, call Network.enable before get(), and confirm that the browser is Chrome/Chromium. On remote grids, make sure the provider permits performance logging and CDP commands.

A known 404 is missing

Check whether the URL is extensionless or generated dynamically; use the CDP resource type rather than a suffix filter. Also inspect whether you drained the log after the request occurred.

The script is listed but has no status

You may have captured requestWillBeSent without yet receiving the response, or the request may have failed. Wait for the page’s real readiness condition, then check for loadingFailed and report incomplete records separately.

Everything fails on a single-page app

Do not stop collection immediately after get(). Drain events around route changes and user actions, and allow expected cancellations caused by superseded navigations.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A cached resource appears to have no transfer

Retain the requestServedFromCache marker and interpret it separately from a network download. Clear cache only when a test specifically requires a cold-cache scenario.

Third-party errors make tests noisy

Restrict the failure predicate to required first-party hosts and paths. Keep a second, non-blocking report for optional resources so you still see regressions without failing every build.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server when your goal is a reliable page capture rather than Selenium-level request assertions. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; bot checks, blank pages, timeouts and failed loads are not billed, and each response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP tools—take_screenshot, get_page_info and capture_pdf—work with Claude, Cursor and other MCP clients.

One request is enough (see the ScreenshotNeo API documentation):

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently asked questions

Can Selenium tell me which request returned 404?

Yes. Match the request by requestId, then read response.status from its responseReceived event and print the URL.

Should I treat a 304 as a failure?

Usually not: 304 is a valid cache-validation response. Your application policy should decide which statuses are acceptable.

Why prefer CDP resource types over file extensions?

Modern build systems often serve generated or extensionless URLs, so protocol classification is less dependent on naming conventions.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Is this limited to JavaScript and CSS?

No. The same event stream can be retained for images, fonts, documents and other protocol resource types; this article filters the report to scripts and stylesheets.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.