@crawlee/basic 4.0.0-beta.99 → 4.0.0-rc.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/README.md +1 -1
- package/index.d.ts +11 -1
- package/index.js +11 -0
- package/internals/autoscaling/autoscaled_pool.d.ts +178 -0
- package/internals/autoscaling/autoscaled_pool.js +387 -0
- package/internals/autoscaling/concurrency_system.d.ts +235 -0
- package/internals/autoscaling/concurrency_system.js +357 -0
- package/internals/autoscaling/cpu_load_signal.d.ts +43 -0
- package/internals/autoscaling/cpu_load_signal.js +46 -0
- package/internals/autoscaling/event_loop_load_signal.d.ts +51 -0
- package/internals/autoscaling/event_loop_load_signal.js +60 -0
- package/internals/autoscaling/index.d.ts +9 -0
- package/internals/autoscaling/index.js +9 -0
- package/internals/autoscaling/load_signal.d.ts +98 -0
- package/internals/autoscaling/load_signal.js +103 -0
- package/internals/autoscaling/memory_load_signal.d.ts +45 -0
- package/internals/autoscaling/memory_load_signal.js +104 -0
- package/internals/autoscaling/snapshotter.d.ts +84 -0
- package/internals/autoscaling/snapshotter.js +67 -0
- package/internals/autoscaling/storage_backend_load_signal.d.ts +56 -0
- package/internals/autoscaling/storage_backend_load_signal.js +73 -0
- package/internals/autoscaling/system_status.d.ts +112 -0
- package/internals/autoscaling/system_status.js +139 -0
- package/internals/autoscaling/weighted_avg.d.ts +5 -0
- package/internals/autoscaling/weighted_avg.js +14 -0
- package/internals/basic-crawler.d.ts +185 -389
- package/internals/basic-crawler.js +982 -684
- package/internals/cookie_utils.d.ts +40 -0
- package/internals/cookie_utils.js +122 -0
- package/internals/crawler-run.d.ts +76 -0
- package/internals/crawler-run.js +142 -0
- package/internals/crawlers/context_pipeline.d.ts +75 -0
- package/internals/crawlers/context_pipeline.js +109 -0
- package/internals/crawlers/crawler_commons.d.ts +216 -0
- package/internals/crawlers/crawler_commons.js +1 -0
- package/internals/crawlers/error_snapshotter.d.ts +29 -0
- package/internals/crawlers/error_snapshotter.js +116 -0
- package/internals/crawlers/error_tracker.d.ts +51 -0
- package/internals/crawlers/error_tracker.js +316 -0
- package/internals/crawlers/index.d.ts +4 -0
- package/internals/crawlers/index.js +3 -0
- package/internals/crawlers/internals/types.d.ts +7 -0
- package/internals/crawlers/internals/types.js +1 -0
- package/internals/crawlers/statistics.d.ts +316 -0
- package/internals/crawlers/statistics.js +525 -0
- package/internals/crawling_request.d.ts +86 -0
- package/internals/crawling_request.js +127 -0
- package/internals/enqueue_links/enqueue_links.d.ts +152 -0
- package/internals/enqueue_links/enqueue_links.js +76 -0
- package/internals/enqueue_links/index.d.ts +3 -0
- package/internals/enqueue_links/index.js +3 -0
- package/internals/enqueue_links/shared.d.ts +106 -0
- package/internals/enqueue_links/shared.js +258 -0
- package/internals/errors.d.ts +68 -0
- package/internals/errors.js +83 -0
- package/internals/http.d.ts +9 -0
- package/internals/http.js +28 -0
- package/internals/request-timeout.d.ts +6 -0
- package/internals/request-timeout.js +27 -2
- package/internals/router.d.ts +299 -0
- package/internals/router.js +312 -0
- package/internals/send-request.d.ts +2 -1
- package/internals/send-request.js +1 -0
- package/internals/session_pool/consts.d.ts +1 -0
- package/internals/session_pool/consts.js +1 -0
- package/internals/session_pool/errors.d.ts +7 -0
- package/internals/session_pool/errors.js +11 -0
- package/internals/session_pool/fingerprint.d.ts +9 -0
- package/internals/session_pool/fingerprint.js +30 -0
- package/internals/session_pool/index.d.ts +4 -0
- package/internals/session_pool/index.js +4 -0
- package/internals/session_pool/session.d.ts +151 -0
- package/internals/session_pool/session.js +225 -0
- package/internals/session_pool/session_pool.d.ts +193 -0
- package/internals/session_pool/session_pool.js +453 -0
- package/internals/sitemap_request_loader.d.ts +158 -0
- package/internals/sitemap_request_loader.js +432 -0
- package/internals/throttling_request_manager.d.ts +229 -0
- package/internals/throttling_request_manager.js +843 -0
- package/package.json +17 -14
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { Awaitable, Dictionary, HttpRequestOptions, ISession, ProxyInfo, SendRequestOptions } from '@crawlee/types';
|
|
2
|
+
import type { ReadonlyDeep } from 'type-fest';
|
|
3
|
+
import type { AddRequestsBatchedResult, CrawleeLogger, Dataset, KeyValueStore, Request, RequestOptions, Source, StorageIdentifier } from '@crawlee/core';
|
|
4
|
+
import type { EnqueueUrlsOptions } from '../enqueue_links/index.js';
|
|
5
|
+
import type { CrawlingRequest } from '../crawling_request.js';
|
|
6
|
+
/** @internal */
|
|
7
|
+
export type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
8
|
+
/**
|
|
9
|
+
* A request input (URL string, request-options object, or {@link Request}) whose `userData` is typed
|
|
10
|
+
* according to its `label`, based on a router's route map.
|
|
11
|
+
*
|
|
12
|
+
* When the route map is open (the default `Record<string, ...>`), this is just the regular loose
|
|
13
|
+
* {@link Source} input. When the map declares concrete labels, providing a `label` requires the matching
|
|
14
|
+
* `userData` shape and rejects labels not present in the map; unlabeled requests keep loose `userData`.
|
|
15
|
+
*/
|
|
16
|
+
export type LabeledSource<Routes extends Record<keyof Routes, Dictionary>> = string extends keyof Routes ? string | Source : string | Request | ({
|
|
17
|
+
requestsFromUrl?: string;
|
|
18
|
+
regex?: RegExp;
|
|
19
|
+
} & ({
|
|
20
|
+
[Label in keyof Routes & string]: Omit<Partial<RequestOptions<Routes[Label]>>, 'label'> & {
|
|
21
|
+
label: Label;
|
|
22
|
+
};
|
|
23
|
+
}[keyof Routes & string] | (Omit<Partial<RequestOptions>, 'label'> & {
|
|
24
|
+
label?: undefined;
|
|
25
|
+
})));
|
|
26
|
+
/**
|
|
27
|
+
* The iterable/array of {@link LabeledSource} inputs accepted by the label-aware `addRequests`/`run`
|
|
28
|
+
* methods of a crawler bound to a typed router.
|
|
29
|
+
*/
|
|
30
|
+
export type TypedRequestsLike<Routes extends Record<keyof Routes, Dictionary>> = AsyncIterable<LabeledSource<Routes>> | Iterable<LabeledSource<Routes>> | LabeledSource<Routes>[];
|
|
31
|
+
/**
|
|
32
|
+
* The label-aware `addRequests` method signature exposed on a request handler's context when the crawler is
|
|
33
|
+
* bound to a typed router. Mirrors {@link RestrictedCrawlingContext.addRequests} with typed sources.
|
|
34
|
+
*/
|
|
35
|
+
export type TypedContextAddRequests<Routes extends Record<keyof Routes, Dictionary>> = (requestsLike: ReadonlyDeep<LabeledSource<Routes>[]>, options?: ReadonlyDeep<EnqueueUrlsOptions>) => Promise<AddRequestsBatchedResult>;
|
|
36
|
+
/**
|
|
37
|
+
* An `enqueueLinks`-options object with its `label`/`userData` retyped according to a router's route map: a
|
|
38
|
+
* declared `label` requires the matching `userData` shape (unknown labels are rejected), while unlabeled
|
|
39
|
+
* calls keep loose `userData`. Returns the options unchanged when the route map is open (the default).
|
|
40
|
+
*/
|
|
41
|
+
type TypedEnqueueLinksOptions<Options, Routes extends Record<keyof Routes, Dictionary>> = string extends keyof Routes ? Options : Omit<Options, 'label' | 'userData'> & ({
|
|
42
|
+
[Label in keyof Routes & string]: {
|
|
43
|
+
label: Label;
|
|
44
|
+
userData?: Routes[Label];
|
|
45
|
+
};
|
|
46
|
+
}[keyof Routes & string] | {
|
|
47
|
+
label?: undefined;
|
|
48
|
+
userData?: Dictionary;
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* Transforms a context's existing `enqueueLinks` method so that the `label`/`userData` in its options follow
|
|
52
|
+
* the router's route map, while preserving everything else about the signature (argument optionality and
|
|
53
|
+
* return type, which differ between crawler types).
|
|
54
|
+
*/
|
|
55
|
+
export type TypedContextEnqueueLinks<EnqueueLinks, Routes extends Record<keyof Routes, Dictionary>> = EnqueueLinks extends (options?: infer Options) => infer Result ? (options?: TypedEnqueueLinksOptions<Options, Routes>) => Result : EnqueueLinks extends (options: infer Options) => infer Result ? (options: TypedEnqueueLinksOptions<Options, Routes>) => Result : EnqueueLinks;
|
|
56
|
+
/** A {@link Request} that has been dispatched, so its `id` and `loadedUrl` are guaranteed to be present. */
|
|
57
|
+
export type LoadedRequest<R extends Request> = R & Required<Pick<R, 'id' | 'loadedUrl'>>;
|
|
58
|
+
/** @internal */
|
|
59
|
+
export type LoadedContext<Context extends RestrictedCrawlingContext> = IsAny<Context> extends true ? Context : {
|
|
60
|
+
request: LoadedRequest<Context['request']>;
|
|
61
|
+
} & Omit<Context, 'request'>;
|
|
62
|
+
export interface RestrictedCrawlingContext<UserData extends Dictionary = Dictionary> {
|
|
63
|
+
/** @internal */
|
|
64
|
+
id: string;
|
|
65
|
+
session: ISession;
|
|
66
|
+
/**
|
|
67
|
+
* An object with information about currently used proxy by the crawler
|
|
68
|
+
* and configured by the {@link ProxyConfiguration} class.
|
|
69
|
+
*/
|
|
70
|
+
proxyInfo?: ProxyInfo;
|
|
71
|
+
/**
|
|
72
|
+
* The request being processed.
|
|
73
|
+
*/
|
|
74
|
+
request: CrawlingRequest<UserData>;
|
|
75
|
+
/**
|
|
76
|
+
* This function allows you to push data to a {@link Dataset} specified by name, or the one currently used by the crawler.
|
|
77
|
+
*
|
|
78
|
+
* Shortcut for `crawler.pushData()`.
|
|
79
|
+
*
|
|
80
|
+
* @param [data] Data to be pushed to the default dataset.
|
|
81
|
+
*/
|
|
82
|
+
pushData(data: ReadonlyDeep<Parameters<Dataset['pushData']>[0]>, datasetIdentifier?: string | StorageIdentifier): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Add requests directly to the request queue currently used by the crawler.
|
|
85
|
+
*
|
|
86
|
+
* Optionally, the function allows you to filter the target URLs using an array of glob or regexp patterns,
|
|
87
|
+
* the same way {@link CrawlingContext.enqueueLinks|`enqueueLinks`} does for extracted links.
|
|
88
|
+
*
|
|
89
|
+
* @param requests The requests to add
|
|
90
|
+
* @param options Options for the request queue
|
|
91
|
+
*/
|
|
92
|
+
addRequests: (requestsLike: ReadonlyDeep<(string | Source)[]>, options?: ReadonlyDeep<EnqueueUrlsOptions>) => Promise<AddRequestsBatchedResult>;
|
|
93
|
+
/**
|
|
94
|
+
* Returns the state - a piece of mutable persistent data shared across all the request handler runs.
|
|
95
|
+
*/
|
|
96
|
+
useState: <State extends Dictionary = Dictionary>(defaultValue?: State) => Promise<State>;
|
|
97
|
+
/**
|
|
98
|
+
* Get a key-value store with given name or id, or the default one for the crawler.
|
|
99
|
+
*/
|
|
100
|
+
getKeyValueStore: (identifier?: string | StorageIdentifier) => Promise<Pick<KeyValueStore, 'id' | 'name' | 'getValue' | 'getAutoSavedValue' | 'setValue' | 'getPublicUrl'>>;
|
|
101
|
+
/**
|
|
102
|
+
* A preconfigured logger for the request handler.
|
|
103
|
+
*/
|
|
104
|
+
log: CrawleeLogger;
|
|
105
|
+
/**
|
|
106
|
+
* Stops processing the current request and marks it as handled without counting it as a failure: no retry, no
|
|
107
|
+
* `failedRequestHandler`, and no error log. The request's `state` becomes {@link RequestState.SKIPPED|`SKIPPED`}
|
|
108
|
+
* and {@link BasicCrawlerOptions.onSkippedRequest|`onSkippedRequest`} fires with the `'manual'` reason.
|
|
109
|
+
*
|
|
110
|
+
* Storage writes made for the request so far are rolled back, as with a failure, unless
|
|
111
|
+
* {@link BasicCrawlerOptions.transactionalStorage|`transactionalStorage`} is disabled.
|
|
112
|
+
*
|
|
113
|
+
* Works in `errorHandler` too, skipping a request that would otherwise be retried. In `failedRequestHandler` it
|
|
114
|
+
* has no effect, since the request has already failed.
|
|
115
|
+
*
|
|
116
|
+
* ```ts
|
|
117
|
+
* preNavigationHooks: [
|
|
118
|
+
* async ({ request, skipRequest }) => {
|
|
119
|
+
* if (await alreadyScraped(request.url)) skipRequest('already scraped');
|
|
120
|
+
* },
|
|
121
|
+
* ],
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @param message Passed to `onSkippedRequest`.
|
|
125
|
+
*/
|
|
126
|
+
skipRequest(message?: string): never;
|
|
127
|
+
}
|
|
128
|
+
export interface CrawlingContext<UserData extends Dictionary = Dictionary> extends RestrictedCrawlingContext<UserData> {
|
|
129
|
+
/**
|
|
130
|
+
* Fires HTTP request via the internal HTTP client, allowing to override the request options on the fly.
|
|
131
|
+
*
|
|
132
|
+
* This is handy when you work with a browser crawler but want to execute some requests outside it (e.g. API requests).
|
|
133
|
+
* Check the [Skipping navigations for certain requests](https://crawlee.dev/js/docs/examples/skip-navigation) example for
|
|
134
|
+
* more detailed explanation of how to do that.
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* async requestHandler({ sendRequest }) {
|
|
138
|
+
* const { body } = await sendRequest({
|
|
139
|
+
* // override headers only
|
|
140
|
+
* headers: { ... },
|
|
141
|
+
* });
|
|
142
|
+
* },
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
sendRequest: (requestOverrides?: Partial<HttpRequestOptions>, optionsOverrides?: SendRequestOptions) => Promise<Response>;
|
|
146
|
+
/**
|
|
147
|
+
* Register a function to be called at the very end of the request handling process. This is useful for resources that should be accessible to error handlers, for instance.
|
|
148
|
+
*
|
|
149
|
+
* The callback runs *outside* the request's storage transaction, so storage writes made here are
|
|
150
|
+
* applied immediately and are **not** rolled back when the request fails. In
|
|
151
|
+
* {@link AdaptivePlaywrightCrawler} it also runs once per request handler attempt, so a write
|
|
152
|
+
* here can land more than once for a single request. Push results from the request handler itself.
|
|
153
|
+
*/
|
|
154
|
+
registerDeferredCleanup(cleanup: () => Promise<unknown>): void;
|
|
155
|
+
/**
|
|
156
|
+
* Registers `callback` to run once the request's storage writes have been committed, or once
|
|
157
|
+
* committing them has failed - the point where a write made in the handler can finally be reacted
|
|
158
|
+
* to where it was made. Two things belong here: side effects that must agree with what actually
|
|
159
|
+
* landed (result counters, progress reporting), and handling of a write that did not land at all.
|
|
160
|
+
*
|
|
161
|
+
* ```ts
|
|
162
|
+
* async requestHandler({ pushData, afterStorageCommit, useState }) {
|
|
163
|
+
* const state = await useState({ itemCount: 0 });
|
|
164
|
+
*
|
|
165
|
+
* await pushData(item);
|
|
166
|
+
* afterStorageCommit((error) => {
|
|
167
|
+
* if (error) {
|
|
168
|
+
* throw new NonRetryableError('The page is too big to store', { cause: error });
|
|
169
|
+
* }
|
|
170
|
+
* state.itemCount++;
|
|
171
|
+
* });
|
|
172
|
+
* }
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* `useState()` is deliberately not transactional, which is what makes the counter case necessary:
|
|
176
|
+
* incrementing in the handler would keep the increment for a request that pushes an item and then
|
|
177
|
+
* fails, while the item itself is rolled back.
|
|
178
|
+
*
|
|
179
|
+
* Callbacks run in registration order, after the commit and before the request is marked as handled.
|
|
180
|
+
* The first one to throw propagates and the rest do not run; on a failed commit its error replaces
|
|
181
|
+
* the commit error, so a callback can decide what the request fails with. After a *successful*
|
|
182
|
+
* commit, a callback that throws fails the request **without a retry** whatever it throws - the
|
|
183
|
+
* writes are already durable and retrying would duplicate them, so anything that is not already a
|
|
184
|
+
* {@link NonRetryableError} is wrapped in an {@link AfterCommitError}.
|
|
185
|
+
*
|
|
186
|
+
* Callbacks are *not* run when the request handler itself fails - nothing was written, and
|
|
187
|
+
* `errorHandler` / `failedRequestHandler` cover that.
|
|
188
|
+
*
|
|
189
|
+
* Throws when storage is not transactional (`transactionalStorage: false`) - writes are then applied
|
|
190
|
+
* as they are made and throw at the call site on their own. In {@link AdaptivePlaywrightCrawler}
|
|
191
|
+
* the callback belongs to the current request handler attempt, so it runs only for the attempt whose
|
|
192
|
+
* writes are the ones being committed.
|
|
193
|
+
*/
|
|
194
|
+
afterStorageCommit(callback: (error?: Error) => Awaitable<void>): void;
|
|
195
|
+
/**
|
|
196
|
+
* Gives the current request `secs` more seconds to finish, for when how long it needs is only apparent
|
|
197
|
+
* once it is already running - a listing page that turns out to have far more to scroll through than
|
|
198
|
+
* usual, say. Prefer `requestHandlerTimeoutSecs`, or a per-route override via
|
|
199
|
+
* {@link Router.addHandler|`router.addHandler`}, whenever the time needed is known up front.
|
|
200
|
+
*
|
|
201
|
+
* ```ts
|
|
202
|
+
* router.addHandler('LIST', async ({ extendTimeout, page }) => {
|
|
203
|
+
* const pageCount = await countPages(page);
|
|
204
|
+
* extendTimeout(pageCount * 10);
|
|
205
|
+
* await scrapeAllPages(page);
|
|
206
|
+
* });
|
|
207
|
+
* ```
|
|
208
|
+
*
|
|
209
|
+
* Extends the request handler's own timeout and the crawler's internal one together, so the extension
|
|
210
|
+
* is not immediately undone by the latter. On a locking storage backend the request's lock is
|
|
211
|
+
* extended by the same amount, so the extra time is not spent while the queue considers the request
|
|
212
|
+
* free to hand out again. Calling it from a handler that has already timed out does nothing.
|
|
213
|
+
*/
|
|
214
|
+
extendTimeout(secs: number): void;
|
|
215
|
+
}
|
|
216
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CrawlingContext } from '../crawlers/crawler_commons.js';
|
|
2
|
+
import type { ErrnoException } from './error_tracker.js';
|
|
3
|
+
import type { SnapshottableProperties } from './internals/types.js';
|
|
4
|
+
interface ErrorSnapshot {
|
|
5
|
+
screenshotFileName?: string;
|
|
6
|
+
screenshotFileUrl?: string;
|
|
7
|
+
htmlFileName?: string;
|
|
8
|
+
htmlFileUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* ErrorSnapshotter class is used to capture a screenshot of the page and a snapshot of the HTML when an error occurs during web crawling.
|
|
12
|
+
*
|
|
13
|
+
* This functionality is opt-in, and can be enabled via the crawler options:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const crawler = new BasicCrawler({
|
|
17
|
+
* // ...
|
|
18
|
+
* statistics: new Statistics({ saveErrorSnapshots: true }),
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare class ErrorSnapshotter {
|
|
23
|
+
#private;
|
|
24
|
+
/**
|
|
25
|
+
* Capture a snapshot of the error context.
|
|
26
|
+
*/
|
|
27
|
+
captureSnapshot(error: ErrnoException, context: CrawlingContext & SnapshottableProperties): Promise<ErrorSnapshot>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
const MAX_ERROR_CHARACTERS = 30;
|
|
3
|
+
const MAX_HASH_LENGTH = 30;
|
|
4
|
+
const MAX_FILENAME_LENGTH = 250;
|
|
5
|
+
const BASE_MESSAGE = 'An error occurred';
|
|
6
|
+
const SNAPSHOT_PREFIX = 'ERROR_SNAPSHOT';
|
|
7
|
+
/**
|
|
8
|
+
* ErrorSnapshotter class is used to capture a screenshot of the page and a snapshot of the HTML when an error occurs during web crawling.
|
|
9
|
+
*
|
|
10
|
+
* This functionality is opt-in, and can be enabled via the crawler options:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const crawler = new BasicCrawler({
|
|
14
|
+
* // ...
|
|
15
|
+
* statistics: new Statistics({ saveErrorSnapshots: true }),
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class ErrorSnapshotter {
|
|
20
|
+
/**
|
|
21
|
+
* Capture a snapshot of the error context.
|
|
22
|
+
*/
|
|
23
|
+
async captureSnapshot(error, context) {
|
|
24
|
+
try {
|
|
25
|
+
const page = context?.page;
|
|
26
|
+
const body = context?.body;
|
|
27
|
+
const keyValueStore = await context?.getKeyValueStore();
|
|
28
|
+
// If the key-value store is not available, or the body and page are not available, return empty filenames
|
|
29
|
+
if (!keyValueStore || (!body && !page)) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
const fileName = this.#generateFilename(error);
|
|
33
|
+
let screenshotFileName;
|
|
34
|
+
let htmlFileName;
|
|
35
|
+
if (page) {
|
|
36
|
+
const capturedFiles = await this.#contextCaptureSnapshot(context, fileName);
|
|
37
|
+
if (capturedFiles) {
|
|
38
|
+
screenshotFileName = capturedFiles.screenshotFileName;
|
|
39
|
+
htmlFileName = capturedFiles.htmlFileName;
|
|
40
|
+
}
|
|
41
|
+
// If the snapshot for browsers failed to capture the HTML, try to capture it from the page content
|
|
42
|
+
if (!htmlFileName) {
|
|
43
|
+
const html = await page.content();
|
|
44
|
+
htmlFileName = html ? await this.#saveHTMLSnapshot(html, keyValueStore, fileName) : undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (typeof body === 'string') {
|
|
48
|
+
// for non-browser contexts
|
|
49
|
+
htmlFileName = await this.#saveHTMLSnapshot(body, keyValueStore, fileName);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
screenshotFileName,
|
|
53
|
+
screenshotFileUrl: screenshotFileName && (await keyValueStore.getPublicUrl(screenshotFileName)),
|
|
54
|
+
htmlFileName,
|
|
55
|
+
htmlFileUrl: htmlFileName && (await keyValueStore.getPublicUrl(htmlFileName)),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Captures a snapshot of the current page using the context.saveSnapshot function.
|
|
64
|
+
* This function is applicable for browser contexts only.
|
|
65
|
+
* Returns an object containing the filenames of the screenshot and HTML file.
|
|
66
|
+
*/
|
|
67
|
+
async #contextCaptureSnapshot(context, fileName) {
|
|
68
|
+
try {
|
|
69
|
+
await context.saveSnapshot({ key: fileName });
|
|
70
|
+
return {
|
|
71
|
+
screenshotFileName: `${fileName}.jpg`,
|
|
72
|
+
htmlFileName: `${fileName}.html`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Save the HTML snapshot of the page, and return the key it was stored under.
|
|
81
|
+
*/
|
|
82
|
+
async #saveHTMLSnapshot(html, keyValueStore, fileName) {
|
|
83
|
+
try {
|
|
84
|
+
await keyValueStore.setValue(fileName, html, { contentType: 'text/html' });
|
|
85
|
+
// The record key is `fileName` - returning it with an `.html` suffix (as v3 did,
|
|
86
|
+
// where local storage put the extension in the key) would break `getPublicUrl`.
|
|
87
|
+
return fileName;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Generate a unique fileName for each error snapshot.
|
|
95
|
+
*/
|
|
96
|
+
#generateFilename(error) {
|
|
97
|
+
// Create a hash of the error stack trace
|
|
98
|
+
const errorStackHash = crypto
|
|
99
|
+
.createHash('sha1')
|
|
100
|
+
.update(error.stack || error.message || '')
|
|
101
|
+
.digest('hex')
|
|
102
|
+
.slice(0, MAX_HASH_LENGTH);
|
|
103
|
+
const errorMessagePrefix = (error.message || BASE_MESSAGE).slice(0, MAX_ERROR_CHARACTERS).trim();
|
|
104
|
+
/**
|
|
105
|
+
* Remove non-word characters from the start and end of a string.
|
|
106
|
+
*/
|
|
107
|
+
const sanitizeString = (str) => {
|
|
108
|
+
return str.replace(/^\W+|\W+$/g, '');
|
|
109
|
+
};
|
|
110
|
+
// Generate fileName and remove disallowed characters
|
|
111
|
+
const fileName = `${SNAPSHOT_PREFIX}_${sanitizeString(errorStackHash)}_${sanitizeString(errorMessagePrefix)}`
|
|
112
|
+
.replace(/\W+/g, '-') // Replace non-word characters with a dash
|
|
113
|
+
.slice(0, MAX_FILENAME_LENGTH);
|
|
114
|
+
return fileName;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { CrawlingContext } from '../crawlers/crawler_commons.js';
|
|
2
|
+
/**
|
|
3
|
+
* Node.js Error interface
|
|
4
|
+
*/
|
|
5
|
+
export interface ErrnoException extends Error {
|
|
6
|
+
errno?: number;
|
|
7
|
+
code?: string | number;
|
|
8
|
+
path?: string;
|
|
9
|
+
syscall?: string;
|
|
10
|
+
cause?: any;
|
|
11
|
+
}
|
|
12
|
+
export interface ErrorTrackerOptions {
|
|
13
|
+
showErrorCode: boolean;
|
|
14
|
+
showErrorName: boolean;
|
|
15
|
+
showStackTrace: boolean;
|
|
16
|
+
showFullStack: boolean;
|
|
17
|
+
showErrorMessage: boolean;
|
|
18
|
+
showFullMessage: boolean;
|
|
19
|
+
saveErrorSnapshots: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* This class tracks errors and computes a summary of information like:
|
|
23
|
+
* - where the errors happened
|
|
24
|
+
* - what the error names are
|
|
25
|
+
* - what the error codes are
|
|
26
|
+
* - what is the general error message
|
|
27
|
+
*
|
|
28
|
+
* This is extremely useful when there are dynamic error messages, such as argument validation.
|
|
29
|
+
*
|
|
30
|
+
* Since the structure of the `tracker.result` object differs when using different options,
|
|
31
|
+
* it's typed as `Record<string, unknown>`. The most deep object has a `count` property, which is a number.
|
|
32
|
+
*
|
|
33
|
+
* It's possible to get the total amount of errors via the `tracker.total` property.
|
|
34
|
+
*/
|
|
35
|
+
export declare class ErrorTracker {
|
|
36
|
+
#private;
|
|
37
|
+
/** The grouped error tree the tracker accumulates. Read-only: the object is mutated in place as errors arrive. */
|
|
38
|
+
get result(): Record<string, unknown>;
|
|
39
|
+
/** The total number of errors passed to the tracker, including the same error seen repeatedly. */
|
|
40
|
+
get total(): number;
|
|
41
|
+
constructor(options?: Partial<ErrorTrackerOptions>);
|
|
42
|
+
add(error: ErrnoException): void;
|
|
43
|
+
/**
|
|
44
|
+
* This method is async, because it captures a snapshot of the error context.
|
|
45
|
+
* We added this new method to avoid breaking changes.
|
|
46
|
+
*/
|
|
47
|
+
addAsync(error: ErrnoException, context?: CrawlingContext): Promise<void>;
|
|
48
|
+
getUniqueErrorCount(): number;
|
|
49
|
+
getMostPopularErrors(count: number): [number, string[]][];
|
|
50
|
+
reset(): void;
|
|
51
|
+
}
|