autokap 1.9.8 → 1.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-pool.js +12 -0
- package/dist/browser.js +31 -4
- package/dist/opcode-runner.js +44 -14
- package/dist/web-playwright-local.js +9 -1
- package/package.json +1 -1
package/dist/browser-pool.js
CHANGED
|
@@ -48,6 +48,12 @@ export const CHROMIUM_ARGS = [
|
|
|
48
48
|
const MAX_CONCURRENT = parseInt(process.env.BROWSER_POOL_MAX_CONCURRENT ?? '5', 10);
|
|
49
49
|
/** Restart the shared browser process after this many captures to prevent memory leaks. */
|
|
50
50
|
const MAX_CAPTURES_BEFORE_RESTART = 100;
|
|
51
|
+
/**
|
|
52
|
+
* Default per-action Playwright timeout for pooled contexts. Below the 30s
|
|
53
|
+
* CAPTURE_SCREENSHOT global deadline so a starved call fails with budget left
|
|
54
|
+
* for recovery instead of eating the whole variant. See usage in acquireContext.
|
|
55
|
+
*/
|
|
56
|
+
const POOL_CONTEXT_ACTION_TIMEOUT_MS = 25000;
|
|
51
57
|
function normalizeDeviceScaleFactor(value) {
|
|
52
58
|
if (!Number.isFinite(value))
|
|
53
59
|
return 2;
|
|
@@ -79,6 +85,12 @@ class BrowserPool {
|
|
|
79
85
|
// any conflict). See PRIVACY_HEADERS / installAnalyticsBlock (AUT-234).
|
|
80
86
|
extraHTTPHeaders: { ...PRIVACY_HEADERS, ...(extra ?? {}) },
|
|
81
87
|
});
|
|
88
|
+
// Stop every Playwright call from inheriting the built-in 30 000 ms default.
|
|
89
|
+
// Pooled captures run many variants per Cloud Run container; under CPU
|
|
90
|
+
// contention a starved call (e.g. page.screenshot) would otherwise block the
|
|
91
|
+
// full 30s — equal to the CAPTURE_SCREENSHOT global deadline — leaving no
|
|
92
|
+
// budget for recovery and voiding the variant. 25s sits under that cap.
|
|
93
|
+
context.setDefaultTimeout(POOL_CONTEXT_ACTION_TIMEOUT_MS);
|
|
82
94
|
// Block third-party analytics so pooled (server-side) captures don't
|
|
83
95
|
// register a phantom visit in the captured site's analytics (AUT-234).
|
|
84
96
|
// Skippable per-project via blockAnalytics === false.
|
package/dist/browser.js
CHANGED
|
@@ -209,6 +209,29 @@ async function seedCloudChromiumPreferences(userDataDir) {
|
|
|
209
209
|
};
|
|
210
210
|
await writeFile(preferencesPath, `${JSON.stringify(preferences)}\n`, 'utf8');
|
|
211
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Default per-action timeout applied to every Playwright context we create.
|
|
214
|
+
* Without this, calls (page.screenshot / page.title / locator actions) inherit
|
|
215
|
+
* Playwright's built-in 30 000 ms default. Under Cloud Run CPU contention (many
|
|
216
|
+
* variants per container) a single starved call would then block for the full
|
|
217
|
+
* 30s — which equals the CAPTURE_SCREENSHOT global deadline, leaving zero budget
|
|
218
|
+
* for recovery/recapture and voiding the whole variant. 25s sits just under that
|
|
219
|
+
* global cap so a hung call fails with budget left for a clean, bounded error.
|
|
220
|
+
* NAVIGATE keeps its own bounded path — we intentionally do NOT touch
|
|
221
|
+
* navigationTimeout here.
|
|
222
|
+
*/
|
|
223
|
+
const CONTEXT_ACTION_TIMEOUT_MS = 25000;
|
|
224
|
+
/**
|
|
225
|
+
* Explicit ceiling for the heavy screenshot primitives. Below the context
|
|
226
|
+
* default so an already-abandoned capture (the runner gives up earlier, ~12s)
|
|
227
|
+
* stops burning renderer CPU quickly instead of contending with sibling
|
|
228
|
+
* variants until the 25s default fires.
|
|
229
|
+
*/
|
|
230
|
+
const SCREENSHOT_PRIMITIVE_TIMEOUT_MS = 20000;
|
|
231
|
+
/** Apply our engine-wide Playwright defaults to a freshly created context. */
|
|
232
|
+
function applyContextDefaults(context) {
|
|
233
|
+
context.setDefaultTimeout(CONTEXT_ACTION_TIMEOUT_MS);
|
|
234
|
+
}
|
|
212
235
|
async function withHelperTimeout(label, timeoutMs, work) {
|
|
213
236
|
if (!timeoutMs || timeoutMs <= 0) {
|
|
214
237
|
return work();
|
|
@@ -1085,6 +1108,7 @@ export class Browser {
|
|
|
1085
1108
|
headless: false,
|
|
1086
1109
|
args: clipArgs,
|
|
1087
1110
|
});
|
|
1111
|
+
applyContextDefaults(instance.context);
|
|
1088
1112
|
instance.browser = instance.context.browser();
|
|
1089
1113
|
instance.persistentContext = true;
|
|
1090
1114
|
// launchPersistentContext opens an initial about:blank page automatically.
|
|
@@ -1110,6 +1134,7 @@ export class Browser {
|
|
|
1110
1134
|
args: clipArgs,
|
|
1111
1135
|
});
|
|
1112
1136
|
instance.context = await instance.browser.newContext(contextOptions);
|
|
1137
|
+
applyContextDefaults(instance.context);
|
|
1113
1138
|
}
|
|
1114
1139
|
// Block third-party analytics beacons so clip/video capture doesn't
|
|
1115
1140
|
// register a phantom visit either (AUT-234). Context-level so it covers
|
|
@@ -1253,6 +1278,7 @@ export class Browser {
|
|
|
1253
1278
|
args: CHROMIUM_ARGS,
|
|
1254
1279
|
});
|
|
1255
1280
|
this.context = await this.browser.newContext(this.buildContextOptions());
|
|
1281
|
+
applyContextDefaults(this.context);
|
|
1256
1282
|
if (this.options.blockAnalytics !== false) {
|
|
1257
1283
|
await installAnalyticsBlock(this.context);
|
|
1258
1284
|
}
|
|
@@ -1345,6 +1371,7 @@ export class Browser {
|
|
|
1345
1371
|
this.context = null;
|
|
1346
1372
|
}
|
|
1347
1373
|
this.context = await this.browser.newContext(this.buildContextOptions());
|
|
1374
|
+
applyContextDefaults(this.context);
|
|
1348
1375
|
if (this.options.blockAnalytics !== false) {
|
|
1349
1376
|
await installAnalyticsBlock(this.context);
|
|
1350
1377
|
}
|
|
@@ -1840,7 +1867,7 @@ export class Browser {
|
|
|
1840
1867
|
// terminal block where ANSI escape literals were producing tofu boxes).
|
|
1841
1868
|
await stripInvisibleControlChars(page);
|
|
1842
1869
|
await logFontDiagnostics(page, 'before screenshot');
|
|
1843
|
-
const screenshot = Buffer.from(await page.screenshot({ type: 'png', fullPage: false }));
|
|
1870
|
+
const screenshot = Buffer.from(await page.screenshot({ type: 'png', fullPage: false, timeout: SCREENSHOT_PRIMITIVE_TIMEOUT_MS }));
|
|
1844
1871
|
await logCaptureRenderDiagnostics(page, screenshot, this.options, 'after screenshot');
|
|
1845
1872
|
return screenshot;
|
|
1846
1873
|
}
|
|
@@ -5330,7 +5357,7 @@ export class Browser {
|
|
|
5330
5357
|
try {
|
|
5331
5358
|
await this.waitForFontsBeforeScreenshot(page);
|
|
5332
5359
|
await logFontDiagnostics(page, 'before region screenshot');
|
|
5333
|
-
const fullPage = Buffer.from(await page.screenshot({ type: 'png', fullPage: true }));
|
|
5360
|
+
const fullPage = Buffer.from(await page.screenshot({ type: 'png', fullPage: true, timeout: SCREENSHOT_PRIMITIVE_TIMEOUT_MS }));
|
|
5334
5361
|
const image = sharp(fullPage);
|
|
5335
5362
|
const meta = await image.metadata();
|
|
5336
5363
|
const imageWidth = meta.width ?? 0;
|
|
@@ -5453,7 +5480,7 @@ export class Browser {
|
|
|
5453
5480
|
const dpr = pageInfo.dpr;
|
|
5454
5481
|
await this.waitForFontsBeforeScreenshot(page);
|
|
5455
5482
|
await logFontDiagnostics(page, 'before selector screenshot');
|
|
5456
|
-
const fullPage = Buffer.from(await page.screenshot({ type: 'png', fullPage: true }));
|
|
5483
|
+
const fullPage = Buffer.from(await page.screenshot({ type: 'png', fullPage: true, timeout: SCREENSHOT_PRIMITIVE_TIMEOUT_MS }));
|
|
5457
5484
|
const image = sharp(fullPage);
|
|
5458
5485
|
const meta = await image.metadata();
|
|
5459
5486
|
const imgW = meta.width ?? 0;
|
|
@@ -5517,7 +5544,7 @@ export class Browser {
|
|
|
5517
5544
|
const dpr = pageInfo.dpr;
|
|
5518
5545
|
await this.waitForFontsBeforeScreenshot(page);
|
|
5519
5546
|
await logFontDiagnostics(page, 'before bounding-region screenshot');
|
|
5520
|
-
const fullPage = Buffer.from(await page.screenshot({ type: 'png', fullPage: true }));
|
|
5547
|
+
const fullPage = Buffer.from(await page.screenshot({ type: 'png', fullPage: true, timeout: SCREENSHOT_PRIMITIVE_TIMEOUT_MS }));
|
|
5521
5548
|
const image = sharp(fullPage);
|
|
5522
5549
|
const meta = await image.metadata();
|
|
5523
5550
|
const imgW = meta.width ?? 0;
|
package/dist/opcode-runner.js
CHANGED
|
@@ -55,6 +55,15 @@ export class NoOpRecoveryChain {
|
|
|
55
55
|
}
|
|
56
56
|
const MIN_CLIP_FINALIZATION_TIMEOUT_MS = 30000;
|
|
57
57
|
const DEFAULT_VIDEO_RECORDING_RESOLUTION = { width: 1920, height: 1080 };
|
|
58
|
+
/**
|
|
59
|
+
* Per-call bounds for the CAPTURE_SCREENSHOT action, kept well under the ~30s
|
|
60
|
+
* global capture deadline so a hung Playwright call fails early and leaves budget
|
|
61
|
+
* for recovery/recapture instead of voiding the variant. Fixed constants because
|
|
62
|
+
* executeOpcodeAction has no access to the deadline; 12s + 3×3s worst case still
|
|
63
|
+
* leaves >=12s of the 30s budget for the recovery path.
|
|
64
|
+
*/
|
|
65
|
+
const CAPTURE_BUFFER_TIMEOUT_MS = 12000;
|
|
66
|
+
const CAPTURE_METADATA_TIMEOUT_MS = 3000;
|
|
58
67
|
/**
|
|
59
68
|
* The compiled per-opcode action budget. For CAPTURE_SCREENSHOT this governs
|
|
60
69
|
* ONLY the deterministic capture (visual stabilize + screenshot + favicon/title
|
|
@@ -463,9 +472,12 @@ async function executeOpcode(opcode, index, adapter, verifier, breaker, recovery
|
|
|
463
472
|
error: `recovery succeeded but produced no ${opcode.kind === 'END_CLIP' ? 'clip' : 'screenshot'} artifact: ${reason}`,
|
|
464
473
|
};
|
|
465
474
|
};
|
|
466
|
-
// Track page context for circuit breaker
|
|
475
|
+
// Track page context for circuit breaker. Bounded + best-effort: this runs on
|
|
476
|
+
// EVERY opcode inside its global-deadline window, so a getCurrentUrl starved by
|
|
477
|
+
// CPU contention must not eat the budget the capture + recovery need (it would
|
|
478
|
+
// otherwise be capped only by the 25s context default).
|
|
467
479
|
try {
|
|
468
|
-
const url = await adapter.getCurrentUrl();
|
|
480
|
+
const url = await withTimeout(() => adapter.getCurrentUrl(), CAPTURE_METADATA_TIMEOUT_MS);
|
|
469
481
|
breaker.setPage(url);
|
|
470
482
|
}
|
|
471
483
|
catch { /* ignore */ }
|
|
@@ -879,25 +891,43 @@ async function executeOpcodeAction(opcode, opcodeIndex, adapter, artifacts, tele
|
|
|
879
891
|
lowConfidenceReasons.push(`captured before visual stability: ${stability.reason}`);
|
|
880
892
|
}
|
|
881
893
|
const captureLowConfidenceReason = lowConfidenceReasons.join('; ') || undefined;
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
//
|
|
894
|
+
// Capture the screenshot buffer FIRST and bound it — this is the only
|
|
895
|
+
// artifact-critical call. Under CPU contention (many cloud variants per
|
|
896
|
+
// container) a Playwright screenshot can otherwise block the full ~30s
|
|
897
|
+
// global deadline, leaving zero budget for recovery/recapture and voiding
|
|
898
|
+
// the variant. Failing at ~12s keeps ≥12s of the 30s budget for the
|
|
899
|
+
// recovery path (handleFailure → recapture in `failWithRecovery`).
|
|
900
|
+
const buffer = await withTimeout(() => takeCaptureBuffer(adapter, opcode), CAPTURE_BUFFER_TIMEOUT_MS);
|
|
901
|
+
// Everything below is best-effort browser-mockup metadata. The buffer is
|
|
902
|
+
// already in hand, so a slow/hung metadata call must NEVER throw out of
|
|
903
|
+
// the action and discard a good screenshot. Each is individually bounded
|
|
904
|
+
// and degrades gracefully (empty URL / no favicon / hostname title).
|
|
905
|
+
let captureUrl = '';
|
|
906
|
+
try {
|
|
907
|
+
captureUrl = await withTimeout(() => adapter.getCurrentUrl(), CAPTURE_METADATA_TIMEOUT_MS);
|
|
908
|
+
}
|
|
909
|
+
catch { /* empty captureUrl is tolerated downstream */ }
|
|
885
910
|
let tabIconData;
|
|
886
911
|
let tabIconMimeType;
|
|
887
912
|
if (adapter.extractFavicon) {
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
913
|
+
try {
|
|
914
|
+
const favicon = await withTimeout(() => adapter.extractFavicon(), CAPTURE_METADATA_TIMEOUT_MS);
|
|
915
|
+
if (favicon) {
|
|
916
|
+
tabIconData = favicon.buffer;
|
|
917
|
+
tabIconMimeType = favicon.mimeType;
|
|
918
|
+
}
|
|
892
919
|
}
|
|
920
|
+
catch { /* ship the screenshot without a favicon */ }
|
|
893
921
|
}
|
|
894
|
-
//
|
|
895
|
-
// title instead of falling back to the hostname.
|
|
922
|
+
// Real tab title for the browser mockup; falls back to hostname when null.
|
|
896
923
|
let pageTitle;
|
|
897
924
|
if (adapter.getPageTitle) {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
925
|
+
try {
|
|
926
|
+
const resolved = await withTimeout(() => adapter.getPageTitle(), CAPTURE_METADATA_TIMEOUT_MS);
|
|
927
|
+
if (resolved)
|
|
928
|
+
pageTitle = resolved;
|
|
929
|
+
}
|
|
930
|
+
catch { /* fall back to the hostname mockup */ }
|
|
901
931
|
}
|
|
902
932
|
artifacts.push({
|
|
903
933
|
mediaMode: 'screenshot',
|
|
@@ -194,7 +194,15 @@ export class WebPlaywrightLocal {
|
|
|
194
194
|
async getPageTitle() {
|
|
195
195
|
const page = await this.browser.currentPage;
|
|
196
196
|
try {
|
|
197
|
-
|
|
197
|
+
// Metadata for the browser-mockup tab, not the artifact itself. page.title()
|
|
198
|
+
// takes no timeout and inherits Playwright's 30s default; under CPU
|
|
199
|
+
// contention (Cloud Run) it can swallow the whole capture action budget
|
|
200
|
+
// AFTER the screenshot buffer was already produced, discarding a good shot.
|
|
201
|
+
// Bound it tightly and degrade to the hostname fallback (null) on timeout.
|
|
202
|
+
const raw = await Promise.race([
|
|
203
|
+
page.title(),
|
|
204
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 2000)),
|
|
205
|
+
]);
|
|
198
206
|
const trimmed = raw?.trim();
|
|
199
207
|
if (!trimmed)
|
|
200
208
|
return null;
|