Headless Chromium & the Chrome DevTools Protocol

Controlling any Chromium browser programmatically, headed or headless. Remote debugging, backtraces, profiling, and every useful CDP command you'll actually reach for.

March 2026

What is CDP

The Chrome DevTools Protocol is a JSON-RPC-style protocol exposed over WebSocket by Chromium-based browsers. It's the same wire protocol that Chrome DevTools uses internally. When you open the Elements panel, set a breakpoint, or profile a page, the frontend is sending CDP messages to the browser backend.

Anything DevTools can do, you can do programmatically. That includes navigating pages, intercepting network requests, executing JavaScript in page context, capturing screenshots, recording performance traces, setting debugger breakpoints, and injecting input events. The protocol surface is large: over 40 domains, hundreds of commands, and hundreds of events.

CDP is a browser capability, not a headless-specific feature. The protocol works identically whether the browser window is visible on screen or running headless. You can launch Chrome normally, add --remote-debugging-port=9222, and control it programmatically while watching everything happen in the UI. That's actually one of the most useful debugging setups: your script drives the browser while you observe it visually in real time.

Higher-level libraries like Puppeteer and Playwright are built on top of CDP (Playwright also supports its own protocol for Firefox/WebKit). They're convenient, but they abstract away most of the protocol's power. Understanding the raw protocol gives you access to everything the browser exposes, including things no library wraps.

Launching Chromium with CDP

The only flag you need for CDP access is --remote-debugging-port. Everything else is optional. This works in both headed mode (normal browser window) and headless mode (no UI). The protocol, the WebSocket endpoint, the available commands, and the behavior are all identical either way. The choice between headed and headless is purely about whether you want a visible window.

Headed mode (visible browser window)

This is the most useful mode for development and debugging. You can watch your script navigate, click, and interact while inspecting state programmatically at the same time.

# macOS — launch Chrome with a visible window and CDP enabled
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  --remote-debugging-port=9222

# Linux
chromium-browser --remote-debugging-port=9222

# Isolate from your normal browsing profile
chromium --remote-debugging-port=9222 \
  --user-data-dir=/tmp/chrome-debug \
  --no-first-run

Once launched, the browser window works normally. You can interact with it manually while your script also sends CDP commands. You can even open DevTools in the browser window alongside your programmatic CDP session. Both use the same protocol; they coexist without conflict.

Headed mode is underrated Most CDP tutorials jump straight to headless, but headed mode is where you'll spend the most time during development. Being able to visually confirm what your script is doing, inspect the DOM in DevTools, and interact manually while your automation runs is invaluable. Switch to headless only when you're deploying to CI, servers, or containers where there's no display.

Headless mode (no visible window)

For automation on servers, CI pipelines, containers, or anywhere you don't have (or need) a display. Chrome and Chromium have shipped --headless since 2017. The "new headless" mode (Chrome 112+) runs the full browser engine, sharing the same code path as headed mode. The old headless was a separate, stripped-down implementation that's now deprecated.

# macOS (Chrome)
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  --headless=new \
  --remote-debugging-port=9222

# Linux
chromium-browser --headless=new --remote-debugging-port=9222

# Specify a user data dir to isolate sessions
chromium --headless=new \
  --remote-debugging-port=9222 \
  --user-data-dir=/tmp/chrome-debug \
  --no-first-run \
  --disable-default-apps
Why --headless=new instead of --headless? The plain --headless flag still invokes the old headless mode in some Chrome versions. Using --headless=new guarantees you get the modern implementation that supports extensions, DevTools protocol fully, and renders identically to headed mode.

Useful launch flags

FlagPurpose
--remote-debugging-port=9222Open CDP WebSocket on this port
--remote-debugging-address=0.0.0.0Bind to all interfaces (default: 127.0.0.1)
--headless=newNew headless mode (Chrome 112+)
--disable-gpuDisable GPU compositing (useful in containers)
--no-sandboxRequired when running as root (Docker)
--disable-dev-shm-usageUse /tmp instead of /dev/shm (Docker fix for small shared mem)
--user-data-dir=PATHIsolate profile data per session
--window-size=1920,1080Set viewport dimensions
--proxy-server=host:portRoute traffic through a proxy
--ignore-certificate-errorsAccept self-signed certs (testing only)
--disable-extensionsSkip loading extensions
--enable-logging=stderrPrint Chrome's internal logs to stderr
--v=1Verbose logging level (higher = more output)
--remote-allow-origins=*Allow any origin to connect to CDP (Chrome 111+)
Security note Binding the debugging port to 0.0.0.0 exposes full browser control to the network. Anyone who can reach port 9222 can read cookies, execute JS, and navigate to file:// URLs. In production or shared environments, always bind to 127.0.0.1 and use SSH tunnels for remote access.

Verifying the endpoint

Once Chrome is running with --remote-debugging-port=9222, it exposes a few HTTP endpoints:

# List open targets (pages, service workers, etc.)
curl -s http://localhost:9222/json/list | jq .

# Browser version info
curl -s http://localhost:9222/json/version | jq .

# Open a new tab
curl -s http://localhost:9222/json/new?http://example.com

# Close a tab by target ID
curl -s http://localhost:9222/json/close/TARGET_ID

The /json/version response includes a webSocketDebuggerUrl field. That's the WebSocket endpoint for browser-level CDP commands. Each target in /json/list has its own webSocketDebuggerUrl for page-level commands.

Your first CDP script

Raw WebSocket approach (Node.js)

No libraries. Just a WebSocket, JSON-RPC message IDs, and the CDP spec. This is the most educational way to understand what's happening.

import WebSocket from 'ws';

// Discover the WebSocket URL from the /json/version endpoint
const info = await fetch('http://localhost:9222/json/version')
  .then(r => r.json());
const ws = new WebSocket(info.webSocketDebuggerUrl);

let msgId = 0;
const pending = new Map();

function send(method, params = {}) {
  return new Promise((resolve, reject) => {
    const id = ++msgId;
    pending.set(id, { resolve, reject });
    ws.send(JSON.stringify({ id, method, params }));
  });
}

ws.on('message', (data) => {
  const msg = JSON.parse(data);
  if (msg.id && pending.has(msg.id)) {
    const { resolve, reject } = pending.get(msg.id);
    pending.delete(msg.id);
    msg.error ? reject(msg.error) : resolve(msg.result);
  } else if (msg.method) {
    // This is an event (e.g., Network.requestWillBeSent)
    console.log(`[event]`, msg.method, msg.params);
  }
});

ws.on('open', async () => {
  // Create a new target (tab)
  const { targetId } = await send('Target.createTarget', {
    url: 'about:blank'
  });

  // Attach to it to get a session for page-level commands
  const { sessionId } = await send('Target.attachToTarget', {
    targetId,
    flatten: true
  });

  // Now send commands scoped to this session
  const sendToPage = (method, params = {}) => {
    return new Promise((resolve, reject) => {
      const id = ++msgId;
      pending.set(id, { resolve, reject });
      ws.send(JSON.stringify({ id, method, params, sessionId }));
    });
  };

  await sendToPage('Page.enable');
  await sendToPage('Network.enable');

  await sendToPage('Page.navigate', {
    url: 'https://example.com'
  });

  // Wait for load, then screenshot
  await new Promise(r => setTimeout(r, 2000));

  const { data } = await sendToPage('Page.captureScreenshot', {
    format: 'png'
  });

  const fs = await import('fs');
  fs.writeFileSync('screenshot.png', Buffer.from(data, 'base64'));
  console.log('Screenshot saved.');

  ws.close();
});

The key concepts: every CDP message is JSON with an id (for request/response correlation), a method (domain.command), and params. Events arrive without an id and carry a method field. Sessions (via Target.attachToTarget with flatten: true) multiplex multiple targets over a single WebSocket.

Puppeteer

Puppeteer is the most common high-level wrapper. It manages browser launch, provides a page API, and handles CDP session management. You can still drop down to raw CDP when needed.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: 'new',
  args: ['--remote-debugging-port=9222']
});

const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle0' });

// High-level API
await page.screenshot({ path: 'screenshot.png', fullPage: true });

// Drop to raw CDP when you need something Puppeteer doesn't wrap
const cdp = await page.createCDPSession();

// Enable the Debugger domain
await cdp.send('Debugger.enable');

// Get a JS heap snapshot
await cdp.send('HeapProfiler.enable');
cdp.on('HeapProfiler.addHeapSnapshotChunk', ({ chunk }) => {
  process.stdout.write(chunk);
});
await cdp.send('HeapProfiler.takeHeapSnapshot', {
  reportProgress: false
});

await browser.close();

Playwright

Playwright has its own protocol for cross-browser support, but exposes CDP for Chromium targets.

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();

// Get a CDP session from any Playwright page
const cdp = await page.context().newCDPSession(page);

await cdp.send('Performance.enable');
await page.goto('https://example.com');

const { metrics } = await cdp.send('Performance.getMetrics');
console.log(metrics);

await browser.close();

Python

The pychrome library gives you a clean CDP wrapper. For async workflows, nodriver (successor to undetected-chromedriver) or the cdp package work well.

import pychrome
import base64

browser = pychrome.Browser(url="http://127.0.0.1:9222")
tab = browser.new_tab()

# Start listening for events
tab.start()
tab.call_method("Page.enable")
tab.call_method("Network.enable")

# Navigate
tab.call_method("Page.navigate", url="https://example.com")
tab.wait(3)

# Screenshot
result = tab.call_method("Page.captureScreenshot", format="png")
with open("screenshot.png", "wb") as f:
    f.write(base64.b64decode(result["data"]))

# Evaluate JS in page context
result = tab.call_method("Runtime.evaluate",
    expression="document.title")
print(result["result"]["value"])

tab.stop()
browser.close_tab(tab)

Reference script: chromium-browse

A working implementation of everything above is available as chromium-browse, installable via Homebrew from the mac-security tap. It controls any Chromium-based browser purely through raw CDP over WebSocket. No Puppeteer, no Playwright, no third-party browser engine. Just the protocol.

Install and usage See the chromium-browse guide for installation, setup, and usage instructions. This section focuses on the CDP internals the script demonstrates.

What the script does

  1. Launches the browser in --headless=new mode with a temporary user data directory (isolated profile, no leftover state)
  2. Reads a list of URLs from urls.txt, shuffles the order each round
  3. For each URL: navigates via Page.navigate, waits for load, scrolls the viewport randomly using Runtime.evaluate
  4. Dwells on each page for a randomized period (configurable min/max)
  5. Discovers same-domain links using Runtime.evaluate to query the DOM for <a href> elements
  6. Follows a random subset of those links (up to --max-depth levels deep), skipping assets like images, PDFs, fonts, and off-domain URLs
  7. Cleans up the temp profile directory on exit

CDP commands used

The script uses a small but representative slice of the protocol. This is a useful reference for anyone building their own CDP automation from scratch. Every technique is transferable to Chrome, Edge, Brave, Island, or any Chromium fork. Swap the browser binary path and the rest works unchanged.

CommandPurpose in the script
Target.createTargetOpen a new tab for each browsing session
Target.attachToTargetGet a session ID to send page-scoped commands
Page.enableEnable page lifecycle events (needed to detect load completion)
Page.navigateNavigate to each URL from the list
Runtime.evaluateExecute JavaScript in page context: scroll the viewport, extract <a href> links from the DOM, read document.readyState
Network.enableMonitor network activity to confirm page loads complete
Target.closeTargetClean up tabs after each visit

How it connects

The script follows the same pattern shown in the raw WebSocket example above:

  1. Launch the browser with --remote-debugging-port=PORT and a disposable --user-data-dir
  2. Poll http://localhost:PORT/json/version until the browser is ready (the CDP endpoint takes a moment to come up)
  3. Connect to the webSocketDebuggerUrl from the version response
  4. Send JSON-RPC messages with incrementing IDs, correlate responses by ID, and handle events by method name
  5. Use Target.createTarget + Target.attachToTarget with flatten: true to scope commands to individual tabs

The Python implementation uses the websockets library for the WebSocket connection and aiohttp for the initial HTTP endpoint check. Everything else is stdlib. Total external dependencies: two packages.

Remote debugging

The debugging port isn't just for headless automation. It's a full remote debugging interface. You can attach Chrome DevTools from another machine, connect multiple clients, and debug browsers running in containers, VMs, or on mobile devices.

Over the network

Launch Chrome on the target machine binding to all interfaces:

chromium --headless=new \
  --remote-debugging-port=9222 \
  --remote-debugging-address=0.0.0.0 \
  --remote-allow-origins=*

On your workstation, open chrome://inspect in Chrome, click "Configure" next to "Discover network targets," and add TARGET_IP:9222. The remote browser's tabs will appear in the list, and you can click "inspect" to get a full DevTools window connected to the remote target.

Seriously: don't expose this to the internet There is no authentication on the debugging port. No password, no token, no TLS. If the port is reachable, the attacker has full control of the browser process, including the ability to read/write files accessible to the Chrome user via file:// URLs.

SSH tunnel (the right way)

Keep the debugging port bound to localhost and tunnel through SSH:

# On local workstation: forward local 9222 to remote's localhost:9222
ssh -L 9222:localhost:9222 user@remote-host

# Now access DevTools at chrome://inspect targeting localhost:9222
# Or connect programmatically
curl -s http://localhost:9222/json/version

This gives you encrypted, authenticated access without exposing the port.

Docker containers

# Dockerfile
FROM chromedp/headless-shell:latest
EXPOSE 9222
ENTRYPOINT ["/headless-shell/headless-shell", \
  "--remote-debugging-address=0.0.0.0", \
  "--remote-debugging-port=9222", \
  "--no-sandbox", \
  "--disable-dev-shm-usage"]
# Run it
docker run -d -p 9222:9222 --name chrome-headless chromedp/headless-shell

# Connect from host
curl -s http://localhost:9222/json/version

For Docker, chromedp/headless-shell is the minimal image (~100MB). If you need the full browser with font rendering and locales, use browserless/chrome or build your own from the Debian Chromium package.

Android device debugging

Chrome on Android supports USB debugging via ADB:

# Enable USB Debugging in Android Developer Settings first

# Forward the device's debugging port to your machine
adb forward tcp:9222 localabstract:chrome_devtools_remote

# Now localhost:9222 reaches the phone's Chrome
curl -s http://localhost:9222/json/list

# Or use chrome://inspect with USB discovery enabled

This works with the standard Chrome app on Android. Enable "USB web debugging" in Chrome's developer settings (chrome://flags), or launch Chrome with --remote-debugging-port via ADB shell.

Backtraces & stack capture

CDP's Debugger domain gives you deep access to the JavaScript call stack, pause/resume control, and exception handling. This is where the protocol really shines for debugging complex applications.

Pause on exceptions

// Pause on all exceptions (caught and uncaught)
await cdp.send('Debugger.enable');
await cdp.send('Debugger.setPauseOnExceptions', {
  state: 'all'  // 'none' | 'uncaught' | 'all'
});

// Listen for when execution pauses
cdp.on('Debugger.paused', async (params) => {
  console.log('Paused. Reason:', params.reason);
  console.log('Hit breakpoints:', params.hitBreakpoints);

  // Walk the call frames
  for (const frame of params.callFrames) {
    console.log(`  ${frame.functionName || '(anonymous)'}`
      + ` at ${frame.url}:${frame.location.lineNumber}`);

    // Inspect scope variables at this frame
    for (const scope of frame.scopeChain) {
      if (scope.type === 'local') {
        const { result } = await cdp.send(
          'Runtime.getProperties', {
            objectId: scope.object.objectId,
            ownProperties: true
          }
        );
        console.log('    Local vars:',
          result.map(p => `${p.name}=${p.value?.description}`));
      }
    }
  }

  // Resume execution
  await cdp.send('Debugger.resume');
});

Set breakpoints by URL pattern

// Break on a specific line
await cdp.send('Debugger.setBreakpointByUrl', {
  lineNumber: 42,
  urlRegex: '.*app\\.js$'
});

// Conditional breakpoint
await cdp.send('Debugger.setBreakpointByUrl', {
  lineNumber: 100,
  url: 'https://example.com/app.js',
  condition: 'items.length > 50'
});

Async stack traces

// Enable async call stacks (critical for understanding
// promise chains, setTimeout callbacks, event handlers)
await cdp.send('Debugger.setAsyncCallStackDepth', {
  maxDepth: 32
});

// Now when paused, callFrames will include the async parent chain
// params.asyncStackTrace links to the parent async context
cdp.on('Debugger.paused', (params) => {
  let asyncTrace = params.asyncStackTrace;
  while (asyncTrace) {
    console.log(`Async: ${asyncTrace.description}`);
    for (const frame of asyncTrace.callFrames) {
      console.log(`  ${frame.functionName} (${frame.url}:${frame.lineNumber})`);
    }
    asyncTrace = asyncTrace.parent;
  }
});

Runtime stack traces (no pausing needed)

// Get a stack trace from any console message
await cdp.send('Runtime.enable');

cdp.on('Runtime.consoleAPICalled', (params) => {
  console.log(`console.${params.type}:`,
    params.args.map(a => a.value || a.description));
  if (params.stackTrace) {
    for (const frame of params.stackTrace.callFrames) {
      console.log(`  at ${frame.functionName} (${frame.url}:${frame.lineNumber})`);
    }
  }
});

// Capture stack traces for uncaught exceptions
cdp.on('Runtime.exceptionThrown', (params) => {
  const ex = params.exceptionDetails;
  console.log('Exception:', ex.text);
  if (ex.stackTrace) {
    ex.stackTrace.callFrames.forEach(f =>
      console.log(`  ${f.functionName} ${f.url}:${f.lineNumber}:${f.columnNumber}`)
    );
  }
});

Console.trace() capture

// Inject a console.trace() call to get a backtrace from
// anywhere in the app without setting breakpoints
await cdp.send('Runtime.evaluate', {
  expression: `
    const _origFetch = window.fetch;
    window.fetch = function(...args) {
      console.trace('fetch called with:', args[0]);
      return _origFetch.apply(this, args);
    };
  `
});
// Every fetch() call now emits a full stack trace via
// Runtime.consoleAPICalled with type 'trace'

Profiling & performance

CPU profiling

await cdp.send('Profiler.enable');
await cdp.send('Profiler.start');

// ... do the thing you want to profile ...

const { profile } = await cdp.send('Profiler.stop');

// profile.nodes = array of call tree nodes with hitCount + children
// profile.startTime / endTime in microseconds
// profile.samples = array of node IDs sampled at each interval
// profile.timeDeltas = microsecond deltas between samples

// Save as .cpuprofile format (Chrome DevTools can open this)
fs.writeFileSync('profile.cpuprofile', JSON.stringify(profile));

Heap snapshots

await cdp.send('HeapProfiler.enable');

// Collect chunks as they stream
let snapshotData = '';
cdp.on('HeapProfiler.addHeapSnapshotChunk', ({ chunk }) => {
  snapshotData += chunk;
});

await cdp.send('HeapProfiler.takeHeapSnapshot', {
  reportProgress: true,
  treatGlobalObjectsAsRoots: true,
  captureNumericValue: true
});

// Save as .heapsnapshot (loadable in DevTools Memory panel)
fs.writeFileSync('heap.heapsnapshot', snapshotData);

Performance timeline traces

// Record a full performance trace (same data as DevTools Performance panel)
await cdp.send('Tracing.start', {
  categories: '-*,devtools.timeline,v8.execute,disabled-by-default-devtools.timeline,disabled-by-default-devtools.timeline.frame,toplevel,blink.console,blink.user_timing,latencyInfo,disabled-by-default-devtools.timeline.stack,disabled-by-default-v8.cpu_profiler,disabled-by-default-v8.cpu_profiler.hires',
  options: 'sampling-frequency=10000'
});

// ... interact with the page ...

const chunks = [];
cdp.on('Tracing.dataCollected', ({ value }) => chunks.push(...value));

await cdp.send('Tracing.end');

// Wait for tracingComplete event
await new Promise(resolve =>
  cdp.on('Tracing.tracingComplete', resolve)
);

// Save as .json trace file (load in chrome://tracing or DevTools)
fs.writeFileSync('trace.json', JSON.stringify(chunks));

Performance metrics (instant readings)

await cdp.send('Performance.enable', {
  timeDomain: 'timeTicks'
});

const { metrics } = await cdp.send('Performance.getMetrics');

// Returns an array of { name, value } objects:
// - Timestamp, Documents, Frames, JSEventListeners
// - Nodes, LayoutCount, RecalcStyleCount, LayoutDuration
// - RecalcStyleDuration, ScriptDuration, TaskDuration
// - JSHeapUsedSize, JSHeapTotalSize, FirstMeaningfulPaint
// - DomContentLoaded, NavigationStart

for (const m of metrics) {
  console.log(`${m.name}: ${m.value}`);
}

DevTools hacks

Beyond the standard automation use cases, CDP opens up capabilities that are genuinely hard to get any other way.

Network interception and modification

// Intercept and modify requests in flight
await cdp.send('Fetch.enable', {
  patterns: [
    { urlPattern: '*', requestStage: 'Request' },
    { urlPattern: '*', requestStage: 'Response' }
  ]
});

cdp.on('Fetch.requestPaused', async (params) => {
  if (params.responseStatusCode) {
    // Response stage: modify the response body
    const { body, base64Encoded } = await cdp.send(
      'Fetch.getResponseBody',
      { requestId: params.requestId }
    );

    let decoded = base64Encoded
      ? Buffer.from(body, 'base64').toString()
      : body;

    // Inject a script into every HTML response
    if (params.responseHeaders
        .some(h => h.name.toLowerCase() === 'content-type'
          && h.value.includes('text/html'))) {
      decoded = decoded.replace(
        '</head>',
        '<script>console.log("injected")</script></head>'
      );
    }

    await cdp.send('Fetch.fulfillRequest', {
      requestId: params.requestId,
      responseCode: params.responseStatusCode,
      responseHeaders: params.responseHeaders,
      body: Buffer.from(decoded).toString('base64')
    });
  } else {
    // Request stage: add/modify headers
    const headers = [...params.request.headers];
    await cdp.send('Fetch.continueRequest', {
      requestId: params.requestId,
      headers: [
        ...Object.entries(params.request.headers)
          .map(([name, value]) => ({ name, value })),
        { name: 'X-Custom-Header', value: 'injected' }
      ]
    });
  }
});

Coverage analysis

// Find unused CSS and JS on a page
await cdp.send('Profiler.enable');
await cdp.send('Profiler.startPreciseCoverage', {
  callCount: true,
  detailed: true
});
await cdp.send('CSS.enable');
await cdp.send('CSS.startRuleUsageTracking');

await page.goto('https://example.com');
await page.waitForTimeout(3000);

// JS coverage
const { result: jsCoverage } = await cdp.send(
  'Profiler.takePreciseCoverage'
);

for (const script of jsCoverage) {
  const total = script.functions
    .reduce((sum, fn) => sum + fn.ranges
      .reduce((s, r) => s + r.endOffset - r.startOffset, 0), 0);
  const used = script.functions
    .reduce((sum, fn) => sum + fn.ranges
      .filter(r => r.count > 0)
      .reduce((s, r) => s + r.endOffset - r.startOffset, 0), 0);
  console.log(`${script.url}: ${((used/total)*100).toFixed(1)}% used`);
}

// CSS coverage
const { ruleUsage } = await cdp.send('CSS.stopRuleUsageTracking');
const unusedRules = ruleUsage.filter(r => !r.used);
console.log(`${unusedRules.length} unused CSS rules`);

DOM snapshotting for diffing

// Capture a complete DOM snapshot (useful for visual regression)
const snapshot = await cdp.send('DOMSnapshot.captureSnapshot', {
  computedStyles: ['background-color', 'color', 'font-size',
    'display', 'visibility', 'opacity']
});

// Returns documents[] with nodes, layout, text, styles
// Great for building custom accessibility auditors or
// structural page comparison tools

Geolocation, device, and sensor emulation

// Fake GPS location
await cdp.send('Emulation.setGeolocationOverride', {
  latitude: 37.7749,
  longitude: -122.4194,
  accuracy: 100
});

// Emulate a device
await cdp.send('Emulation.setDeviceMetricsOverride', {
  width: 375,
  height: 812,
  deviceScaleFactor: 3,
  mobile: true
});

// Emulate network conditions
await cdp.send('Network.emulateNetworkConditions', {
  offline: false,
  latency: 150,          // ms
  downloadThroughput: 1.6 * 1024 * 1024 / 8,  // 1.6 Mbps
  uploadThroughput: 750 * 1024 / 8             // 750 Kbps
});

// Override timezone
await cdp.send('Emulation.setTimezoneOverride', {
  timezoneId: 'Asia/Tokyo'
});

// Override locale
await cdp.send('Emulation.setLocaleOverride', {
  locale: 'ja-JP'
});

// Simulate vision deficiency
await cdp.send('Emulation.setEmulatedVisionDeficiency', {
  type: 'deuteranopia'  // protanopia, tritanopia, achromatopsia, blurredVision
});

Synthetic input events

// Synthesize mouse clicks, keyboard input, touch events
// These go through the browser's actual input pipeline

// Click at coordinates
await cdp.send('Input.dispatchMouseEvent', {
  type: 'mousePressed', x: 100, y: 200,
  button: 'left', clickCount: 1
});
await cdp.send('Input.dispatchMouseEvent', {
  type: 'mouseReleased', x: 100, y: 200,
  button: 'left', clickCount: 1
});

// Type text (uses Input.dispatchKeyEvent under the hood)
for (const char of 'hello world') {
  await cdp.send('Input.dispatchKeyEvent', {
    type: 'keyDown', text: char
  });
  await cdp.send('Input.dispatchKeyEvent', {
    type: 'keyUp'
  });
}

// Touch events for mobile emulation
await cdp.send('Input.dispatchTouchEvent', {
  type: 'touchStart',
  touchPoints: [{ x: 150, y: 300 }]
});
await cdp.send('Input.dispatchTouchEvent', {
  type: 'touchEnd',
  touchPoints: []
});

Cookie and storage manipulation

// Read all cookies
const { cookies } = await cdp.send('Network.getAllCookies');

// Set a cookie
await cdp.send('Network.setCookie', {
  name: 'session',
  value: 'abc123',
  domain: 'example.com',
  path: '/',
  httpOnly: true,
  secure: true
});

// Clear all cookies
await cdp.send('Network.clearBrowserCookies');

// Clear localStorage/sessionStorage/indexedDB/caches
await cdp.send('Storage.clearDataForOrigin', {
  origin: 'https://example.com',
  storageTypes: 'all'
});

PDF generation

const { data } = await cdp.send('Page.printToPDF', {
  landscape: false,
  displayHeaderFooter: true,
  headerTemplate: '<div style="font-size:8px;text-align:center;width:100%">Confidential</div>',
  footerTemplate: '<div style="font-size:8px;text-align:center;width:100%">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
  printBackground: true,
  preferCSSPageSize: true
});

fs.writeFileSync('page.pdf', Buffer.from(data, 'base64'));

Accessibility tree inspection

// Get the full accessibility tree
const { nodes } = await cdp.send('Accessibility.getFullAXTree');

// Each node has: role, name, value, description, children
// Build custom a11y auditors that run in CI
for (const node of nodes) {
  if (node.role?.value === 'img' && !node.name?.value) {
    console.log('Image missing alt text:', node.backendDOMNodeId);
  }
}

Intercept file downloads

// Control where downloads go (headless mode)
await cdp.send('Browser.setDownloadBehavior', {
  behavior: 'allowAndName',
  downloadPath: '/tmp/downloads',
  eventsEnabled: true
});

cdp.on('Browser.downloadProgress', (params) => {
  console.log(`Download ${params.guid}: ${params.state}`);
  if (params.state === 'completed') {
    console.log('Saved to:', params.guid);
  }
});

Screencast (stream the viewport)

// Stream frames as base64 images
await cdp.send('Page.startScreencast', {
  format: 'jpeg',
  quality: 60,
  maxWidth: 1280,
  maxHeight: 720,
  everyNthFrame: 2
});

cdp.on('Page.screencastFrame', async (params) => {
  // params.data = base64 jpeg
  // params.metadata = { offsetTop, pageScaleFactor, ... }
  // params.sessionId = acknowledge to receive next frame

  // Process the frame (save, stream to websocket, etc.)
  processFrame(params.data);

  // Acknowledge to get the next frame
  await cdp.send('Page.screencastFrameAck', {
    sessionId: params.sessionId
  });
});

Script injection and monkey-patching

// Run a script on every new document (before any page JS executes)
await cdp.send('Page.addScriptToEvaluateOnNewDocument', {
  source: `
    // Override navigator properties (stealth)
    Object.defineProperty(navigator, 'webdriver', { get: () => false });

    // Instrument all XHR calls
    const _open = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
      console.log('[XHR]', method, url);
      return _open.apply(this, arguments);
    };

    // Hook into postMessage
    const _postMessage = window.postMessage;
    window.postMessage = function(msg, origin) {
      console.log('[postMessage]', msg, origin);
      return _postMessage.apply(this, arguments);
    };

    // Performance observer for long tasks
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.duration > 50) {
          console.warn('[LongTask]', entry.duration.toFixed(1) + 'ms', entry.name);
        }
      }
    }).observe({ type: 'longtask', buffered: true });
  `
});

CDP command reference

The full protocol has 40+ domains and hundreds of commands. This reference covers the ones you'll actually use. Each entry shows the method signature, key parameters, and what it's useful for.

Convention: Domain.method for commands, Domain.eventName for events.

Page domain

Controls page navigation, lifecycle, screenshots, and printing.

Page.enable

Page.enable()

Enables Page domain events. Call this before listening for any Page events. Most domains require an explicit enable call before they emit events.

Page.navigate

Page.navigate({ url, referrer?, transitionType?, frameId? })

Navigate to a URL. Returns frameId, loaderId, and optionally errorText if navigation failed at the network level.

  • url (string) Target URL
  • referrer (string) Referrer URL to send
  • transitionType (string) link, typed, address_bar, auto_bookmark, etc.

Page.reload

Page.reload({ ignoreCache?, scriptToEvaluateOnLoad? })

Reload the current page. ignoreCache bypasses the disk cache (like Ctrl+Shift+R). scriptToEvaluateOnLoad injects JS that runs before any page scripts.

Page.captureScreenshot

Page.captureScreenshot({ format?, quality?, clip?, fromSurface?, captureBeyondViewport?, optimizeForSpeed? })

Returns a base64-encoded image of the viewport or a clipped region.

  • format "jpeg" | "png" | "webp"
  • quality (int, 0-100) Compression quality for jpeg/webp
  • clip { x, y, width, height, scale } Capture a specific region
  • captureBeyondViewport (bool) Capture the full page, not just the visible viewport

Page.printToPDF

Page.printToPDF({ landscape?, displayHeaderFooter?, headerTemplate?, footerTemplate?, printBackground?, scale?, paperWidth?, paperHeight?, marginTop?, marginBottom?, marginLeft?, marginRight?, pageRanges?, preferCSSPageSize? })

Generate a PDF of the page. Returns base64-encoded PDF data. The header/footer templates support CSS and the special classes date, title, url, pageNumber, totalPages.

Page.addScriptToEvaluateOnNewDocument

Page.addScriptToEvaluateOnNewDocument({ source, worldName?, includeCommandLineAPI? })

Register a script to run on every new document before the page's own scripts. Returns an identifier you can use to remove it later. This is the backbone of stealth techniques, instrumentation, and polyfill injection.

Page.startScreencast / Page.stopScreencast

Page.startScreencast({ format?, quality?, maxWidth?, maxHeight?, everyNthFrame? })

Stream viewport frames as events. Each Page.screencastFrame event must be acknowledged with Page.screencastFrameAck before the next frame is sent.

Page events

EventWhen it fires
Page.loadEventFiredWindow load event
Page.domContentEventFiredDOMContentLoaded event
Page.frameNavigatedAfter a frame navigates to a new URL
Page.frameStartedLoadingFrame begins navigation
Page.frameStoppedLoadingFrame finishes loading
Page.lifecycleEventLifecycle events: init, firstPaint, firstContentfulPaint, firstMeaningfulPaint, DOMContentLoaded, load, networkAlmostIdle, networkIdle, firstMeaningfulPaintCandidate
Page.javascriptDialogOpeningalert(), confirm(), prompt(), or beforeunload dialog

Runtime domain

Execute JavaScript, inspect objects, and subscribe to console output.

Runtime.evaluate

Runtime.evaluate({ expression, objectGroup?, includeCommandLineAPI?, silent?, contextId?, returnByValue?, generatePreview?, userGesture?, awaitPromise?, throwOnSideEffect?, timeout?, disableBreaks?, replMode?, allowUnsafeEvalBlockedByCSP? })

The workhorse command. Evaluates a JS expression in the page context and returns the result.

  • expression (string) The JS to evaluate
  • awaitPromise (bool) If the expression returns a promise, wait for it to resolve before returning
  • returnByValue (bool) Return the result serialized as JSON instead of a remote object reference
  • userGesture (bool) Whether to treat evaluation as user-initiated (needed for things that require user activation)
  • contextId (int) Execution context to run in (different contexts for different frames/worlds)

Runtime.callFunctionOn

Runtime.callFunctionOn({ functionDeclaration, objectId?, arguments?, silent?, returnByValue?, generatePreview?, userGesture?, awaitPromise?, executionContextId?, objectGroup? })

Call a function with a specific this value (via objectId). More structured than evaluate when working with specific DOM nodes or JS objects.

Runtime.getProperties

Runtime.getProperties({ objectId, ownProperties?, accessorPropertiesOnly?, generatePreview?, nonIndexedPropertiesOnly? })

Inspect an object's properties by its remote object ID. Returns result (array of property descriptors) and internalProperties (like [[Prototype]], [[Scopes]]).

Runtime events

EventWhen it fires
Runtime.consoleAPICalledAny console.* call. Includes type (log, warn, error, trace, etc.), args, and stackTrace.
Runtime.exceptionThrownUncaught exception. Includes exceptionDetails with message, stackTrace, lineNumber, columnNumber, scriptId.
Runtime.executionContextCreatedNew execution context (frame loaded, worker started). Includes context id, origin, name.
Runtime.executionContextDestroyedContext destroyed (frame navigated away, worker terminated).

Network domain

Monitor and modify HTTP traffic.

Network.enable

Network.enable({ maxTotalBufferSize?, maxResourceBufferSize?, maxPostDataSize? })

Start receiving Network events. Optionally control buffer sizes for response body capture.

Network.getResponseBody

Network.getResponseBody({ requestId })

Get the response body for a completed request. Returns body (string) and base64Encoded (bool). Must be called before the page navigates away, or the data is lost.

Network.setExtraHTTPHeaders

Network.setExtraHTTPHeaders({ headers })

Add extra headers to every outgoing request. headers is an object of name/value pairs.

Network.emulateNetworkConditions

Network.emulateNetworkConditions({ offline, latency, downloadThroughput, uploadThroughput, connectionType? })

Simulate network conditions. latency in ms, throughput in bytes/sec. connectionType can be none, cellular2g, cellular3g, cellular4g, bluetooth, ethernet, wifi, wimax, other.

Network.setCacheDisabled

Network.setCacheDisabled({ cacheDisabled })

Toggle the disk cache. Useful for testing without cached resources.

Network.setCookie / Network.getAllCookies / Network.clearBrowserCookies

Full cookie management. setCookie accepts name, value, domain, path, secure, httpOnly, sameSite, expires, priority, sameParty.

Network events

EventWhat it contains
Network.requestWillBeSentRequest URL, method, headers, postData, initiator (with stack trace of what triggered it), redirectResponse
Network.responseReceivedResponse status, headers, mimeType, timing info, securityDetails (TLS cert info)
Network.loadingFinishedRequest completed. encodedDataLength gives actual bytes transferred.
Network.loadingFailedRequest failed. Includes errorText, canceled (bool), blockedReason.
Network.webSocketCreatedWebSocket opened. URL and initiator.
Network.webSocketFrameReceivedWebSocket message received. Includes payload data.
Network.webSocketFrameSentWebSocket message sent.

DOM domain

Query and manipulate the DOM tree.

DOM.getDocument

DOM.getDocument({ depth?, pierce? })

Returns the root DOM node. depth controls how many levels to return (-1 for entire subtree). pierce traverses into shadow DOMs and iframes.

DOM.querySelector / DOM.querySelectorAll

DOM.querySelector({ nodeId, selector })

Run a CSS selector on a node. Returns the matching nodeId(s). Use after DOM.getDocument to get the root nodeId.

DOM.getOuterHTML / DOM.setOuterHTML

DOM.getOuterHTML({ nodeId?, backendNodeId?, objectId? })

Read or replace the outer HTML of a node. Useful for extracting markup or live-editing the DOM.

DOM.getBoxModel

DOM.getBoxModel({ nodeId?, backendNodeId?, objectId? })

Returns the CSS box model (content, padding, border, margin quads in viewport coordinates). Useful for calculating click coordinates or building custom highlighting overlays.

DOM.resolveNode

DOM.resolveNode({ nodeId?, backendNodeId?, objectGroup?, executionContextId? })

Bridge between DOM and Runtime domains. Converts a DOM nodeId to a Runtime.RemoteObject, so you can call Runtime.callFunctionOn against it.

Debugger domain

Breakpoints, stepping, call stacks, and source maps.

Debugger.enable

Debugger.enable({ maxScriptsCacheSize? })

Enable the Debugger domain. Returns debuggerId. Required before setting breakpoints or receiving pause events.

Debugger.setBreakpointByUrl

Debugger.setBreakpointByUrl({ lineNumber, url?, urlRegex?, scriptHash?, columnNumber?, condition? })

Set a breakpoint that persists across navigations. urlRegex lets you match patterns (e.g., .*bundle\\.js$). condition is a JS expression that must evaluate to true for the breakpoint to hit.

Debugger.setBreakpoint

Debugger.setBreakpoint({ location: { scriptId, lineNumber, columnNumber? }, condition? })

Set a breakpoint in a specific script by scriptId. More precise than URL-based, but breaks if the script is reloaded.

Debugger.setPauseOnExceptions

Debugger.setPauseOnExceptions({ state })

state: "none", "uncaught", or "all". "all" pauses on caught exceptions too, which is great for finding swallowed errors.

Debugger.setAsyncCallStackDepth

Debugger.setAsyncCallStackDepth({ maxDepth })

Controls how many async stack frames are captured. Set to 0 to disable, or 32+ for deep promise chains. Each paused event's asyncStackTrace field will link through the chain.

Stepping commands

CommandBehavior
Debugger.resumeContinue execution
Debugger.stepOverStep to next line (skip into function calls)
Debugger.stepIntoStep into the next function call
Debugger.stepOutStep out of the current function
Debugger.pausePause execution immediately

Debugger events

EventContents
Debugger.pausedcallFrames (full stack with scope chains), reason (breakpoint, exception, debugCommand, XHR, DOM, etc.), data, hitBreakpoints, asyncStackTrace, asyncStackTraceId
Debugger.resumedExecution resumed
Debugger.scriptParsedScript loaded. scriptId, url, startLine, endLine, sourceMapURL, hasSourceURL, isModule
Debugger.scriptFailedToParseScript parse error. Same fields as scriptParsed plus error info.

Profiler domain

CPU profiling and code coverage.

Profiler.start / Profiler.stop

Profiler.start() / Profiler.stop()

start begins CPU sampling. stop returns a profile object with nodes (call tree), samples (sampled node IDs), timeDeltas (microsecond intervals), and startTime/endTime. Save as .cpuprofile to open in DevTools.

Profiler.startPreciseCoverage / Profiler.takePreciseCoverage

Profiler.startPreciseCoverage({ callCount?, detailed?, allowTriggeredUpdates? })

Tracks which JS functions and byte ranges actually execute. detailed: true gives block-level coverage (not just function-level). takePreciseCoverage returns the coverage data without stopping collection.

Performance domain

Performance.getMetrics

Performance.getMetrics()

Instant snapshot of browser performance counters. Returns an array of { name, value }:

  • Timestamp, Documents, Frames, JSEventListeners, Nodes
  • LayoutCount, RecalcStyleCount, LayoutDuration, RecalcStyleDuration
  • ScriptDuration, TaskDuration
  • JSHeapUsedSize, JSHeapTotalSize
  • FirstMeaningfulPaint, DomContentLoaded, NavigationStart

Tracing.start / Tracing.end

Tracing.start({ categories?, options?, bufferUsageReportingInterval?, transferMode?, streamFormat?, streamCompression?, traceConfig? })

Record a full performance trace. The categories string controls which trace categories are active. Data arrives via Tracing.dataCollected events. Save the collected data as JSON and load it in chrome://tracing or the DevTools Performance panel.

Key trace categories:

  • devtools.timeline Main thread timeline events
  • v8.execute V8 script execution
  • disabled-by-default-devtools.timeline.stack Stack traces in timeline
  • disabled-by-default-v8.cpu_profiler V8 CPU profiler samples in trace
  • blink.user_timing performance.mark() / performance.measure()
  • toplevel Task scheduling

Target domain

Manage browser targets: tabs, workers, service workers, extensions.

Target.createTarget

Target.createTarget({ url, width?, height?, browserContextId?, enableBeginFrameControl?, newWindow?, background? })

Open a new tab. browserContextId creates it in an incognito context (via Target.createBrowserContext). Returns targetId.

Target.attachToTarget

Target.attachToTarget({ targetId, flatten? })

Attach to a target to send it commands. flatten: true multiplexes the session over the existing WebSocket. Returns sessionId.

Target.createBrowserContext

Target.createBrowserContext({ disposeOnDetach?, proxyServer?, proxyBypassList? })

Create an incognito-like browser context with its own cookie jar and cache. You can assign a per-context proxy. Tabs created with this browserContextId are isolated.

Target.getTargets

Target.getTargets({ filter? })

List all targets. Each target has: targetId, type (page, background_page, service_worker, worker, iframe, browser, other), title, url, attached, browserContextId.

Input domain

Synthesize keyboard, mouse, and touch events.

Input.dispatchKeyEvent

Input.dispatchKeyEvent({ type, modifiers?, timestamp?, text?, unmodifiedText?, keyIdentifier?, code?, key?, windowsVirtualKeyCode?, nativeVirtualKeyCode?, autoRepeat?, isKeypad?, isSystemKey?, location?, commands? })

type: "keyDown", "keyUp", "rawKeyDown", "char". For typing text, use keyDown with text set to the character, then keyUp. For special keys, set key to the key name (e.g., "Enter", "Tab", "ArrowDown") and code to the physical key (e.g., "Enter", "Tab").

Input.dispatchMouseEvent

Input.dispatchMouseEvent({ type, x, y, modifiers?, timestamp?, button?, buttons?, clickCount?, force?, tangentialPressure?, tiltX?, tiltY?, twist?, deltaX?, deltaY?, pointerType? })

type: "mousePressed", "mouseReleased", "mouseMoved", "mouseWheel". Coordinates are in CSS pixels relative to the viewport. deltaX/deltaY for scroll events.

Input.dispatchTouchEvent

Input.dispatchTouchEvent({ type, touchPoints, modifiers?, timestamp? })

type: "touchStart", "touchEnd", "touchMove", "touchCancel". touchPoints is an array of { x, y, radiusX?, radiusY?, rotationAngle?, force?, tangentialPressure?, tiltX?, tiltY?, twist?, id? }. Multi-touch: include multiple points.

Emulation domain

Override device characteristics, location, timezone, vision, media, and more.

Emulation.setDeviceMetricsOverride

Emulation.setDeviceMetricsOverride({ width, height, deviceScaleFactor, mobile, screenWidth?, screenHeight?, positionX?, positionY?, dontSetVisibleSize?, screenOrientation?, viewport?, displayFeature? })

Override viewport dimensions and device pixel ratio. Set mobile: true to enable mobile viewport behavior (meta viewport, touch events, scrollbar overlay).

Emulation.setGeolocationOverride

Emulation.setGeolocationOverride({ latitude?, longitude?, accuracy? })

Fake the device's GPS coordinates. Call with no params to clear the override.

Emulation.setUserAgentOverride

Emulation.setUserAgentOverride({ userAgent, acceptLanguage?, platform?, userAgentMetadata? })

Override the User-Agent string, accept-language, and navigator.platform. userAgentMetadata handles Client Hints (brands, fullVersion, platform, mobile, architecture, model).

Other Emulation commands

CommandWhat it does
setTimezoneOverrideOverride Intl.DateTimeFormat timezone
setLocaleOverrideOverride navigator.language
setEmulatedMediaOverride CSS media type (screen/print) and features (prefers-color-scheme, prefers-reduced-motion, etc.)
setEmulatedVisionDeficiencySimulate color blindness: protanopia, deuteranopia, tritanopia, achromatopsia, blurredVision
setCPUThrottlingRateThrottle CPU by a factor (e.g., 4 = 4x slowdown)
setTouchEmulationEnabledEnable touch event emulation
setScrollbarsHiddenHide scrollbars (for clean screenshots)
setDocumentCookieDisabledBlock document.cookie access

Security domain

Security.enable

Enables the Security domain. Once enabled, you receive Security.securityStateChanged events with detailed cert/TLS information for the current page.

Security.setIgnoreCertificateErrors

Security.setIgnoreCertificateErrors({ ignore })

Accept all certificates regardless of validity. Equivalent to --ignore-certificate-errors but controllable at runtime. Only for testing environments.

Log domain

Log.enable

Start receiving Log.entryAdded events. These are browser-level log entries, not console.log() calls (which come from Runtime). Includes network errors, security warnings, deprecation notices, and intervention reports.

Each entry has: source (xml, javascript, network, storage, appcache, rendering, security, deprecation, worker, violation, intervention, recommendation, other), level (verbose, info, warning, error), text, url, lineNumber, stackTrace, networkRequestId.

Fetch domain

Request interception with more control than Network.setRequestInterception.

Fetch.enable

Fetch.enable({ patterns?, handleAuthRequests? })

patterns is an array of { urlPattern, resourceType?, requestStage }. requestStage is "Request" (before the request is sent) or "Response" (after response headers arrive). If handleAuthRequests is true, you'll get Fetch.authRequired events for HTTP auth challenges.

Fetch.continueRequest

Fetch.continueRequest({ requestId, url?, method?, postData?, headers?, interceptResponse? })

Continue a paused request, optionally modifying URL, method, body, or headers. This is how you redirect, rewrite, or augment requests in flight.

Fetch.fulfillRequest

Fetch.fulfillRequest({ requestId, responseCode, responseHeaders?, binaryResponseHeaders?, body?, responsePhrase? })

Provide a complete response to a paused request. body is base64-encoded. This lets you serve mock responses, modify response bodies, or build local overrides for any URL.

Fetch.failRequest

Fetch.failRequest({ requestId, reason })

Fail a paused request with a network error. reason: Failed, Aborted, TimedOut, AccessDenied, ConnectionClosed, ConnectionReset, ConnectionRefused, ConnectionAborted, ConnectionFailed, NameNotResolved, InternetDisconnected, AddressUnreachable, BlockedByClient, BlockedByResponse.

Fetch.getResponseBody

Fetch.getResponseBody({ requestId })

Get the response body during response-stage interception. Returns body and base64Encoded. Only works with requestStage: "Response" interception.

Other domains worth knowing

HeapProfiler

Heap snapshots, allocation tracking, and object sampling.

  • takeHeapSnapshot Full V8 heap snapshot (stream via addHeapSnapshotChunk events)
  • startSampling / stopSampling Allocation sampling profiler (lighter than full snapshot)
  • startTrackingHeapObjects / stopTrackingHeapObjects Track every allocation (heavy, use for leak detection)
  • getObjectByHeapObjectId Resolve a heap object to a Runtime.RemoteObject

Accessibility

  • getFullAXTree Complete accessibility tree for the page
  • getPartialAXTree Subtree for a specific node
  • queryAXTree Search the accessibility tree by role or name

CSS

  • getMatchedStylesForNode All CSS rules matching a node (inherited, matched, pseudo-element)
  • getComputedStyleForNode Final computed values for all CSS properties
  • setStyleTexts Edit CSS rules live
  • startRuleUsageTracking / stopRuleUsageTracking CSS coverage
  • forcePseudoState Force :hover, :active, :focus, :focus-within, :focus-visible on a node

DOMSnapshot

  • captureSnapshot Flat arrays of the entire DOM tree with layout, style, and text data. More efficient than walking DOM.getDocument for large pages.

Browser

  • getVersion Browser name, version, user agent, V8 version
  • getWindowBounds / setWindowBounds Position and resize the browser window
  • setDownloadBehavior Control download paths and get download progress events
  • grantPermissions Grant permissions (geolocation, midi, notifications, camera, microphone, etc.)
  • crash Intentionally crash the browser (testing crash recovery)

Storage

  • clearDataForOrigin Wipe specific storage types: cookies, file_systems, indexeddb, local_storage, shader_cache, websql, service_workers, cache_storage, interest_groups, shared_storage, storage_buckets, all
  • getUsageAndQuota Storage usage breakdown by type for an origin
  • getCookies / setCookies / clearCookies Cookie operations scoped by browserContextId
  • trackCacheStorageForOrigin Monitor Cache API operations

ServiceWorker

  • enable Start receiving service worker lifecycle events
  • unregister Force-unregister a service worker by scope URL
  • skipWaiting Force the waiting worker to activate
  • inspectWorker Attach DevTools to a specific worker

SystemInfo

  • getInfo Returns GPU info (driver, vendor), OS name/version, command line args
  • getProcessInfo Memory/CPU usage for each Chrome process (browser, renderer, GPU, etc.)
  • getFeatureState Check if a Chrome feature is enabled

Further reading