@detiq/crawler 0.1.8 → 0.2.0
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/crawler.js +81 -3
- package/dist/crawler.js.map +2 -2
- package/dist/job-queue.js +2 -0
- package/dist/job-queue.js.map +2 -2
- package/package.json +1 -1
- package/src/crawler.ts +88 -3
- package/src/job-queue.ts +2 -0
- package/types/crawler.d.ts +12 -0
package/dist/crawler.js
CHANGED
|
@@ -314,6 +314,15 @@ const TwoFASchema = z.object({
|
|
|
314
314
|
});
|
|
315
315
|
async function crawlProject(opts, onProgress) {
|
|
316
316
|
if (opts.screenshotDir) await fs.mkdir(opts.screenshotDir, { recursive: true });
|
|
317
|
+
const uploadToCloud = async (localPath, key) => {
|
|
318
|
+
if (!opts.storageConfig) return void 0;
|
|
319
|
+
try {
|
|
320
|
+
const { uploadScreenshotToTenantStorage } = await import("@detiq/app-brain");
|
|
321
|
+
return await uploadScreenshotToTenantStorage(opts.storageConfig, localPath, key);
|
|
322
|
+
} catch {
|
|
323
|
+
return void 0;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
317
326
|
const isDocker = !!process.env.PLAYWRIGHT_IN_DOCKER || process.env.NODE_ENV === "production";
|
|
318
327
|
const toStagehandModel = (model, provider) => model.includes("/") ? model : `${provider}/${model}`;
|
|
319
328
|
let stagehandModel = toStagehandModel(config.llmModel, "openai");
|
|
@@ -601,6 +610,15 @@ async function crawlProject(opts, onProgress) {
|
|
|
601
610
|
localStorage.setItem("token", token);
|
|
602
611
|
}, opts.authToken);
|
|
603
612
|
}
|
|
613
|
+
const crawlerSecret = process.env.CRAWLER_INTERNAL_SECRET;
|
|
614
|
+
if (crawlerSecret) {
|
|
615
|
+
const loginPattern = `${new URL(opts.appUrl).origin}**/auth/login*`;
|
|
616
|
+
await page.route(loginPattern, (route) => {
|
|
617
|
+
route.continue({
|
|
618
|
+
headers: { ...route.request().headers(), "x-zeta-crawler": crawlerSecret }
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
}
|
|
604
622
|
await page.addInitScript(() => {
|
|
605
623
|
try {
|
|
606
624
|
window.__zetaCLS = 0;
|
|
@@ -1179,7 +1197,12 @@ async function crawlProject(opts, onProgress) {
|
|
|
1179
1197
|
let aiRateLimitWarned = false;
|
|
1180
1198
|
let totalAttemptsThisSegment = 0;
|
|
1181
1199
|
const maxAttempts = Math.max(segmentSize * 4, 200);
|
|
1200
|
+
let pendingSessionRestore = null;
|
|
1182
1201
|
while (queue.length > 0 && visited.size < maxScreens && visited.size < segmentSize) {
|
|
1202
|
+
if (pendingSessionRestore) {
|
|
1203
|
+
await pendingSessionRestore();
|
|
1204
|
+
pendingSessionRestore = null;
|
|
1205
|
+
}
|
|
1183
1206
|
if (++totalAttemptsThisSegment > maxAttempts) {
|
|
1184
1207
|
logger.warn({ maxAttempts, visited: visited.size, queued: queue.length }, "[crawler] attempt cap reached \u2014 stopping to prevent URL explosion");
|
|
1185
1208
|
break;
|
|
@@ -1251,6 +1274,45 @@ async function crawlProject(opts, onProgress) {
|
|
|
1251
1274
|
}
|
|
1252
1275
|
};
|
|
1253
1276
|
rawPage.on("response", onRedirectResponse);
|
|
1277
|
+
const AUTH_PATH_REGEX = /\/(login|register|signup|sign-in|sign-up|forgot-password|reset-password)([-/]|$)/i;
|
|
1278
|
+
const isAuthRoute = AUTH_PATH_REGEX.test(new URL(url).pathname);
|
|
1279
|
+
const browserCtx = playwrightBrowser?.contexts()[0];
|
|
1280
|
+
if (isAuthRoute && browserCtx) {
|
|
1281
|
+
const currentState = await browserCtx.storageState();
|
|
1282
|
+
const hasSession = (currentState.cookies?.length ?? 0) > 0 || currentState.origins?.some((o) => (o.localStorage?.length ?? 0) > 0);
|
|
1283
|
+
if (hasSession) {
|
|
1284
|
+
pendingSessionRestore = async () => {
|
|
1285
|
+
const ctx = playwrightBrowser?.contexts()[0];
|
|
1286
|
+
if (ctx) {
|
|
1287
|
+
await ctx.addCookies(currentState.cookies);
|
|
1288
|
+
await rawPage.evaluate((origins) => {
|
|
1289
|
+
for (const entry of origins ?? []) {
|
|
1290
|
+
if (window.location.origin === entry.origin) {
|
|
1291
|
+
for (const item of entry.localStorage) {
|
|
1292
|
+
try {
|
|
1293
|
+
localStorage.setItem(item.name, item.value);
|
|
1294
|
+
} catch {
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}, currentState.origins).catch(() => {
|
|
1300
|
+
});
|
|
1301
|
+
logger.info("[crawler] restored session state after public auth route");
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
await browserCtx.clearCookies();
|
|
1305
|
+
await rawPage.evaluate(() => {
|
|
1306
|
+
try {
|
|
1307
|
+
localStorage.clear();
|
|
1308
|
+
sessionStorage.clear();
|
|
1309
|
+
} catch {
|
|
1310
|
+
}
|
|
1311
|
+
}).catch(() => {
|
|
1312
|
+
});
|
|
1313
|
+
logger.info({ url }, "[crawler] temporarily cleared session for public auth route");
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1254
1316
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch((navErr) => {
|
|
1255
1317
|
logger.warn({ url, err: navErr?.message ?? String(navErr) }, "[crawler] navigation failed \u2014 skipping");
|
|
1256
1318
|
});
|
|
@@ -1733,7 +1795,10 @@ async function crawlProject(opts, onProgress) {
|
|
|
1733
1795
|
}
|
|
1734
1796
|
})()}`);
|
|
1735
1797
|
const [, elements, nextLinks, atElements] = await Promise.all([
|
|
1736
|
-
page.screenshot({ path: shotPath, fullPage: true }).then(
|
|
1798
|
+
page.screenshot({ path: shotPath, fullPage: true }).then(async () => {
|
|
1799
|
+
const cloudUrl = await uploadToCloud(shotPath, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(shotPath)}`);
|
|
1800
|
+
return AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, "DESKTOP", cloudUrl);
|
|
1801
|
+
}).catch((err) => console.error("[crawler] page.screenshot failed:", err)),
|
|
1737
1802
|
extractElements(stagehand, page, elementLLM ?? void 0).catch((err) => {
|
|
1738
1803
|
console.error("[crawler] extractElements failed:", err);
|
|
1739
1804
|
return [];
|
|
@@ -2291,6 +2356,10 @@ ${snippet}`,
|
|
|
2291
2356
|
}
|
|
2292
2357
|
}
|
|
2293
2358
|
}
|
|
2359
|
+
if (pendingSessionRestore) {
|
|
2360
|
+
await pendingSessionRestore();
|
|
2361
|
+
pendingSessionRestore = null;
|
|
2362
|
+
}
|
|
2294
2363
|
try {
|
|
2295
2364
|
const { runUrlPatternCompletion } = await import("./url-pattern-completer.js");
|
|
2296
2365
|
const llmCfgForPatterns = await getTaskAgentLLMConfig(opts.tenantId, "crawl").catch(() => null);
|
|
@@ -2610,7 +2679,8 @@ ${snippet}`,
|
|
|
2610
2679
|
mobilePage.screenshot({ path: shotPath, fullPage: true }),
|
|
2611
2680
|
domFallbackElements(mobilePage)
|
|
2612
2681
|
]);
|
|
2613
|
-
await
|
|
2682
|
+
const cloudUrl = await uploadToCloud(shotPath, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(shotPath)}`);
|
|
2683
|
+
await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, disc.screenId, shotPath, disc.version, profile, cloudUrl);
|
|
2614
2684
|
AppBrain.autoCreateVisualBaseline(opts.tenantId, opts.projectId, disc.screenId, profile, shotPath, gitInfo.branch, gitInfo.commit || void 0).catch(() => {
|
|
2615
2685
|
});
|
|
2616
2686
|
for (const el of vpElements) {
|
|
@@ -3572,7 +3642,15 @@ async function captureAuthDiscoveryScreen(page, opts, requestedUrl, source, ordi
|
|
|
3572
3642
|
});
|
|
3573
3643
|
const screen = await AppBrain.saveScreen(opts.tenantId, opts.projectId, name, screenUrl, opts.jobId, source === "entry" ? "ENTRY_POINT" : "SEED_URL").catch(() => null);
|
|
3574
3644
|
if (!screen) return { captured: false, finalUrl, flowType };
|
|
3575
|
-
|
|
3645
|
+
let authCloudUrl;
|
|
3646
|
+
if (opts.storageConfig) {
|
|
3647
|
+
try {
|
|
3648
|
+
const { uploadScreenshotToTenantStorage } = await import("@detiq/app-brain");
|
|
3649
|
+
authCloudUrl = await uploadScreenshotToTenantStorage(opts.storageConfig, shotPath, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(shotPath)}`);
|
|
3650
|
+
} catch {
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, "DESKTOP", authCloudUrl).catch(() => {
|
|
3576
3654
|
});
|
|
3577
3655
|
await AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
|
|
3578
3656
|
screenId: screen.id,
|