@crawlee/basic 4.0.0-beta.126 → 4.0.0-beta.127
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.
|
|
@@ -834,24 +834,24 @@ export class BasicCrawler {
|
|
|
834
834
|
}
|
|
835
835
|
finally {
|
|
836
836
|
await this.teardown();
|
|
837
|
-
await this.
|
|
837
|
+
await this.statistics.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
|
}
|
|
@@ -1447,7 +1447,7 @@ export class BasicCrawler {
|
|
|
1447
1447
|
});
|
|
1448
1448
|
}
|
|
1449
1449
|
})();
|
|
1450
|
-
await Promise.all([requestManagerPersistPromise, this.
|
|
1450
|
+
await Promise.all([requestManagerPersistPromise, this.statistics.persistState?.()]);
|
|
1451
1451
|
}
|
|
1452
1452
|
/**
|
|
1453
1453
|
* Fetches the next request to process from the underlying request provider.
|
|
@@ -1478,7 +1478,7 @@ export class BasicCrawler {
|
|
|
1478
1478
|
await transaction?.commit();
|
|
1479
1479
|
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
1480
|
isRequestLocked = false; // markRequestAsHandled succeeded and unlocked the request
|
|
1481
|
-
this.
|
|
1481
|
+
this.statistics.finishJob(statisticsId, request.retryCount);
|
|
1482
1482
|
// reclaim session if request finishes successfully
|
|
1483
1483
|
request.state = RequestState.DONE;
|
|
1484
1484
|
crawlingContext.session.markGood();
|
|
@@ -1608,7 +1608,7 @@ export class BasicCrawler {
|
|
|
1608
1608
|
}
|
|
1609
1609
|
const shouldRetryRequest = this.canRequestBeRetried(request, error);
|
|
1610
1610
|
if (shouldRetryRequest) {
|
|
1611
|
-
await this.
|
|
1611
|
+
await this.statistics.errorTrackerRetry.addAsync(error, crawlingContext);
|
|
1612
1612
|
await this.errorHandler?.(crawlingContext, // valid cast - ExtendedContext transitively extends CrawlingContext
|
|
1613
1613
|
error);
|
|
1614
1614
|
if (error instanceof SessionError) {
|
|
@@ -1637,16 +1637,16 @@ export class BasicCrawler {
|
|
|
1637
1637
|
// This is to make sure the error snapshot is not duplicated in the errorTrackerRetry and errorTracker objects.
|
|
1638
1638
|
const { noRetry, maxRetries } = request;
|
|
1639
1639
|
if (noRetry || !maxRetries) {
|
|
1640
|
-
await this.
|
|
1640
|
+
await this.statistics.errorTracker.addAsync(error, crawlingContext);
|
|
1641
1641
|
}
|
|
1642
1642
|
else {
|
|
1643
|
-
this.
|
|
1643
|
+
this.statistics.errorTracker.add(error);
|
|
1644
1644
|
}
|
|
1645
1645
|
// If we get here, the request is either not retryable
|
|
1646
1646
|
// or failed more than retryCount times and will not be retried anymore.
|
|
1647
1647
|
// Mark the request as failed and do not retry.
|
|
1648
1648
|
await source.markRequestAsHandled(request);
|
|
1649
|
-
this.
|
|
1649
|
+
this.statistics.failJob(request.id || request.uniqueKey, request.retryCount);
|
|
1650
1650
|
await this.handleFailedRequestHandler(crawlingContext, error); // This function prints an error message.
|
|
1651
1651
|
}
|
|
1652
1652
|
async handleFailedRequestHandler(crawlingContext, error) {
|
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.127",
|
|
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.127",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.127",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.127",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.127",
|
|
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.127"
|
|
57
57
|
},
|
|
58
58
|
"lerna": {
|
|
59
59
|
"command": {
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "4aa4a4d8d105bb530649ac5cd9197a3169106bd9"
|
|
66
66
|
}
|