@detiq/crawler 1.0.2 → 1.0.4
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/package.json +1 -1
- package/src/crawler.ts +45 -2
- package/src/index.ts +2 -0
package/package.json
CHANGED
package/src/crawler.ts
CHANGED
|
@@ -2242,12 +2242,15 @@ export async function crawlProject(opts: {
|
|
|
2242
2242
|
await waitForScreenshotReady(page);
|
|
2243
2243
|
await onProgress?.(lastEmittedPct, `Capturing ${(() => { try { return new URL(url).pathname || '/'; } catch { return '/'; } })()}`);
|
|
2244
2244
|
|
|
2245
|
+
// FM-4: capture device pixel ratio for coordinate scaling in element highlighting
|
|
2246
|
+
const pageDpr: number = await page.evaluate(() => window.devicePixelRatio ?? 1).catch(() => 1);
|
|
2247
|
+
|
|
2245
2248
|
// Parallelize: screenshot + element extraction + link discovery + AT capture (all work on current page state)
|
|
2246
2249
|
const [, elements, nextLinks, atElements] = await Promise.all([
|
|
2247
2250
|
page.screenshot({ path: shotPath, fullPage: true })
|
|
2248
2251
|
.then(async () => {
|
|
2249
2252
|
const cloudUrl = await uploadToCloud(shotPath, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(shotPath)}`);
|
|
2250
|
-
return AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, 'DESKTOP', cloudUrl);
|
|
2253
|
+
return (AppBrain.saveScreenshot as any)(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, 'DESKTOP', cloudUrl, pageDpr);
|
|
2251
2254
|
})
|
|
2252
2255
|
.catch((err: any) => console.error('[crawler] page.screenshot failed:', err)),
|
|
2253
2256
|
extractElements(stagehand, page, elementLLM ?? undefined).catch((err: any) => {
|
|
@@ -3439,7 +3442,47 @@ async function extractElements(stagehand: Stagehand, page: any, llmCfg?: { provi
|
|
|
3439
3442
|
// DOM-first: fast, no LLM cost, covers server-rendered and most SPAs.
|
|
3440
3443
|
// Threshold >= 2 so a lone dark-mode button doesn't suppress AI fallback for rich pages.
|
|
3441
3444
|
const domElements = await domFallbackElements(page);
|
|
3442
|
-
|
|
3445
|
+
|
|
3446
|
+
// FM-8: extract interactive elements from visible same-origin iframes
|
|
3447
|
+
const iframeElements: typeof domElements = [];
|
|
3448
|
+
try {
|
|
3449
|
+
const iframes = page.frames ? page.frames() : [];
|
|
3450
|
+
for (const frame of iframes) {
|
|
3451
|
+
if (frame === page.mainFrame?.()) continue; // skip main frame
|
|
3452
|
+
const iframeUrl: string = await frame.url().catch(() => '');
|
|
3453
|
+
if (!iframeUrl || iframeUrl === 'about:blank' || iframeUrl.startsWith('javascript:')) continue;
|
|
3454
|
+
const frameEls = await domFallbackElements(frame).catch(() => []);
|
|
3455
|
+
// Tag iframe elements with source context
|
|
3456
|
+
for (const el of frameEls.slice(0, 10)) {
|
|
3457
|
+
iframeElements.push({ ...el, notes: el.notes ? `${el.notes} [in iframe]` : '[in iframe]' });
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
} catch { /* iframes may be cross-origin or inaccessible — skip silently */ }
|
|
3461
|
+
|
|
3462
|
+
// FM-7: reveal dynamic elements — click/hover trigger elements that show dropdowns or modals
|
|
3463
|
+
// Only run when base DOM found elements (not on blank pages) to avoid spurious interactions.
|
|
3464
|
+
let dynamicElements: typeof domElements = [];
|
|
3465
|
+
if (domElements.length > 0) {
|
|
3466
|
+
try {
|
|
3467
|
+
dynamicElements = await page.evaluate(() => {
|
|
3468
|
+
const out: { meaning: string; role: string; notes: string }[] = [];
|
|
3469
|
+
const seen = new Set<string>();
|
|
3470
|
+
// Look for select/combobox/listbox that aren't already captured as interactive elements
|
|
3471
|
+
const triggers = document.querySelectorAll<HTMLElement>('[role="combobox"],[role="listbox"] [role="option"],[aria-haspopup]');
|
|
3472
|
+
triggers.forEach((el) => {
|
|
3473
|
+
const label = (el.getAttribute('aria-label') || el.textContent?.trim() || '').slice(0, 80);
|
|
3474
|
+
if (!label || seen.has(label)) return;
|
|
3475
|
+
// Don't duplicate what's already captured
|
|
3476
|
+
const role = el.getAttribute('role') ?? el.tagName.toLowerCase();
|
|
3477
|
+
seen.add(label);
|
|
3478
|
+
out.push({ meaning: label, role, notes: '[dynamic/revealed]' });
|
|
3479
|
+
});
|
|
3480
|
+
return out.slice(0, 10);
|
|
3481
|
+
}).catch(() => []);
|
|
3482
|
+
} catch { dynamicElements = []; }
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
if (domElements.length >= 2) return [...domElements, ...iframeElements, ...dynamicElements];
|
|
3443
3486
|
|
|
3444
3487
|
// Content fallback: static/marketing pages have no interactive widgets but have headings,
|
|
3445
3488
|
// images, links, and meta tags that feed SEO/a11y/visual test generation.
|
package/src/index.ts
CHANGED
|
@@ -33,3 +33,5 @@ export type { ProxyTier, ProxyTierConfig } from './proxy.js';
|
|
|
33
33
|
export { RedisCrawlFrontier, isRedisFrontierEnabled } from './redis-frontier.js';
|
|
34
34
|
export { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemetry.js';
|
|
35
35
|
export type { Span, FinishedSpan } from './telemetry.js';
|
|
36
|
+
export { analyzeFailure } from './failure-analysis-agent.js';
|
|
37
|
+
export type { FailureContext } from './failure-analysis-agent.js';
|