Browser and Driver Logs
Turn on the logs that explain failures: browser console output via BiDi, chromedriver and geckodriver logs, Selenium's own client logging, network failures, and how to collect them per test.
A screenshot shows what the page looked like; logs show why. There are four sources, each answering a different question:
| Log | Answers | How to get it |
|---|---|---|
| Browser console | Did the app throw or log an error? | BiDi script handlers (all browsers) or the legacy browser log type (Chromium only) |
| Driver log | What did chromedriver or geckodriver do with each command? | Service log options |
| Selenium client log | What did the binding send and receive? | Language logging framework |
| Network | Which requests failed or were slow? | BiDi network events |
Browser Console via BiDi
The cross-browser way and the one to use in new code. Register handlers once per driver and collect into a list you can attach to the report or assert on.
// options.setCapability("webSocketUrl", true) when creating the driverpublic final class ConsoleCollector { private final List<String> entries = new CopyOnWriteArrayList<>();
public ConsoleCollector(RemoteWebDriver driver) { driver.script().addConsoleMessageHandler(e -> entries.add(String.format("%s [%s] %s", e.getTimestamp(), e.getLevel(), e.getText()))); driver.script().addJavaScriptErrorHandler(e -> entries.add(String.format("%s [uncaught] %s%n%s", e.getTimestamp(), e.getText(), e.getStackTrace()))); }
public List<String> errors() { return entries.stream().filter(s -> s.contains("[error]") || s.contains("[uncaught]")).toList(); } public String dump() { return String.join(System.lineSeparator(), entries); }}
// In a testConsoleCollector console = new ConsoleCollector(driver);// ... scenario ...assertTrue("Console errors:\n" + console.dump(), console.errors().isEmpty());# options.enable_bidi = Trueclass ConsoleCollector: def __init__(self, driver): self.entries = [] driver.script.add_console_message_handler( lambda e: self.entries.append(f"{e.timestamp} [{e.level}] {e.text}")) driver.script.add_javascript_error_handler( lambda e: self.entries.append(f"{e.timestamp} [uncaught] {e.text}\n{e.stack_trace}"))
@property def errors(self): return [s for s in self.entries if "[error]" in s or "[uncaught]" in s]
def dump(self): return "\n".join(self.entries)
# pytest fixture@pytest.fixturedef console(driver): return ConsoleCollector(driver)
def test_checkout(driver, console): ... assert not console.errors, "Console errors:\n" + console.dump()// new chrome.Options().enableBidi()class ConsoleCollector {entries = [];static async attach(driver) { const c = new ConsoleCollector(); await driver.script().addConsoleMessageHandler((e) => c.entries.push(`${e.timestamp} [${e.level}] ${e.text}`)); await driver.script().addJavaScriptErrorHandler((e) => c.entries.push(`${e.timestamp} [uncaught] ${e.text}\n${e.stackTrace}`)); return c;}get errors() { return this.entries.filter((s) => s.includes('[error]') || s.includes('[uncaught]')); }dump() { return this.entries.join('\n'); }}
const console = await ConsoleCollector.attach(driver);// ... scenario ...assert.deepStrictEqual(console.errors, [], 'Console errors:\n' + console.dump());// new ChromeOptions { UseWebSocketUrl = true }public sealed class ConsoleCollector{ private readonly ConcurrentQueue<string> _entries = new();
public static async Task<ConsoleCollector> AttachAsync(IWebDriver driver) { var c = new ConsoleCollector(); var bidi = await driver.AsBiDiAsync(); await bidi.Log.OnEntryAddedAsync(e => c._entries.Enqueue($"{e.Timestamp} [{e.Level}] {e.Text}")); return c; }
public IEnumerable<string> Errors => _entries.Where(s => s.Contains("[error]")); public string Dump() => string.Join(Environment.NewLine, _entries);}
var console = await ConsoleCollector.AttachAsync(driver);// ... scenario ...Assert.That(console.Errors, Is.Empty, "Console errors:\n" + console.Dump());The legacy driver.manage().logs().get("browser") API still works on Chromium and returns everything since the last call, but it is Chromium-only and not part of the W3C specification; Java removed its deprecated logging classes in 4.45. Treat it as a fallback for old suites.
Driver Logs: chromedriver and geckodriver
When Selenium says element not interactable and you disagree, the driver log tells you what the driver actually checked. Enable it through the service builder; write to a file per test run.
// ChromeChromeDriverService chromeService = new ChromeDriverService.Builder() .withLogLevel(ChromiumDriverLogLevel.DEBUG) // ALL, DEBUG, INFO, WARNING, SEVERE, OFF .withLogFile(new File("target/logs/chromedriver.log")) .withAppendLog(true) .withReadableTimestamp(true) .build();WebDriver chrome = new ChromeDriver(chromeService, new ChromeOptions());
// FirefoxGeckoDriverService geckoService = new GeckoDriverService.Builder() .withLogLevel(FirefoxDriverLogLevel.TRACE) // TRACE, DEBUG, CONFIG, INFO, WARN, ERROR, FATAL .withLogFile(new File("target/logs/geckodriver.log")) .withTruncatedLogs(false) // keep long messages intact .build();WebDriver firefox = new FirefoxDriver(geckoService, new FirefoxOptions());from selenium.webdriver.chrome.service import Service as ChromeServicefrom selenium.webdriver.firefox.service import Service as FirefoxService
# Chromechrome_service = ChromeService( log_output="logs/chromedriver.log", service_args=["--log-level=DEBUG", "--append-log", "--readable-timestamp"],)chrome = webdriver.Chrome(service=chrome_service)
# Firefoxfirefox_service = FirefoxService( log_output="logs/geckodriver.log", service_args=["--log", "trace", "--log-no-truncate"],)firefox = webdriver.Firefox(service=firefox_service)
# Log to stdout instead (useful in CI): log_output=sys.stdoutconst chrome = require('selenium-webdriver/chrome');const firefox = require('selenium-webdriver/firefox');
// Chromeconst chromeService = new chrome.ServiceBuilder().loggingTo('logs/chromedriver.log').enableVerboseLogging();const chromeDriver = await new Builder().forBrowser('chrome').setChromeService(chromeService).build();
// Firefoxconst ffService = new firefox.ServiceBuilder().enableVerboseLogging(true); // trace levelconst ffDriver = await new Builder().forBrowser('firefox').setFirefoxService(ffService).build();// Chromevar chromeService = ChromeDriverService.CreateDefaultService();chromeService.LogPath = "logs/chromedriver.log";chromeService.EnableVerboseLogging = true;chromeService.EnableAppendLog = true;var chrome = new ChromeDriver(chromeService, new ChromeOptions());
// Firefoxvar ffService = FirefoxDriverService.CreateDefaultService();ffService.LogLevel = FirefoxDriverLogLevel.Trace;ffService.LogPath = "logs/geckodriver.log";var firefox = new FirefoxDriver(ffService, new FirefoxOptions());Driver logs at DEBUG or TRACE are large (tens of MB per test). Enable them for a specific failing test, not the whole suite, and delete them after reading.
Reading a chromedriver log: each command appears as [COMMAND] with its JSON, then [RESPONSE]. For a click, look for the interactability check and the coordinates it computed; if another element is at those coordinates, you have found your overlay.
Selenium Client Logging
The binding’s own log shows the HTTP traffic it sends: the command, the request body, the response. It is the source of truth when you suspect the binding, a proxy or a Grid is misbehaving.
// Selenium Java uses java.util.loggingimport java.util.logging.*;
Logger root = Logger.getLogger("org.openqa.selenium");root.setLevel(Level.FINE);ConsoleHandler handler = new ConsoleHandler();handler.setLevel(Level.FINE);root.addHandler(handler);
// Or route to SLF4J/Logback with the jul-to-slf4j bridge and configure there.// Level FINE shows each command; FINER includes request/response bodies.import logging
logging.basicConfig(level=logging.WARNING)logging.getLogger("selenium").setLevel(logging.DEBUG)
# Just the wire protocol:logging.getLogger("selenium.webdriver.remote.remote_connection").setLevel(logging.DEBUG)
# Also useful: urllib3 to see HTTP connection reuse and retrieslogging.getLogger("urllib3").setLevel(logging.DEBUG)const { logging } = require('selenium-webdriver');
// Log every command and response to stderrlogging.installConsoleHandler();logging.getLogger('webdriver.http').setLevel(logging.Level.ALL);
// Or set SELENIUM_REMOTE_LOGGING or DEBUG=selenium-webdriver:* depending on version// .NET binding logs through OpenQA.Selenium.Internal.Logging (4.12+)using OpenQA.Selenium.Internal.Logging;
Log.SetLevel(LogEventLevel.Trace); // Trace shows request and response bodiesLog.Handlers.Add(new FileLogHandler("logs/selenium.log"));// Console: Log.Handlers.Add(new ConsoleLogHandler());Network Failures via BiDi
A 500 from an API explains far more failures than a missing element does. Collect failed responses the same way as console output.
import org.openqa.selenium.bidi.module.Network;
Network network = new Network(driver);List<String> failures = new CopyOnWriteArrayList<>();
network.onResponseCompleted(e -> { long status = e.getResponseData().getStatus(); if (status >= 400) failures.add(status + " " + e.getRequest().getMethod() + " " + e.getResponseData().getUrl());});network.onFetchError(e -> failures.add("FETCH ERROR " + e.getRequest().getUrl() + ": " + e.getErrorText()));
// After the scenario:if (!failures.isEmpty()) System.out.println("Network failures:\n" + String.join("\n", failures));failures = []
def on_response(response): if response.status >= 400: failures.append(f"{response.status} {response.url}")
driver.network.add_response_handler(on_response)
# After the scenario:if failures: print("Network failures:\n" + "\n".join(failures))const { Network } = require('selenium-webdriver/bidi/network');
const network = await Network(driver);const failures = [];await network.responseCompleted((e) => {if (e.response.status >= 400) failures.push(`${e.response.status} ${e.request.method} ${e.response.url}`);});await network.fetchError((e) => failures.push(`FETCH ERROR ${e.request.url}: ${e.errorText}`));var bidi = await driver.AsBiDiAsync();var failures = new ConcurrentBag<string>();
await bidi.Network.OnResponseCompletedAsync(e =>{ if (e.Response.Status >= 400) failures.Add($"{e.Response.Status} {e.Request.Method} {e.Response.Url}");});await bidi.Network.OnFetchErrorAsync(e => failures.Add($"FETCH ERROR {e.Request.Url}: {e.ErrorText}"));Putting It in the Failure Hook
Combine everything into one evidence bundle written when a test fails: screenshot, page source, console entries, network failures, and the driver log path. The Reporting lesson shows how to attach that bundle to Allure or Extent; the AI-Assisted Testing lesson shows the file-based version.
Summary
- Console logs and JavaScript errors: BiDi
scripthandlers, cross-browser; assert that there are none. - Driver logs: service builders, DEBUG or TRACE, file per run, enable only when hunting a specific failure.
- Client logs: your language’s logging framework at DEBUG on the Selenium logger to see protocol traffic.
- Network failures: BiDi
networkevents, collected per test. - Bundle all of it in the failure hook so nobody has to rerun a failed test to understand it.