@detiq/crawler 1.1.19 → 1.1.20
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/auth.js +12 -2
- package/dist/auth.js.map +2 -2
- package/dist/crawler.js +370 -155
- package/dist/crawler.js.map +2 -2
- package/dist/job-queue.js +46 -3
- package/dist/job-queue.js.map +2 -2
- package/dist/native-crawler.js +6 -6
- package/dist/native-crawler.js.map +2 -2
- package/package.json +1 -1
- package/src/auth.ts +13 -1
- package/src/crawler.ts +307 -60
- package/src/job-queue.ts +30 -3
- package/src/native-crawler.ts +6 -4
- package/types/crawler.d.ts +4 -0
- package/types/job-queue.d.ts +3 -1
package/dist/crawler.js
CHANGED
|
@@ -196,25 +196,53 @@ function normalizeAuthUrl(url) {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
async function capturePageHtml(page) {
|
|
199
|
+
let html = null;
|
|
199
200
|
try {
|
|
200
201
|
if (typeof page.content === "function") {
|
|
201
|
-
const
|
|
202
|
-
if (typeof
|
|
202
|
+
const raw = await page.content();
|
|
203
|
+
if (typeof raw === "string" && raw.trim()) html = raw;
|
|
203
204
|
}
|
|
204
205
|
} catch (err) {
|
|
205
206
|
logger.warn({ err: String(err?.message ?? err) }, "[crawler] page.content() failed; trying DOM evaluate fallback");
|
|
206
207
|
}
|
|
208
|
+
if (!html) {
|
|
209
|
+
try {
|
|
210
|
+
html = await page.evaluate(() => {
|
|
211
|
+
const type = document.doctype;
|
|
212
|
+
const doctype = type ? "<!DOCTYPE " + type.name + (type.publicId ? ' PUBLIC "' + type.publicId + '"' : "") + (type.systemId ? ' "' + type.systemId + '"' : "") + ">" : "<!DOCTYPE html>";
|
|
213
|
+
return doctype + "\n" + (document.documentElement?.outerHTML ?? document.body?.outerHTML ?? "");
|
|
214
|
+
});
|
|
215
|
+
if (typeof html !== "string" || !html.trim()) html = null;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
logger.warn({ err: String(err?.message ?? err) }, "[crawler] DOM snapshot capture failed");
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!html) return null;
|
|
207
222
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
223
|
+
html = await page.evaluate(() => {
|
|
224
|
+
document.querySelectorAll("script").forEach((s) => s.remove());
|
|
225
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
226
|
+
try {
|
|
227
|
+
const cssText = Array.from(sheet.cssRules).map((r) => r.cssText).join("\n");
|
|
228
|
+
const style = document.createElement("style");
|
|
229
|
+
style.textContent = cssText;
|
|
230
|
+
const owner = sheet.ownerNode;
|
|
231
|
+
owner?.replaceWith(style);
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const doctype = (() => {
|
|
236
|
+
const dt = document.doctype;
|
|
237
|
+
return dt ? `<!DOCTYPE ${dt.name}>` : "<!DOCTYPE html>";
|
|
238
|
+
})();
|
|
239
|
+
return doctype + "\n" + (document.documentElement?.outerHTML ?? "");
|
|
212
240
|
});
|
|
213
|
-
|
|
241
|
+
if (typeof html !== "string" || !html.trim()) html = null;
|
|
214
242
|
} catch (err) {
|
|
215
|
-
logger.warn({ err: String(err?.message ?? err) }, "[crawler]
|
|
216
|
-
return null;
|
|
243
|
+
logger.warn({ err: String(err?.message ?? err) }, "[crawler] CSS inlining failed; using raw HTML");
|
|
217
244
|
}
|
|
245
|
+
return html;
|
|
218
246
|
}
|
|
219
247
|
async function launchChromeCDP(execPath, extraArgs, proxy) {
|
|
220
248
|
const port = 9200 + Math.floor(Math.random() * 700);
|
|
@@ -780,6 +808,14 @@ async function crawlProject(opts, onProgress) {
|
|
|
780
808
|
const cloudUrl = await uploadToCloud(popupShotPath, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(popupShotPath)}`).catch(() => void 0);
|
|
781
809
|
await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, popupScreen.id, popupShotPath, popupScreen.version, "DESKTOP", cloudUrl).catch(() => {
|
|
782
810
|
});
|
|
811
|
+
const popupDom = await capturePageHtml(popup);
|
|
812
|
+
if (popupDom) {
|
|
813
|
+
const popupDomPath = popupShotPath.replace(/\.(png|jpg|jpeg)$/i, ".dom.html");
|
|
814
|
+
await fs.writeFile(popupDomPath, popupDom, "utf8");
|
|
815
|
+
const popupDomKey = `${opts.tenantId}/${opts.projectId}/dom-snapshots/${path.basename(popupDomPath)}`;
|
|
816
|
+
AppBrain.updateScreenDomSnapshot(opts.tenantId, popupScreen.id, popupDomPath, await uploadToCloud(popupDomPath, popupDomKey).catch(() => void 0)).catch(() => {
|
|
817
|
+
});
|
|
818
|
+
}
|
|
783
819
|
const popupEls = await domFallbackElements(popup, popup).catch(() => []);
|
|
784
820
|
for (const el of popupEls) {
|
|
785
821
|
AppBrain.saveElement(opts.tenantId, opts.projectId, popupScreen.id, el.meaning, el.role, el.expectedData, el.notes, void 0, true, el.boundingRect, el.ariaState, el.parentLandmark).catch(() => {
|
|
@@ -931,7 +967,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
931
967
|
} catch {
|
|
932
968
|
return "";
|
|
933
969
|
}
|
|
934
|
-
}).filter((h, i, arr) => h && arr.indexOf(h) === i).slice(0,
|
|
970
|
+
}).filter((h, i, arr) => h && arr.indexOf(h) === i).slice(0, 50);
|
|
935
971
|
}, _appOrigin).catch(() => []);
|
|
936
972
|
for (const link of publicNavLinks) {
|
|
937
973
|
if (_norm(link) === _startNorm) continue;
|
|
@@ -968,6 +1004,14 @@ async function crawlProject(opts, onProgress) {
|
|
|
968
1004
|
const cloudUrl = await uploadToCloud(shotPath, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(shotPath)}`).catch(() => void 0);
|
|
969
1005
|
await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, "DESKTOP", cloudUrl).catch(() => {
|
|
970
1006
|
});
|
|
1007
|
+
const pubDom = await capturePageHtml(page);
|
|
1008
|
+
if (pubDom) {
|
|
1009
|
+
const pubDomPath = shotPath.replace(/\.(png|jpg|jpeg)$/i, ".dom.html");
|
|
1010
|
+
await fs.writeFile(pubDomPath, pubDom, "utf8");
|
|
1011
|
+
const pubDomKey = `${opts.tenantId}/${opts.projectId}/dom-snapshots/${path.basename(pubDomPath)}`;
|
|
1012
|
+
AppBrain.updateScreenDomSnapshot(opts.tenantId, screen.id, pubDomPath, await uploadToCloud(pubDomPath, pubDomKey).catch(() => void 0)).catch(() => {
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
971
1015
|
await AppBrain.clearScreenElements(opts.tenantId, opts.projectId, screen.id).catch(() => {
|
|
972
1016
|
});
|
|
973
1017
|
for (const el of preAuthElements) {
|
|
@@ -1369,6 +1413,8 @@ async function crawlProject(opts, onProgress) {
|
|
|
1369
1413
|
let maxSeenTotal = 0;
|
|
1370
1414
|
let lastEmittedPct = 20;
|
|
1371
1415
|
const crawlLoopStart = Date.now();
|
|
1416
|
+
const CRAWL_GLOBAL_TIMEOUT_MS = parseInt(process.env.CRAWL_GLOBAL_TIMEOUT_MS ?? "") || 2 * 60 * 60 * 1e3;
|
|
1417
|
+
const isCrawlTimedOut = () => Date.now() - crawlLoopStart >= CRAWL_GLOBAL_TIMEOUT_MS;
|
|
1372
1418
|
const startUrl = page.url();
|
|
1373
1419
|
const resumeUrls = opts.resumeUrls ?? [];
|
|
1374
1420
|
const gqlIntrospectionPromise = (async () => {
|
|
@@ -1398,6 +1444,10 @@ async function crawlProject(opts, onProgress) {
|
|
|
1398
1444
|
];
|
|
1399
1445
|
const queued = new Set(seedUrls.map(canonicalize));
|
|
1400
1446
|
const queue = new CrawlFrontier(opts.traversalStrategy ?? "BFS", seedUrls, opts.bestFirstKeywords, opts.urlFilter);
|
|
1447
|
+
if (selective) {
|
|
1448
|
+
queue.push = () => {
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1401
1451
|
const deduplicator = new ContentDeduplicator(parseFloat(process.env.CRAWLER_DEDUP_THRESHOLD ?? "") || 0.88);
|
|
1402
1452
|
const deadLinkTracker = new DeadLinkTracker();
|
|
1403
1453
|
const redirectRegistry = new RedirectRegistry();
|
|
@@ -1479,7 +1529,8 @@ async function crawlProject(opts, onProgress) {
|
|
|
1479
1529
|
logger.info({ count: g11SitemapUrls.length }, "[crawler] G11: sitemap discovered public URL candidates");
|
|
1480
1530
|
const sitemapOrigin = new URL(opts.appUrl).origin;
|
|
1481
1531
|
let sitemapCaptured = 0;
|
|
1482
|
-
for (const sUrl of g11SitemapUrls
|
|
1532
|
+
for (const sUrl of g11SitemapUrls) {
|
|
1533
|
+
if (isCrawlTimedOut()) break;
|
|
1483
1534
|
try {
|
|
1484
1535
|
const u = new URL(sUrl);
|
|
1485
1536
|
if (u.origin !== sitemapOrigin) continue;
|
|
@@ -1506,8 +1557,18 @@ async function crawlProject(opts, onProgress) {
|
|
|
1506
1557
|
});
|
|
1507
1558
|
const pubScreen = await AppBrain.saveScreen(opts.tenantId, opts.projectId, pageName, sUrl, opts.jobId, "SITEMAP").catch(() => null);
|
|
1508
1559
|
if (pubScreen) {
|
|
1509
|
-
await
|
|
1560
|
+
const sitemapCloudUrl = await uploadToCloud(shotPath, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(shotPath)}`).catch(() => void 0);
|
|
1561
|
+
await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, pubScreen.id, shotPath, pubScreen.version, "DESKTOP", sitemapCloudUrl).catch(() => {
|
|
1510
1562
|
});
|
|
1563
|
+
const sitemapDom = await capturePageHtml(page);
|
|
1564
|
+
if (sitemapDom) {
|
|
1565
|
+
const sitemapDomPath = shotPath.replace(/\.(png|jpg|jpeg)$/i, ".dom.html");
|
|
1566
|
+
await fs.writeFile(sitemapDomPath, sitemapDom, "utf8");
|
|
1567
|
+
const sitemapDomKey = `${opts.tenantId}/${opts.projectId}/dom-snapshots/${path.basename(sitemapDomPath)}`;
|
|
1568
|
+
const sitemapDomCloud = await uploadToCloud(sitemapDomPath, sitemapDomKey).catch(() => void 0);
|
|
1569
|
+
AppBrain.updateScreenDomSnapshot(opts.tenantId, pubScreen.id, sitemapDomPath, sitemapDomCloud).catch(() => {
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1511
1572
|
logger.info({ url: sUrl, name: pageName }, "[crawler] G11: captured public sitemap page");
|
|
1512
1573
|
}
|
|
1513
1574
|
sitemapCaptured++;
|
|
@@ -1558,6 +1619,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
1558
1619
|
async function runLiteWorker(workerPage) {
|
|
1559
1620
|
while (queue.length > 0 && visited.size < maxScreens && visited.size < segmentSize) {
|
|
1560
1621
|
if (opts.abortSignal?.aborted) break;
|
|
1622
|
+
if (isCrawlTimedOut()) break;
|
|
1561
1623
|
if (++totalAttemptsThisSegment > maxAttempts) break;
|
|
1562
1624
|
const url = queue.shift();
|
|
1563
1625
|
if (!url) break;
|
|
@@ -1619,6 +1681,16 @@ async function crawlProject(opts, onProgress) {
|
|
|
1619
1681
|
const name = pathParts.map((p) => p.charAt(0).toUpperCase() + p.slice(1).replace(/[-_]/g, " ")).join(" \u2014 ") || "Home";
|
|
1620
1682
|
const screen = await AppBrain.saveScreen(opts.tenantId, opts.projectId, name, url, opts.jobId, discoveryMethodFor(url));
|
|
1621
1683
|
visited.set(canonUrl, screen.id);
|
|
1684
|
+
const INLINE_AUTH_WALL_RE = /your login session (was|has been) expired|session (has )?expired[^.]*click|please (log ?in|sign ?in) to continue|you (are|were) logged out|your session has timed out/i;
|
|
1685
|
+
const lwBodyText = await workerPage.evaluate(() => document.body?.innerText ?? "").catch(() => "");
|
|
1686
|
+
if (INLINE_AUTH_WALL_RE.test(lwBodyText)) {
|
|
1687
|
+
logger.warn({ url, screenId: screen.id }, "[lite-worker] Inline session-expired content detected \u2014 marking requiresAuth");
|
|
1688
|
+
AppBrain.updateScreenRequiresAuth(opts.tenantId, screen.id, true).catch(() => {
|
|
1689
|
+
});
|
|
1690
|
+
visited.set(canonUrl, "_auth_wall_");
|
|
1691
|
+
if (!revisitQueue.includes(url)) revisitQueue.push(url);
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1622
1694
|
const shotPath = path.join(opts.screenshotDir ?? "/tmp", `${screen.id}-desktop-v${screen.version}.png`);
|
|
1623
1695
|
let liteElements = await domFallbackElements(workerPage).catch(() => []);
|
|
1624
1696
|
if (liteElements.length === 0) {
|
|
@@ -1643,6 +1715,15 @@ async function crawlProject(opts, onProgress) {
|
|
|
1643
1715
|
}).catch(() => {
|
|
1644
1716
|
})
|
|
1645
1717
|
]);
|
|
1718
|
+
const lwDom = await capturePageHtml(workerPage);
|
|
1719
|
+
if (lwDom) {
|
|
1720
|
+
const lwDomPath = shotPath.replace(/\.(png|jpg|jpeg)$/i, ".dom.html");
|
|
1721
|
+
await fs.writeFile(lwDomPath, lwDom, "utf8");
|
|
1722
|
+
const lwDomKey = `${opts.tenantId}/${opts.projectId}/dom-snapshots/${path.basename(lwDomPath)}`;
|
|
1723
|
+
const lwDomCloud = await uploadToCloud(lwDomPath, lwDomKey).catch(() => void 0);
|
|
1724
|
+
AppBrain.updateScreenDomSnapshot(opts.tenantId, screen.id, lwDomPath, lwDomCloud).catch(() => {
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1646
1727
|
const _navEls = await workerPage.evaluate(() => {
|
|
1647
1728
|
if (typeof globalThis.__name === "undefined") globalThis.__name = (fn) => fn;
|
|
1648
1729
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1762,8 +1843,8 @@ async function crawlProject(opts, onProgress) {
|
|
|
1762
1843
|
if (allElements.length > 0) {
|
|
1763
1844
|
await AppBrain.clearScreenElements(opts.tenantId, opts.projectId, screen.id).catch(() => {
|
|
1764
1845
|
});
|
|
1765
|
-
for (let _ei = 0; _ei < allElements.length; _ei +=
|
|
1766
|
-
const _batch = allElements.slice(_ei, _ei +
|
|
1846
|
+
for (let _ei = 0; _ei < allElements.length; _ei += 2) {
|
|
1847
|
+
const _batch = allElements.slice(_ei, _ei + 2);
|
|
1767
1848
|
await Promise.all(_batch.map(
|
|
1768
1849
|
(el) => AppBrain.saveElement(opts.tenantId, opts.projectId, screen.id, el.meaning, el.role, el.expectedData, el.notes, void 0, true, el.boundingRect, el.ariaState, el.parentLandmark).catch(() => {
|
|
1769
1850
|
})
|
|
@@ -1875,6 +1956,13 @@ async function crawlProject(opts, onProgress) {
|
|
|
1875
1956
|
logger.info("Abort signal received \u2014 stopping crawl");
|
|
1876
1957
|
throw new Error("CRAWL_ABORTED");
|
|
1877
1958
|
}
|
|
1959
|
+
if (isCrawlTimedOut()) {
|
|
1960
|
+
const elapsedMin = Math.round((Date.now() - crawlLoopStart) / 6e4);
|
|
1961
|
+
logger.warn({ elapsedMin, visited: visited.size, queued: queue.length }, "[crawler] Global timeout reached \u2014 stopping BFS to prevent runaway crawl");
|
|
1962
|
+
await onProgress?.(89, `Crawl timeout after ${elapsedMin}min \u2014 ${visited.size} pages captured`).catch(() => {
|
|
1963
|
+
});
|
|
1964
|
+
break;
|
|
1965
|
+
}
|
|
1878
1966
|
if (opts.autoThrottle && navCount > 0) {
|
|
1879
1967
|
const delayMs = Math.min(5e3, Math.max(0, avgNavMs * 0.5));
|
|
1880
1968
|
if (delayMs > 100) await page.waitForTimeout(delayMs).catch(() => {
|
|
@@ -2069,8 +2157,18 @@ async function crawlProject(opts, onProgress) {
|
|
|
2069
2157
|
}).catch(() => "Modal");
|
|
2070
2158
|
const modalScreen = await AppBrain.saveScreen(opts.tenantId, opts.projectId, modalName, url, opts.jobId, discoveryMethodFor(url)).catch(() => null);
|
|
2071
2159
|
if (modalScreen) {
|
|
2072
|
-
await
|
|
2160
|
+
const modalCloudUrl = await uploadToCloud(modalShotPath, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(modalShotPath)}`).catch(() => void 0);
|
|
2161
|
+
await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, modalScreen.id, modalShotPath, modalScreen.version, "DESKTOP", modalCloudUrl).catch(() => {
|
|
2073
2162
|
});
|
|
2163
|
+
const modalDom = await capturePageHtml(page);
|
|
2164
|
+
if (modalDom) {
|
|
2165
|
+
const modalDomPath = modalShotPath.replace(/\.(png|jpg|jpeg)$/i, ".dom.html");
|
|
2166
|
+
await fs.writeFile(modalDomPath, modalDom, "utf8");
|
|
2167
|
+
const modalDomKey = `${opts.tenantId}/${opts.projectId}/dom-snapshots/${path.basename(modalDomPath)}`;
|
|
2168
|
+
const modalDomCloud = await uploadToCloud(modalDomPath, modalDomKey).catch(() => void 0);
|
|
2169
|
+
AppBrain.updateScreenDomSnapshot(opts.tenantId, modalScreen.id, modalDomPath, modalDomCloud).catch(() => {
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2074
2172
|
const modalElements = await domFallbackElements(page, rawPage ?? void 0).catch(() => []);
|
|
2075
2173
|
for (const el of modalElements) {
|
|
2076
2174
|
AppBrain.saveElement(opts.tenantId, opts.projectId, modalScreen.id, el.meaning, el.role, el.expectedData, el.notes, void 0, true, el.boundingRect, el.ariaState, el.parentLandmark).catch(() => {
|
|
@@ -2115,6 +2213,11 @@ async function crawlProject(opts, onProgress) {
|
|
|
2115
2213
|
}
|
|
2116
2214
|
}).catch(() => {
|
|
2117
2215
|
});
|
|
2216
|
+
await opts.onCheckpoint?.(
|
|
2217
|
+
Array.from(visited.entries()).filter(([_k, v]) => v !== "_pending_").map(([k]) => k),
|
|
2218
|
+
queue.filter((u) => !visited.has(canonicalize(u)))
|
|
2219
|
+
).catch(() => {
|
|
2220
|
+
});
|
|
2118
2221
|
throw new Error(`[AUTH_FAILURE] Login wall detected: ${loginWallRedirectCount} URLs redirected to the login page. Add credentials in project settings and retry the crawl.`);
|
|
2119
2222
|
}
|
|
2120
2223
|
}
|
|
@@ -2150,6 +2253,11 @@ async function crawlProject(opts, onProgress) {
|
|
|
2150
2253
|
payload: { url, landed: landedUrl, reason: "Re-authentication failed. Session is unrecoverable. Re-capture the session in project settings and retry the crawl." }
|
|
2151
2254
|
}).catch(() => {
|
|
2152
2255
|
});
|
|
2256
|
+
await opts.onCheckpoint?.(
|
|
2257
|
+
Array.from(visited.entries()).filter(([_k, v]) => v !== "_pending_").map(([k]) => k),
|
|
2258
|
+
queue.filter((u) => !visited.has(canonicalize(u)))
|
|
2259
|
+
).catch(() => {
|
|
2260
|
+
});
|
|
2153
2261
|
throw new Error("[AUTH_FAILURE] Re-authentication failed. Session is unrecoverable. Re-capture the session in project settings and retry the crawl.");
|
|
2154
2262
|
}
|
|
2155
2263
|
} else {
|
|
@@ -2164,6 +2272,11 @@ async function crawlProject(opts, onProgress) {
|
|
|
2164
2272
|
payload: { url, landed: landedUrl, reason: "Session expired and no credentials are configured. Add credentials in project settings and re-capture the session." }
|
|
2165
2273
|
}).catch(() => {
|
|
2166
2274
|
});
|
|
2275
|
+
await opts.onCheckpoint?.(
|
|
2276
|
+
Array.from(visited.entries()).filter(([_k, v]) => v !== "_pending_").map(([k]) => k),
|
|
2277
|
+
queue.filter((u) => !visited.has(canonicalize(u)))
|
|
2278
|
+
).catch(() => {
|
|
2279
|
+
});
|
|
2167
2280
|
throw new Error("[AUTH_FAILURE] Session expired and no credentials are configured. Add credentials in project settings and re-capture the session.");
|
|
2168
2281
|
}
|
|
2169
2282
|
}
|
|
@@ -2762,120 +2875,139 @@ async function crawlProject(opts, onProgress) {
|
|
|
2762
2875
|
})();
|
|
2763
2876
|
}
|
|
2764
2877
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2878
|
+
const INLINE_AUTH_WALL_RE = /your login session (was|has been) expired|session (has )?expired[^.]*click|please (log ?in|sign ?in) to continue|you (are|were) logged out|your session has timed out/i;
|
|
2879
|
+
const pageBodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
|
|
2880
|
+
if (INLINE_AUTH_WALL_RE.test(pageBodyText)) {
|
|
2881
|
+
logger.warn({ url, screenId: screen.id }, "[crawler] Inline session-expired content detected \u2014 skipping DOM snapshot, marking requiresAuth");
|
|
2882
|
+
AppBrain.updateScreenRequiresAuth(opts.tenantId, screen.id, true).catch(() => {
|
|
2883
|
+
});
|
|
2884
|
+
AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
|
|
2885
|
+
screenId: screen.id,
|
|
2886
|
+
jobId: opts.jobId,
|
|
2887
|
+
type: "AUTH_FAILURE",
|
|
2888
|
+
payload: { url, reason: "Inline session-expired page rendered without URL redirect. Re-authenticate and re-crawl." }
|
|
2889
|
+
}).catch(() => {
|
|
2890
|
+
});
|
|
2891
|
+
} else {
|
|
2892
|
+
try {
|
|
2893
|
+
const domContent = await capturePageHtml(page);
|
|
2894
|
+
if (!domContent) {
|
|
2895
|
+
logger.warn({ url, screenId: screen.id }, "[crawler] markdown extraction skipped: DOM snapshot unavailable");
|
|
2896
|
+
} else {
|
|
2897
|
+
const domPath = shotPath.replace(/\.(png|jpg|jpeg)$/i, ".dom.html");
|
|
2898
|
+
await fs.writeFile(domPath, domContent, "utf8");
|
|
2899
|
+
(async () => {
|
|
2900
|
+
const domKey = `${opts.tenantId}/${opts.projectId}/dom-snapshots/${path.basename(domPath)}`;
|
|
2901
|
+
const domCloudUrl = await uploadToCloud(domPath, domKey).catch(() => void 0);
|
|
2902
|
+
AppBrain.updateScreenDomSnapshot(opts.tenantId, screen.id, domPath, domCloudUrl).catch(() => {
|
|
2903
|
+
});
|
|
2904
|
+
})();
|
|
2905
|
+
const [iframeContent, shadowContent] = await Promise.all([
|
|
2906
|
+
extractIframeContent(page).catch(() => []),
|
|
2907
|
+
pierceShadowDom(page).catch(() => "")
|
|
2793
2908
|
]);
|
|
2794
|
-
const
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2909
|
+
const supplemental = [
|
|
2910
|
+
...iframeContent.map((f) => f.markdown).filter(Boolean),
|
|
2911
|
+
shadowContent
|
|
2912
|
+
].join("\n\n").trim();
|
|
2913
|
+
const extracted = extractMarkdown(
|
|
2914
|
+
supplemental ? domContent + "\n<!-- iframe/shadow -->\n" + supplemental.slice(0, 3e3) : domContent,
|
|
2915
|
+
url
|
|
2916
|
+
);
|
|
2917
|
+
let fitMarkdown;
|
|
2918
|
+
if (extracted) {
|
|
2919
|
+
const mdPath = shotPath.replace(/\.(png|jpg|jpeg)$/i, ".md");
|
|
2920
|
+
fitMarkdown = buildFitMarkdown(extracted.markdown, [
|
|
2921
|
+
...domainProfile.importantScreenPatterns,
|
|
2922
|
+
...domainProfile.workflowGoals,
|
|
2923
|
+
...opts.bestFirstKeywords ?? []
|
|
2924
|
+
]);
|
|
2925
|
+
const { text: scrubbed, scrubbed: hadPii } = scrubPii(extracted.markdown);
|
|
2926
|
+
if (hadPii) logger.info({ url }, "[crawler] PII detected and redacted from markdown");
|
|
2927
|
+
const dedupResult = deduplicator.check(url, scrubbed);
|
|
2928
|
+
if (dedupResult.isDuplicate) {
|
|
2929
|
+
logger.info({ url, similarUrl: dedupResult.similarUrl, score: dedupResult.similarityScore }, "[crawler] near-duplicate page skipped");
|
|
2930
|
+
}
|
|
2931
|
+
await fs.writeFile(mdPath, scrubbed, "utf8");
|
|
2932
|
+
const summaryPromise = (async () => {
|
|
2933
|
+
try {
|
|
2934
|
+
const llmCfg = await getCachedCrawlLLM(opts.tenantId);
|
|
2935
|
+
const snippet = extracted.markdown.slice(0, 1200);
|
|
2936
|
+
if (snippet.trim().length < 80) return void 0;
|
|
2937
|
+
const summarySeed = deterministicSeed(`${opts.tenantId}:${opts.projectId ?? ""}:page-summary:${url}`);
|
|
2938
|
+
const { text, usage } = await askTracked(
|
|
2939
|
+
"You are a QA analyst. Output ONLY a single complete sentence (no preamble, no quotes) describing what this screen does or shows. Be specific. Stop after the first sentence.",
|
|
2940
|
+
`Screen: "${name}"
|
|
2810
2941
|
URL: ${url}
|
|
2811
2942
|
|
|
2812
2943
|
Content:
|
|
2813
2944
|
${snippet}`,
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2945
|
+
llmCfg,
|
|
2946
|
+
200,
|
|
2947
|
+
void 0,
|
|
2948
|
+
{ temperature: 0, seed: summarySeed }
|
|
2949
|
+
);
|
|
2950
|
+
if (usage) {
|
|
2951
|
+
recordLLMCall({
|
|
2952
|
+
tenantId: opts.tenantId,
|
|
2953
|
+
projectId: opts.projectId,
|
|
2954
|
+
jobId: opts.jobId,
|
|
2955
|
+
agentType: "crawl_summary",
|
|
2956
|
+
label: `page_summary:${name}`,
|
|
2957
|
+
usage
|
|
2958
|
+
}).catch(() => {
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
const clean = text.trim().replace(/^["']|["']$/g, "");
|
|
2962
|
+
const firstSentence = clean.match(/^[^.!?]+[.!?]/)?.[0] ?? clean;
|
|
2963
|
+
return firstSentence.trim();
|
|
2964
|
+
} catch {
|
|
2965
|
+
return void 0;
|
|
2829
2966
|
}
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
markdown: fitMarkdown ?? extracted.markdown,
|
|
2842
|
-
heading: extracted.title || name
|
|
2843
|
-
}).catch((err) => console.warn("[crawler] markdown extraction save failed:", err));
|
|
2844
|
-
summaryPromise.then((aiSummary) => {
|
|
2845
|
-
if (aiSummary) AppBrain.updateScreenAiSummary(opts.tenantId, screen.id, aiSummary).catch(() => {
|
|
2967
|
+
})();
|
|
2968
|
+
AppBrain.updateScreenMarkdown(opts.tenantId, opts.projectId, screen.id, {
|
|
2969
|
+
markdownPath: mdPath,
|
|
2970
|
+
markdownMethod: extracted.method,
|
|
2971
|
+
markdownWordCount: extracted.wordCount,
|
|
2972
|
+
markdown: fitMarkdown ?? extracted.markdown,
|
|
2973
|
+
heading: extracted.title || name
|
|
2974
|
+
}).catch((err) => console.warn("[crawler] markdown extraction save failed:", err));
|
|
2975
|
+
summaryPromise.then((aiSummary) => {
|
|
2976
|
+
if (aiSummary) AppBrain.updateScreenAiSummary(opts.tenantId, screen.id, aiSummary).catch(() => {
|
|
2977
|
+
});
|
|
2846
2978
|
});
|
|
2979
|
+
}
|
|
2980
|
+
const structuredJson = summarizeStructuredElements(atElements);
|
|
2981
|
+
const artifact = buildCrawlArtifactEnvelope({
|
|
2982
|
+
tenantId: opts.tenantId,
|
|
2983
|
+
projectId: opts.projectId,
|
|
2984
|
+
jobId: opts.jobId,
|
|
2985
|
+
url,
|
|
2986
|
+
canonicalUrl: canonicalize(url),
|
|
2987
|
+
screenId: screen.id,
|
|
2988
|
+
screenshotPath: shotPath,
|
|
2989
|
+
domPath,
|
|
2990
|
+
domContent,
|
|
2991
|
+
markdown: extracted?.markdown,
|
|
2992
|
+
fitMarkdown,
|
|
2993
|
+
accessibilityElements: atElements,
|
|
2994
|
+
structuredJson,
|
|
2995
|
+
evidence: [
|
|
2996
|
+
{ type: "domain_profile", payload: { industry: domainProfile.industry, confidence: domainProfile.confidence } },
|
|
2997
|
+
{ type: "engine_route", payload: routeDecision },
|
|
2998
|
+
{ type: "screen_fingerprints", payload: { urlCanonicalHash: sha256(canonicalize(url)) } }
|
|
2999
|
+
]
|
|
2847
3000
|
});
|
|
3001
|
+
AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
|
|
3002
|
+
screenId: screen.id,
|
|
3003
|
+
jobId: opts.jobId,
|
|
3004
|
+
type: "SCREEN_ARTIFACT_ENVELOPE",
|
|
3005
|
+
payload: artifact
|
|
3006
|
+
}).catch((err) => console.warn("[crawler] artifact evidence save failed:", err?.message ?? err));
|
|
2848
3007
|
}
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
tenantId: opts.tenantId,
|
|
2852
|
-
projectId: opts.projectId,
|
|
2853
|
-
jobId: opts.jobId,
|
|
2854
|
-
url,
|
|
2855
|
-
canonicalUrl: canonicalize(url),
|
|
2856
|
-
screenId: screen.id,
|
|
2857
|
-
screenshotPath: shotPath,
|
|
2858
|
-
domPath,
|
|
2859
|
-
domContent,
|
|
2860
|
-
markdown: extracted?.markdown,
|
|
2861
|
-
fitMarkdown,
|
|
2862
|
-
accessibilityElements: atElements,
|
|
2863
|
-
structuredJson,
|
|
2864
|
-
evidence: [
|
|
2865
|
-
{ type: "domain_profile", payload: { industry: domainProfile.industry, confidence: domainProfile.confidence } },
|
|
2866
|
-
{ type: "engine_route", payload: routeDecision },
|
|
2867
|
-
{ type: "screen_fingerprints", payload: { urlCanonicalHash: sha256(canonicalize(url)) } }
|
|
2868
|
-
]
|
|
2869
|
-
});
|
|
2870
|
-
AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
|
|
2871
|
-
screenId: screen.id,
|
|
2872
|
-
jobId: opts.jobId,
|
|
2873
|
-
type: "SCREEN_ARTIFACT_ENVELOPE",
|
|
2874
|
-
payload: artifact
|
|
2875
|
-
}).catch((err) => console.warn("[crawler] artifact evidence save failed:", err?.message ?? err));
|
|
3008
|
+
} catch (err) {
|
|
3009
|
+
logger.warn({ url, screenId: screen.id, err: String(err?.message ?? err) }, "[crawler] markdown extraction failed");
|
|
2876
3010
|
}
|
|
2877
|
-
} catch (err) {
|
|
2878
|
-
logger.warn({ url, screenId: screen.id, err: String(err?.message ?? err) }, "[crawler] markdown extraction failed");
|
|
2879
3011
|
}
|
|
2880
3012
|
if (a11yScore !== null) {
|
|
2881
3013
|
AppBrain.updateScreenA11y(opts.tenantId, screen.id, { a11yScore }).catch((err) => console.warn("[crawler] a11y score update failed:", err));
|
|
@@ -3046,6 +3178,8 @@ ${snippet}`,
|
|
|
3046
3178
|
const driftedToAuthWall = !weWantedThisPage && (BFS_LOGIN_PATH.test(postExtractParsed.pathname) || BFS_ERROR_PATH.test(postExtractParsed.pathname));
|
|
3047
3179
|
if (driftedToAuthWall) {
|
|
3048
3180
|
logger.warn({ url, postExtractUrl }, "[crawler] Page drifted to auth wall during element extraction \u2014 skipping element save to prevent login-page element contamination");
|
|
3181
|
+
AppBrain.updateScreenRequiresAuth(opts.tenantId, screen.id, true).catch(() => {
|
|
3182
|
+
});
|
|
3049
3183
|
AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
|
|
3050
3184
|
screenId: screen.id,
|
|
3051
3185
|
jobId: opts.jobId,
|
|
@@ -3063,8 +3197,8 @@ ${snippet}`,
|
|
|
3063
3197
|
} else {
|
|
3064
3198
|
await AppBrain.clearScreenElements(opts.tenantId, opts.projectId, screen.id).catch(() => {
|
|
3065
3199
|
});
|
|
3066
|
-
for (let _ei = 0; _ei < elements.length; _ei +=
|
|
3067
|
-
const _batch = elements.slice(_ei, _ei +
|
|
3200
|
+
for (let _ei = 0; _ei < elements.length; _ei += 2) {
|
|
3201
|
+
const _batch = elements.slice(_ei, _ei + 2);
|
|
3068
3202
|
await Promise.all(_batch.map(async (el) => {
|
|
3069
3203
|
const saved = await AppBrain.saveElement(opts.tenantId, opts.projectId, screen.id, el.meaning, el.role, el.expectedData, el.notes, void 0, true, el.boundingRect, el.ariaState, el.parentLandmark);
|
|
3070
3204
|
const label = el.meaning;
|
|
@@ -3075,7 +3209,7 @@ ${snippet}`,
|
|
|
3075
3209
|
{ type: "aria-label", value: label, confidence: 0.85 },
|
|
3076
3210
|
{ type: "tag-hint", value: selectorHint, confidence: 0.6 }
|
|
3077
3211
|
];
|
|
3078
|
-
AppBrain.saveElementFingerprint(opts.tenantId, opts.projectId, saved.id, selectors).catch(console.warn);
|
|
3212
|
+
await AppBrain.saveElementFingerprint(opts.tenantId, opts.projectId, saved.id, selectors).catch(console.warn);
|
|
3079
3213
|
}));
|
|
3080
3214
|
}
|
|
3081
3215
|
}
|
|
@@ -3104,7 +3238,7 @@ ${snippet}`,
|
|
|
3104
3238
|
try {
|
|
3105
3239
|
const targetPath = new URL(lm.href).pathname;
|
|
3106
3240
|
const canonLmHref = canonicalize(lm.href);
|
|
3107
|
-
if (!SKIP_PATH.test(targetPath) && isUsefulNavigationUrl(lm.href, origin) && !queued.has(canonLmHref) && !visited.has(canonLmHref) && queued.size < maxScreens *
|
|
3241
|
+
if (!SKIP_PATH.test(targetPath) && isUsefulNavigationUrl(lm.href, origin) && !queued.has(canonLmHref) && !visited.has(canonLmHref) && queued.size < maxScreens * 3) {
|
|
3108
3242
|
if (robotsCache && !await robotsCache.isAllowed(lm.href)) continue;
|
|
3109
3243
|
queued.add(canonLmHref);
|
|
3110
3244
|
queue.push(lm.href, { parentElements: elements.length });
|
|
@@ -3125,7 +3259,7 @@ ${snippet}`,
|
|
|
3125
3259
|
} catch {
|
|
3126
3260
|
}
|
|
3127
3261
|
const canonHref = canonicalize(href);
|
|
3128
|
-
if (!queued.has(canonHref) && !visited.has(canonHref) && queued.size < maxScreens *
|
|
3262
|
+
if (!queued.has(canonHref) && !visited.has(canonHref) && queued.size < maxScreens * 3) {
|
|
3129
3263
|
try {
|
|
3130
3264
|
const hrefPath = new URL(href).pathname;
|
|
3131
3265
|
if (isGeneratedDynamicPath(hrefPath)) continue;
|
|
@@ -3178,7 +3312,7 @@ ${snippet}`,
|
|
|
3178
3312
|
const jsDetectedRoutes = await detectJsRoutes(page, origin);
|
|
3179
3313
|
for (const route of jsDetectedRoutes) {
|
|
3180
3314
|
const canonRoute = canonicalize(route);
|
|
3181
|
-
if (isUsefulNavigationUrl(route, origin) && !queued.has(canonRoute) && !visited.has(canonRoute) && queued.size < maxScreens *
|
|
3315
|
+
if (isUsefulNavigationUrl(route, origin) && !queued.has(canonRoute) && !visited.has(canonRoute) && queued.size < maxScreens * 3) {
|
|
3182
3316
|
queued.add(canonRoute);
|
|
3183
3317
|
queue.push(route, { parentElements: elements.length });
|
|
3184
3318
|
logger.info({ route }, "[P2] JS router: discovered route added to queue");
|
|
@@ -3192,7 +3326,7 @@ ${snippet}`,
|
|
|
3192
3326
|
try {
|
|
3193
3327
|
const abs = `${origin}${relPath}`;
|
|
3194
3328
|
const canon = canonicalize(abs);
|
|
3195
|
-
if (isUsefulNavigationUrl(abs, origin) && !queued.has(canon) && !visited.has(canon) && queued.size < maxScreens *
|
|
3329
|
+
if (isUsefulNavigationUrl(abs, origin) && !queued.has(canon) && !visited.has(canon) && queued.size < maxScreens * 3) {
|
|
3196
3330
|
queued.add(canon);
|
|
3197
3331
|
queue.push(abs);
|
|
3198
3332
|
logger.info({ url: abs }, "[P4] XHR: page URL from JSON response added to queue");
|
|
@@ -3200,7 +3334,7 @@ ${snippet}`,
|
|
|
3200
3334
|
} catch {
|
|
3201
3335
|
}
|
|
3202
3336
|
}
|
|
3203
|
-
if (nextLinks.length === 0 && elements.length < (parseInt(process.env.CRAWL_SKYVERN_PAGE_MIN_ELEMENTS ?? "") || 5) && opts.skyvernConfig) {
|
|
3337
|
+
if (!selective && nextLinks.length === 0 && elements.length < (parseInt(process.env.CRAWL_SKYVERN_PAGE_MIN_ELEMENTS ?? "") || 5) && opts.skyvernConfig) {
|
|
3204
3338
|
try {
|
|
3205
3339
|
logger.info({ url }, "Gap 5: thin page detected \u2014 asking Skyvern to discover navigation URLs");
|
|
3206
3340
|
const skyvernNav = await skyvernNavigate(
|
|
@@ -3210,7 +3344,7 @@ ${snippet}`,
|
|
|
3210
3344
|
);
|
|
3211
3345
|
for (const skyvernUrl of skyvernNav.visitedUrls) {
|
|
3212
3346
|
const canonSkyvernUrl = canonicalize(skyvernUrl);
|
|
3213
|
-
if (!queued.has(canonSkyvernUrl) && !visited.has(canonSkyvernUrl) && queued.size < maxScreens *
|
|
3347
|
+
if (!queued.has(canonSkyvernUrl) && !visited.has(canonSkyvernUrl) && queued.size < maxScreens * 3) {
|
|
3214
3348
|
logger.info({ url: skyvernUrl }, "Gap 5: Skyvern-discovered URL added to ZeTa crawl queue");
|
|
3215
3349
|
queued.add(canonSkyvernUrl);
|
|
3216
3350
|
queue.push(skyvernUrl);
|
|
@@ -3265,9 +3399,9 @@ ${snippet}`,
|
|
|
3265
3399
|
lastEmittedPct = clampedPct;
|
|
3266
3400
|
await onProgress?.(clampedPct, `Crawling screens (${visited.size} mapped, ${queue.length} pending)`);
|
|
3267
3401
|
logger.info({ name, elements: elements.length }, "Screen crawled");
|
|
3268
|
-
if (++checkpointSuccessCount %
|
|
3402
|
+
if (++checkpointSuccessCount % 5 === 0) {
|
|
3269
3403
|
await opts.onCheckpoint?.(
|
|
3270
|
-
Array.from(visited.
|
|
3404
|
+
Array.from(visited.entries()).filter(([_k, v]) => v !== "_pending_").map(([k]) => k),
|
|
3271
3405
|
queue.filter((u) => !visited.has(canonicalize(u)))
|
|
3272
3406
|
);
|
|
3273
3407
|
}
|
|
@@ -3295,7 +3429,7 @@ ${snippet}`,
|
|
|
3295
3429
|
}
|
|
3296
3430
|
const successfulScreenCount = discovered.filter((d) => d.screenId).length;
|
|
3297
3431
|
const skyvernMinScreens = parseInt(process.env.CRAWL_SKYVERN_MIN_SCREENS ?? "") || 10;
|
|
3298
|
-
if (successfulScreenCount < skyvernMinScreens && opts.skyvernConfig) {
|
|
3432
|
+
if (!selective && successfulScreenCount < skyvernMinScreens && opts.skyvernConfig) {
|
|
3299
3433
|
await onProgress?.(78, "Few screens found \u2014 using Skyvern to discover additional navigation URLs");
|
|
3300
3434
|
const currentUrl = page.url();
|
|
3301
3435
|
const skyvernNav = await skyvernNavigate(
|
|
@@ -3310,7 +3444,7 @@ ${snippet}`,
|
|
|
3310
3444
|
if (skyvernUrls.length > 0) {
|
|
3311
3445
|
logger.info({ count: skyvernUrls.length }, "Gap 5: Skyvern found new URLs \u2014 processing with ZeTa native crawler");
|
|
3312
3446
|
for (const skyvernUrl of skyvernUrls) {
|
|
3313
|
-
if (visited.size >= maxScreens) break;
|
|
3447
|
+
if (isCrawlTimedOut() || visited.size >= maxScreens) break;
|
|
3314
3448
|
try {
|
|
3315
3449
|
await page.goto(skyvernUrl, { waitUntil: "domcontentloaded", timeout: 2e4 });
|
|
3316
3450
|
await new Promise((r) => setTimeout(r, 1500));
|
|
@@ -3319,7 +3453,17 @@ ${snippet}`,
|
|
|
3319
3453
|
const shotPath = `${opts.screenshotDir ?? "/tmp"}/${screen.id}-v${screen.version}.png`;
|
|
3320
3454
|
await waitForScreenshotReady(page, rawPage ?? void 0);
|
|
3321
3455
|
await page.screenshot({ path: shotPath, fullPage: true });
|
|
3322
|
-
await
|
|
3456
|
+
const skyvernCloudUrl = await uploadToCloud(shotPath, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(shotPath)}`).catch(() => void 0);
|
|
3457
|
+
await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, "DESKTOP", skyvernCloudUrl);
|
|
3458
|
+
const skyvernDom = await capturePageHtml(page);
|
|
3459
|
+
if (skyvernDom) {
|
|
3460
|
+
const skyvernDomPath = shotPath.replace(/\.(png|jpg|jpeg)$/i, ".dom.html");
|
|
3461
|
+
await fs.writeFile(skyvernDomPath, skyvernDom, "utf8");
|
|
3462
|
+
const skyvernDomKey = `${opts.tenantId}/${opts.projectId}/dom-snapshots/${path.basename(skyvernDomPath)}`;
|
|
3463
|
+
const skyvernDomCloud = await uploadToCloud(skyvernDomPath, skyvernDomKey).catch(() => void 0);
|
|
3464
|
+
AppBrain.updateScreenDomSnapshot(opts.tenantId, screen.id, skyvernDomPath, skyvernDomCloud).catch(() => {
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3323
3467
|
const [elems, links, linkMappings] = await Promise.all([
|
|
3324
3468
|
extractElements(stagehand, page, elementLLM ?? void 0, rawPage ?? void 0),
|
|
3325
3469
|
discoverLinks(stagehand, origin, page, linkLLM ?? void 0, aiSkipCount > 0, rawPage ?? void 0),
|
|
@@ -3345,7 +3489,7 @@ ${snippet}`,
|
|
|
3345
3489
|
logger.warn({ url: skyvernUrl, err: String(err) }, "Gap 5: ZeTa crawl of Skyvern URL failed");
|
|
3346
3490
|
}
|
|
3347
3491
|
}
|
|
3348
|
-
while (queue.length > 0 && visited.size < maxScreens) {
|
|
3492
|
+
while (queue.length > 0 && visited.size < maxScreens && !isCrawlTimedOut()) {
|
|
3349
3493
|
const href = queue.shift();
|
|
3350
3494
|
const canonHref = canonicalize(href);
|
|
3351
3495
|
if (visited.has(canonHref)) continue;
|
|
@@ -3358,7 +3502,17 @@ ${snippet}`,
|
|
|
3358
3502
|
const sp = `${opts.screenshotDir ?? "/tmp"}/${sc.id}-v${sc.version}.png`;
|
|
3359
3503
|
await waitForScreenshotReady(page, rawPage ?? void 0);
|
|
3360
3504
|
await page.screenshot({ path: sp, fullPage: true });
|
|
3361
|
-
await
|
|
3505
|
+
const skyvernDrainCloudUrl = await uploadToCloud(sp, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(sp)}`).catch(() => void 0);
|
|
3506
|
+
await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, sc.id, sp, sc.version, "DESKTOP", skyvernDrainCloudUrl);
|
|
3507
|
+
const skyvernDrainDom = await capturePageHtml(page);
|
|
3508
|
+
if (skyvernDrainDom) {
|
|
3509
|
+
const skyvernDrainDomPath = sp.replace(/\.(png|jpg|jpeg)$/i, ".dom.html");
|
|
3510
|
+
await fs.writeFile(skyvernDrainDomPath, skyvernDrainDom, "utf8");
|
|
3511
|
+
const skyvernDrainDomKey = `${opts.tenantId}/${opts.projectId}/dom-snapshots/${path.basename(skyvernDrainDomPath)}`;
|
|
3512
|
+
const skyvernDrainDomCloud = await uploadToCloud(skyvernDrainDomPath, skyvernDrainDomKey).catch(() => void 0);
|
|
3513
|
+
AppBrain.updateScreenDomSnapshot(opts.tenantId, sc.id, skyvernDrainDomPath, skyvernDrainDomCloud).catch(() => {
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3362
3516
|
const [elems2, linkMappings2] = await Promise.all([
|
|
3363
3517
|
extractElements(stagehand, page, elementLLM ?? void 0, rawPage ?? void 0),
|
|
3364
3518
|
extractLinkMappings(page, origin)
|
|
@@ -3413,7 +3567,8 @@ ${snippet}`,
|
|
|
3413
3567
|
if (revisitQueue.length > 0) {
|
|
3414
3568
|
await onProgress?.(79, `Re-visiting ${revisitQueue.length} page(s) with async content\u2026`);
|
|
3415
3569
|
logger.info({ count: revisitQueue.length }, "[P3a] Starting async content re-visit pass");
|
|
3416
|
-
for (const revisitUrl of revisitQueue.slice(0,
|
|
3570
|
+
for (const revisitUrl of revisitQueue.slice(0, 20)) {
|
|
3571
|
+
if (isCrawlTimedOut()) break;
|
|
3417
3572
|
const rawScreenId = visited.get(canonicalize(revisitUrl));
|
|
3418
3573
|
const isAuthWallRevisit = rawScreenId === "_auth_wall_";
|
|
3419
3574
|
if (!rawScreenId) continue;
|
|
@@ -3439,8 +3594,18 @@ ${snippet}`,
|
|
|
3439
3594
|
}
|
|
3440
3595
|
const shotPath2 = `${opts.screenshotDir ?? "/tmp"}/${existingScreenId}-revisit.png`;
|
|
3441
3596
|
await page.screenshot({ path: shotPath2, fullPage: true });
|
|
3442
|
-
await
|
|
3597
|
+
const p3aCloudUrl = await uploadToCloud(shotPath2, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(shotPath2)}`).catch(() => void 0);
|
|
3598
|
+
await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, existingScreenId, shotPath2, 1, "DESKTOP", p3aCloudUrl).catch(() => {
|
|
3443
3599
|
});
|
|
3600
|
+
const p3aDom = await capturePageHtml(page);
|
|
3601
|
+
if (p3aDom) {
|
|
3602
|
+
const p3aDomPath = shotPath2.replace(/\.(png|jpg|jpeg)$/i, ".dom.html");
|
|
3603
|
+
await fs.writeFile(p3aDomPath, p3aDom, "utf8");
|
|
3604
|
+
const p3aDomKey = `${opts.tenantId}/${opts.projectId}/dom-snapshots/${path.basename(p3aDomPath)}`;
|
|
3605
|
+
const p3aDomCloud = await uploadToCloud(p3aDomPath, p3aDomKey).catch(() => void 0);
|
|
3606
|
+
AppBrain.updateScreenDomSnapshot(opts.tenantId, existingScreenId, p3aDomPath, p3aDomCloud).catch(() => {
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3444
3609
|
const revisitElements = await extractElements(stagehand, page, elementLLM ?? void 0, rawPage ?? void 0).catch(() => []);
|
|
3445
3610
|
if (revisitElements.length > 0) {
|
|
3446
3611
|
await AppBrain.clearScreenElements(opts.tenantId, opts.projectId, existingScreenId).catch(() => {
|
|
@@ -3453,7 +3618,7 @@ ${snippet}`,
|
|
|
3453
3618
|
const revisitLinks = await discoverLinks(stagehand, origin, page, linkLLM ?? void 0, false, rawPage ?? void 0).catch(() => []);
|
|
3454
3619
|
for (const href of revisitLinks) {
|
|
3455
3620
|
const canon = canonicalize(href);
|
|
3456
|
-
if (isUsefulNavigationUrl(href, origin) && !queued.has(canon) && !visited.has(canon) && queued.size < maxScreens *
|
|
3621
|
+
if (isUsefulNavigationUrl(href, origin) && !queued.has(canon) && !visited.has(canon) && queued.size < maxScreens * 3) {
|
|
3457
3622
|
queued.add(canon);
|
|
3458
3623
|
queue.push(href);
|
|
3459
3624
|
}
|
|
@@ -3469,6 +3634,7 @@ ${snippet}`,
|
|
|
3469
3634
|
logger.info({ newLinks: p3bCount }, "[P3b] Extended BFS \u2014 processing links discovered during revisit pass");
|
|
3470
3635
|
await onProgress?.(82, `Extended crawl: ${p3bCount} newly discovered page(s)\u2026`);
|
|
3471
3636
|
while (queue.length > 0 && visited.size < maxScreens && visited.size < segmentSize) {
|
|
3637
|
+
if (isCrawlTimedOut()) break;
|
|
3472
3638
|
const href = queue.shift();
|
|
3473
3639
|
if (!href) break;
|
|
3474
3640
|
const canonHref = canonicalize(href);
|
|
@@ -3489,6 +3655,15 @@ ${snippet}`,
|
|
|
3489
3655
|
const cloudUrl3 = await uploadToCloud(sp, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(sp)}`).catch(() => "");
|
|
3490
3656
|
await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, sp, screen.version, "DESKTOP", cloudUrl3).catch(() => {
|
|
3491
3657
|
});
|
|
3658
|
+
const p3bDom = await capturePageHtml(page);
|
|
3659
|
+
if (p3bDom) {
|
|
3660
|
+
const p3bDomPath = sp.replace(/\.(png|jpg|jpeg)$/i, ".dom.html");
|
|
3661
|
+
await fs.writeFile(p3bDomPath, p3bDom, "utf8");
|
|
3662
|
+
const p3bDomKey = `${opts.tenantId}/${opts.projectId}/dom-snapshots/${path.basename(p3bDomPath)}`;
|
|
3663
|
+
const p3bDomCloud = await uploadToCloud(p3bDomPath, p3bDomKey).catch(() => void 0);
|
|
3664
|
+
AppBrain.updateScreenDomSnapshot(opts.tenantId, screen.id, p3bDomPath, p3bDomCloud).catch(() => {
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3492
3667
|
const [p3bElems, p3bLinks] = await Promise.all([
|
|
3493
3668
|
extractElements(stagehand, page, elementLLM ?? void 0, rawPage ?? void 0).catch(() => []),
|
|
3494
3669
|
discoverLinks(stagehand, origin, page, linkLLM ?? void 0, aiSkipCount > 0, rawPage ?? void 0).catch(() => [])
|
|
@@ -3496,8 +3671,8 @@ ${snippet}`,
|
|
|
3496
3671
|
if (p3bElems.length > 0) {
|
|
3497
3672
|
await AppBrain.clearScreenElements(opts.tenantId, opts.projectId, screen.id).catch(() => {
|
|
3498
3673
|
});
|
|
3499
|
-
for (let _ei3 = 0; _ei3 < p3bElems.length; _ei3 +=
|
|
3500
|
-
const _b3 = p3bElems.slice(_ei3, _ei3 +
|
|
3674
|
+
for (let _ei3 = 0; _ei3 < p3bElems.length; _ei3 += 2) {
|
|
3675
|
+
const _b3 = p3bElems.slice(_ei3, _ei3 + 2);
|
|
3501
3676
|
await Promise.all(_b3.map(
|
|
3502
3677
|
(el) => AppBrain.saveElement(opts.tenantId, opts.projectId, screen.id, el.meaning, el.role, el.expectedData, el.notes, void 0, true, el.boundingRect, el.ariaState, el.parentLandmark).catch(() => {
|
|
3503
3678
|
})
|
|
@@ -3506,7 +3681,7 @@ ${snippet}`,
|
|
|
3506
3681
|
}
|
|
3507
3682
|
for (const link of p3bLinks) {
|
|
3508
3683
|
const c = canonicalize(link);
|
|
3509
|
-
if (isUsefulNavigationUrl(link, origin) && !queued.has(c) && !visited.has(c) && queued.size < maxScreens *
|
|
3684
|
+
if (isUsefulNavigationUrl(link, origin) && !queued.has(c) && !visited.has(c) && queued.size < maxScreens * 3) {
|
|
3510
3685
|
queued.add(c);
|
|
3511
3686
|
queue.push(link);
|
|
3512
3687
|
}
|
|
@@ -3543,9 +3718,9 @@ ${snippet}`,
|
|
|
3543
3718
|
});
|
|
3544
3719
|
logger.info({ confirmed: confirmedGaps.length, total: gapCandidates.length }, "[P6] Gap pass: visiting confirmed gap URLs");
|
|
3545
3720
|
const gapLoginPattern = /\/(login|signin|sign-in|auth|log-in)(\/|$|\?)/i;
|
|
3546
|
-
for (const gap of confirmedGaps.slice(0, parseInt(process.env.CRAWLER_GAP_PASS_MAX_VISITS ?? "") ||
|
|
3721
|
+
for (const gap of confirmedGaps.slice(0, parseInt(process.env.CRAWLER_GAP_PASS_MAX_VISITS ?? "") || 25)) {
|
|
3722
|
+
if (isCrawlTimedOut() || visited.size >= maxScreens) break;
|
|
3547
3723
|
const gapUrl = gap.confirmedUrl;
|
|
3548
|
-
if (visited.size >= maxScreens) break;
|
|
3549
3724
|
try {
|
|
3550
3725
|
await page.goto(gapUrl, { waitUntil: "domcontentloaded", timeout: 2e4 });
|
|
3551
3726
|
if (gapLoginPattern.test(page.url())) {
|
|
@@ -3558,7 +3733,17 @@ ${snippet}`,
|
|
|
3558
3733
|
const gapShot = `${opts.screenshotDir ?? "/tmp"}/${gapScreen.id}-v${gapScreen.version}.png`;
|
|
3559
3734
|
await waitForScreenshotReady(page, rawPage ?? void 0);
|
|
3560
3735
|
await page.screenshot({ path: gapShot, fullPage: true });
|
|
3561
|
-
await
|
|
3736
|
+
const gapCloudUrl = await uploadToCloud(gapShot, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(gapShot)}`).catch(() => void 0);
|
|
3737
|
+
await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, gapScreen.id, gapShot, gapScreen.version, "DESKTOP", gapCloudUrl);
|
|
3738
|
+
const gapDom = await capturePageHtml(page);
|
|
3739
|
+
if (gapDom) {
|
|
3740
|
+
const gapDomPath = gapShot.replace(/\.(png|jpg|jpeg)$/i, ".dom.html");
|
|
3741
|
+
await fs.writeFile(gapDomPath, gapDom, "utf8");
|
|
3742
|
+
const gapDomKey = `${opts.tenantId}/${opts.projectId}/dom-snapshots/${path.basename(gapDomPath)}`;
|
|
3743
|
+
const gapDomCloud = await uploadToCloud(gapDomPath, gapDomKey).catch(() => void 0);
|
|
3744
|
+
AppBrain.updateScreenDomSnapshot(opts.tenantId, gapScreen.id, gapDomPath, gapDomCloud).catch(() => {
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3562
3747
|
const [gapElems, gapLinks, gapLinkMaps] = await Promise.all([
|
|
3563
3748
|
extractElements(stagehand, page, elementLLM ?? void 0, rawPage ?? void 0).catch(() => []),
|
|
3564
3749
|
discoverLinks(stagehand, origin, page, linkLLM ?? void 0, false, rawPage ?? void 0).catch(() => []),
|
|
@@ -3575,7 +3760,7 @@ ${snippet}`,
|
|
|
3575
3760
|
}
|
|
3576
3761
|
for (const href of gapLinks) {
|
|
3577
3762
|
const canon = canonicalize(href);
|
|
3578
|
-
if (isUsefulNavigationUrl(href, origin) && !queued.has(canon) && !visited.has(canon) && queued.size < maxScreens *
|
|
3763
|
+
if (isUsefulNavigationUrl(href, origin) && !queued.has(canon) && !visited.has(canon) && queued.size < maxScreens * 3) {
|
|
3579
3764
|
queued.add(canon);
|
|
3580
3765
|
queue.push(href);
|
|
3581
3766
|
}
|
|
@@ -3777,6 +3962,7 @@ ${snippet}`,
|
|
|
3777
3962
|
});
|
|
3778
3963
|
const mobilePage = await mobileCtx.newPage();
|
|
3779
3964
|
for (const disc of discovered) {
|
|
3965
|
+
if (isCrawlTimedOut()) break;
|
|
3780
3966
|
if (!disc.screenId) continue;
|
|
3781
3967
|
try {
|
|
3782
3968
|
await mobilePage.goto(disc.url, { waitUntil: "domcontentloaded", timeout: 2e4 });
|
|
@@ -3938,8 +4124,14 @@ ${snippet}`,
|
|
|
3938
4124
|
}
|
|
3939
4125
|
})();
|
|
3940
4126
|
await onProgress?.(98, "Finalizing");
|
|
4127
|
+
const wasTimeout = isCrawlTimedOut();
|
|
3941
4128
|
const pendingUrls = queue.filter((u) => !visited.has(canonicalize(u)));
|
|
4129
|
+
const visitedUrls = Array.from(visited.entries()).filter(([_k, v]) => v !== "_pending_").map(([k]) => k);
|
|
3942
4130
|
const hasMoreSegments = pendingUrls.length > 0;
|
|
4131
|
+
if (wasTimeout && pendingUrls.length > 0) {
|
|
4132
|
+
const elapsedMin = Math.round((Date.now() - crawlLoopStart) / 6e4);
|
|
4133
|
+
crawlWarnings.push(`global-timeout: crawl stopped after ${elapsedMin} minutes with ${pendingUrls.length} URLs remaining`);
|
|
4134
|
+
}
|
|
3943
4135
|
const screensSucceeded = discovered.filter((d) => !!d.screenId).length;
|
|
3944
4136
|
const screensErrored = discovered.filter((d) => !!d.error && !d.screenId).length;
|
|
3945
4137
|
if (screensSucceeded >= 3) {
|
|
@@ -3963,6 +4155,10 @@ ${snippet}`,
|
|
|
3963
4155
|
performanceMetrics,
|
|
3964
4156
|
pendingUrls,
|
|
3965
4157
|
// feed into next segment's resumeUrls
|
|
4158
|
+
visitedUrls,
|
|
4159
|
+
// all fully-processed URLs for skipUrls in continuation
|
|
4160
|
+
wasTimeout,
|
|
4161
|
+
// true when global timeout triggered the BFS break
|
|
3966
4162
|
segmentIndex: segIdx,
|
|
3967
4163
|
// which segment this was
|
|
3968
4164
|
hasMoreSegments,
|
|
@@ -4016,7 +4212,7 @@ async function extractElements(stagehand, page, llmCfg, rawPage) {
|
|
|
4016
4212
|
const iframeUrl = await frame.url().catch(() => "");
|
|
4017
4213
|
if (!iframeUrl || iframeUrl === "about:blank" || iframeUrl.startsWith("javascript:")) continue;
|
|
4018
4214
|
const frameEls = await domFallbackElements(frame).catch(() => []);
|
|
4019
|
-
for (const el of frameEls.slice(0,
|
|
4215
|
+
for (const el of frameEls.slice(0, 30)) {
|
|
4020
4216
|
iframeElements.push({ ...el, notes: el.notes ? `${el.notes} [in iframe]` : "[in iframe]" });
|
|
4021
4217
|
}
|
|
4022
4218
|
}
|
|
@@ -4036,7 +4232,7 @@ async function extractElements(stagehand, page, llmCfg, rawPage) {
|
|
|
4036
4232
|
seen.add(label);
|
|
4037
4233
|
out.push({ meaning: label, role, notes: "[dynamic/revealed]" });
|
|
4038
4234
|
});
|
|
4039
|
-
return out.slice(0,
|
|
4235
|
+
return out.slice(0, 30);
|
|
4040
4236
|
}).catch(() => []);
|
|
4041
4237
|
} catch {
|
|
4042
4238
|
dynamicElements = [];
|
|
@@ -4081,7 +4277,7 @@ async function extractElements(stagehand, page, llmCfg, rawPage) {
|
|
|
4081
4277
|
]);
|
|
4082
4278
|
if (!extracted) return domElements;
|
|
4083
4279
|
const filtered = extracted.elements.filter((e) => !ELEMENT_NOISE.test(e.meaning ?? ""));
|
|
4084
|
-
return filtered.slice(0,
|
|
4280
|
+
return filtered.slice(0, 40);
|
|
4085
4281
|
} catch {
|
|
4086
4282
|
return domElements;
|
|
4087
4283
|
}
|
|
@@ -4104,7 +4300,7 @@ async function discoverLinks(stagehand, origin, page, llmCfg, skipAi = false, ra
|
|
|
4104
4300
|
});
|
|
4105
4301
|
}
|
|
4106
4302
|
const allDomLinks = Array.from(/* @__PURE__ */ new Set([...domLinks, ...interactiveLinks]));
|
|
4107
|
-
if (allDomLinks.length >=
|
|
4303
|
+
if (allDomLinks.length >= 50 || skipAi) return allDomLinks;
|
|
4108
4304
|
try {
|
|
4109
4305
|
const extracted = await Promise.race([
|
|
4110
4306
|
stagehand.extract(
|
|
@@ -4426,7 +4622,7 @@ async function contentFallbackElements(page) {
|
|
|
4426
4622
|
const mainContent = document.querySelector('main, [role="main"], article') ?? document.body;
|
|
4427
4623
|
let listItemCount = 0;
|
|
4428
4624
|
mainContent.querySelectorAll("li").forEach((li) => {
|
|
4429
|
-
if (listItemCount >=
|
|
4625
|
+
if (listItemCount >= 50) return;
|
|
4430
4626
|
if (!isVisible(li)) return;
|
|
4431
4627
|
const text = li.textContent?.replace(/\s+/g, " ").trim().slice(0, 150);
|
|
4432
4628
|
if (text && text.length > 3 && text.length < 200) {
|
|
@@ -4641,7 +4837,7 @@ async function contentFallbackElements(page) {
|
|
|
4641
4837
|
const navTags = /* @__PURE__ */ new Set(["nav", "header", "footer"]);
|
|
4642
4838
|
let contentLinkCount = 0;
|
|
4643
4839
|
document.querySelectorAll("a[href]").forEach((a) => {
|
|
4644
|
-
if (contentLinkCount >=
|
|
4840
|
+
if (contentLinkCount >= 40) return;
|
|
4645
4841
|
if (!isVisible(a)) return;
|
|
4646
4842
|
let p = a.parentElement;
|
|
4647
4843
|
while (p) {
|
|
@@ -5084,7 +5280,7 @@ async function interactAndCollectLinks(page, origin) {
|
|
|
5084
5280
|
const tabCount = await page.evaluate(
|
|
5085
5281
|
() => document.querySelectorAll('[role="tab"]').length
|
|
5086
5282
|
).catch(() => 0);
|
|
5087
|
-
for (let i = 0; i < Math.min(tabCount,
|
|
5283
|
+
for (let i = 0; i < Math.min(tabCount, 30); i++) {
|
|
5088
5284
|
try {
|
|
5089
5285
|
const clicked = await page.evaluate((idx) => {
|
|
5090
5286
|
const tabs = document.querySelectorAll('[role="tab"]');
|
|
@@ -5128,7 +5324,7 @@ async function interactAndCollectLinks(page, origin) {
|
|
|
5128
5324
|
const accordionCount = await page.evaluate(
|
|
5129
5325
|
() => document.querySelectorAll('details:not([open]) > summary, [data-state="closed"] > button, [aria-expanded="false"][class*="accordion" i]').length
|
|
5130
5326
|
).catch(() => 0);
|
|
5131
|
-
for (let i = 0; i < Math.min(accordionCount,
|
|
5327
|
+
for (let i = 0; i < Math.min(accordionCount, 20); i++) {
|
|
5132
5328
|
try {
|
|
5133
5329
|
await page.evaluate((idx) => {
|
|
5134
5330
|
const items = document.querySelectorAll(
|
|
@@ -5166,7 +5362,7 @@ async function expandNavMenus(page) {
|
|
|
5166
5362
|
while (p) {
|
|
5167
5363
|
const tag = p.tagName?.toLowerCase();
|
|
5168
5364
|
const role = p.getAttribute("role") ?? "";
|
|
5169
|
-
if (["
|
|
5365
|
+
if (["table", "select"].includes(tag ?? "") || ["dialog", "alertdialog", "combobox", "listbox", "grid", "treegrid"].includes(role)) return true;
|
|
5170
5366
|
p = p.parentElement;
|
|
5171
5367
|
}
|
|
5172
5368
|
return false;
|
|
@@ -5196,7 +5392,7 @@ async function expandNavMenus(page) {
|
|
|
5196
5392
|
const navHandles = await page.$$(
|
|
5197
5393
|
'nav > ul > li, nav > li, header > ul > li, header > nav > ul > li, [role="navigation"] > ul > li, [role="menubar"] > [role="menuitem"], [class*="nav"] > ul > li, [class*="menu"] > li'
|
|
5198
5394
|
).catch(() => []);
|
|
5199
|
-
for (const h of navHandles.slice(0,
|
|
5395
|
+
for (const h of navHandles.slice(0, 40)) {
|
|
5200
5396
|
try {
|
|
5201
5397
|
await h.hover({ force: true, timeout: 600 });
|
|
5202
5398
|
await page.waitForTimeout(280);
|
|
@@ -5245,6 +5441,10 @@ async function domFallbackLinks(page, origin, rawPage) {
|
|
|
5245
5441
|
root.querySelectorAll("[data-href]").forEach((el) => {
|
|
5246
5442
|
add(el.getAttribute("data-href") || "", el.textContent || "");
|
|
5247
5443
|
});
|
|
5444
|
+
root.querySelectorAll("form[action]").forEach((form) => {
|
|
5445
|
+
const method = (form.getAttribute("method") || "get").toLowerCase();
|
|
5446
|
+
if (method !== "post") add(form.getAttribute("action") || "", form.getAttribute("aria-label") || "");
|
|
5447
|
+
});
|
|
5248
5448
|
root.querySelectorAll("*").forEach((el) => {
|
|
5249
5449
|
try {
|
|
5250
5450
|
if (el.shadowRoot && !rootSeen.has(el.shadowRoot)) {
|
|
@@ -5652,6 +5852,21 @@ async function captureAuthDiscoveryScreen(page, opts, requestedUrl, source, ordi
|
|
|
5652
5852
|
await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, "DESKTOP", authCloudUrl).catch(() => {
|
|
5653
5853
|
});
|
|
5654
5854
|
}
|
|
5855
|
+
const authDomContent = await capturePageHtml(page);
|
|
5856
|
+
if (authDomContent && opts.screenshotDir) {
|
|
5857
|
+
const authDomPath = shotPath.replace(/\.(png|jpg|jpeg)$/i, ".dom.html");
|
|
5858
|
+
await fs.writeFile(authDomPath, authDomContent, "utf8");
|
|
5859
|
+
let authDomCloudUrl;
|
|
5860
|
+
if (opts.storageConfig) {
|
|
5861
|
+
try {
|
|
5862
|
+
const { uploadScreenshotToTenantStorage } = await import("@detiq/app-brain");
|
|
5863
|
+
authDomCloudUrl = await uploadScreenshotToTenantStorage(opts.storageConfig, authDomPath, `${opts.tenantId}/${opts.projectId}/dom-snapshots/${path.basename(authDomPath)}`);
|
|
5864
|
+
} catch {
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
await AppBrain.updateScreenDomSnapshot(opts.tenantId, screen.id, authDomPath, authDomCloudUrl).catch(() => {
|
|
5868
|
+
});
|
|
5869
|
+
}
|
|
5655
5870
|
await AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
|
|
5656
5871
|
screenId: screen.id,
|
|
5657
5872
|
jobId: opts.jobId,
|