@detiq/crawler 1.0.4 → 1.0.6
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.d.ts +14 -0
- package/dist/crawler.js +170 -39
- package/dist/crawler.js.map +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +2 -2
- package/dist/job-queue.d.ts +7 -0
- package/dist/job-queue.js +43 -8
- package/dist/job-queue.js.map +2 -2
- package/dist/phash.d.ts +12 -0
- package/dist/phash.js +21 -0
- package/dist/phash.js.map +2 -2
- package/dist/session-capture.d.ts +4 -1
- package/dist/stealth/human-behavior.js +12 -11
- package/dist/stealth/human-behavior.js.map +2 -2
- package/dist/test-executor.d.ts +8 -1
- package/dist/visual-diff-verdict.d.ts +1 -1
- package/package.json +1 -1
- package/src/crawler.ts +98 -27
- package/src/job-queue.ts +50 -4
- package/types/index.d.ts +2 -0
- package/types/job-queue.d.ts +7 -0
- package/types/phash.d.ts +12 -0
package/dist/index.d.ts
CHANGED
|
@@ -33,3 +33,5 @@ export type { ProxyTier, ProxyTierConfig } from './proxy.js';
|
|
|
33
33
|
export { RedisCrawlFrontier, isRedisFrontierEnabled } from './redis-frontier.js';
|
|
34
34
|
export { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemetry.js';
|
|
35
35
|
export type { Span, FinishedSpan } from './telemetry.js';
|
|
36
|
+
export { analyzeFailure } from './failure-analysis-agent.js';
|
|
37
|
+
export type { FailureContext } from './failure-analysis-agent.js';
|
package/dist/index.js
CHANGED
|
@@ -24,11 +24,13 @@ import { extractMarkdown } from "./markdown-extract.js";
|
|
|
24
24
|
import { TieredProxyManager } from "./proxy.js";
|
|
25
25
|
import { RedisCrawlFrontier, isRedisFrontierEnabled } from "./redis-frontier.js";
|
|
26
26
|
import { startSpan, endSpan, withSpan, increment, flushMetrics } from "./telemetry.js";
|
|
27
|
+
import { analyzeFailure } from "./failure-analysis-agent.js";
|
|
27
28
|
export {
|
|
28
29
|
CHROME_TLS_HEADERS,
|
|
29
30
|
RedisCrawlFrontier,
|
|
30
31
|
TieredProxyManager,
|
|
31
32
|
adBlockerProfile,
|
|
33
|
+
analyzeFailure,
|
|
32
34
|
chromeTlsAgent,
|
|
33
35
|
closeLiveView,
|
|
34
36
|
completeLiveView,
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["export * from './crawler.js';\nexport * from './visual-diff-verdict.js';\nexport * from './auth.js';\nexport * from './screen-reader.js';\nexport * from './job-queue.js';\nexport { startSqsWorker } from './sqs-queue.js';\nexport * from './session-capture.js';\nexport * from './session-export.js';\nexport * from './test-executor.js';\nexport { startLiveView, getFrame, liveViewNavigate, completeLiveView, closeLiveView, sendInput } from './live-view.js';\nexport { waitForEmailOTP, getInboxEmail } from './email-otp.js';\nexport { getSsoToken, getSsoClientCredentialsToken } from './sso-token.js';\nexport type { SsoConfig, SsoProvider } from './sso-token.js';\nexport * from './engine-router.js';\nexport * from './profiles/domain-profile.js';\nexport * from './policies/crawl-safety.js';\nexport * from './extractors/artifact-envelope.js';\nexport * from './network-observer.js';\nexport { createChromeTlsAgent, probeTlsFingerprint, chromeTlsAgent, CHROME_TLS_HEADERS } from './tls-fingerprint.js';\nexport type { TlsFingerprintOptions } from './tls-fingerprint.js';\nexport { prepareExtensionProfile, adBlockerProfile } from './browser-extension.js';\nexport type { ExtensionSource, ExtensionProfile } from './browser-extension.js';\nexport { createRedisQueue, maybeCreateRedisQueue, isRedisQueueEnabled } from './redis-queue.js';\nexport type { RedisQueueJob, RedisQueueClient } from './redis-queue.js';\nexport { discoverProjectUrls, discoverSitemapUrls } from './sitemap.js';\nexport type { UrlCandidate } from './sitemap.js';\nexport { httpCrawl } from './http-engine.js';\nexport { extractMarkdown } from './markdown-extract.js';\nexport type { ExtractedMarkdown } from './markdown-extract.js';\nexport type { HttpScreen, HttpCrawlOpts, HttpCrawlResult, HttpOutputFormat } from './http-engine.js';\nexport { TieredProxyManager } from './proxy.js';\nexport type { ProxyTier, ProxyTierConfig } from './proxy.js';\nexport { RedisCrawlFrontier, isRedisFrontierEnabled } from './redis-frontier.js';\nexport { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemetry.js';\nexport type { Span, FinishedSpan } from './telemetry.js';\n"],
|
|
5
|
-
"mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB;AAC/B,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,eAAe,UAAU,kBAAkB,kBAAkB,eAAe,iBAAiB;AACtG,SAAS,iBAAiB,qBAAqB;AAC/C,SAAS,aAAa,oCAAoC;AAE1D,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB,qBAAqB,gBAAgB,0BAA0B;AAE9F,SAAS,yBAAyB,wBAAwB;AAE1D,SAAS,kBAAkB,uBAAuB,2BAA2B;AAE7E,SAAS,qBAAqB,2BAA2B;AAEzD,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAGhC,SAAS,0BAA0B;AAEnC,SAAS,oBAAoB,8BAA8B;AAC3D,SAAS,WAAW,SAAS,UAAU,WAAW,oBAAoB;",
|
|
4
|
+
"sourcesContent": ["export * from './crawler.js';\nexport * from './visual-diff-verdict.js';\nexport * from './auth.js';\nexport * from './screen-reader.js';\nexport * from './job-queue.js';\nexport { startSqsWorker } from './sqs-queue.js';\nexport * from './session-capture.js';\nexport * from './session-export.js';\nexport * from './test-executor.js';\nexport { startLiveView, getFrame, liveViewNavigate, completeLiveView, closeLiveView, sendInput } from './live-view.js';\nexport { waitForEmailOTP, getInboxEmail } from './email-otp.js';\nexport { getSsoToken, getSsoClientCredentialsToken } from './sso-token.js';\nexport type { SsoConfig, SsoProvider } from './sso-token.js';\nexport * from './engine-router.js';\nexport * from './profiles/domain-profile.js';\nexport * from './policies/crawl-safety.js';\nexport * from './extractors/artifact-envelope.js';\nexport * from './network-observer.js';\nexport { createChromeTlsAgent, probeTlsFingerprint, chromeTlsAgent, CHROME_TLS_HEADERS } from './tls-fingerprint.js';\nexport type { TlsFingerprintOptions } from './tls-fingerprint.js';\nexport { prepareExtensionProfile, adBlockerProfile } from './browser-extension.js';\nexport type { ExtensionSource, ExtensionProfile } from './browser-extension.js';\nexport { createRedisQueue, maybeCreateRedisQueue, isRedisQueueEnabled } from './redis-queue.js';\nexport type { RedisQueueJob, RedisQueueClient } from './redis-queue.js';\nexport { discoverProjectUrls, discoverSitemapUrls } from './sitemap.js';\nexport type { UrlCandidate } from './sitemap.js';\nexport { httpCrawl } from './http-engine.js';\nexport { extractMarkdown } from './markdown-extract.js';\nexport type { ExtractedMarkdown } from './markdown-extract.js';\nexport type { HttpScreen, HttpCrawlOpts, HttpCrawlResult, HttpOutputFormat } from './http-engine.js';\nexport { TieredProxyManager } from './proxy.js';\nexport type { ProxyTier, ProxyTierConfig } from './proxy.js';\nexport { RedisCrawlFrontier, isRedisFrontierEnabled } from './redis-frontier.js';\nexport { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemetry.js';\nexport type { Span, FinishedSpan } from './telemetry.js';\nexport { analyzeFailure } from './failure-analysis-agent.js';\nexport type { FailureContext } from './failure-analysis-agent.js';\n"],
|
|
5
|
+
"mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB;AAC/B,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,eAAe,UAAU,kBAAkB,kBAAkB,eAAe,iBAAiB;AACtG,SAAS,iBAAiB,qBAAqB;AAC/C,SAAS,aAAa,oCAAoC;AAE1D,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB,qBAAqB,gBAAgB,0BAA0B;AAE9F,SAAS,yBAAyB,wBAAwB;AAE1D,SAAS,kBAAkB,uBAAuB,2BAA2B;AAE7E,SAAS,qBAAqB,2BAA2B;AAEzD,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAGhC,SAAS,0BAA0B;AAEnC,SAAS,oBAAoB,8BAA8B;AAC3D,SAAS,WAAW,SAAS,UAAU,WAAW,oBAAoB;AAEtE,SAAS,sBAAsB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/job-queue.d.ts
CHANGED
|
@@ -84,6 +84,13 @@ export declare function claimNextQueuedJob(): Promise<ClaimedJob | null>;
|
|
|
84
84
|
* died (crash, restart, OOM) without ever marking them DONE/FAILED. Idempotent and
|
|
85
85
|
* safe to run from every worker process; only affects rows past the timeout. */
|
|
86
86
|
export declare function requeueStaleRunningJobs(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Requeues any RUNNING jobs claimed by a different worker (i.e., a previous
|
|
89
|
+
* process instance). Call once at startup before the poll loop begins — this
|
|
90
|
+
* provides immediate recovery instead of waiting the full STALE_JOB_TIMEOUT_MINUTES.
|
|
91
|
+
* Safe to call from multiple replicas simultaneously (each uses its own WORKER_ID).
|
|
92
|
+
*/
|
|
93
|
+
export declare function requeueOrphanedJobs(): Promise<void>;
|
|
87
94
|
/**
|
|
88
95
|
* Starts the durable job worker poll loop. This is what makes job processing
|
|
89
96
|
* survive a restart — enqueueJob()'s immediate same-process claim attempt is
|
package/dist/job-queue.js
CHANGED
|
@@ -130,6 +130,26 @@ async function requeueStaleRunningJobs() {
|
|
|
130
130
|
logger.warn({ jobIds: rows.map((r) => r.id), staleTimeoutMinutes: STALE_JOB_TIMEOUT_MINUTES }, "[job-worker] requeued stale RUNNING job(s)");
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
+
async function requeueOrphanedJobs() {
|
|
134
|
+
const rows = await prismaAdmin.$queryRaw`
|
|
135
|
+
UPDATE "PlatformJob"
|
|
136
|
+
SET status = 'QUEUED',
|
|
137
|
+
"workerId" = NULL,
|
|
138
|
+
result = jsonb_set(
|
|
139
|
+
COALESCE(result::jsonb, '{}'::jsonb),
|
|
140
|
+
'{step}',
|
|
141
|
+
to_jsonb('Worker restarted — queued for recovery'::text),
|
|
142
|
+
true
|
|
143
|
+
)
|
|
144
|
+
WHERE status = 'RUNNING'
|
|
145
|
+
AND "workerId" IS NOT NULL
|
|
146
|
+
AND "workerId" != ${WORKER_ID}
|
|
147
|
+
RETURNING id
|
|
148
|
+
`;
|
|
149
|
+
if (rows.length > 0) {
|
|
150
|
+
logger.warn({ jobIds: rows.map((r) => r.id), workerId: WORKER_ID }, "[job-worker] requeued orphaned RUNNING job(s) from dead worker");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
133
153
|
async function dispatchClaimedJob(job) {
|
|
134
154
|
const dispatcher = jobDispatchTable?.[job.type];
|
|
135
155
|
if (!dispatcher) {
|
|
@@ -181,6 +201,9 @@ function startJobWorkerLoop(opts) {
|
|
|
181
201
|
}
|
|
182
202
|
};
|
|
183
203
|
workerLoopTimer = setInterval(tick, pollIntervalMs);
|
|
204
|
+
requeueOrphanedJobs().catch(
|
|
205
|
+
(err) => logger.error({ err: String(err) }, "[job-worker] startup orphan sweep failed")
|
|
206
|
+
);
|
|
184
207
|
void tick();
|
|
185
208
|
}
|
|
186
209
|
function stopJobWorkerLoop() {
|
|
@@ -238,14 +261,24 @@ async function getJob(tenantId, jobId) {
|
|
|
238
261
|
);
|
|
239
262
|
}
|
|
240
263
|
async function listJobs(tenantId, projectId) {
|
|
241
|
-
return withTenant(
|
|
242
|
-
|
|
243
|
-
(tx) => tx.platformJob.findMany({
|
|
264
|
+
return withTenant(tenantId, async (tx) => {
|
|
265
|
+
const jobs = await tx.platformJob.findMany({
|
|
244
266
|
where: { tenantId, projectId },
|
|
245
267
|
orderBy: { queuedAt: "desc" },
|
|
246
268
|
take: 50
|
|
247
|
-
})
|
|
248
|
-
|
|
269
|
+
});
|
|
270
|
+
const jobIds = jobs.map((j) => j.id);
|
|
271
|
+
if (jobIds.length === 0) return jobs;
|
|
272
|
+
const modelRows = await tx.lLMCallLog.groupBy({
|
|
273
|
+
by: ["jobId"],
|
|
274
|
+
where: { tenantId, jobId: { in: jobIds } },
|
|
275
|
+
_max: { model: true, calledAt: true }
|
|
276
|
+
});
|
|
277
|
+
const modelByJobId = new Map(
|
|
278
|
+
modelRows.filter((r) => r.jobId && r._max?.model).map((r) => [r.jobId, r._max.model])
|
|
279
|
+
);
|
|
280
|
+
return jobs.map((j) => ({ ...j, llmModel: modelByJobId.get(j.id) ?? null }));
|
|
281
|
+
});
|
|
249
282
|
}
|
|
250
283
|
async function cancelJob(tenantId, jobId) {
|
|
251
284
|
const controller = runningControllers.get(jobId);
|
|
@@ -374,13 +407,14 @@ async function investigateScreenChanges(tenantId, projectId, changedScreens) {
|
|
|
374
407
|
async function processJob(jobId, config, opts) {
|
|
375
408
|
const { tenantId, projectId } = config;
|
|
376
409
|
const jobStartedAt = /* @__PURE__ */ new Date();
|
|
377
|
-
await withTenant(
|
|
410
|
+
const claimed = await withTenant(
|
|
378
411
|
tenantId,
|
|
379
|
-
(tx) => tx.platformJob.
|
|
380
|
-
where: { id: jobId },
|
|
412
|
+
(tx) => tx.platformJob.updateMany({
|
|
413
|
+
where: { id: jobId, status: { not: "CANCELLED" } },
|
|
381
414
|
data: { status: "RUNNING", startedAt: jobStartedAt, heartbeatAt: jobStartedAt, workerId: WORKER_ID }
|
|
382
415
|
})
|
|
383
416
|
);
|
|
417
|
+
if (claimed.count === 0) return;
|
|
384
418
|
const controller = new AbortController();
|
|
385
419
|
runningControllers.set(jobId, controller);
|
|
386
420
|
const heartbeatTimer = setInterval(() => {
|
|
@@ -2265,6 +2299,7 @@ export {
|
|
|
2265
2299
|
listJobs,
|
|
2266
2300
|
processDataRetentionPurge,
|
|
2267
2301
|
processJob,
|
|
2302
|
+
requeueOrphanedJobs,
|
|
2268
2303
|
requeueStaleRunningJobs,
|
|
2269
2304
|
rerunJob,
|
|
2270
2305
|
resumeJob,
|