@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/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
|
-
|
|
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 },
|
package/src/native-crawler.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/types/crawler.d.ts
CHANGED
|
@@ -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[];
|
package/types/job-queue.d.ts
CHANGED
|
@@ -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
|