@detiq/crawler 1.1.27 → 1.1.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@detiq/crawler",
3
- "version": "1.1.27",
3
+ "version": "1.1.29",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
package/src/crawler.ts CHANGED
@@ -7013,6 +7013,18 @@ async function runAuthDiscoveryPhase(
7013
7013
  }
7014
7014
 
7015
7015
  const finalUrl = page.url?.() ?? current.url;
7016
+
7017
+ // Skip auth seed paths that redirect to a non-auth URL — the page captured would be the
7018
+ // post-login destination (e.g. dashboard) not the actual auth screen.
7019
+ if (current.source === 'seed_path' && finalUrl !== current.url) {
7020
+ try {
7021
+ const origPath = new URL(current.url).pathname;
7022
+ const finalPath = new URL(finalUrl).pathname;
7023
+ const isAuthSeed = /login|sign-?in|signin|auth|sso|forgot-?password|reset-?password|2fa|mfa/i.test(origPath);
7024
+ if (isAuthSeed && finalPath !== origPath) continue;
7025
+ } catch { /* non-parseable URL — allow capture */ }
7026
+ }
7027
+
7016
7028
  const captureKey = normalizeAuthUrl(finalUrl);
7017
7029
  if (!capturedKeys.has(captureKey)) {
7018
7030
  const result = await captureAuthDiscoveryScreen(page, opts, current.url, current.source, ordinal++);
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
2
2
  import test from 'node:test';
3
3
  import assert from 'node:assert/strict';
4
4
  import { prisma, prismaAdmin } from '@detiq/database';
5
- import { claimNextQueuedJob, claimSpecificJob, requeueStaleRunningJobs } from './job-queue.js';
5
+ import { claimNextQueuedJob, claimSpecificJob, requeueStaleRunningJobs, classifyFailure, FailureType, recordJobFailure, resumeJob } from './job-queue.js';
6
6
 
7
7
  function id(prefix: string) {
8
8
  return `${prefix}_${crypto.randomUUID()}`;
@@ -13,7 +13,7 @@ async function makeFixtureProject() {
13
13
  const projectId = id('project');
14
14
  await prisma.$transaction(async (tx) => {
15
15
  await tx.$executeRawUnsafe(`SELECT set_config('app.current_tenant', $1, true)`, tenantId);
16
- await tx.tenant.create({ data: { id: tenantId, name: 'Job Queue Test Tenant', slug: tenantId } });
16
+ await tx.tenant.create({ data: { id: tenantId, name: 'Job Queue Test Tenant', slug: tenantId, isolation: 'SHARED', region: 'US' } });
17
17
  await tx.project.create({ data: { id: projectId, tenantId, name: 'Job Queue Test Project' } });
18
18
  });
19
19
  return { tenantId, projectId };
@@ -118,3 +118,70 @@ test('requeueStaleRunningJobs: a job with no heartbeat falls back to startedAt',
118
118
  const job = await prismaAdmin.platformJob.findUnique({ where: { id: jobId } });
119
119
  assert.equal(job!.status, 'QUEUED');
120
120
  });
121
+
122
+ test('classifyFailure: classifies errors accurately across categories', () => {
123
+ assert.equal(classifyFailure(new Error('ETIMEDOUT: network connection reset')), FailureType.TRANSIENT);
124
+ assert.equal(classifyFailure(new Error('Rate limit exceeded: 429 Too Many Requests')), FailureType.TRANSIENT);
125
+ assert.equal(classifyFailure(new Error('FATAL: master_enc_key environment variable missing')), FailureType.FATAL);
126
+ assert.equal(classifyFailure(new Error('PrismaClientInitializationError: database connection refused')), FailureType.INFRASTRUCTURE);
127
+ assert.equal(classifyFailure(new Error('Worker exited unexpectedly: sigkill out of memory')), FailureType.INFRASTRUCTURE);
128
+ assert.equal(classifyFailure(new Error('Validation failed: 400 bad request')), FailureType.LOGICAL);
129
+ });
130
+
131
+ test('recordJobFailure: handles transient error with RETRY_WAIT and exponential backoff', async () => {
132
+ const { tenantId, projectId } = await makeFixtureProject();
133
+ const jobId = await insertQueuedJob(tenantId, projectId);
134
+
135
+ await recordJobFailure(tenantId, jobId, new Error('network timeout 503'));
136
+
137
+ const job = await prismaAdmin.platformJob.findUnique({ where: { id: jobId } });
138
+ assert.equal(job!.status, 'RETRY_WAIT');
139
+ assert.equal(job!.failureType, 'TRANSIENT');
140
+ assert.equal(job!.retryCount, 1);
141
+ assert.equal(job!.dlq, false);
142
+ assert.ok(job!.nextRetryAt && job!.nextRetryAt > new Date());
143
+ });
144
+
145
+ test('recordJobFailure: immediately routes FATAL errors to DLQ with 0 retries', async () => {
146
+ const { tenantId, projectId } = await makeFixtureProject();
147
+ const jobId = await insertQueuedJob(tenantId, projectId);
148
+
149
+ await recordJobFailure(tenantId, jobId, new Error('invalid configuration: master_enc_key not set'));
150
+
151
+ const job = await prismaAdmin.platformJob.findUnique({ where: { id: jobId } });
152
+ assert.equal(job!.status, 'FAILED');
153
+ assert.equal(job!.failureType, 'FATAL');
154
+ assert.equal(job!.dlq, true);
155
+ assert.ok(job!.dlqAt);
156
+ assert.ok(job!.dlqReason?.includes('master_enc_key'));
157
+ });
158
+
159
+ test('resumeJob: restores visitedUrls and pendingQueue from PlatformJob.checkpoint column', async () => {
160
+ const { tenantId, projectId } = await makeFixtureProject();
161
+ const jobId = id('job');
162
+ await prismaAdmin.platformJob.create({
163
+ data: {
164
+ id: jobId,
165
+ tenantId,
166
+ projectId,
167
+ type: 'CRAWL',
168
+ status: 'FAILED',
169
+ config: { tenantId, projectId, appUrl: 'https://example.com' },
170
+ checkpoint: {
171
+ visitedUrls: ['https://example.com/page1', 'https://example.com/page2'],
172
+ pendingQueue: ['https://example.com/page3'],
173
+ },
174
+ checkpointAt: new Date(),
175
+ },
176
+ });
177
+
178
+ const resumedJobId = await resumeJob(tenantId, jobId);
179
+ assert.ok(resumedJobId);
180
+
181
+ const resumedJob = await prismaAdmin.platformJob.findUnique({ where: { id: resumedJobId } });
182
+ assert.ok(resumedJob);
183
+ const cfg = resumedJob!.config as any;
184
+ assert.deepEqual(cfg.startUrls, ['https://example.com/page3']);
185
+ assert.ok(cfg.skipUrls.includes('https://example.com/page1'));
186
+ assert.ok(cfg.skipUrls.includes('https://example.com/page2'));
187
+ });