Choose the right integration strategy and understand bot detection trade-offs.
| Approach | Bot detection risk | Best for |
| Regular Playwright | Low-Moderate | Simple DOM extraction, server-rendered sites |
Passive interception (page.on('response')) | Low | SPAs that load data via API calls during navigation |
In-browser fetch (pageRequest()) | Moderate | API endpoints normally called with fetch/XHR |
| Direct HTTP from Node.js | Very high | Sites with no bot detection where API speed matters |
.libretto/sessions/<session>/network.jsonl with jq to review all requests and responses. Look for calls to bot detection services (Cloudflare, Akamai, PerimeterX, DataDome) or challenge endpoints.npx libretto exec to evaluate window.fetch.toString() in the browser console. If it returns actual JavaScript (not "[native code]"), the site monkey-patches fetch and you should prefer passive interception over in-browser fetch.network.jsonl before replacing browser behavior with pageRequest(). Browsers attach request-context headers that say what caused the request. A top-level navigation has sec-fetch-dest: document and sec-fetch-mode: navigate. A JavaScript fetch has sec-fetch-dest: empty and sec-fetch-mode: cors or same-origin. Script, image, stylesheet, and iframe loads have their own shapes too.npx libretto snapshot to check for challenge pages, CAPTCHAs, or interstitials that indicate detection.page.evaluate().12345678910111213// Navigate and interact await page.goto('https://example.com/search'); await page.fill('#query', 'search term'); await page.click('#submit'); await page.waitForSelector('.results'); // Extract data from the DOM const results = await page.evaluate(() => { return Array.from(document.querySelectorAll('.result-item')).map(el => ({ title: el.querySelector('h2')?.textContent, price: el.querySelector('.price')?.textContent, })); });
headless: false and Playwright's trace viewerplaywright-extra with the stealth plugin to patch common fingerprint leaks, or run Playwright with a persistent browser context that looks more like a real browser profile.pageRequest()) for verified API endpoints that the site normally calls with fetch/XHR. It gives you full control over which endpoints you call, structured JSON responses, and the real browser's network fingerprint.page.on('response') interception for data capture. This avoids making any extra requests that could trigger detection.page.on('response')) when:pageRequest()) when:fetch (or you can work around it)