@govtechsg/oobee 0.10.98 → 0.11.1
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/AGENTS.md +40 -1
- package/CRAWL.md +260 -0
- package/README.md +6 -0
- package/dist/combine.js +13 -0
- package/dist/constants/common.js +7 -0
- package/dist/constants/constants.js +17 -0
- package/dist/crawlers/commonCrawlerFunc.js +89 -48
- package/dist/crawlers/crawlDomain.js +103 -32
- package/dist/crawlers/crawlLocalFile.js +2 -0
- package/dist/crawlers/crawlSitemap.js +141 -20
- package/dist/crawlers/custom/utils.js +2 -0
- package/dist/crawlers/pageCapture.js +134 -0
- package/dist/mergeAxeResults.js +2 -1
- package/dist/utils.js +36 -9
- package/oobee-client-scanner.js +2 -2
- package/package.json +1 -1
- package/src/combine.ts +14 -0
- package/src/constants/common.ts +9 -0
- package/src/constants/constants.ts +17 -0
- package/src/crawlers/commonCrawlerFunc.ts +101 -62
- package/src/crawlers/crawlDomain.ts +105 -32
- package/src/crawlers/crawlLocalFile.ts +3 -0
- package/src/crawlers/crawlSitemap.ts +148 -21
- package/src/crawlers/custom/utils.ts +3 -0
- package/src/crawlers/pageCapture.ts +172 -0
- package/src/mergeAxeResults.ts +2 -1
- package/src/utils.ts +31 -8
- /package/{0a93900f-3345-4f25-8be7-c2d3748034be.txt → 45de6711-5a2f-492e-99b1-290059c74264.txt} +0 -0
package/dist/utils.js
CHANGED
|
@@ -346,15 +346,34 @@ export const cleanUp = async (randomToken, isError = false) => {
|
|
|
346
346
|
catch (error) {
|
|
347
347
|
consoleLogger.warn(`Unable to force remove userDataDirectory: ${error.message}`);
|
|
348
348
|
}
|
|
349
|
-
// Also remove _pool* sibling directories created by browser pool re-launches
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
// Also remove _pool* sibling directories created by browser pool re-launches.
|
|
350
|
+
// On Windows, Chrome writes files (optimization_guide_model_store,
|
|
351
|
+
// segmentation_platform, first_party_sets.db, etc.) during its shutdown
|
|
352
|
+
// sequence and may still hold file locks. Retry up to 10 times at 5s intervals.
|
|
353
|
+
const removePoolDirs = () => {
|
|
354
|
+
// On Windows, glob interprets backslashes as escape characters, not path
|
|
355
|
+
// separators. Normalize to forward slashes so the pattern actually matches.
|
|
356
|
+
const globPattern = `${constants.userDataDirectory}_pool*`.replace(/\\/g, '/');
|
|
357
|
+
const poolDirs = globSync(globPattern);
|
|
352
358
|
for (const dir of poolDirs) {
|
|
353
359
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
354
360
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
361
|
+
return poolDirs.length;
|
|
362
|
+
};
|
|
363
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
364
|
+
try {
|
|
365
|
+
if (removePoolDirs() === 0)
|
|
366
|
+
break; // nothing left to clean
|
|
367
|
+
break; // rmSync succeeded for all dirs
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
if (attempt < 9) {
|
|
371
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
consoleLogger.warn('Unable to remove pool directories after 10 attempts');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
358
377
|
}
|
|
359
378
|
}
|
|
360
379
|
if (process.env.TMPDIR && fs.existsSync('/.dockerenv'))
|
|
@@ -374,7 +393,7 @@ export const cleanUp = async (randomToken, isError = false) => {
|
|
|
374
393
|
const crawleeDir = path.join(storagePath, 'crawlee');
|
|
375
394
|
const dataset = await Dataset.open(crawleeDir);
|
|
376
395
|
await dataset.drop();
|
|
377
|
-
const requestQueue = await RequestQueue.open(crawleeDir);
|
|
396
|
+
const requestQueue = await RequestQueue.open(`${crawleeDir}_rq`);
|
|
378
397
|
await requestQueue.drop();
|
|
379
398
|
}
|
|
380
399
|
catch (error) {
|
|
@@ -386,6 +405,12 @@ export const cleanUp = async (randomToken, isError = false) => {
|
|
|
386
405
|
catch (error) {
|
|
387
406
|
consoleLogger.warn(`Unable to force remove crawlee folder: ${error.message}`);
|
|
388
407
|
}
|
|
408
|
+
try {
|
|
409
|
+
fs.rmSync(path.join(storagePath, 'crawlee_rq'), { recursive: true, force: true });
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
consoleLogger.warn(`Unable to force remove crawlee_rq folder: ${error.message}`);
|
|
413
|
+
}
|
|
389
414
|
try {
|
|
390
415
|
fs.rmSync(path.join(storagePath, 'pdfs'), { recursive: true, force: true });
|
|
391
416
|
}
|
|
@@ -808,9 +833,11 @@ export const zipResults = async (zipName, resultsPath) => {
|
|
|
808
833
|
if (!stats.isDirectory()) {
|
|
809
834
|
throw new Error(`resultsPath is not a directory: ${resultsPath}`);
|
|
810
835
|
}
|
|
811
|
-
async function addFolderToZip(folderPath, zipFolder) {
|
|
836
|
+
async function addFolderToZip(folderPath, zipFolder, isRoot = false) {
|
|
812
837
|
const items = await fs.readdir(folderPath);
|
|
813
838
|
for (const item of items) {
|
|
839
|
+
if (isRoot && item.startsWith('crawlee'))
|
|
840
|
+
continue;
|
|
814
841
|
const fullPath = path.join(folderPath, item);
|
|
815
842
|
const stats = await fs.stat(fullPath);
|
|
816
843
|
if (stats.isDirectory()) {
|
|
@@ -824,7 +851,7 @@ export const zipResults = async (zipName, resultsPath) => {
|
|
|
824
851
|
}
|
|
825
852
|
}
|
|
826
853
|
const zip = new JSZip();
|
|
827
|
-
await addFolderToZip(resultsPath, zip);
|
|
854
|
+
await addFolderToZip(resultsPath, zip, true);
|
|
828
855
|
const zipStream = zip.generateNodeStream({
|
|
829
856
|
type: 'nodebuffer',
|
|
830
857
|
streamFiles: true,
|
package/oobee-client-scanner.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* DO NOT EDIT MANUALLY. Re-generate with: node dist/generateOobeeClientScanner.js
|
|
4
4
|
*
|
|
5
5
|
* Embedded at generation time:
|
|
6
|
-
* App version : 0.
|
|
6
|
+
* App version : 0.11.1
|
|
7
7
|
* Sentry DSN : (from OOBEE_SENTRY_DSN env var or constants.ts default)
|
|
8
8
|
* Sentry SDK : @sentry/browser 10.58.0 (loaded from CDN at runtime)
|
|
9
9
|
*
|
|
@@ -34887,7 +34887,7 @@
|
|
|
34887
34887
|
// ── Sentry browser telemetry (Sentry JS SDK, loaded from CDN) ────────────
|
|
34888
34888
|
|
|
34889
34889
|
var _oobeeSentryDsn = "https://3b8c7ee46b06f33815a1301b6713ebc3@o4509047624761344.ingest.us.sentry.io/4509327783559168";
|
|
34890
|
-
var _oobeeAppVersion = "0.
|
|
34890
|
+
var _oobeeAppVersion = "0.11.1";
|
|
34891
34891
|
var _oobeeSentryVersion = "10.58.0";
|
|
34892
34892
|
var _oobeeSentryInitialized = false;
|
|
34893
34893
|
var _oobeeSentryLoadPromise = null;
|
package/package.json
CHANGED
package/src/combine.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getS3UploadPrefix,
|
|
19
19
|
uploadFolderToS3,
|
|
20
20
|
} from './services/s3Uploader.js';
|
|
21
|
+
import { writeManifest, resetCaptureEntries, isPageCaptureEnabled } from './crawlers/pageCapture.js';
|
|
21
22
|
|
|
22
23
|
// Class exports
|
|
23
24
|
export class ViewportSettingsClass {
|
|
@@ -75,6 +76,10 @@ const combineRun = async (details: Data, deviceToScan: string) => {
|
|
|
75
76
|
process.env.CRAWLEE_STORAGE_DIR = randomToken;
|
|
76
77
|
constants.sitemapFetchedLinks = null;
|
|
77
78
|
|
|
79
|
+
if (isPageCaptureEnabled() && !process.env.OOBEE_SCAN_PRODUCT) {
|
|
80
|
+
process.env.OOBEE_SCAN_PRODUCT = 'U&A';
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
if (process.env.CRAWLEE_SYSTEM_INFO_V2 === undefined) {
|
|
79
84
|
// Set the environment variable to enable system info v2
|
|
80
85
|
// Resolves issue with when wmic is not installed on Windows
|
|
@@ -96,6 +101,13 @@ const combineRun = async (details: Data, deviceToScan: string) => {
|
|
|
96
101
|
consoleLogger.info(`Suppressed Playwright post-close connection error: ${err.message}`);
|
|
97
102
|
return;
|
|
98
103
|
}
|
|
104
|
+
// Suppress EPERM errors from Crawlee's async lock-file operations that fire
|
|
105
|
+
// after a crawl phase finishes. On Windows, the sitemap phase's async lock
|
|
106
|
+
// cleanup can race with the domain phase starting in the same directory.
|
|
107
|
+
if (err.message?.includes('EPERM') && err.message?.includes('.json.lock')) {
|
|
108
|
+
consoleLogger.info(`Suppressed Crawlee lock-file EPERM (stale async cleanup): ${err.message}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
99
111
|
throw err;
|
|
100
112
|
};
|
|
101
113
|
process.on('uncaughtException', psTreeHandler);
|
|
@@ -278,6 +290,8 @@ const combineRun = async (details: Data, deviceToScan: string) => {
|
|
|
278
290
|
if (scanDetails.urlsCrawled) {
|
|
279
291
|
if (scanDetails.urlsCrawled.scanned.length > 0) {
|
|
280
292
|
await createAndUpdateResultsFolders(randomToken);
|
|
293
|
+
await writeManifest(randomToken);
|
|
294
|
+
resetCaptureEntries();
|
|
281
295
|
const pagesNotScanned = [
|
|
282
296
|
...urlsCrawledObj.error,
|
|
283
297
|
...urlsCrawledObj.invalid,
|
package/src/constants/common.ts
CHANGED
|
@@ -2157,6 +2157,15 @@ export const getPlaywrightLaunchOptions = (browser?: string): LaunchOptions => {
|
|
|
2157
2157
|
!(browser === BrowserTypes.CHROME && arg === '--edge-skip-compat-layer-relaunch'),
|
|
2158
2158
|
);
|
|
2159
2159
|
|
|
2160
|
+
// Cap browser disk cache to 10MB per instance to prevent storage bloat
|
|
2161
|
+
// during long crawls with multiple pool rotations
|
|
2162
|
+
finalArgs.push('--disk-cache-size=10485760');
|
|
2163
|
+
|
|
2164
|
+
// Prevent Windows from throttling background Chromium processes
|
|
2165
|
+
if (os.platform() === 'win32') {
|
|
2166
|
+
finalArgs.push('--disable-features=UseEcoQoSForBackgroundProcess');
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2160
2169
|
// Headless flags (unchanged)
|
|
2161
2170
|
if (process.env.CRAWLEE_HEADLESS === '1') {
|
|
2162
2171
|
if (!finalArgs.includes('--mute-audio')) finalArgs.push('--mute-audio');
|
|
@@ -30,10 +30,27 @@ export const blackListedFileExtensions = [
|
|
|
30
30
|
'svg',
|
|
31
31
|
'gif',
|
|
32
32
|
'woff',
|
|
33
|
+
'woff2',
|
|
33
34
|
'zip',
|
|
34
35
|
'webp',
|
|
35
36
|
'json',
|
|
36
37
|
'xml',
|
|
38
|
+
'ico',
|
|
39
|
+
'bmp',
|
|
40
|
+
'tiff',
|
|
41
|
+
'tif',
|
|
42
|
+
'avi',
|
|
43
|
+
'mov',
|
|
44
|
+
'wmv',
|
|
45
|
+
'flv',
|
|
46
|
+
'ogg',
|
|
47
|
+
'wav',
|
|
48
|
+
'doc',
|
|
49
|
+
'docx',
|
|
50
|
+
'xls',
|
|
51
|
+
'xlsx',
|
|
52
|
+
'ppt',
|
|
53
|
+
'pptx',
|
|
37
54
|
];
|
|
38
55
|
|
|
39
56
|
export const getIntermediateScreenshotsPath = (datasetsPath: string): string =>
|
|
@@ -1135,10 +1135,9 @@ export const createCrawleeSubFolders = async (
|
|
|
1135
1135
|
randomToken: string,
|
|
1136
1136
|
): Promise<{ dataset: Dataset; requestQueue: RequestQueue }> => {
|
|
1137
1137
|
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
const
|
|
1141
|
-
const requestQueue = await RequestQueue.open(crawleeDir);
|
|
1138
|
+
const baseDir = path.join(getStoragePath(randomToken), "crawlee");
|
|
1139
|
+
const dataset = await Dataset.open(baseDir);
|
|
1140
|
+
const requestQueue = await RequestQueue.open(`${baseDir}_rq`);
|
|
1142
1141
|
return { dataset, requestQueue };
|
|
1143
1142
|
};
|
|
1144
1143
|
|
|
@@ -1215,77 +1214,90 @@ export const postNavigationHooks = [
|
|
|
1215
1214
|
|
|
1216
1215
|
export const getPreLaunchHook = (userDataDirectory: string) => {
|
|
1217
1216
|
let launchCount = 0;
|
|
1217
|
+
let previousPoolDir: string | null = null;
|
|
1218
1218
|
|
|
1219
1219
|
return async (_pageId: string, launchContext: any) => {
|
|
1220
1220
|
const fsp = await import('fs/promises').then(m => m.default);
|
|
1221
1221
|
launchCount += 1;
|
|
1222
1222
|
|
|
1223
|
-
//
|
|
1224
|
-
//
|
|
1225
|
-
//
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1223
|
+
// Clean up the previous pool directory. When preLaunchHooks fires, the
|
|
1224
|
+
// previous browser has been retired and is finishing its last pages.
|
|
1225
|
+
// Schedule cleanup with enough delay for Chrome to fully exit —
|
|
1226
|
+
// closeInactiveBrowserAfterSecs (30s) + Windows file-lock grace.
|
|
1227
|
+
if (previousPoolDir) {
|
|
1228
|
+
const dirToClean = previousPoolDir;
|
|
1229
|
+
const isWin = process.platform === 'win32';
|
|
1230
|
+
const delay = isWin ? 40000 : 35000;
|
|
1231
|
+
setTimeout(async () => {
|
|
1232
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1233
|
+
try {
|
|
1234
|
+
await fsp.rm(dirToClean, { recursive: true, force: true });
|
|
1235
|
+
return;
|
|
1236
|
+
} catch {
|
|
1237
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}, delay);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Every browser gets its own directory. The base userDataDirectory is
|
|
1244
|
+
// treated as a read-only cookie source (the pristine clone of the user's
|
|
1245
|
+
// profile) and is never used directly by a running browser.
|
|
1246
|
+
const effectiveDir = `${userDataDirectory}_pool${launchCount}`;
|
|
1230
1247
|
|
|
1231
1248
|
await fsp.mkdir(effectiveDir, { recursive: true });
|
|
1232
1249
|
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
if (await fsp.stat(localStateSrc).catch(() => null)) {
|
|
1241
|
-
await fsp.copyFile(localStateSrc, path.join(effectiveDir, 'Local State')).catch(() => {});
|
|
1242
|
-
}
|
|
1250
|
+
// Copy auth-relevant files from the pristine base directory so
|
|
1251
|
+
// authenticated sessions are preserved across pool rotations.
|
|
1252
|
+
try {
|
|
1253
|
+
const localStateSrc = path.join(userDataDirectory, 'Local State');
|
|
1254
|
+
if (await fsp.stat(localStateSrc).catch(() => null)) {
|
|
1255
|
+
await fsp.copyFile(localStateSrc, path.join(effectiveDir, 'Local State')).catch(() => {});
|
|
1256
|
+
}
|
|
1243
1257
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
);
|
|
1249
|
-
|
|
1250
|
-
for (const profile of profileDirs) {
|
|
1251
|
-
const srcProfile = path.join(userDataDirectory, profile.name);
|
|
1252
|
-
const destProfile = path.join(effectiveDir, profile.name);
|
|
1253
|
-
await fsp.mkdir(destProfile, { recursive: true }).catch(() => {});
|
|
1254
|
-
|
|
1255
|
-
// Cookies (macOS layout: <Profile>/Cookies)
|
|
1256
|
-
const cookiesSrc = path.join(srcProfile, 'Cookies');
|
|
1257
|
-
if (await fsp.stat(cookiesSrc).catch(() => null)) {
|
|
1258
|
-
await fsp.copyFile(cookiesSrc, path.join(destProfile, 'Cookies')).catch(() => {});
|
|
1259
|
-
}
|
|
1258
|
+
const entries = await fsp.readdir(userDataDirectory, { withFileTypes: true }).catch(() => []);
|
|
1259
|
+
const profileDirs = (entries as any[]).filter(
|
|
1260
|
+
(e: any) => e.isDirectory() && /^(Default|Profile \d+)$/i.test(e.name),
|
|
1261
|
+
);
|
|
1260
1262
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
await fsp.mkdir(destNetwork, { recursive: true }).catch(() => {});
|
|
1266
|
-
await fsp.copyFile(networkCookiesSrc, path.join(destNetwork, 'Cookies')).catch(() => {});
|
|
1267
|
-
}
|
|
1263
|
+
for (const profile of profileDirs) {
|
|
1264
|
+
const srcProfile = path.join(userDataDirectory, profile.name);
|
|
1265
|
+
const destProfile = path.join(effectiveDir, profile.name);
|
|
1266
|
+
await fsp.mkdir(destProfile, { recursive: true }).catch(() => {});
|
|
1268
1267
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1268
|
+
// Cookies (macOS layout: <Profile>/Cookies)
|
|
1269
|
+
const cookiesSrc = path.join(srcProfile, 'Cookies');
|
|
1270
|
+
if (await fsp.stat(cookiesSrc).catch(() => null)) {
|
|
1271
|
+
await fsp.copyFile(cookiesSrc, path.join(destProfile, 'Cookies')).catch(() => {});
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Cookies (Windows layout: <Profile>/Network/Cookies)
|
|
1275
|
+
const networkCookiesSrc = path.join(srcProfile, 'Network', 'Cookies');
|
|
1276
|
+
if (await fsp.stat(networkCookiesSrc).catch(() => null)) {
|
|
1277
|
+
const destNetwork = path.join(destProfile, 'Network');
|
|
1278
|
+
await fsp.mkdir(destNetwork, { recursive: true }).catch(() => {});
|
|
1279
|
+
await fsp.copyFile(networkCookiesSrc, path.join(destNetwork, 'Cookies')).catch(() => {});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Local Storage (auth tokens, session data)
|
|
1283
|
+
const localStorageSrc = path.join(srcProfile, 'Local Storage');
|
|
1284
|
+
const localStorageStat = await fsp.stat(localStorageSrc).catch(() => null);
|
|
1285
|
+
if (localStorageStat && localStorageStat.isDirectory()) {
|
|
1286
|
+
const destLocalStorage = path.join(destProfile, 'Local Storage');
|
|
1287
|
+
await fsp.mkdir(destLocalStorage, { recursive: true }).catch(() => {});
|
|
1288
|
+
const lsFiles = await fsp.readdir(localStorageSrc).catch(() => []);
|
|
1289
|
+
await Promise.all(
|
|
1290
|
+
lsFiles.map(f =>
|
|
1291
|
+
fsp.copyFile(
|
|
1292
|
+
path.join(localStorageSrc, f),
|
|
1293
|
+
path.join(destLocalStorage, f),
|
|
1294
|
+
).catch(() => {}),
|
|
1295
|
+
),
|
|
1296
|
+
);
|
|
1285
1297
|
}
|
|
1286
|
-
} catch {
|
|
1287
|
-
// Silent fallback: use empty profile if clone fails
|
|
1288
1298
|
}
|
|
1299
|
+
} catch {
|
|
1300
|
+
// Silent fallback: use empty profile if clone fails
|
|
1289
1301
|
}
|
|
1290
1302
|
|
|
1291
1303
|
// Clean any stale lock files that may block browser launches on Windows
|
|
@@ -1299,11 +1311,38 @@ export const getPreLaunchHook = (userDataDirectory: string) => {
|
|
|
1299
1311
|
];
|
|
1300
1312
|
await Promise.all(lockFiles.map(f => fsp.rm(f, { force: true }).catch(() => {})));
|
|
1301
1313
|
|
|
1314
|
+
previousPoolDir = effectiveDir;
|
|
1302
1315
|
// eslint-disable-next-line no-param-reassign
|
|
1303
1316
|
launchContext.userDataDir = effectiveDir;
|
|
1304
1317
|
};
|
|
1305
1318
|
};
|
|
1306
1319
|
|
|
1320
|
+
export const getPostPageCloseHook = (userDataDirectory: string) => {
|
|
1321
|
+
return async (_pageId: string, browserController: any) => {
|
|
1322
|
+
if (browserController.activePages === 0) {
|
|
1323
|
+
const dir = browserController.launchContext?.userDataDir;
|
|
1324
|
+
if (dir && dir !== userDataDirectory && dir.includes('_pool')) {
|
|
1325
|
+
const fsp = await import('fs/promises').then(m => m.default);
|
|
1326
|
+
// Chrome on Windows writes optimization/segmentation data during shutdown;
|
|
1327
|
+
// wait longer (5s) for the process to fully release file handles.
|
|
1328
|
+
const isWin = process.platform === 'win32';
|
|
1329
|
+
const initialDelay = isWin ? 5000 : 2000;
|
|
1330
|
+
setTimeout(async () => {
|
|
1331
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1332
|
+
try {
|
|
1333
|
+
await fsp.rm(dir, { recursive: true, force: true });
|
|
1334
|
+
return;
|
|
1335
|
+
} catch {
|
|
1336
|
+
// Retry after a short back-off (Windows file locks)
|
|
1337
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}, initialDelay);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1307
1346
|
export const failedRequestHandler = async ({ request }: { request: Request }) => {
|
|
1308
1347
|
guiInfoLog(guiInfoStatusTypes.ERROR, { numScanned: 0, urlScanned: request.url });
|
|
1309
1348
|
log.error(`Failed Request - ${request.url}: ${request.errorMessages}`);
|
|
@@ -5,6 +5,7 @@ import type { PlaywrightCrawlingContext, RequestOptions } from 'crawlee';
|
|
|
5
5
|
import {
|
|
6
6
|
createCrawleeSubFolders,
|
|
7
7
|
getPreLaunchHook,
|
|
8
|
+
getPostPageCloseHook,
|
|
8
9
|
preNavigationHooks,
|
|
9
10
|
runAxeScript,
|
|
10
11
|
isUrlPdf,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
} from './pdfScanFunc.js';
|
|
41
42
|
import { consoleLogger, guiInfoLog } from '../logs.js';
|
|
42
43
|
import { ViewportSettingsClass } from '../combine.js';
|
|
44
|
+
import { capturePageData } from './pageCapture.js';
|
|
43
45
|
|
|
44
46
|
const isBlacklisted = (url: string, blacklistedPatterns: string[]) => {
|
|
45
47
|
if (!blacklistedPatterns) {
|
|
@@ -398,7 +400,7 @@ const crawlDomain = async ({
|
|
|
398
400
|
launcher: constants.launcher,
|
|
399
401
|
launchOptions: getPlaywrightLaunchOptions(browser),
|
|
400
402
|
},
|
|
401
|
-
retryOnBlocked:
|
|
403
|
+
retryOnBlocked: false,
|
|
402
404
|
browserPoolOptions: {
|
|
403
405
|
useFingerprints: false,
|
|
404
406
|
retireBrowserAfterPageCount: 500,
|
|
@@ -418,12 +420,22 @@ const crawlDomain = async ({
|
|
|
418
420
|
};
|
|
419
421
|
},
|
|
420
422
|
],
|
|
423
|
+
postPageCloseHooks: [getPostPageCloseHook(userDataDirectory)],
|
|
421
424
|
},
|
|
422
425
|
requestQueue,
|
|
423
426
|
maxRequestRetries: 3,
|
|
424
|
-
maxSessionRotations: 1,
|
|
425
427
|
preNavigationHooks: [
|
|
426
428
|
...preNavigationHooks(extraHTTPHeaders),
|
|
429
|
+
async ({ request }) => {
|
|
430
|
+
const url = request.url.toLowerCase();
|
|
431
|
+
try {
|
|
432
|
+
const pathname = new URL(url).pathname;
|
|
433
|
+
const ext = pathname.split('.').pop();
|
|
434
|
+
if (ext && blackListedFileExtensions.includes(ext)) {
|
|
435
|
+
request.skipNavigation = true;
|
|
436
|
+
}
|
|
437
|
+
} catch {}
|
|
438
|
+
},
|
|
427
439
|
],
|
|
428
440
|
postNavigationHooks: [
|
|
429
441
|
async crawlingContext => {
|
|
@@ -555,21 +567,17 @@ const crawlDomain = async ({
|
|
|
555
567
|
(request.skipNavigation && actualUrl === 'about:blank')
|
|
556
568
|
) {
|
|
557
569
|
if (!isScanPdfs) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
httpStatusCode: 0,
|
|
570
|
-
});
|
|
571
|
-
*/
|
|
572
|
-
|
|
570
|
+
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
571
|
+
numScanned: urlsCrawled.scanned.length,
|
|
572
|
+
urlScanned: request.url,
|
|
573
|
+
});
|
|
574
|
+
urlsCrawled.userExcluded.push({
|
|
575
|
+
url: request.url,
|
|
576
|
+
pageTitle: request.url,
|
|
577
|
+
actualUrl: request.url,
|
|
578
|
+
metadata: STATUS_CODE_METADATA[1],
|
|
579
|
+
httpStatusCode: 1,
|
|
580
|
+
});
|
|
573
581
|
return;
|
|
574
582
|
}
|
|
575
583
|
const { pdfFileName, url: downloadedPdfUrl } = handlePdfDownload(
|
|
@@ -585,20 +593,17 @@ const crawlDomain = async ({
|
|
|
585
593
|
}
|
|
586
594
|
|
|
587
595
|
if (isBlacklistedFileExtensions(actualUrl, blackListedFileExtensions)) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
httpStatusCode: 0,
|
|
600
|
-
});
|
|
601
|
-
*/
|
|
596
|
+
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
597
|
+
numScanned: urlsCrawled.scanned.length,
|
|
598
|
+
urlScanned: request.url,
|
|
599
|
+
});
|
|
600
|
+
urlsCrawled.userExcluded.push({
|
|
601
|
+
url: request.url,
|
|
602
|
+
pageTitle: request.url,
|
|
603
|
+
actualUrl,
|
|
604
|
+
metadata: STATUS_CODE_METADATA[1],
|
|
605
|
+
httpStatusCode: 1,
|
|
606
|
+
});
|
|
602
607
|
return;
|
|
603
608
|
}
|
|
604
609
|
|
|
@@ -639,6 +644,21 @@ const crawlDomain = async ({
|
|
|
639
644
|
}
|
|
640
645
|
|
|
641
646
|
const responseStatus = response?.status();
|
|
647
|
+
if (responseStatus === 403) {
|
|
648
|
+
rateController.onFailure(responseStatus, activeCrawler.autoscaledPool);
|
|
649
|
+
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
650
|
+
numScanned: urlsCrawled.scanned.length,
|
|
651
|
+
urlScanned: request.url,
|
|
652
|
+
});
|
|
653
|
+
urlsCrawled.userExcluded.push({
|
|
654
|
+
url: request.url,
|
|
655
|
+
pageTitle: request.url,
|
|
656
|
+
actualUrl,
|
|
657
|
+
metadata: STATUS_CODE_METADATA[403] || STATUS_CODE_METADATA[599],
|
|
658
|
+
httpStatusCode: 403,
|
|
659
|
+
});
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
642
662
|
if (responseStatus && responseStatus >= 300) {
|
|
643
663
|
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
644
664
|
numScanned: urlsCrawled.scanned.length,
|
|
@@ -656,6 +676,8 @@ const crawlDomain = async ({
|
|
|
656
676
|
|
|
657
677
|
const results = await runAxeScript({ includeScreenshots, page, randomToken, ruleset });
|
|
658
678
|
|
|
679
|
+
await capturePageData(page, actualUrl, randomToken);
|
|
680
|
+
|
|
659
681
|
// Detect JS redirects that fire during/after axe scan.
|
|
660
682
|
// Listen for navigation, then give a brief window for pending redirects to complete.
|
|
661
683
|
try {
|
|
@@ -800,7 +822,56 @@ const crawlDomain = async ({
|
|
|
800
822
|
return;
|
|
801
823
|
}
|
|
802
824
|
|
|
825
|
+
// Handle download-triggered navigation errors: Playwright throws
|
|
826
|
+
// "Download is starting" when page.goto() hits a file download URL.
|
|
827
|
+
// Crawlee retries 3 times (all fail) then lands here.
|
|
828
|
+
const isDownloadError = request.errorMessages?.some(
|
|
829
|
+
(msg: string) => msg.includes('Download is starting'),
|
|
830
|
+
);
|
|
831
|
+
if (isDownloadError) {
|
|
832
|
+
if (isScanPdfs) {
|
|
833
|
+
// Re-enqueue with skipNavigation so the requestHandler's PDF download path handles it
|
|
834
|
+
try {
|
|
835
|
+
await requestQueue.addRequest({
|
|
836
|
+
url: request.url,
|
|
837
|
+
skipNavigation: true,
|
|
838
|
+
label: request.url,
|
|
839
|
+
uniqueKey: `download_${request.url}`,
|
|
840
|
+
});
|
|
841
|
+
} catch {}
|
|
842
|
+
} else {
|
|
843
|
+
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
844
|
+
numScanned: urlsCrawled.scanned.length,
|
|
845
|
+
urlScanned: request.url,
|
|
846
|
+
});
|
|
847
|
+
urlsCrawled.userExcluded.push({
|
|
848
|
+
url: request.url,
|
|
849
|
+
pageTitle: request.url,
|
|
850
|
+
actualUrl: request.url,
|
|
851
|
+
metadata: STATUS_CODE_METADATA[1],
|
|
852
|
+
httpStatusCode: 1,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
803
858
|
const status = response?.status();
|
|
859
|
+
|
|
860
|
+
// Re-enqueue rate-limited (403) URLs once for a retry after concurrency recovers.
|
|
861
|
+
// Call onFailure to reduce concurrency immediately on rate-limit detection.
|
|
862
|
+
if (status === 403 && !request.userData?.rateLimitRetried) {
|
|
863
|
+
rateController.onFailure(status, crawler.autoscaledPool);
|
|
864
|
+
try {
|
|
865
|
+
await requestQueue.addRequest({
|
|
866
|
+
url: request.url,
|
|
867
|
+
label: request.url,
|
|
868
|
+
uniqueKey: `ratelimit_${request.url}`,
|
|
869
|
+
userData: { rateLimitRetried: true },
|
|
870
|
+
});
|
|
871
|
+
} catch {}
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
804
875
|
if (rateController.onFailure(status, crawler.autoscaledPool)) {
|
|
805
876
|
consoleLogger.info(
|
|
806
877
|
`Aborting crawl: consecutive HTTP failures threshold reached (site may be rate-limiting). Successfully scanned ${urlsCrawled.scanned.length} pages.`,
|
|
@@ -844,7 +915,9 @@ const crawlDomain = async ({
|
|
|
844
915
|
|
|
845
916
|
// Additional passes: keep re-visiting scanned seed-hostname pages for
|
|
846
917
|
// click-discovery until no new pages are found or limits are reached.
|
|
847
|
-
|
|
918
|
+
// Skip when called from intelligent sitemap — the domain phase is only meant
|
|
919
|
+
// to discover new pages via <a> links, not re-click 3000+ already-scanned pages.
|
|
920
|
+
if (!safeMode && !isAbortingScanNow && !durationExceeded && !fromCrawlIntelligentSitemap) {
|
|
848
921
|
const seedHostname = new URL(url).hostname;
|
|
849
922
|
const clickPassVisited = new Set<string>();
|
|
850
923
|
let prevScannedCount: number;
|
|
@@ -21,6 +21,7 @@ import { runPdfScan, mapPdfScanResults, doPdfScreenshots } from './pdfScanFunc.j
|
|
|
21
21
|
import { guiInfoLog } from '../logs.js';
|
|
22
22
|
import crawlSitemap from './crawlSitemap.js';
|
|
23
23
|
import { getPdfStoragePath, getStoragePath, register } from '../utils.js';
|
|
24
|
+
import { capturePageData } from './pageCapture.js';
|
|
24
25
|
|
|
25
26
|
export const crawlLocalFile = async ({
|
|
26
27
|
url,
|
|
@@ -186,6 +187,8 @@ export const crawlLocalFile = async ({
|
|
|
186
187
|
|
|
187
188
|
const actualUrl = page.url() || request.loadedUrl || url;
|
|
188
189
|
|
|
190
|
+
await capturePageData(page, actualUrl, randomToken);
|
|
191
|
+
|
|
189
192
|
guiInfoLog(guiInfoStatusTypes.SCANNED, {
|
|
190
193
|
numScanned: urlsCrawled.scanned.length,
|
|
191
194
|
urlScanned: url,
|