@detiq/crawler 0.1.8 → 0.1.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/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(() => AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, "DESKTOP")).catch((err) => console.error("[crawler] page.screenshot failed:", err)),
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 AppBrain.saveScreenshot(opts.tenantId, opts.projectId, disc.screenId, shotPath, disc.version, profile);
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
- await AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, "DESKTOP").catch(() => {
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,