From aaec97e160dbc4c40ecc6cd0d46328222e34f8d2 Mon Sep 17 00:00:00 2001 From: Sitaraman Subramanian Date: Tue, 14 Apr 2026 10:01:22 +0530 Subject: [PATCH] Exclude captcha, analytics, and tracking requests from networkidle calculations Playwright's networkidle waits for 500ms of zero inflight requests. Captcha providers, analytics SDKs, and session heartbeat endpoints poll continuously, preventing networkidle from ever firing on real-world sites. This patch adds URL-based filtering to _inflightRequestStarted and _inflightRequestFinished in FrameManager, following the existing _isFavicon exclusion pattern. Matching requests are never added to the inflight set, so they don't delay the 500ms idle timer. Excluded patterns: - Captcha: Cloudflare Turnstile, reCAPTCHA, hCaptcha, Arkose Labs - Analytics: Google Analytics, GTM - Session recording: Hotjar, FullStory, LogRocket, Mouseflow, Clarity - Telemetry: Datadog, Sentry, New Relic - Fraud detection: Forter - Generic: /heartbeat, /keepalive, /keep-alive, /beacon --- driver_patches/framesPatch.ts | 59 +++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/driver_patches/framesPatch.ts b/driver_patches/framesPatch.ts index e86322b..fe85009 100644 --- a/driver_patches/framesPatch.ts +++ b/driver_patches/framesPatch.ts @@ -1,6 +1,41 @@ import { type Project, SyntaxKind } from "ts-morph"; import { assertDefined } from "./utils.ts"; +// URL patterns excluded from networkidle calculations. +// These are captcha providers, analytics, tracking, fraud detection, and session +// heartbeat endpoints that poll continuously and would prevent networkidle from firing. +const NETWORKIDLE_EXCLUDED_URL_PATTERNS = [ + // -- Captcha providers -- + 'challenges.cloudflare.com', + 'google.com/recaptcha', + 'www.gstatic.com/recaptcha', + 'hcaptcha.com', + 'api.funcaptcha.com', + 'client-api.arkoselabs.com', + // -- Analytics & tracking -- + 'google-analytics.com', + 'googletagmanager.com', + 'analytics.google.com', + // -- Session recording & heatmaps -- + 'hotjar.com', + 'fullstory.com', + 'logrocket.com', + 'mouseflow.com', + 'clarity.ms', + // -- Telemetry & monitoring -- + 'browser-intake-datadoghq.com', + 'sentry.io', + 'newrelic.com', + 'nr-data.net', + // -- Fraud detection -- + 'forter.com', + // -- Common polling/heartbeat patterns -- + '/heartbeat', + '/keepalive', + '/keep-alive', + '/beacon', +]; + // ---------------- // server/frames.ts // ---------------- @@ -29,6 +64,30 @@ export function patchFrames(project: Project) { "frame._isolatedWorld = undefined;" ]); + // -- _inflightRequestStarted Method (captcha networkidle exclusion) -- + const inflightStartedMethod = frameManagerClass.getMethodOrThrow("_inflightRequestStarted"); + const inflightStartedBody = inflightStartedMethod.getBodyOrThrow().asKindOrThrow(SyntaxKind.Block); + const faviconCheckStart = assertDefined( + inflightStartedBody.getStatements().find(s => s.getText().includes('request._isFavicon')), + '_isFavicon check in _inflightRequestStarted' + ); + inflightStartedBody.insertStatements(faviconCheckStart.getChildIndex() + 1, [ + `const _reqUrl = request.url();`, + `if (${JSON.stringify(NETWORKIDLE_EXCLUDED_URL_PATTERNS)}.some(p => _reqUrl.includes(p))) return;`, + ]); + + // -- _inflightRequestFinished Method (captcha networkidle exclusion) -- + const inflightFinishedMethod = frameManagerClass.getMethodOrThrow("_inflightRequestFinished"); + const inflightFinishedBody = inflightFinishedMethod.getBodyOrThrow().asKindOrThrow(SyntaxKind.Block); + const faviconCheckFinish = assertDefined( + inflightFinishedBody.getStatements().find(s => s.getText().includes('request._isFavicon')), + '_isFavicon check in _inflightRequestFinished' + ); + inflightFinishedBody.insertStatements(faviconCheckFinish.getChildIndex() + 1, [ + `const _reqUrl = request.url();`, + `if (${JSON.stringify(NETWORKIDLE_EXCLUDED_URL_PATTERNS)}.some(p => _reqUrl.includes(p))) return;`, + ]); + // ------- Frame Class ------- const frameClass = framesSourceFile.getClassOrThrow("Frame"); // Add Properties to the Frame Class