@crawlee/basic 4.0.0-beta.126 → 4.0.0-beta.128
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.
|
@@ -282,7 +282,7 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
|
|
|
282
282
|
* implementing {@link IStatistics}.
|
|
283
283
|
*
|
|
284
284
|
* Custom fields declared via {@link StatisticsOptions.stateExtension|`stateExtension`} are carried over to
|
|
285
|
-
* {@link BasicCrawler.
|
|
285
|
+
* {@link BasicCrawler.statistics|`crawler.statistics.state`}:
|
|
286
286
|
*
|
|
287
287
|
* ```ts
|
|
288
288
|
* const statistics = new Statistics({ stateExtension: { defaultState: { productsFound: 0 } } });
|
|
@@ -295,8 +295,8 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
|
|
|
295
295
|
* });
|
|
296
296
|
*
|
|
297
297
|
* await crawler.run();
|
|
298
|
-
* // the custom fields are typed on `crawler.
|
|
299
|
-
* console.log(crawler.
|
|
298
|
+
* // the custom fields are typed on `crawler.statistics` too
|
|
299
|
+
* console.log(crawler.statistics.state.productsFound);
|
|
300
300
|
* ```
|
|
301
301
|
*/
|
|
302
302
|
statistics?: IStatistics<StatisticStateExtension>;
|
|
@@ -464,7 +464,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
464
464
|
* The statistics instance collecting the crawler's run statistics - either the injected `statistics` option or a
|
|
465
465
|
* crawler-built default. Typed as {@link IStatistics} so custom implementations can be plugged in.
|
|
466
466
|
*/
|
|
467
|
-
get
|
|
467
|
+
get statistics(): IStatistics<StatisticStateExtension>;
|
|
468
468
|
/**
|
|
469
469
|
* The main request-handling component of the crawler. It manages the requests that the crawler processes,
|
|
470
470
|
* combining any provided request loader and/or queue. It's initialized during the crawler startup or lazily
|
|
@@ -84,14 +84,14 @@ export class BasicCrawler {
|
|
|
84
84
|
* Used to detect and warn about multiple crawlers sharing the same state.
|
|
85
85
|
*/
|
|
86
86
|
static #useStateAnonymousIndices = new Set();
|
|
87
|
-
/** Backs the {@link BasicCrawler.
|
|
88
|
-
#
|
|
87
|
+
/** Backs the {@link BasicCrawler.statistics|`statistics`} getter. */
|
|
88
|
+
#statisticsDep;
|
|
89
89
|
/**
|
|
90
90
|
* The statistics instance collecting the crawler's run statistics - either the injected `statistics` option or a
|
|
91
91
|
* crawler-built default. Typed as {@link IStatistics} so custom implementations can be plugged in.
|
|
92
92
|
*/
|
|
93
|
-
get
|
|
94
|
-
return this.#
|
|
93
|
+
get statistics() {
|
|
94
|
+
return this.#statisticsDep.value;
|
|
95
95
|
}
|
|
96
96
|
/**
|
|
97
97
|
* The main request-handling component of the crawler. It manages the requests that the crawler processes,
|
|
@@ -201,7 +201,7 @@ export class BasicCrawler {
|
|
|
201
201
|
#sameDomainDelaySecs;
|
|
202
202
|
maxRequestsPerCrawl;
|
|
203
203
|
get handledRequestsCount() {
|
|
204
|
-
return this.
|
|
204
|
+
return this.statistics.state.requestsFinished + this.statistics.state.requestsFailed;
|
|
205
205
|
}
|
|
206
206
|
#statusMessageLoggingInterval;
|
|
207
207
|
#statusMessageCallback;
|
|
@@ -387,7 +387,7 @@ export class BasicCrawler {
|
|
|
387
387
|
this.maxRequestRetries = maxRequestRetries;
|
|
388
388
|
this.maxCrawlDepth = maxCrawlDepth;
|
|
389
389
|
this.#sameDomainDelaySecs = sameDomainDelaySecs;
|
|
390
|
-
this.#
|
|
390
|
+
this.#statisticsDep = OwnedOrInjected.resolve(statistics,
|
|
391
391
|
// A crawler-built default tracks the built-in fields only. A non-empty `StatisticStateExtension` can
|
|
392
392
|
// only be satisfied by an injected instance carrying the matching `state`, so this branch does
|
|
393
393
|
// not run in that case - hence the cast.
|
|
@@ -436,7 +436,7 @@ export class BasicCrawler {
|
|
|
436
436
|
// Started here, rather than in `handleRequest`, so that a failure during context pipeline
|
|
437
437
|
// initialization (e.g. a browser page timing out before the request handler ever runs) is
|
|
438
438
|
// still accounted for by `failJob` below - which is a no-op without a matching `startJob`.
|
|
439
|
-
this.
|
|
439
|
+
this.statistics.startJob(request.id || request.uniqueKey);
|
|
440
440
|
const crawlingContext = { request };
|
|
441
441
|
try {
|
|
442
442
|
// The transaction spans the whole pipeline call, covering the navigation hooks
|
|
@@ -453,7 +453,7 @@ export class BasicCrawler {
|
|
|
453
453
|
// ContextPipelineInterruptedError means the request was intentionally skipped
|
|
454
454
|
// (e.g., doesn't match enqueue strategy after redirect). Just return gracefully.
|
|
455
455
|
if (error instanceof ContextPipelineInterruptedError) {
|
|
456
|
-
this.
|
|
456
|
+
this.statistics.discardJob(request.id || request.uniqueKey);
|
|
457
457
|
await this.timeoutAndRetry(async () => this.requestManager?.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${crawlingContext.request.url} (${crawlingContext.request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
458
458
|
return;
|
|
459
459
|
}
|
|
@@ -714,11 +714,11 @@ export class BasicCrawler {
|
|
|
714
714
|
});
|
|
715
715
|
}
|
|
716
716
|
getPeriodicLogger() {
|
|
717
|
-
let previousState = { ...this.
|
|
717
|
+
let previousState = { ...this.statistics.state };
|
|
718
718
|
const getOperationMode = () => {
|
|
719
|
-
const { requestsFailed } = this.
|
|
719
|
+
const { requestsFailed } = this.statistics.state;
|
|
720
720
|
const { requestsFailed: previousRequestsFailed } = previousState;
|
|
721
|
-
previousState = { ...this.
|
|
721
|
+
previousState = { ...this.statistics.state };
|
|
722
722
|
const failedDelta = requestsFailed - previousRequestsFailed;
|
|
723
723
|
if (failedDelta > 0) {
|
|
724
724
|
return { mode: 'ERROR', failedDelta };
|
|
@@ -733,12 +733,12 @@ export class BasicCrawler {
|
|
|
733
733
|
}
|
|
734
734
|
else {
|
|
735
735
|
const total = await this.requestManager?.getTotalCount();
|
|
736
|
-
message = `Crawled ${this.
|
|
736
|
+
message = `Crawled ${this.statistics.state.requestsFinished}${total ? `/${total}` : ''} pages, ${this.statistics.state.requestsFailed} failed requests, desired concurrency ${this.concurrencySystem?.desiredConcurrency ?? 0}.`;
|
|
737
737
|
}
|
|
738
738
|
if (this.#statusMessageCallback) {
|
|
739
739
|
await this.#statusMessageCallback({
|
|
740
740
|
crawler: this,
|
|
741
|
-
state: this.
|
|
741
|
+
state: this.statistics.state,
|
|
742
742
|
previousState,
|
|
743
743
|
message,
|
|
744
744
|
});
|
|
@@ -785,7 +785,7 @@ export class BasicCrawler {
|
|
|
785
785
|
}
|
|
786
786
|
}
|
|
787
787
|
// A supplied statistics instance keeps whatever state it was handed - only wipe a default we built.
|
|
788
|
-
await this.#
|
|
788
|
+
await this.#statisticsDep.ifOwned(async (stats) => {
|
|
789
789
|
stats.reset();
|
|
790
790
|
await stats.resetStore();
|
|
791
791
|
});
|
|
@@ -804,7 +804,7 @@ export class BasicCrawler {
|
|
|
804
804
|
}
|
|
805
805
|
try {
|
|
806
806
|
await this.init();
|
|
807
|
-
await this.
|
|
807
|
+
await this.statistics.startCapturing();
|
|
808
808
|
}
|
|
809
809
|
catch (error) {
|
|
810
810
|
// Clean up here before propagating, otherwise a failed startup would leave the process hanging.
|
|
@@ -833,25 +833,25 @@ export class BasicCrawler {
|
|
|
833
833
|
await this.#autoscaledPool.run();
|
|
834
834
|
}
|
|
835
835
|
finally {
|
|
836
|
+
await this.statistics.stopCapturing();
|
|
836
837
|
await this.teardown();
|
|
837
|
-
await this.stats.stopCapturing();
|
|
838
838
|
process.off('SIGINT', sigintHandler);
|
|
839
839
|
eventManager.off(EventType.MIGRATING, boundPauseOnMigration);
|
|
840
840
|
eventManager.off(EventType.ABORTING, boundPauseOnMigration);
|
|
841
|
-
const finalStats = this.
|
|
841
|
+
const finalStats = this.statistics.calculate();
|
|
842
842
|
stats = {
|
|
843
|
-
requestsFinished: this.
|
|
844
|
-
requestsFailed: this.
|
|
845
|
-
retryHistogram: this.
|
|
843
|
+
requestsFinished: this.statistics.state.requestsFinished,
|
|
844
|
+
requestsFailed: this.statistics.state.requestsFailed,
|
|
845
|
+
retryHistogram: this.statistics.requestRetryHistogram,
|
|
846
846
|
...finalStats,
|
|
847
847
|
};
|
|
848
848
|
this.log.info('Final request statistics:', stats);
|
|
849
|
-
if (this.
|
|
849
|
+
if (this.statistics.errorTracker.total !== 0) {
|
|
850
850
|
const prettify = ([count, info]) => `${count}x: ${info.at(-1).trim()} (${info[0]})`;
|
|
851
851
|
this.log.info(`Error analysis:`, {
|
|
852
|
-
totalErrors: this.
|
|
853
|
-
uniqueErrors: this.
|
|
854
|
-
mostCommonErrors: this.
|
|
852
|
+
totalErrors: this.statistics.errorTracker.total,
|
|
853
|
+
uniqueErrors: this.statistics.errorTracker.getUniqueErrorCount(),
|
|
854
|
+
mostCommonErrors: this.statistics.errorTracker.getMostPopularErrors(3).map(prettify),
|
|
855
855
|
});
|
|
856
856
|
}
|
|
857
857
|
const client = serviceLocator.getStorageBackend();
|
|
@@ -866,7 +866,7 @@ export class BasicCrawler {
|
|
|
866
866
|
finished = true;
|
|
867
867
|
}
|
|
868
868
|
periodicLogger.stop();
|
|
869
|
-
this.setStatusMessage(`Finished! Total ${this.
|
|
869
|
+
this.setStatusMessage(`Finished! Total ${this.statistics.state.requestsFinished + this.statistics.state.requestsFailed} requests: ${this.statistics.state.requestsFinished} succeeded, ${this.statistics.state.requestsFailed} failed.`, { isStatusMessageTerminal: true, level: 'INFO' });
|
|
870
870
|
this.running = false;
|
|
871
871
|
this.hasFinishedBefore = true;
|
|
872
872
|
}
|
|
@@ -1150,7 +1150,9 @@ export class BasicCrawler {
|
|
|
1150
1150
|
// Only log the limit message when an explicit `limit` was passed (not the internal
|
|
1151
1151
|
// `maxRequestsPerCrawl`-derived one), and only once per call.
|
|
1152
1152
|
if (options.limit !== undefined && allSkipped.some((s) => s.reason === 'limit')) {
|
|
1153
|
-
this.log.info(
|
|
1153
|
+
this.log.info(requestLimit === options.limit
|
|
1154
|
+
? `Skipping requests in this call due to the enqueueLinks limit of ${options.limit}.`
|
|
1155
|
+
: `Skipping requests in this call due to the remaining maxRequestsPerCrawl budget of ${requestLimit}, which is lower than the enqueueLinks limit of ${options.limit}.`);
|
|
1154
1156
|
}
|
|
1155
1157
|
await Promise.all(allSkipped.map(async ({ url, reason }) => {
|
|
1156
1158
|
await this.handleSkippedRequest({ url, reason });
|
|
@@ -1447,7 +1449,7 @@ export class BasicCrawler {
|
|
|
1447
1449
|
});
|
|
1448
1450
|
}
|
|
1449
1451
|
})();
|
|
1450
|
-
await Promise.all([requestManagerPersistPromise, this.
|
|
1452
|
+
await Promise.all([requestManagerPersistPromise, this.statistics.persistState?.()]);
|
|
1451
1453
|
}
|
|
1452
1454
|
/**
|
|
1453
1455
|
* Fetches the next request to process from the underlying request provider.
|
|
@@ -1478,7 +1480,7 @@ export class BasicCrawler {
|
|
|
1478
1480
|
await transaction?.commit();
|
|
1479
1481
|
await this.timeoutAndRetry(async () => requestSource.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${request.url} (${request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
1480
1482
|
isRequestLocked = false; // markRequestAsHandled succeeded and unlocked the request
|
|
1481
|
-
this.
|
|
1483
|
+
this.statistics.finishJob(statisticsId, request.retryCount);
|
|
1482
1484
|
// reclaim session if request finishes successfully
|
|
1483
1485
|
request.state = RequestState.DONE;
|
|
1484
1486
|
crawlingContext.session.markGood();
|
|
@@ -1608,7 +1610,7 @@ export class BasicCrawler {
|
|
|
1608
1610
|
}
|
|
1609
1611
|
const shouldRetryRequest = this.canRequestBeRetried(request, error);
|
|
1610
1612
|
if (shouldRetryRequest) {
|
|
1611
|
-
await this.
|
|
1613
|
+
await this.statistics.errorTrackerRetry.addAsync(error, crawlingContext);
|
|
1612
1614
|
await this.errorHandler?.(crawlingContext, // valid cast - ExtendedContext transitively extends CrawlingContext
|
|
1613
1615
|
error);
|
|
1614
1616
|
if (error instanceof SessionError) {
|
|
@@ -1637,16 +1639,16 @@ export class BasicCrawler {
|
|
|
1637
1639
|
// This is to make sure the error snapshot is not duplicated in the errorTrackerRetry and errorTracker objects.
|
|
1638
1640
|
const { noRetry, maxRetries } = request;
|
|
1639
1641
|
if (noRetry || !maxRetries) {
|
|
1640
|
-
await this.
|
|
1642
|
+
await this.statistics.errorTracker.addAsync(error, crawlingContext);
|
|
1641
1643
|
}
|
|
1642
1644
|
else {
|
|
1643
|
-
this.
|
|
1645
|
+
this.statistics.errorTracker.add(error);
|
|
1644
1646
|
}
|
|
1645
1647
|
// If we get here, the request is either not retryable
|
|
1646
1648
|
// or failed more than retryCount times and will not be retried anymore.
|
|
1647
1649
|
// Mark the request as failed and do not retry.
|
|
1648
1650
|
await source.markRequestAsHandled(request);
|
|
1649
|
-
this.
|
|
1651
|
+
this.statistics.failJob(request.id || request.uniqueKey, request.retryCount);
|
|
1650
1652
|
await this.handleFailedRequestHandler(crawlingContext, error); // This function prints an error message.
|
|
1651
1653
|
}
|
|
1652
1654
|
async handleFailedRequestHandler(crawlingContext, error) {
|
|
@@ -1706,11 +1708,16 @@ export class BasicCrawler {
|
|
|
1706
1708
|
* To stop the crawler gracefully (waiting for all running requests to finish), use {@link BasicCrawler.stop|`crawler.stop()`} instead.
|
|
1707
1709
|
*/
|
|
1708
1710
|
async teardown() {
|
|
1709
|
-
|
|
1711
|
+
// When this crawler initialized the event manager, its close() call emits
|
|
1712
|
+
// the final persistence event after the crawler-specific state has been
|
|
1713
|
+
// saved. External event managers still need an explicit event here.
|
|
1714
|
+
if (!this.#closeEvents) {
|
|
1715
|
+
serviceLocator.getEventManager().emit(EventType.PERSIST_STATE, { isMigrating: false });
|
|
1716
|
+
}
|
|
1717
|
+
await this.#sessionPoolDep.ifOwned(async (pool) => pool.teardown({ persistState: this.#closeEvents ?? false }));
|
|
1710
1718
|
if (this.#closeEvents) {
|
|
1711
1719
|
await serviceLocator.getEventManager().close();
|
|
1712
1720
|
}
|
|
1713
|
-
await this.#sessionPoolDep.ifOwned((pool) => pool.teardown());
|
|
1714
1721
|
await this.#autoscaledPool?.abort();
|
|
1715
1722
|
await this.#concurrencySystemDep?.ifOwned((system) => system.stop());
|
|
1716
1723
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/basic",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.128",
|
|
4
4
|
"description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"@apify/datastructures": "^2.0.0",
|
|
43
43
|
"@apify/timeout": "^0.4.4",
|
|
44
44
|
"@apify/utilities": "^2.15.5",
|
|
45
|
-
"@crawlee/core": "4.0.0-beta.
|
|
46
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
47
|
-
"@crawlee/types": "4.0.0-beta.
|
|
48
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
45
|
+
"@crawlee/core": "4.0.0-beta.128",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.128",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.128",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.128",
|
|
49
49
|
"csv-stringify": "^6.5.2",
|
|
50
50
|
"tldts": "^7.0.6",
|
|
51
51
|
"tslib": "^2.8.1",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"zod": "^4.4.3"
|
|
54
54
|
},
|
|
55
55
|
"optionalDependencies": {
|
|
56
|
-
"@crawlee/impit-client": "^4.0.0-beta.
|
|
56
|
+
"@crawlee/impit-client": "^4.0.0-beta.128"
|
|
57
57
|
},
|
|
58
58
|
"lerna": {
|
|
59
59
|
"command": {
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "2c017f04d564e8fa13855bf19f31f43cb65f4f44"
|
|
66
66
|
}
|