@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/src/job-queue.ts CHANGED
@@ -709,8 +709,20 @@ export async function processJob(jobId: string, config: any, opts?: ProcessJobOp
709
709
  jobCheckpoints.delete(jobId);
710
710
 
711
711
  const pendingUrls: string[] = (result as any)?.pendingUrls ?? [];
712
+ let nextJobId: string | undefined;
712
713
  if (pendingUrls.length > 0) {
713
- logger.info({ pendingUrls: pendingUrls.length }, 'Crawl finished with pending URLs — not auto-queuing (segmented crawls disabled)');
714
+ // visitedUrls includes all attempted URLs (screens, soft-404s, API paths) — more complete than
715
+ // discovered-with-screenId only, preventing the continuation job from re-attempting dead ends.
716
+ const visitedForSkip: string[] = (result as any).visitedUrls ?? ((result as any).discovered ?? [])
717
+ .filter((d: any) => !!d.screenId && !!d.url)
718
+ .map((d: any) => { try { const _u = new URL(d.url as string); _u.hash = ''; return _u.href; } catch { return d.url as string; } });
719
+ const reason = (result as any).wasTimeout ? 'timeout' : 'segment-limit';
720
+ logger.info({ pendingUrls: pendingUrls.length, visited: visitedForSkip.length, reason }, '[crawl] auto-enqueuing continuation job');
721
+ nextJobId = await enqueueJob('CRAWL', {
722
+ ...config,
723
+ startUrls: pendingUrls,
724
+ skipUrls: [...(config.skipUrls ?? []), ...visitedForSkip],
725
+ }).catch((autoErr: any) => { logger.warn({ err: autoErr?.message }, '[crawl] auto-segment enqueue failed (non-fatal)'); return undefined; });
714
726
  }
715
727
 
716
728
  await withTenant(tenantId, (tx) =>
@@ -718,7 +730,7 @@ export async function processJob(jobId: string, config: any, opts?: ProcessJobOp
718
730
  where: { id: jobId },
719
731
  data: {
720
732
  status: 'DONE',
721
- result: { ...(result as any) } as any,
733
+ result: { ...(result as any), ...(nextJobId ? { nextJobId } : {}) } as any,
722
734
  progress: 100,
723
735
  finishedAt: new Date(),
724
736
  },
@@ -848,7 +860,7 @@ export async function processJob(jobId: string, config: any, opts?: ProcessJobOp
848
860
  try {
849
861
  const key = buildArtifactKey(tenantId, projectId, 'dom-snapshots', `${screen.id}.json`, tenantStorageCfg.prefix);
850
862
  const cloudUrl = await uploadFileToTenantStorage(tenantStorageCfg, domPath, key, 'application/json');
851
- await AppBrain.updateScreenDomSnapshot(tenantId, screen.id, cloudUrl);
863
+ await AppBrain.updateScreenDomSnapshot(tenantId, screen.id, domPath, cloudUrl);
852
864
  domUploaded++;
853
865
  } catch (err) {
854
866
  logger.warn({ screenId: screen.id, err }, 'Failed to upload DOM snapshot to cloud storage');
@@ -916,8 +928,23 @@ export async function processJob(jobId: string, config: any, opts?: ProcessJobOp
916
928
  clearInterval(heartbeatTimer);
917
929
  runningControllers.delete(jobId);
918
930
  const aborted = String(err).includes('CRAWL_ABORTED');
931
+ const authFailure = !aborted && (String(err).includes('AUTH_FAILURE') || String(err).includes('SESSION_EXPIRED'));
919
932
  const checkpoint = jobCheckpoints.get(jobId);
920
933
  jobCheckpoints.delete(jobId);
934
+
935
+ // Auth failed mid-crawl with work still pending — auto-requeue continuation job.
936
+ // The crawler checkpoints the remaining queue before throwing, so checkpoint.pendingQueue
937
+ // holds URLs not yet visited. skipUrls prevents re-crawling already-done pages.
938
+ if (authFailure && (checkpoint?.pendingQueue?.length ?? 0) > 0) {
939
+ const pendingAfterAuth: string[] = checkpoint!.pendingQueue!;
940
+ logger.info({ pending: pendingAfterAuth.length }, '[crawl] auth failure mid-crawl — auto-enqueuing continuation with pending URLs');
941
+ enqueueJob('CRAWL', {
942
+ ...config,
943
+ startUrls: pendingAfterAuth,
944
+ skipUrls: [...(config.skipUrls ?? []), ...(checkpoint!.visitedUrls ?? [])],
945
+ }).catch((enqErr: any) => logger.warn({ err: enqErr?.message }, '[crawl] auth-failure auto-segment enqueue failed (non-fatal)'));
946
+ }
947
+
921
948
  await withTenant(tenantId, (tx) =>
922
949
  tx.platformJob.update({
923
950
  where: { id: jobId },
@@ -872,11 +872,12 @@ export async function runNativeCrawl(opts: NativeCrawlerOptions): Promise<void>
872
872
  const screen = await AppBrain.saveScreen(tenantId, projectId, screenName);
873
873
  const shotPath = path.join(screenshotDir, `${screen.id}-native-v${screen.version}.png`);
874
874
  await fs.writeFile(shotPath, screenshotBuf);
875
- await AppBrain.saveScreenshot(tenantId, projectId, screen.id, shotPath, screen.version, 'NATIVE');
875
+ let cloudUrl: string | undefined;
876
876
  if (tenantStorageCfg) {
877
877
  const s3Key = `${tenantId}/${projectId}/screenshots/${screen.id}-native-v${screen.version}.png`;
878
- await uploadScreenshotToTenantStorage(tenantStorageCfg, shotPath, s3Key).catch(() => {});
878
+ cloudUrl = await uploadScreenshotToTenantStorage(tenantStorageCfg, shotPath, s3Key).catch(() => undefined);
879
879
  }
880
+ await AppBrain.saveScreenshot(tenantId, projectId, screen.id, shotPath, screen.version, 'NATIVE', cloudUrl);
880
881
 
881
882
  // c. Parse accessibility tree → save elements
882
883
  // Scroll down to reveal below-fold elements, then scroll back up before parsing
@@ -1024,11 +1025,12 @@ export async function runNativeCrawl(opts: NativeCrawlerOptions): Promise<void>
1024
1025
  const newScreen = await AppBrain.saveScreen(tenantId, projectId, newScreenName);
1025
1026
  const newShotPath = path.join(screenshotDir, `${newScreen.id}-native-v${newScreen.version}.png`);
1026
1027
  await fs.writeFile(newShotPath, Buffer.from(newScreenshot, 'base64'));
1027
- await AppBrain.saveScreenshot(tenantId, projectId, newScreen.id, newShotPath, newScreen.version, 'NATIVE');
1028
+ let newCloudUrl: string | undefined;
1028
1029
  if (tenantStorageCfg) {
1029
1030
  const s3Key = `${tenantId}/${projectId}/screenshots/${newScreen.id}-native-v${newScreen.version}.png`;
1030
- await uploadScreenshotToTenantStorage(tenantStorageCfg, newShotPath, s3Key).catch(() => {});
1031
+ newCloudUrl = await uploadScreenshotToTenantStorage(tenantStorageCfg, newShotPath, s3Key).catch(() => undefined);
1031
1032
  }
1033
+ await AppBrain.saveScreenshot(tenantId, projectId, newScreen.id, newShotPath, newScreen.version, 'NATIVE', newCloudUrl);
1032
1034
 
1033
1035
  const newXml: string = await driver.getPageSource();
1034
1036
  const newElements = parseAccessibilityTree(newXml, platform);
@@ -164,6 +164,8 @@ export declare function crawlProject(opts: {
164
164
  segmentIndex: number;
165
165
  hasMoreSegments: boolean;
166
166
  domHashes: string[];
167
+ visitedUrls?: undefined;
168
+ wasTimeout?: undefined;
167
169
  crawlWarnings?: undefined;
168
170
  } | {
169
171
  screensFound: number;
@@ -186,6 +188,8 @@ export declare function crawlProject(opts: {
186
188
  apiEndpoints: ApiEndpointObservationInput[];
187
189
  performanceMetrics: PerformanceMetric[];
188
190
  pendingUrls: string[];
191
+ visitedUrls: string[];
192
+ wasTimeout: boolean;
189
193
  segmentIndex: number;
190
194
  hasMoreSegments: boolean;
191
195
  domHashes: string[];
@@ -82,7 +82,9 @@ export declare function claimSpecificJob(jobId: string): Promise<ClaimedJob | nu
82
82
  export declare function claimNextQueuedJob(): Promise<ClaimedJob | null>;
83
83
  /** Requeues RUNNING jobs whose heartbeat has gone stale — the worker that claimed them
84
84
  * died (crash, restart, OOM) without ever marking them DONE/FAILED. Idempotent and
85
- * safe to run from every worker process; only affects rows past the timeout. */
85
+ * safe to run from every worker process; only affects rows past the timeout.
86
+ * API-in-process job types (GENERATE_SCRIPT etc.) are failed directly — requeueing them
87
+ * would cause the worker to claim and fail them with a confusing "claimed by worker" error. */
86
88
  export declare function requeueStaleRunningJobs(): Promise<void>;
87
89
  /**
88
90
  * Requeues any RUNNING jobs claimed by a different worker (i.e., a previous