Documentation / Scripting / CDP recipes
CDP recipes
The Chrome Devtools Protocol tutorial covers the API: cdp.send, cdp.sendAndGet, cdp.on. This page is the companion — what to actually do with that API. Each recipe is short, copy-pasteable, and isolates one CDP capability so you can mix and match.
CDP is Chrome and Edge only. None of these work in Firefox or Safari — Firefox has Bidi, Safari has neither.
Recipe: block third-party domains
Block requests to specific domains before they go out. Useful for measuring "your site without the ad network" or for stripping a slow tag manager:
/**
* @param {import('browsertime').BrowsertimeContext} context
* @param {import('browsertime').BrowsertimeCommands} commands
*/
export default async function (context, commands) {
await commands.cdp.send('Network.setBlockedURLs', {
urls: [
'*google-analytics.com*',
'*doubleclick.net*',
'*facebook.net*',
'*hotjar.com*'
]
});
await commands.cdp.send('Network.enable');
return commands.measure.start('https://www.example.org');
}; The patterns are URL globs (asterisk wildcards) — keep them broad enough to catch all the subdomains. Blocked requests fail with net::ERR_BLOCKED_BY_CLIENT; the page renders without them.
Recipe: modify response headers
Add, change or remove headers on responses. This needs Fetch.enable and a Fetch.requestPaused listener that calls Fetch.continueResponse with the modified headers:
/**
* @param {import('browsertime').BrowsertimeContext} context
* @param {import('browsertime').BrowsertimeCommands} commands
*/
export default async function (context, commands) {
const cdpClient = commands.cdp.getRawClient();
await cdpClient.Fetch.enable({
patterns: [{ urlPattern: '*', requestStage: 'Response' }]
});
cdpClient.Fetch.requestPaused(async event => {
const headers = event.responseHeaders || [];
// Strip Set-Cookie from third-party responses
const filtered = headers.filter(h =>
!(h.name.toLowerCase() === 'set-cookie' && !event.request.url.startsWith('https://www.example.org'))
);
return cdpClient.Fetch.continueResponse({
requestId: event.requestId,
responseCode: event.responseStatusCode,
responseHeaders: filtered
});
});
return commands.measure.start('https://www.example.org');
}; You can use the same shape to add headers (for example a Server-Timing you injected for testing), or to rewrite headers wholesale.
Recipe: override geolocation
Test how a page renders for users in a different geography:
await commands.cdp.send('Emulation.setGeolocationOverride', {
latitude: 35.6762,
longitude: 139.6503,
accuracy: 100
});
return commands.measure.start('https://www.example.org/find-store'); Pair with Pre and post scripts if you want this set once for a whole run rather than re-set on every iteration.
Recipe: capture every network response
Listen for Network.responseReceived and stash what you need. This is how you'd build a "what third parties is this page loading" report:
/**
* @param {import('browsertime').BrowsertimeContext} context
* @param {import('browsertime').BrowsertimeCommands} commands
*/
export default async function (context, commands) {
const responses = [];
await commands.cdp.on('Network.responseReceived', params => {
responses.push({
url: params.response.url,
status: params.response.status,
mimeType: params.response.mimeType
});
});
await commands.measure.start('https://www.example.org');
// Count third-party requests as a custom metric
const thirdParties = responses.filter(r => !r.url.startsWith('https://www.example.org')).length;
commands.measure.add('thirdPartyRequestCount', thirdParties);
}; The HAR file already captures all this — reach for cdp.on only when you need data the HAR doesn't have, or when you want to compute a metric inline.
Recipe: throttle a specific resource
Slow down requests to one URL pattern (typical: simulate a slow CDN serving the hero image). The HAR-side connectivity throttling slows everything; this is for slowing one resource without touching the rest.
const cdpClient = commands.cdp.getRawClient();
await cdpClient.Fetch.enable({
patterns: [{ urlPattern: '*hero-image*', requestStage: 'Request' }]
});
cdpClient.Fetch.requestPaused(async event => {
await new Promise(resolve => setTimeout(resolve, 2000));
return cdpClient.Fetch.continueRequest({ requestId: event.requestId });
});
return commands.measure.start('https://www.example.org'); The setTimeout in the request-pause listener delays continuation by 2000ms. The matching resource arrives 2 seconds late; the rest of the page loads at full speed.
Recipe: simulate offline / no network
Useful for testing offline-first apps and service worker behaviour:
await commands.cdp.send('Network.enable');
await commands.cdp.send('Network.emulateNetworkConditions', {
offline: true,
latency: 0,
downloadThroughput: 0,
uploadThroughput: 0
});
await commands.navigate('https://www.example.org');
// Verify your offline page actually rendered
await commands.wait('id:offline-page'); The same Network.emulateNetworkConditions is what --connectivity custom uses under the hood. Setting offline: true cuts all traffic; setting throughput / latency without offline lets you simulate a slow connection.
Recipe: override the Accept-Language header
Test the localised version of a page without changing your machine's locale:
await commands.cdp.send('Network.setExtraHTTPHeaders', {
headers: { 'Accept-Language': 'sv-SE,sv;q=0.9,en;q=0.5' }
});
return commands.measure.start('https://www.example.org'); The same trick works for any header you want to send on every request — Authorization for an authenticated bot, X-Test-Cell for a test variant, etc.
Recipe: inject JavaScript on every new document
Run a snippet in every page the browser loads, before the page's own JS gets to run. Useful for stubbing globals, setting up A/B variant locks, or installing a measurement hook:
await commands.cdp.send('Page.addScriptToEvaluateOnNewDocument', {
source: `
window.__forceVariant = 'B';
Object.defineProperty(window, 'navigator', {
value: new Proxy(navigator, {
get: (t, k) => k === 'language' ? 'sv-SE' : t[k]
})
});
`
});
return commands.measure.start('https://www.example.org'); Combine recipes — addScriptToEvaluateOnNewDocument plus setExtraHTTPHeaders plus setGeolocationOverride is a complete "fake a Swedish user" setup.
Things to remember
- Enable the domain before subscribing.
Network.enable,Fetch.enable, etc. — most listeners need their domain enabled before they receive events. - Subscriptions are per-page. Listeners installed before
measure.startapply to that page. If you want them on the next page too, install them again or useaddScriptToEvaluateOnNewDocumentfor things that travel. - Fetch.requestPaused must call continueRequest or continueResponse. If you forget, the request hangs forever. Test your handlers.
- Don't run heavy logic in the listener. It runs on every request; a slow handler slows the page. Stash the data, do the work after.