@d-zero/beholder 4.0.0 → 4.1.0
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/CHANGELOG.md +6 -0
- package/README.md +10 -0
- package/dist/index.d.ts +1 -1
- package/dist/scraper.d.ts +4 -1
- package/dist/scraper.js +216 -167
- package/dist/to-console-log-entry.d.ts +14 -0
- package/dist/to-console-log-entry.js +33 -0
- package/dist/to-page-error-entry.d.ts +10 -0
- package/dist/to-page-error-entry.js +19 -0
- package/dist/types.d.ts +35 -0
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/scraper.ts +242 -184
- package/src/to-console-log-entry.spec.ts +108 -0
- package/src/to-console-log-entry.ts +40 -0
- package/src/to-page-error-entry.spec.ts +23 -0
- package/src/to-page-error-entry.ts +22 -0
- package/src/types.ts +32 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/scraper.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ChangePhaseEvent,
|
|
3
|
+
ConsoleLogEntry,
|
|
3
4
|
ResourceEntry,
|
|
4
5
|
ScraperEventTypes,
|
|
5
6
|
ScraperOptions,
|
|
@@ -14,7 +15,7 @@ import type {
|
|
|
14
15
|
SkippedPageData,
|
|
15
16
|
} from './types.js';
|
|
16
17
|
import type { PageScanPhase } from '@d-zero/puppeteer-page-scan';
|
|
17
|
-
import type { Dialog, HTTPRequest, HTTPResponse, Page } from 'puppeteer';
|
|
18
|
+
import type { ConsoleMessage, Dialog, HTTPRequest, HTTPResponse, Page } from 'puppeteer';
|
|
18
19
|
|
|
19
20
|
import { beforePageScan, devicePresets } from '@d-zero/puppeteer-page-scan';
|
|
20
21
|
import { detectCDN } from '@d-zero/shared/detect-cdn';
|
|
@@ -37,6 +38,8 @@ import { measureScrollHeight } from './measure-scroll-height.js';
|
|
|
37
38
|
import { emptyMeta } from './meta/classify.js';
|
|
38
39
|
import { findDisconnectionFailures } from './network-disconnection.js';
|
|
39
40
|
import { parseUrl } from './parse-url.js';
|
|
41
|
+
import { toConsoleLogEntry } from './to-console-log-entry.js';
|
|
42
|
+
import { toPageErrorEntry } from './to-page-error-entry.js';
|
|
40
43
|
|
|
41
44
|
const pid = `${process.pid}`;
|
|
42
45
|
const log = scraperLog.extend(pid);
|
|
@@ -86,7 +89,10 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
86
89
|
* - `type: "error"` with `error` details when scraping fails
|
|
87
90
|
*
|
|
88
91
|
* Sub-resources are collected via the `resourceResponse` event and
|
|
89
|
-
* included in the returned `ScrapeResult.resources`.
|
|
92
|
+
* included in the returned `ScrapeResult.resources`. Console messages and
|
|
93
|
+
* uncaught page errors (internal pages only) are collected via
|
|
94
|
+
* Puppeteer's `console`/`pageerror` page events and included in
|
|
95
|
+
* `ScrapeResult.consoleLogs`.
|
|
90
96
|
* @param page - The Puppeteer page instance to use for navigation and DOM evaluation.
|
|
91
97
|
* @param url - The extended URL to scrape.
|
|
92
98
|
* @param options - Optional scraper configuration overriding defaults.
|
|
@@ -106,6 +112,7 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
106
112
|
const metadataOnly = options?.metadataOnly ?? false;
|
|
107
113
|
const imageLoadTimeout = options?.imageLoadTimeout ?? 5000;
|
|
108
114
|
const resources: ResourceEntry[] = [];
|
|
115
|
+
const consoleLogs: ConsoleLogEntry[] = [];
|
|
109
116
|
const failedRequests: Array<{ url: string; errorText: string }> = [];
|
|
110
117
|
|
|
111
118
|
void this.emit('changePhase', {
|
|
@@ -128,6 +135,7 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
128
135
|
return {
|
|
129
136
|
type: 'skipped',
|
|
130
137
|
resources,
|
|
138
|
+
consoleLogs,
|
|
131
139
|
ignored: {
|
|
132
140
|
url,
|
|
133
141
|
matchedText: url.pathname || '',
|
|
@@ -164,7 +172,7 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
164
172
|
isExternal,
|
|
165
173
|
message: '',
|
|
166
174
|
});
|
|
167
|
-
return { type: 'success', pageData: result, resources };
|
|
175
|
+
return { type: 'success', pageData: result, resources, consoleLogs };
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
let headResult: PageData | SkippedPageData | null = options?.headCheckResult ?? null;
|
|
@@ -186,6 +194,7 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
186
194
|
scrollHeight: headResult.scrollHeight ?? null,
|
|
187
195
|
},
|
|
188
196
|
resources,
|
|
197
|
+
consoleLogs,
|
|
189
198
|
};
|
|
190
199
|
}
|
|
191
200
|
|
|
@@ -197,6 +206,7 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
197
206
|
captureImages,
|
|
198
207
|
imageLoadTimeout,
|
|
199
208
|
resources,
|
|
209
|
+
consoleLogs,
|
|
200
210
|
failedRequests,
|
|
201
211
|
options,
|
|
202
212
|
).catch((error) => {
|
|
@@ -212,6 +222,7 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
212
222
|
return {
|
|
213
223
|
type: 'error',
|
|
214
224
|
resources,
|
|
225
|
+
consoleLogs,
|
|
215
226
|
failedRequests: failedRequests.length > 0 ? failedRequests : undefined,
|
|
216
227
|
error: {
|
|
217
228
|
name: fetchResult.name,
|
|
@@ -246,6 +257,7 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
246
257
|
return {
|
|
247
258
|
type: 'skipped',
|
|
248
259
|
resources,
|
|
260
|
+
consoleLogs,
|
|
249
261
|
ignored: {
|
|
250
262
|
url,
|
|
251
263
|
matchedText: url.pathname || '',
|
|
@@ -263,6 +275,7 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
263
275
|
return {
|
|
264
276
|
type: 'skipped',
|
|
265
277
|
resources,
|
|
278
|
+
consoleLogs,
|
|
266
279
|
ignored: {
|
|
267
280
|
url,
|
|
268
281
|
matchedText: headResult.matched.text,
|
|
@@ -284,6 +297,7 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
284
297
|
type: 'success',
|
|
285
298
|
pageData: headResult,
|
|
286
299
|
resources,
|
|
300
|
+
consoleLogs,
|
|
287
301
|
failedRequests: failedRequests.length > 0 ? failedRequests : undefined,
|
|
288
302
|
};
|
|
289
303
|
}
|
|
@@ -342,7 +356,8 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
342
356
|
* Frame" or "Session closed".
|
|
343
357
|
*
|
|
344
358
|
* Flow:
|
|
345
|
-
* 1. Register request/response/requestfailed listeners to
|
|
359
|
+
* 1. Register request/response/requestfailed/console/pageerror listeners to
|
|
360
|
+
* capture sub-resources and console output (internal pages only)
|
|
346
361
|
* 2. Navigate to URL via `page.goto()` and track redirect chain
|
|
347
362
|
* 3. Wait for DOM content and network idle
|
|
348
363
|
* 4. Check for network disconnection errors and throw to trigger retry
|
|
@@ -354,6 +369,7 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
354
369
|
* @param captureImages - Whether to run the image extraction pipeline
|
|
355
370
|
* @param imageLoadTimeout - Timeout (ms) for waiting lazy-loaded images to complete
|
|
356
371
|
* @param resources - Mutable array to collect captured sub-resources into
|
|
372
|
+
* @param consoleLogs - Mutable array to collect captured console messages / page errors into
|
|
357
373
|
* @param failedRequests - Mutable array to collect failed sub-resource requests into
|
|
358
374
|
* @param options - Additional scraper options (e.g. `disableQueries`, `navigationTimeout`)
|
|
359
375
|
* @returns Full page data or skipped page data if an exclusion rule matched
|
|
@@ -386,6 +402,7 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
386
402
|
captureImages: boolean,
|
|
387
403
|
imageLoadTimeout: number,
|
|
388
404
|
resources: ResourceEntry[],
|
|
405
|
+
consoleLogs: ConsoleLogEntry[],
|
|
389
406
|
failedRequests: Array<{ url: string; errorText: string }>,
|
|
390
407
|
options?: Partial<ScraperOptions>,
|
|
391
408
|
): Promise<PageData | SkippedPageData> {
|
|
@@ -396,12 +413,16 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
396
413
|
const domEvaluationTimeout =
|
|
397
414
|
options?.domEvaluationTimeout ?? DEFAULT_DOM_EVALUATION_TIMEOUT;
|
|
398
415
|
const networkLogs: Record<string, NetworkLog> = {};
|
|
416
|
+
// Tracks in-flight `toConsoleLogEntry()` resolutions (async `jsonValue()`
|
|
417
|
+
// extraction) so callers can await them before reading `consoleLogs`.
|
|
418
|
+
const pendingConsoleWork: Promise<void>[] = [];
|
|
399
419
|
|
|
400
420
|
// Clear stale state from previous retries (@retryable may re-invoke this method
|
|
401
421
|
// with the same page and mutable arrays, so we must reset to avoid accumulation)
|
|
402
422
|
this.#cleanupPageListeners();
|
|
403
423
|
failedRequests.length = 0;
|
|
404
424
|
resources.length = 0;
|
|
425
|
+
consoleLogs.length = 0;
|
|
405
426
|
|
|
406
427
|
// Define named listeners so they can be individually removed on retry/cleanup
|
|
407
428
|
const onDialog = async (dialog: Dialog) => {
|
|
@@ -418,6 +439,8 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
418
439
|
let onRequest: ((req: HTTPRequest) => void) | null = null;
|
|
419
440
|
let onResponse: ((res: HTTPResponse) => void) | null = null;
|
|
420
441
|
let onRequestFailed: ((req: HTTPRequest) => void) | null = null;
|
|
442
|
+
let onConsole: ((msg: ConsoleMessage) => void) | null = null;
|
|
443
|
+
let onPageError: ((error: unknown) => void) | null = null;
|
|
421
444
|
|
|
422
445
|
if (!isExternal) {
|
|
423
446
|
onRequest = (request: HTTPRequest) => {
|
|
@@ -503,9 +526,30 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
503
526
|
failedRequests.push({ url: request.url(), errorText });
|
|
504
527
|
};
|
|
505
528
|
|
|
529
|
+
onConsole = (msg: ConsoleMessage) => {
|
|
530
|
+
pendingConsoleWork.push(
|
|
531
|
+
toConsoleLogEntry(msg, url.withoutHash).then(
|
|
532
|
+
(entry) => {
|
|
533
|
+
consoleLogs.push(entry);
|
|
534
|
+
},
|
|
535
|
+
(error) => {
|
|
536
|
+
// A single malformed console message must not fail the whole
|
|
537
|
+
// scrape (e.g. mid-navigation execution-context teardown).
|
|
538
|
+
log('Error(CONSOLE_LOG): %s', error);
|
|
539
|
+
},
|
|
540
|
+
),
|
|
541
|
+
);
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
onPageError = (error: unknown) => {
|
|
545
|
+
consoleLogs.push(toPageErrorEntry(error, url.withoutHash));
|
|
546
|
+
};
|
|
547
|
+
|
|
506
548
|
page.on('request', onRequest);
|
|
507
549
|
page.on('response', onResponse);
|
|
508
550
|
page.on('requestfailed', onRequestFailed);
|
|
551
|
+
page.on('console', onConsole);
|
|
552
|
+
page.on('pageerror', onPageError);
|
|
509
553
|
}
|
|
510
554
|
|
|
511
555
|
// Store cleanup function for retry/post-fetch removal
|
|
@@ -514,221 +558,235 @@ export default class Scraper extends EventEmitter<ScraperEventTypes> {
|
|
|
514
558
|
if (onRequest) page.off('request', onRequest);
|
|
515
559
|
if (onResponse) page.off('response', onResponse);
|
|
516
560
|
if (onRequestFailed) page.off('requestfailed', onRequestFailed);
|
|
561
|
+
if (onConsole) page.off('console', onConsole);
|
|
562
|
+
if (onPageError) page.off('pageerror', onPageError);
|
|
517
563
|
};
|
|
518
564
|
|
|
519
565
|
const navigationTimeout = options?.navigationTimeout ?? 60_000;
|
|
520
566
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
567
|
+
// The whole navigation/extraction sequence below is wrapped in `finally`
|
|
568
|
+
// so that `pendingConsoleWork` is always awaited exactly once — on every
|
|
569
|
+
// return AND on every throw (e.g. `Page.goto returned null`, network
|
|
570
|
+
// disconnection). A per-return-statement `await` would miss thrown exits
|
|
571
|
+
// and let a still-resolving `toConsoleLogEntry()` push into `consoleLogs`
|
|
572
|
+
// after a `@retryable` retry has already cleared it for a new attempt.
|
|
573
|
+
try {
|
|
574
|
+
void this.emit('changePhase', {
|
|
575
|
+
pid: process.pid,
|
|
576
|
+
name: 'openPage',
|
|
577
|
+
url,
|
|
578
|
+
isExternal,
|
|
579
|
+
message: `%countdown(${navigationTimeout},openPage_${url.withoutHash},s)%s`,
|
|
532
580
|
});
|
|
533
|
-
}
|
|
534
581
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
582
|
+
if (url.username && url.password) {
|
|
583
|
+
await page.setExtraHTTPHeaders({
|
|
584
|
+
Authorization: `Basic ${Buffer.from(`${url.username}:${url.password}`).toString('base64')}`,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
540
587
|
|
|
541
|
-
|
|
542
|
-
const redirectPaths = new Set<string>();
|
|
588
|
+
const res = await page.goto(url.withoutHashAndAuth, { timeout: navigationTimeout });
|
|
543
589
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
.request()
|
|
547
|
-
.redirectChain()
|
|
548
|
-
.map((req) => req.url());
|
|
549
|
-
for (const redirectPath of redirectChain) {
|
|
550
|
-
redirectPaths.add(redirectPath);
|
|
590
|
+
if (!res) {
|
|
591
|
+
throw new Error('The method Page.goto returned null');
|
|
551
592
|
}
|
|
552
|
-
redirectPaths.add(destUrl.withoutHash);
|
|
553
|
-
}
|
|
554
593
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
594
|
+
const destUrl = parseUrl(page.url(), parseOpts)!;
|
|
595
|
+
const redirectPaths = new Set<string>();
|
|
558
596
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
597
|
+
if (url.withoutHash !== destUrl.withoutHash) {
|
|
598
|
+
const redirectChain = res
|
|
599
|
+
.request()
|
|
600
|
+
.redirectChain()
|
|
601
|
+
.map((req) => req.url());
|
|
602
|
+
for (const redirectPath of redirectChain) {
|
|
603
|
+
redirectPaths.add(redirectPath);
|
|
604
|
+
}
|
|
605
|
+
redirectPaths.add(destUrl.withoutHash);
|
|
606
|
+
}
|
|
565
607
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
isTarget: false,
|
|
570
|
-
isExternal,
|
|
571
|
-
redirectPaths: [...redirectPaths],
|
|
572
|
-
status,
|
|
573
|
-
statusText,
|
|
574
|
-
contentType,
|
|
575
|
-
contentLength,
|
|
576
|
-
responseHeaders,
|
|
577
|
-
meta: emptyMeta(),
|
|
578
|
-
imageList: [],
|
|
579
|
-
anchorList: [],
|
|
580
|
-
html: '',
|
|
581
|
-
mainContents: null,
|
|
582
|
-
scrollHeight: null,
|
|
583
|
-
isSkipped: false,
|
|
584
|
-
};
|
|
585
|
-
}
|
|
608
|
+
if (destUrl.hostname !== url.hostname) {
|
|
609
|
+
isExternal = true;
|
|
610
|
+
}
|
|
586
611
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
});
|
|
612
|
+
const status = res.status();
|
|
613
|
+
const statusText = res.statusText();
|
|
614
|
+
const responseHeaders = res.headers();
|
|
615
|
+
const contentType = responseHeaders['content-type']?.split(';')[0] || null;
|
|
616
|
+
const _contentLength = Number.parseInt(responseHeaders['content-length'] ?? '');
|
|
617
|
+
const contentLength = Number.isFinite(_contentLength) ? _contentLength : null;
|
|
594
618
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
619
|
+
if (!isHtmlContentType(contentType)) {
|
|
620
|
+
return {
|
|
621
|
+
url,
|
|
622
|
+
isTarget: false,
|
|
623
|
+
isExternal,
|
|
624
|
+
redirectPaths: [...redirectPaths],
|
|
625
|
+
status,
|
|
626
|
+
statusText,
|
|
627
|
+
contentType,
|
|
628
|
+
contentLength,
|
|
629
|
+
responseHeaders,
|
|
630
|
+
meta: emptyMeta(),
|
|
631
|
+
imageList: [],
|
|
632
|
+
anchorList: [],
|
|
633
|
+
html: '',
|
|
634
|
+
mainContents: null,
|
|
635
|
+
scrollHeight: null,
|
|
636
|
+
isSkipped: false,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
598
639
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
640
|
+
void this.emit('changePhase', {
|
|
641
|
+
pid: process.pid,
|
|
642
|
+
name: 'loadDOMContent',
|
|
643
|
+
url,
|
|
644
|
+
isExternal,
|
|
645
|
+
message: '',
|
|
646
|
+
});
|
|
606
647
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
title: document.title,
|
|
611
|
-
html: document.documentElement.outerHTML,
|
|
612
|
-
};
|
|
613
|
-
});
|
|
648
|
+
await page
|
|
649
|
+
.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 5000 })
|
|
650
|
+
.catch(() => {});
|
|
614
651
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
return {
|
|
652
|
+
void this.emit('changePhase', {
|
|
653
|
+
pid: process.pid,
|
|
654
|
+
name: 'getHTML',
|
|
619
655
|
url,
|
|
620
|
-
isTarget: false,
|
|
621
656
|
isExternal,
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
statusText,
|
|
625
|
-
contentType,
|
|
626
|
-
contentLength,
|
|
627
|
-
responseHeaders,
|
|
628
|
-
meta: externalMeta,
|
|
629
|
-
imageList: [],
|
|
630
|
-
anchorList: [],
|
|
631
|
-
html,
|
|
632
|
-
mainContents: null,
|
|
633
|
-
scrollHeight: null,
|
|
634
|
-
isSkipped: false,
|
|
635
|
-
};
|
|
636
|
-
}
|
|
657
|
+
message: '',
|
|
658
|
+
});
|
|
637
659
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
660
|
+
const { title, html } = await page.evaluate(() => {
|
|
661
|
+
/* global document */
|
|
662
|
+
return {
|
|
663
|
+
title: document.title,
|
|
664
|
+
html: document.documentElement.outerHTML,
|
|
665
|
+
};
|
|
666
|
+
});
|
|
645
667
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
668
|
+
if (isExternal) {
|
|
669
|
+
const externalMeta = emptyMeta();
|
|
670
|
+
externalMeta.title = title;
|
|
671
|
+
return {
|
|
672
|
+
url,
|
|
673
|
+
isTarget: false,
|
|
674
|
+
isExternal,
|
|
675
|
+
redirectPaths: [...redirectPaths],
|
|
676
|
+
status,
|
|
677
|
+
statusText,
|
|
678
|
+
contentType,
|
|
679
|
+
contentLength,
|
|
680
|
+
responseHeaders,
|
|
681
|
+
meta: externalMeta,
|
|
682
|
+
imageList: [],
|
|
683
|
+
anchorList: [],
|
|
684
|
+
html,
|
|
685
|
+
mainContents: null,
|
|
686
|
+
scrollHeight: null,
|
|
687
|
+
isSkipped: false,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
658
690
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
691
|
+
void this.emit('changePhase', {
|
|
692
|
+
pid: process.pid,
|
|
693
|
+
name: 'waitNetworkIdle',
|
|
694
|
+
url,
|
|
695
|
+
isExternal,
|
|
696
|
+
message: '',
|
|
697
|
+
});
|
|
662
698
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
699
|
+
await page
|
|
700
|
+
.waitForNavigation({ waitUntil: 'networkidle0', timeout: 5000 })
|
|
701
|
+
.catch(() => {});
|
|
702
|
+
|
|
703
|
+
// Check for network disconnection errors in failed requests
|
|
704
|
+
const disconnectionFailures = findDisconnectionFailures(failedRequests);
|
|
705
|
+
if (disconnectionFailures.length > 0) {
|
|
706
|
+
const errorSummary = disconnectionFailures
|
|
707
|
+
.map((r) => `${r.url} (${r.errorText})`)
|
|
708
|
+
.join(', ');
|
|
709
|
+
throw new Error(
|
|
710
|
+
`Network disconnection detected during page load: ${errorSummary}`,
|
|
711
|
+
);
|
|
712
|
+
}
|
|
671
713
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
url,
|
|
676
|
-
isExternal,
|
|
677
|
-
message: `%countdown(${domEvaluationTimeout},getMeta_${url.withoutHash},s)%s`,
|
|
678
|
-
});
|
|
679
|
-
const meta = await getMeta(
|
|
680
|
-
page,
|
|
681
|
-
{
|
|
682
|
-
url: url.withoutHashAndAuth,
|
|
683
|
-
html,
|
|
684
|
-
statusCode: status,
|
|
685
|
-
headers: responseHeaders ?? undefined,
|
|
686
|
-
},
|
|
687
|
-
domEvaluationTimeout,
|
|
688
|
-
);
|
|
714
|
+
const mainContents = await getMainContents(page, {
|
|
715
|
+
mainContentSelector: options?.mainContentSelector,
|
|
716
|
+
});
|
|
689
717
|
|
|
690
|
-
|
|
691
|
-
|
|
718
|
+
void this.emit('changePhase', {
|
|
719
|
+
pid: process.pid,
|
|
720
|
+
name: 'getAnchors',
|
|
721
|
+
url,
|
|
722
|
+
isExternal,
|
|
723
|
+
message: `%countdown(${domEvaluationTimeout},getAnchors_${url.withoutHash},s)%s`,
|
|
724
|
+
});
|
|
725
|
+
const anchorList = await getAnchorList(page, parseOpts, domEvaluationTimeout);
|
|
692
726
|
|
|
693
|
-
if (captureImages) {
|
|
694
727
|
void this.emit('changePhase', {
|
|
695
728
|
pid: process.pid,
|
|
696
|
-
name: '
|
|
729
|
+
name: 'getMeta',
|
|
697
730
|
url,
|
|
698
731
|
isExternal,
|
|
699
|
-
message: `%countdown(${domEvaluationTimeout},
|
|
732
|
+
message: `%countdown(${domEvaluationTimeout},getMeta_${url.withoutHash},s)%s`,
|
|
700
733
|
});
|
|
701
|
-
const
|
|
734
|
+
const meta = await getMeta(
|
|
702
735
|
page,
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
736
|
+
{
|
|
737
|
+
url: url.withoutHashAndAuth,
|
|
738
|
+
html,
|
|
739
|
+
statusCode: status,
|
|
740
|
+
headers: responseHeaders ?? undefined,
|
|
741
|
+
},
|
|
706
742
|
domEvaluationTimeout,
|
|
707
743
|
);
|
|
708
|
-
imageList = fetched.imageList;
|
|
709
|
-
scrollHeight = fetched.scrollHeight;
|
|
710
|
-
} else {
|
|
711
|
-
scrollHeight = await measureScrollHeight(page);
|
|
712
|
-
}
|
|
713
744
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
745
|
+
let imageList: ImageElement[] = [];
|
|
746
|
+
let scrollHeight: ScrollHeightData | null = null;
|
|
747
|
+
|
|
748
|
+
if (captureImages) {
|
|
749
|
+
void this.emit('changePhase', {
|
|
750
|
+
pid: process.pid,
|
|
751
|
+
name: 'extractImages',
|
|
752
|
+
url,
|
|
753
|
+
isExternal,
|
|
754
|
+
message: `%countdown(${domEvaluationTimeout},extractImages_${url.withoutHash},s)%s`,
|
|
755
|
+
});
|
|
756
|
+
const fetched = await this.#fetchImages(
|
|
757
|
+
page,
|
|
758
|
+
url.withoutHashAndAuth,
|
|
759
|
+
isExternal,
|
|
760
|
+
imageLoadTimeout,
|
|
761
|
+
domEvaluationTimeout,
|
|
762
|
+
);
|
|
763
|
+
imageList = fetched.imageList;
|
|
764
|
+
scrollHeight = fetched.scrollHeight;
|
|
765
|
+
} else {
|
|
766
|
+
scrollHeight = await measureScrollHeight(page);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
url,
|
|
771
|
+
isTarget: true,
|
|
772
|
+
isExternal,
|
|
773
|
+
redirectPaths: [...redirectPaths],
|
|
774
|
+
status,
|
|
775
|
+
statusText,
|
|
776
|
+
contentType,
|
|
777
|
+
contentLength,
|
|
778
|
+
responseHeaders,
|
|
779
|
+
meta,
|
|
780
|
+
anchorList,
|
|
781
|
+
imageList,
|
|
782
|
+
html,
|
|
783
|
+
mainContents,
|
|
784
|
+
scrollHeight,
|
|
785
|
+
isSkipped: false,
|
|
786
|
+
};
|
|
787
|
+
} finally {
|
|
788
|
+
await Promise.all(pendingConsoleWork);
|
|
789
|
+
}
|
|
732
790
|
}
|
|
733
791
|
/**
|
|
734
792
|
* Extracts image data from the page across multiple device presets.
|