Files
hyperframes/packages/cli/src/capture/navigateForCapture.ts
T
Xuanru LiandCursor 43dba22057 fix(capture): bound scroll/evaluate timeouts so heavy pages keep capturing (#3236)
* fix(capture): bound scroll/evaluate timeouts so heavy pages keep capturing

Fonts/Vercel-class sites were failing after navigation when a single in-page
scroll/evaluate hung until protocolTimeout. Drive lazy scroll from Node,
degrade on evaluate timeouts, and stop labeling those failures as bot blocks.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(capture): enforce stage budgets and surface evaluate degradation

Bound each scroll/content evaluate with remaining stage time so a wedged
page.evaluate cannot outlive the advertised 15s/8s budgets, skip recovery
CDP calls after expiry, and return/propagate timed-out animation and
screenshot work so caller warnings are reachable.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(capture): prefer TimeoutError instanceof for timeout classification

Use puppeteer-core TimeoutError as the primary signal for navigation vs
evaluate/protocol timeouts, with message checks only as a fallback for
string formatting and non-TimeoutError cases.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 18:26:22 -07:00

64 lines
1.7 KiB
TypeScript

import { isNavigationTimeoutError } from "./captureTimeout.js";
export { isNavigationTimeoutError };
export const NETWORK_IDLE_ATTEMPT_MS = 30_000;
export type CaptureGotoWaitUntil = "networkidle2" | "domcontentloaded";
export interface CaptureGotoOptions {
waitUntil: CaptureGotoWaitUntil;
timeout: number;
}
export interface CaptureGotoPage<TResponse = unknown> {
goto(url: string, options: CaptureGotoOptions): Promise<TResponse>;
}
export interface NavigateForCaptureResult<TResponse = unknown> {
response: TResponse;
waitUntil: CaptureGotoWaitUntil;
networkIdleTimeoutMs: number;
fellBackFromNetworkIdle: boolean;
}
export function networkIdleAttemptTimeoutMs(totalTimeoutMs: number): number {
return Math.min(NETWORK_IDLE_ATTEMPT_MS, Math.max(0, totalTimeoutMs));
}
export async function navigateForCapture<TResponse>(
page: CaptureGotoPage<TResponse>,
url: string,
totalTimeoutMs: number,
): Promise<NavigateForCaptureResult<TResponse>> {
const networkIdleTimeoutMs = networkIdleAttemptTimeoutMs(totalTimeoutMs);
try {
const response = await page.goto(url, {
waitUntil: "networkidle2",
timeout: networkIdleTimeoutMs,
});
return {
response,
waitUntil: "networkidle2",
networkIdleTimeoutMs,
fellBackFromNetworkIdle: false,
};
} catch (err) {
if (!isNavigationTimeoutError(err) || networkIdleTimeoutMs >= totalTimeoutMs) {
throw err;
}
}
const fallbackTimeoutMs = totalTimeoutMs - networkIdleTimeoutMs;
const response = await page.goto(url, {
waitUntil: "domcontentloaded",
timeout: fallbackTimeoutMs,
});
return {
response,
waitUntil: "domcontentloaded",
networkIdleTimeoutMs,
fellBackFromNetworkIdle: true,
};
}