@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@detiq/crawler",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
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
- if (domElements.length >= 2) return domElements;
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';