@nitpicker/crawler 0.15.0 → 0.16.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/lib/archive/archive.d.ts +39 -1
- package/lib/archive/archive.js +49 -0
- package/lib/archive/create-adjunct-tables.d.ts +3 -0
- package/lib/archive/create-adjunct-tables.js +42 -0
- package/lib/archive/database.d.ts +31 -1
- package/lib/archive/database.js +42 -0
- package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.d.ts +18 -0
- package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.js +23 -0
- package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.d.ts +12 -0
- package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.js +15 -0
- package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.d.ts +14 -0
- package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.js +30 -0
- package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.d.ts +21 -0
- package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.js +27 -0
- package/lib/archive/types.d.ts +13 -0
- package/lib/classify-error-kind.d.ts +1 -0
- package/lib/classify-error-kind.js +14 -0
- package/lib/crawler/assert-chrome-installed.d.ts +24 -0
- package/lib/crawler/assert-chrome-installed.js +43 -0
- package/lib/crawler/crawler.d.ts +12 -0
- package/lib/crawler/crawler.js +239 -29
- package/lib/crawler/decode-auth-credential.d.ts +29 -0
- package/lib/crawler/decode-auth-credential.js +39 -0
- package/lib/crawler/dedupe/compute-meta-signature.d.ts +30 -0
- package/lib/crawler/dedupe/compute-meta-signature.js +0 -0
- package/lib/crawler/dedupe/compute-shape-key.d.ts +37 -0
- package/lib/crawler/dedupe/compute-shape-key.js +56 -0
- package/lib/crawler/dedupe/dedupe-cap-tracker.d.ts +84 -0
- package/lib/crawler/dedupe/dedupe-cap-tracker.js +185 -0
- package/lib/crawler/dedupe/is-predicted-content-duplicate.d.ts +24 -0
- package/lib/crawler/dedupe/is-predicted-content-duplicate.js +26 -0
- package/lib/crawler/dedupe/is-shape-capped.d.ts +10 -0
- package/lib/crawler/dedupe/is-shape-capped.js +12 -0
- package/lib/crawler/dedupe/resolve-og-url-mismatch.d.ts +31 -0
- package/lib/crawler/dedupe/resolve-og-url-mismatch.js +40 -0
- package/lib/crawler/dedupe/types.d.ts +42 -0
- package/lib/crawler/dedupe/types.js +1 -0
- package/lib/crawler/fetch-destination.js +14 -2
- package/lib/crawler/generate-predicted-urls.d.ts +12 -0
- package/lib/crawler/generate-predicted-urls.js +33 -2
- package/lib/crawler/is-puppeteer-fallback-candidate.js +3 -0
- package/lib/crawler/types.d.ts +38 -0
- package/lib/crawler-orchestrator.d.ts +12 -0
- package/lib/crawler-orchestrator.js +106 -1
- package/lib/crawler.d.ts +1 -0
- package/lib/crawler.js +1 -0
- package/lib/permanent-error-kinds.d.ts +9 -4
- package/lib/permanent-error-kinds.js +10 -4
- package/lib/types.d.ts +2 -1
- package/package.json +2 -2
package/lib/archive/archive.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TemplateClusterReason } from './db-ops/analysis/types.js';
|
|
2
|
-
import type { Config, InsertNetworkOutageParams, InventoryRunMeta, PageSource } from './types.js';
|
|
2
|
+
import type { Config, InsertDedupeCapEventParams, InsertNetworkOutageParams, InventoryRunMeta, PageSource } from './types.js';
|
|
3
3
|
import type { OutageWindow } from '../is-within-outage-window.js';
|
|
4
4
|
import type { PageData, CrawlerError, Resource } from '../utils/types/types.js';
|
|
5
5
|
import type { ConsoleLogEntry } from '@d-zero/beholder';
|
|
@@ -49,6 +49,17 @@ export default class Archive extends ArchiveAccessor {
|
|
|
49
49
|
* @deprecated This method is no longer functional.
|
|
50
50
|
*/
|
|
51
51
|
abort(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Adds onto the `rejected_count` of a shape's `dedupe_cap_events` row,
|
|
54
|
+
* looked up by `shape_key` rather than `id` — used for a shape that
|
|
55
|
+
* capped in an earlier session (preloaded into `DedupeCapTracker`'s
|
|
56
|
+
* sticky set) and so has no event id from the current session.
|
|
57
|
+
*
|
|
58
|
+
* Thin facade over {@link Database.accumulateDedupeCapRejectedCount}.
|
|
59
|
+
* @param shapeKey - The capped shape whose rejection count to accumulate.
|
|
60
|
+
* @param rejectedCount - Additional anchors rejected for this shape in the current session.
|
|
61
|
+
*/
|
|
62
|
+
accumulateDedupeCapRejectedCount(shapeKey: string, rejectedCount: number): Promise<void>;
|
|
52
63
|
/**
|
|
53
64
|
* Records a crawler-level error to both the human-readable `error.log` (full
|
|
54
65
|
* stack, for debugging) and the structured `crawl_errors` table (queryable,
|
|
@@ -80,6 +91,15 @@ export default class Archive extends ArchiveAccessor {
|
|
|
80
91
|
* @param endedAt - Epoch ms the outage is considered to have ended.
|
|
81
92
|
*/
|
|
82
93
|
closeNetworkOutage(id: number, endedAt: number): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Finalizes a `dedupe_cap_events` row by stamping `rejected_count` — a
|
|
96
|
+
* no-op if already finalized.
|
|
97
|
+
*
|
|
98
|
+
* Thin facade over {@link Database.finalizeDedupeCapEvent}.
|
|
99
|
+
* @param id - The `dedupe_cap_events.id` to finalize.
|
|
100
|
+
* @param rejectedCount - Number of anchors rejected for this shape after it capped.
|
|
101
|
+
*/
|
|
102
|
+
finalizeDedupeCapEvent(id: number, rejectedCount: number): Promise<void>;
|
|
83
103
|
/**
|
|
84
104
|
* Retrieves the current crawling state, including lists of scraped and pending URLs.
|
|
85
105
|
* @returns An object with `scraped` and `pending` URL arrays.
|
|
@@ -136,6 +156,15 @@ export default class Archive extends ArchiveAccessor {
|
|
|
136
156
|
* @returns The base URL string.
|
|
137
157
|
*/
|
|
138
158
|
getUrl(): Promise<string>;
|
|
159
|
+
/**
|
|
160
|
+
* Appends one row (`rejected_count = NULL`) to the `dedupe_cap_events`
|
|
161
|
+
* journal.
|
|
162
|
+
*
|
|
163
|
+
* Thin facade over {@link Database.insertDedupeCapEvent}.
|
|
164
|
+
* @param params - The newly-capped shape's fields to record.
|
|
165
|
+
* @returns The autoincremented `id` of the inserted row.
|
|
166
|
+
*/
|
|
167
|
+
insertDedupeCapEvent(params: InsertDedupeCapEventParams): Promise<number>;
|
|
139
168
|
/**
|
|
140
169
|
* Pre-insert inventory non-HTML URLs as `source='inventory-seed'`
|
|
141
170
|
* placeholders in the `resources` table — the non-HTML counterpart of
|
|
@@ -174,6 +203,15 @@ export default class Archive extends ArchiveAccessor {
|
|
|
174
203
|
* @returns The autoincremented `id` of the inserted row.
|
|
175
204
|
*/
|
|
176
205
|
insertNetworkOutage(params: InsertNetworkOutageParams): Promise<number>;
|
|
206
|
+
/**
|
|
207
|
+
* Every distinct `dedupe_cap_events.shape_key` recorded in this archive.
|
|
208
|
+
* Consumed by `CrawlerOrchestrator` to preload `DedupeCapTracker`'s
|
|
209
|
+
* sticky set on `--resume` / `--append` / `--retry-failed` /
|
|
210
|
+
* `--inventory`, mirroring {@link listDnsBurnedHostCandidates}'s
|
|
211
|
+
* writer-only exposure.
|
|
212
|
+
* @returns Distinct shape keys already confirmed capped.
|
|
213
|
+
*/
|
|
214
|
+
listDedupeCapShapeKeys(): Promise<string[]>;
|
|
177
215
|
/**
|
|
178
216
|
* Hostnames whose `crawl_errors` history is consistently DNS failures and
|
|
179
217
|
* for which no recent 2xx/3xx page or resource is recorded. Consumed by
|
package/lib/archive/archive.js
CHANGED
|
@@ -94,6 +94,20 @@ export default class Archive extends ArchiveAccessor {
|
|
|
94
94
|
* @deprecated This method is no longer functional.
|
|
95
95
|
*/
|
|
96
96
|
abort() { }
|
|
97
|
+
/**
|
|
98
|
+
* Adds onto the `rejected_count` of a shape's `dedupe_cap_events` row,
|
|
99
|
+
* looked up by `shape_key` rather than `id` — used for a shape that
|
|
100
|
+
* capped in an earlier session (preloaded into `DedupeCapTracker`'s
|
|
101
|
+
* sticky set) and so has no event id from the current session.
|
|
102
|
+
*
|
|
103
|
+
* Thin facade over {@link Database.accumulateDedupeCapRejectedCount}.
|
|
104
|
+
* @param shapeKey - The capped shape whose rejection count to accumulate.
|
|
105
|
+
* @param rejectedCount - Additional anchors rejected for this shape in the current session.
|
|
106
|
+
*/
|
|
107
|
+
async accumulateDedupeCapRejectedCount(shapeKey, rejectedCount) {
|
|
108
|
+
dbLog('Accumulate dedupe cap rejected count shapeKey=%s rejectedCount=%d', shapeKey, rejectedCount);
|
|
109
|
+
return await this.#db.accumulateDedupeCapRejectedCount(shapeKey, rejectedCount);
|
|
110
|
+
}
|
|
97
111
|
/**
|
|
98
112
|
* Records a crawler-level error to both the human-readable `error.log` (full
|
|
99
113
|
* stack, for debugging) and the structured `crawl_errors` table (queryable,
|
|
@@ -135,6 +149,18 @@ export default class Archive extends ArchiveAccessor {
|
|
|
135
149
|
dbLog('Close network outage id=%d endedAt=%d', id, endedAt);
|
|
136
150
|
return await this.#db.closeNetworkOutage(id, endedAt);
|
|
137
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Finalizes a `dedupe_cap_events` row by stamping `rejected_count` — a
|
|
154
|
+
* no-op if already finalized.
|
|
155
|
+
*
|
|
156
|
+
* Thin facade over {@link Database.finalizeDedupeCapEvent}.
|
|
157
|
+
* @param id - The `dedupe_cap_events.id` to finalize.
|
|
158
|
+
* @param rejectedCount - Number of anchors rejected for this shape after it capped.
|
|
159
|
+
*/
|
|
160
|
+
async finalizeDedupeCapEvent(id, rejectedCount) {
|
|
161
|
+
dbLog('Finalize dedupe cap event id=%d rejectedCount=%d', id, rejectedCount);
|
|
162
|
+
return await this.#db.finalizeDedupeCapEvent(id, rejectedCount);
|
|
163
|
+
}
|
|
138
164
|
/**
|
|
139
165
|
* Retrieves the current crawling state, including lists of scraped and pending URLs.
|
|
140
166
|
* @returns An object with `scraped` and `pending` URL arrays.
|
|
@@ -202,6 +228,18 @@ export default class Archive extends ArchiveAccessor {
|
|
|
202
228
|
async getUrl() {
|
|
203
229
|
return this.#db.getBaseUrl();
|
|
204
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Appends one row (`rejected_count = NULL`) to the `dedupe_cap_events`
|
|
233
|
+
* journal.
|
|
234
|
+
*
|
|
235
|
+
* Thin facade over {@link Database.insertDedupeCapEvent}.
|
|
236
|
+
* @param params - The newly-capped shape's fields to record.
|
|
237
|
+
* @returns The autoincremented `id` of the inserted row.
|
|
238
|
+
*/
|
|
239
|
+
async insertDedupeCapEvent(params) {
|
|
240
|
+
dbLog('Insert dedupe cap event: shapeKey=%s', params.shapeKey);
|
|
241
|
+
return await this.#db.insertDedupeCapEvent(params);
|
|
242
|
+
}
|
|
205
243
|
/**
|
|
206
244
|
* Pre-insert inventory non-HTML URLs as `source='inventory-seed'`
|
|
207
245
|
* placeholders in the `resources` table — the non-HTML counterpart of
|
|
@@ -255,6 +293,17 @@ export default class Archive extends ArchiveAccessor {
|
|
|
255
293
|
dbLog('Insert network outage: startedAt=%d probeHost=%s', params.startedAt, params.probeHost);
|
|
256
294
|
return await this.#db.insertNetworkOutage(params);
|
|
257
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Every distinct `dedupe_cap_events.shape_key` recorded in this archive.
|
|
298
|
+
* Consumed by `CrawlerOrchestrator` to preload `DedupeCapTracker`'s
|
|
299
|
+
* sticky set on `--resume` / `--append` / `--retry-failed` /
|
|
300
|
+
* `--inventory`, mirroring {@link listDnsBurnedHostCandidates}'s
|
|
301
|
+
* writer-only exposure.
|
|
302
|
+
* @returns Distinct shape keys already confirmed capped.
|
|
303
|
+
*/
|
|
304
|
+
async listDedupeCapShapeKeys() {
|
|
305
|
+
return this.#db.listDedupeCapShapeKeys();
|
|
306
|
+
}
|
|
258
307
|
/**
|
|
259
308
|
* Hostnames whose `crawl_errors` history is consistently DNS failures and
|
|
260
309
|
* for which no recent 2xx/3xx page or resource is recorded. Consumed by
|
|
@@ -16,6 +16,9 @@ import type { Knex } from 'knex';
|
|
|
16
16
|
* - `inventory_runs` — `--inventory` audit log (no FK; append-only)
|
|
17
17
|
* - `network_outages` — operator-network-outage journal (no FK; append-only
|
|
18
18
|
* except `ended_at`, which is written once on recovery)
|
|
19
|
+
* - `dedupe_cap_events` — `--dedupe-cap` same-cluster soft-cap audit log (no
|
|
20
|
+
* FK; append-only except `rejected_count`, which is written once at
|
|
21
|
+
* `crawlEnd`)
|
|
19
22
|
* - `analysis_text_refs` + `analysis_violations` — analyze-phase findings,
|
|
20
23
|
* FK → `content_items(id)`
|
|
21
24
|
* - `page_templates` — DOM-structure template classification (`--templates`,
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* - `inventory_runs` — `--inventory` audit log (no FK; append-only)
|
|
16
16
|
* - `network_outages` — operator-network-outage journal (no FK; append-only
|
|
17
17
|
* except `ended_at`, which is written once on recovery)
|
|
18
|
+
* - `dedupe_cap_events` — `--dedupe-cap` same-cluster soft-cap audit log (no
|
|
19
|
+
* FK; append-only except `rejected_count`, which is written once at
|
|
20
|
+
* `crawlEnd`)
|
|
18
21
|
* - `analysis_text_refs` + `analysis_violations` — analyze-phase findings,
|
|
19
22
|
* FK → `content_items(id)`
|
|
20
23
|
* - `page_templates` — DOM-structure template classification (`--templates`,
|
|
@@ -326,6 +329,45 @@ export async function createAdjunctTables(instance) {
|
|
|
326
329
|
t.integer('trigger_host_count').notNullable();
|
|
327
330
|
});
|
|
328
331
|
}
|
|
332
|
+
if (!(await instance.schema.hasTable('dedupe_cap_events'))) {
|
|
333
|
+
await instance.schema.createTable('dedupe_cap_events', (t) => {
|
|
334
|
+
// One row per URL shape the `--dedupe-cap` same-cluster soft cap
|
|
335
|
+
// (`DedupeCapTracker`) confirmed as a trap during this crawl. No
|
|
336
|
+
// index: a crawl produces at most a handful of these rows (same
|
|
337
|
+
// reasoning as `network_outages`, above).
|
|
338
|
+
t.increments('id');
|
|
339
|
+
// The URL shape key (`computeShapeKey`) that capped — a template
|
|
340
|
+
// with placeholders (e.g. `example.com/news/date/{n}/`), not a
|
|
341
|
+
// literal URL.
|
|
342
|
+
t.string('shape_key').notNullable();
|
|
343
|
+
// One concrete URL matching this shape, captured at cap time so a
|
|
344
|
+
// human reading the audit log can identify what was being
|
|
345
|
+
// crawled — `shape_key` alone is a template, not a navigable URL.
|
|
346
|
+
t.string('sample_url').notNullable();
|
|
347
|
+
// `computeBodyHash` result recorded at cap time. Nullable only in
|
|
348
|
+
// the sense that BLOB columns are nullable by default; every row
|
|
349
|
+
// this feature writes populates it (a page with no rendered
|
|
350
|
+
// `<body>` never reaches the tracker — see `Crawler#handleResult`).
|
|
351
|
+
t.binary('body_hash').nullable();
|
|
352
|
+
// The Misra-Gries threshold that actually triggered the cap,
|
|
353
|
+
// after halving for the `body_hash`-match / `og:url`-mismatch
|
|
354
|
+
// confidence signals — NOT necessarily equal to `--dedupe-cap`'s
|
|
355
|
+
// raw value.
|
|
356
|
+
t.integer('effective_threshold').notNullable();
|
|
357
|
+
// The tracker's Misra-Gries counter value at cap time: a LOWER
|
|
358
|
+
// BOUND on the number of matching-signature pages seen for this
|
|
359
|
+
// shape, not an exact observation count (see `DedupeCapTracker`).
|
|
360
|
+
t.integer('observed_count').notNullable();
|
|
361
|
+
t.integer('detected_at').notNullable();
|
|
362
|
+
// NULL until `crawlEnd` finalizes it (see
|
|
363
|
+
// `Crawler#getDedupeCapRejections`). Unlike `network_outages.ended_at`,
|
|
364
|
+
// a NULL here has no ambiguous "still ongoing" reading — a
|
|
365
|
+
// crawl that never reached `crawlEnd` simply left the count
|
|
366
|
+
// undetermined, so no boot-time reconciliation pass is needed
|
|
367
|
+
// (readers display "unknown", not "0" or "unbounded").
|
|
368
|
+
t.integer('rejected_count').nullable();
|
|
369
|
+
});
|
|
370
|
+
}
|
|
329
371
|
if (!(await instance.schema.hasTable('analysis_text_refs'))) {
|
|
330
372
|
await instance.raw(`
|
|
331
373
|
CREATE TABLE analysis_text_refs (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TemplateClusterReason } from './db-ops/analysis/types.js';
|
|
2
2
|
import type { JsonLdRow, MainContentAudioRow, MainContentButtonRow, MainContentCanvasRow, MainContentHeadingRow, MainContentIframeRow, MainContentImageRow, MainContentTableRow, MainContentVideoRow, TagRow } from './meta/types.js';
|
|
3
|
-
import type { Config, DatabaseOption, DB_Redirect, DB_Resource, DatabaseEvent, InsertNetworkOutageParams, InventoryRunMeta, PageFilter, PageSource } from './types.js';
|
|
3
|
+
import type { Config, DatabaseOption, DB_Redirect, DB_Resource, DatabaseEvent, InsertDedupeCapEventParams, InsertNetworkOutageParams, InventoryRunMeta, PageFilter, PageSource } from './types.js';
|
|
4
4
|
import type { OutageWindow } from '../is-within-outage-window.js';
|
|
5
5
|
import type { PageData, Resource } from '../utils/types/types.js';
|
|
6
6
|
import type { ConsoleLogEntry } from '@d-zero/beholder';
|
|
@@ -32,6 +32,14 @@ import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-awa
|
|
|
32
32
|
export declare class Database extends EventEmitter<DatabaseEvent> {
|
|
33
33
|
#private;
|
|
34
34
|
private constructor();
|
|
35
|
+
/**
|
|
36
|
+
* Adds onto the `rejected_count` of a shape's `dedupe_cap_events` row,
|
|
37
|
+
* looked up by `shape_key` rather than `id`. Delegates to
|
|
38
|
+
* {@link accumulateDedupeCapRejectedCountOp}.
|
|
39
|
+
* @param shapeKey - The capped shape whose rejection count to accumulate.
|
|
40
|
+
* @param rejectedCount - Additional anchors rejected for this shape in the current session.
|
|
41
|
+
*/
|
|
42
|
+
accumulateDedupeCapRejectedCount(shapeKey: string, rejectedCount: number): Promise<void>;
|
|
35
43
|
/**
|
|
36
44
|
* Forces a WAL checkpoint, writing all pending WAL data back to the main
|
|
37
45
|
* database file. Delegates to {@link checkpointOp}.
|
|
@@ -49,6 +57,14 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
|
|
|
49
57
|
* Delegates to {@link destroyOp}.
|
|
50
58
|
*/
|
|
51
59
|
destroy(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Finalizes a `dedupe_cap_events` row by stamping `rejected_count` — a
|
|
62
|
+
* no-op if the row is already finalized. Delegates to
|
|
63
|
+
* {@link finalizeDedupeCapEventOp}.
|
|
64
|
+
* @param id - The `dedupe_cap_events.id` to finalize.
|
|
65
|
+
* @param rejectedCount - Number of anchors rejected for this shape after it capped.
|
|
66
|
+
*/
|
|
67
|
+
finalizeDedupeCapEvent(id: number, rejectedCount: number): Promise<void>;
|
|
52
68
|
/**
|
|
53
69
|
* Retrieves all anchors (outgoing links) on a specific page.
|
|
54
70
|
* Delegates to {@link getAnchorsOnPageOp}.
|
|
@@ -273,6 +289,13 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
|
|
|
273
289
|
* @param isExternal - Whether the URL is external to the crawl scope.
|
|
274
290
|
*/
|
|
275
291
|
insertCrawlError(url: string | null, message: string, isExternal?: boolean): Promise<void>;
|
|
292
|
+
/**
|
|
293
|
+
* Appends one row (`rejected_count = NULL`) to the `dedupe_cap_events`
|
|
294
|
+
* journal. Delegates to {@link insertDedupeCapEventOp}.
|
|
295
|
+
* @param params - The newly-capped shape's fields to record.
|
|
296
|
+
* @returns The autoincremented `id` of the newly-inserted row.
|
|
297
|
+
*/
|
|
298
|
+
insertDedupeCapEvent(params: InsertDedupeCapEventParams): Promise<number>;
|
|
276
299
|
/**
|
|
277
300
|
* Pre-insert inventory non-HTML URLs into `resources` as placeholder rows.
|
|
278
301
|
* Delegates to {@link insertInventoryResourcesOp}.
|
|
@@ -317,6 +340,13 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
|
|
|
317
340
|
* @param pageUrl - The URL of the page that references the resource.
|
|
318
341
|
*/
|
|
319
342
|
insertResourceReferrers(src: string, pageUrl: string): Promise<void>;
|
|
343
|
+
/**
|
|
344
|
+
* Every distinct `dedupe_cap_events.shape_key` recorded in this archive.
|
|
345
|
+
* Delegates to {@link listDedupeCapShapeKeysOp}.
|
|
346
|
+
* @returns Distinct shape keys already confirmed capped, or `[]` on an
|
|
347
|
+
* archive that predates `dedupe_cap_events` or has recorded none.
|
|
348
|
+
*/
|
|
349
|
+
listDedupeCapShapeKeys(): Promise<string[]>;
|
|
320
350
|
/**
|
|
321
351
|
* Hostnames whose `crawl_errors` history is consistently DNS failures and
|
|
322
352
|
* for which no recent 2xx-3xx page or resource is recorded.
|
package/lib/archive/database.js
CHANGED
|
@@ -16,6 +16,10 @@ import { getName as getNameOp } from './db-ops/config/get-name.js';
|
|
|
16
16
|
import { setConfig as setConfigOp } from './db-ops/config/set-config.js';
|
|
17
17
|
import { updateConfig as updateConfigOp } from './db-ops/config/update-config.js';
|
|
18
18
|
import { replaceConsoleLogs as replaceConsoleLogsOp } from './db-ops/console-logs/replace-console-logs.js';
|
|
19
|
+
import { accumulateDedupeCapRejectedCount as accumulateDedupeCapRejectedCountOp } from './db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.js';
|
|
20
|
+
import { finalizeDedupeCapEvent as finalizeDedupeCapEventOp } from './db-ops/dedupe-cap/finalize-dedupe-cap-event.js';
|
|
21
|
+
import { insertDedupeCapEvent as insertDedupeCapEventOp } from './db-ops/dedupe-cap/insert-dedupe-cap-event.js';
|
|
22
|
+
import { listDedupeCapShapeKeys as listDedupeCapShapeKeysOp } from './db-ops/dedupe-cap/list-dedupe-cap-shape-keys.js';
|
|
19
23
|
import { insertCrawlError as insertCrawlErrorOp } from './db-ops/errors/insert-crawl-error.js';
|
|
20
24
|
import { insertPageError as insertPageErrorOp } from './db-ops/errors/insert-page-error.js';
|
|
21
25
|
import { listDnsBurnedHostCandidates as listDnsBurnedHostCandidatesOp } from './db-ops/errors/list-dns-burned-host-candidates.js';
|
|
@@ -118,6 +122,16 @@ export class Database extends EventEmitter {
|
|
|
118
122
|
},
|
|
119
123
|
});
|
|
120
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Adds onto the `rejected_count` of a shape's `dedupe_cap_events` row,
|
|
127
|
+
* looked up by `shape_key` rather than `id`. Delegates to
|
|
128
|
+
* {@link accumulateDedupeCapRejectedCountOp}.
|
|
129
|
+
* @param shapeKey - The capped shape whose rejection count to accumulate.
|
|
130
|
+
* @param rejectedCount - Additional anchors rejected for this shape in the current session.
|
|
131
|
+
*/
|
|
132
|
+
async accumulateDedupeCapRejectedCount(shapeKey, rejectedCount) {
|
|
133
|
+
return emitErrorAndRetry(this, 'Database.accumulateDedupeCapRejectedCount', async () => await accumulateDedupeCapRejectedCountOp(this.#instance, shapeKey, rejectedCount), retrySetting);
|
|
134
|
+
}
|
|
121
135
|
/**
|
|
122
136
|
* Forces a WAL checkpoint, writing all pending WAL data back to the main
|
|
123
137
|
* database file. Delegates to {@link checkpointOp}.
|
|
@@ -141,6 +155,16 @@ export class Database extends EventEmitter {
|
|
|
141
155
|
async destroy() {
|
|
142
156
|
await destroyOp(this.#instance);
|
|
143
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Finalizes a `dedupe_cap_events` row by stamping `rejected_count` — a
|
|
160
|
+
* no-op if the row is already finalized. Delegates to
|
|
161
|
+
* {@link finalizeDedupeCapEventOp}.
|
|
162
|
+
* @param id - The `dedupe_cap_events.id` to finalize.
|
|
163
|
+
* @param rejectedCount - Number of anchors rejected for this shape after it capped.
|
|
164
|
+
*/
|
|
165
|
+
async finalizeDedupeCapEvent(id, rejectedCount) {
|
|
166
|
+
return emitErrorAndRetry(this, 'Database.finalizeDedupeCapEvent', async () => await finalizeDedupeCapEventOp(this.#instance, id, rejectedCount), retrySetting);
|
|
167
|
+
}
|
|
144
168
|
/**
|
|
145
169
|
* Retrieves all anchors (outgoing links) on a specific page.
|
|
146
170
|
* Delegates to {@link getAnchorsOnPageOp}.
|
|
@@ -422,6 +446,15 @@ export class Database extends EventEmitter {
|
|
|
422
446
|
async insertCrawlError(url, message, isExternal = false) {
|
|
423
447
|
return emitErrorAndRetry(this, 'Database.insertCrawlError', async () => await insertCrawlErrorOp(this.#instance, url, message, isExternal), retrySetting);
|
|
424
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* Appends one row (`rejected_count = NULL`) to the `dedupe_cap_events`
|
|
451
|
+
* journal. Delegates to {@link insertDedupeCapEventOp}.
|
|
452
|
+
* @param params - The newly-capped shape's fields to record.
|
|
453
|
+
* @returns The autoincremented `id` of the newly-inserted row.
|
|
454
|
+
*/
|
|
455
|
+
async insertDedupeCapEvent(params) {
|
|
456
|
+
return emitErrorAndRetry(this, 'Database.insertDedupeCapEvent', async () => await insertDedupeCapEventOp(this.#instance, params), retrySetting);
|
|
457
|
+
}
|
|
425
458
|
/**
|
|
426
459
|
* Pre-insert inventory non-HTML URLs into `resources` as placeholder rows.
|
|
427
460
|
* Delegates to {@link insertInventoryResourcesOp}.
|
|
@@ -478,6 +511,15 @@ export class Database extends EventEmitter {
|
|
|
478
511
|
async insertResourceReferrers(src, pageUrl) {
|
|
479
512
|
return emitErrorAndRetry(this, 'Database.insertResourceReferrers', async () => await insertResourceReferrersOp(this.#instance, this.#writeRefCaches, src, pageUrl), retrySetting);
|
|
480
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Every distinct `dedupe_cap_events.shape_key` recorded in this archive.
|
|
516
|
+
* Delegates to {@link listDedupeCapShapeKeysOp}.
|
|
517
|
+
* @returns Distinct shape keys already confirmed capped, or `[]` on an
|
|
518
|
+
* archive that predates `dedupe_cap_events` or has recorded none.
|
|
519
|
+
*/
|
|
520
|
+
async listDedupeCapShapeKeys() {
|
|
521
|
+
return emitErrorAndRetry(this, 'Database.listDedupeCapShapeKeys', async () => await listDedupeCapShapeKeysOp(this.#instance), retrySetting);
|
|
522
|
+
}
|
|
481
523
|
/**
|
|
482
524
|
* Hostnames whose `crawl_errors` history is consistently DNS failures and
|
|
483
525
|
* for which no recent 2xx-3xx page or resource is recorded.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Adds `rejectedCount` onto the `rejected_count` of the `dedupe_cap_events`
|
|
4
|
+
* row for `shapeKey`, treating a still-`NULL` count as `0`. Unlike
|
|
5
|
+
* `finalize-dedupe-cap-event.ts` (which stamps a session's own newly-capped
|
|
6
|
+
* shape exactly once, guarded by `whereNull`), this targets a shape that
|
|
7
|
+
* capped in an EARLIER session and was preloaded into `DedupeCapTracker`'s
|
|
8
|
+
* sticky set (see `DedupeCapTracker`'s constructor JSDoc) — gate rejections
|
|
9
|
+
* for such a shape still occur in the current session, but no `dedupeCap`
|
|
10
|
+
* event (and thus no new row) is ever emitted for it, since the tracker
|
|
11
|
+
* short-circuits on an already-sticky shape before `observe` runs. Matches
|
|
12
|
+
* by `shape_key` rather than `id` because the caller (`CrawlerOrchestrator`)
|
|
13
|
+
* only has the shape key for a preloaded-sticky shape, never its row id.
|
|
14
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
15
|
+
* @param shapeKey - The capped shape whose rejection count to accumulate.
|
|
16
|
+
* @param rejectedCount - Additional anchors rejected for this shape in the current session.
|
|
17
|
+
*/
|
|
18
|
+
export declare function accumulateDedupeCapRejectedCount(knex: Knex, shapeKey: string, rejectedCount: number): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds `rejectedCount` onto the `rejected_count` of the `dedupe_cap_events`
|
|
3
|
+
* row for `shapeKey`, treating a still-`NULL` count as `0`. Unlike
|
|
4
|
+
* `finalize-dedupe-cap-event.ts` (which stamps a session's own newly-capped
|
|
5
|
+
* shape exactly once, guarded by `whereNull`), this targets a shape that
|
|
6
|
+
* capped in an EARLIER session and was preloaded into `DedupeCapTracker`'s
|
|
7
|
+
* sticky set (see `DedupeCapTracker`'s constructor JSDoc) — gate rejections
|
|
8
|
+
* for such a shape still occur in the current session, but no `dedupeCap`
|
|
9
|
+
* event (and thus no new row) is ever emitted for it, since the tracker
|
|
10
|
+
* short-circuits on an already-sticky shape before `observe` runs. Matches
|
|
11
|
+
* by `shape_key` rather than `id` because the caller (`CrawlerOrchestrator`)
|
|
12
|
+
* only has the shape key for a preloaded-sticky shape, never its row id.
|
|
13
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
14
|
+
* @param shapeKey - The capped shape whose rejection count to accumulate.
|
|
15
|
+
* @param rejectedCount - Additional anchors rejected for this shape in the current session.
|
|
16
|
+
*/
|
|
17
|
+
export async function accumulateDedupeCapRejectedCount(knex, shapeKey, rejectedCount) {
|
|
18
|
+
await knex('dedupe_cap_events')
|
|
19
|
+
.where({ shape_key: shapeKey })
|
|
20
|
+
.update({
|
|
21
|
+
rejected_count: knex.raw('COALESCE(rejected_count, 0) + ?', [rejectedCount]),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Finalizes a `dedupe_cap_events` row by stamping `rejected_count` — but
|
|
4
|
+
* ONLY if it is still unset. The `whereNull('rejected_count')` guard makes
|
|
5
|
+
* this idempotent, mirroring `close-network-outage.ts`'s `ended_at` guard: a
|
|
6
|
+
* second call matches zero rows and is a silent no-op rather than
|
|
7
|
+
* overwriting an already-finalized count.
|
|
8
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
9
|
+
* @param id - The `dedupe_cap_events.id` to finalize.
|
|
10
|
+
* @param rejectedCount - Number of anchors rejected for this shape after it capped.
|
|
11
|
+
*/
|
|
12
|
+
export declare function finalizeDedupeCapEvent(knex: Knex, id: number, rejectedCount: number): Promise<void>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finalizes a `dedupe_cap_events` row by stamping `rejected_count` — but
|
|
3
|
+
* ONLY if it is still unset. The `whereNull('rejected_count')` guard makes
|
|
4
|
+
* this idempotent, mirroring `close-network-outage.ts`'s `ended_at` guard: a
|
|
5
|
+
* second call matches zero rows and is a silent no-op rather than
|
|
6
|
+
* overwriting an already-finalized count.
|
|
7
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
8
|
+
* @param id - The `dedupe_cap_events.id` to finalize.
|
|
9
|
+
* @param rejectedCount - Number of anchors rejected for this shape after it capped.
|
|
10
|
+
*/
|
|
11
|
+
export async function finalizeDedupeCapEvent(knex, id, rejectedCount) {
|
|
12
|
+
await knex('dedupe_cap_events').where({ id }).whereNull('rejected_count').update({
|
|
13
|
+
rejected_count: rejectedCount,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InsertDedupeCapEventParams } from '../../types.js';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
/**
|
|
4
|
+
* Appends one row to the `dedupe_cap_events` journal, with `rejected_count`
|
|
5
|
+
* left `NULL` — the row starts life without a finalized rejection count.
|
|
6
|
+
*
|
|
7
|
+
* Called the instant `DedupeCapTracker#observe` confirms a URL shape as a
|
|
8
|
+
* same-cluster trap (the `dedupeCap` event). See
|
|
9
|
+
* `finalize-dedupe-cap-event.ts` for how `rejected_count` is later set.
|
|
10
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
11
|
+
* @param params - The newly-capped shape's fields to record.
|
|
12
|
+
* @returns The autoincremented `id` of the newly-inserted row.
|
|
13
|
+
*/
|
|
14
|
+
export declare function insertDedupeCapEvent(knex: Knex, params: InsertDedupeCapEventParams): Promise<number>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appends one row to the `dedupe_cap_events` journal, with `rejected_count`
|
|
3
|
+
* left `NULL` — the row starts life without a finalized rejection count.
|
|
4
|
+
*
|
|
5
|
+
* Called the instant `DedupeCapTracker#observe` confirms a URL shape as a
|
|
6
|
+
* same-cluster trap (the `dedupeCap` event). See
|
|
7
|
+
* `finalize-dedupe-cap-event.ts` for how `rejected_count` is later set.
|
|
8
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
9
|
+
* @param params - The newly-capped shape's fields to record.
|
|
10
|
+
* @returns The autoincremented `id` of the newly-inserted row.
|
|
11
|
+
*/
|
|
12
|
+
export async function insertDedupeCapEvent(knex, params) {
|
|
13
|
+
const inserted = await knex
|
|
14
|
+
.from('dedupe_cap_events')
|
|
15
|
+
.insert({
|
|
16
|
+
shape_key: params.shapeKey,
|
|
17
|
+
sample_url: params.sampleUrl,
|
|
18
|
+
body_hash: params.bodyHash,
|
|
19
|
+
effective_threshold: params.effectiveThreshold,
|
|
20
|
+
observed_count: params.observedCount,
|
|
21
|
+
detected_at: params.detectedAt,
|
|
22
|
+
rejected_count: null,
|
|
23
|
+
})
|
|
24
|
+
.returning('id');
|
|
25
|
+
const id = inserted[0]?.id;
|
|
26
|
+
if (typeof id !== 'number') {
|
|
27
|
+
throw new TypeError('insertDedupeCapEvent: INSERT returned no row id');
|
|
28
|
+
}
|
|
29
|
+
return id;
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Every distinct `dedupe_cap_events.shape_key` recorded in this archive —
|
|
4
|
+
* used by `CrawlerOrchestrator` to preload `DedupeCapTracker`'s sticky set
|
|
5
|
+
* on `--resume` / `--append` / `--retry-failed` / `--inventory`, so a trap
|
|
6
|
+
* this crawl already paid the cost of discovering once is not re-admitted
|
|
7
|
+
* in a later session. Fresh (non-resuming) crawls do not call this — there
|
|
8
|
+
* is no archive history to seed from.
|
|
9
|
+
*
|
|
10
|
+
* Unlike `listDnsBurnedHostCandidates`, no additional exclusion logic is
|
|
11
|
+
* needed: once `DedupeCapTracker` confirms a shape as a trap, it stays
|
|
12
|
+
* confirmed — there is no equivalent of "the host might have recovered
|
|
13
|
+
* since".
|
|
14
|
+
*
|
|
15
|
+
* Returns `[]` on legacy archives that pre-date the `dedupe_cap_events`
|
|
16
|
+
* table (self-healed on next writer open, so this is never a permanent
|
|
17
|
+
* state) or that have recorded no capped shapes.
|
|
18
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
19
|
+
* @returns Distinct shape keys already confirmed capped.
|
|
20
|
+
*/
|
|
21
|
+
export declare function listDedupeCapShapeKeys(knex: Knex): Promise<string[]>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every distinct `dedupe_cap_events.shape_key` recorded in this archive —
|
|
3
|
+
* used by `CrawlerOrchestrator` to preload `DedupeCapTracker`'s sticky set
|
|
4
|
+
* on `--resume` / `--append` / `--retry-failed` / `--inventory`, so a trap
|
|
5
|
+
* this crawl already paid the cost of discovering once is not re-admitted
|
|
6
|
+
* in a later session. Fresh (non-resuming) crawls do not call this — there
|
|
7
|
+
* is no archive history to seed from.
|
|
8
|
+
*
|
|
9
|
+
* Unlike `listDnsBurnedHostCandidates`, no additional exclusion logic is
|
|
10
|
+
* needed: once `DedupeCapTracker` confirms a shape as a trap, it stays
|
|
11
|
+
* confirmed — there is no equivalent of "the host might have recovered
|
|
12
|
+
* since".
|
|
13
|
+
*
|
|
14
|
+
* Returns `[]` on legacy archives that pre-date the `dedupe_cap_events`
|
|
15
|
+
* table (self-healed on next writer open, so this is never a permanent
|
|
16
|
+
* state) or that have recorded no capped shapes.
|
|
17
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
18
|
+
* @returns Distinct shape keys already confirmed capped.
|
|
19
|
+
*/
|
|
20
|
+
export async function listDedupeCapShapeKeys(knex) {
|
|
21
|
+
const hasTable = await knex.schema.hasTable('dedupe_cap_events');
|
|
22
|
+
if (!hasTable) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const rows = (await knex('dedupe_cap_events').distinct('shape_key'));
|
|
26
|
+
return rows.map((row) => row.shape_key);
|
|
27
|
+
}
|
package/lib/archive/types.d.ts
CHANGED
|
@@ -186,6 +186,19 @@ export interface InsertNetworkOutageParams {
|
|
|
186
186
|
triggerErrorCount: number;
|
|
187
187
|
triggerHostCount: number;
|
|
188
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Fields required to record a newly-capped URL shape via
|
|
191
|
+
* `Database.insertDedupeCapEvent`. camelCase, mapped to snake_case columns
|
|
192
|
+
* on write — same convention as {@link InsertNetworkOutageParams}.
|
|
193
|
+
*/
|
|
194
|
+
export interface InsertDedupeCapEventParams {
|
|
195
|
+
shapeKey: string;
|
|
196
|
+
sampleUrl: string;
|
|
197
|
+
bodyHash: Buffer;
|
|
198
|
+
effectiveThreshold: number;
|
|
199
|
+
observedCount: number;
|
|
200
|
+
detectedAt: number;
|
|
201
|
+
}
|
|
189
202
|
/**
|
|
190
203
|
* Filter type for querying pages from the database.
|
|
191
204
|
*
|
|
@@ -14,6 +14,7 @@ import type { ErrorKind } from './types.js';
|
|
|
14
14
|
* classifyErrorKind('getaddrinfo ENOTFOUND www.example.com'); // 'dns'
|
|
15
15
|
* classifyErrorKind('gave up after 3 retries — Race 180,000ms'); // 'timeout'
|
|
16
16
|
* classifyErrorKind('Protocol error (Page.reload): Target closed'); // 'protocol'
|
|
17
|
+
* classifyErrorKind('Maximum number of redirects exceeded'); // 'redirect-loop'
|
|
17
18
|
* ```
|
|
18
19
|
*/
|
|
19
20
|
export declare function classifyErrorKind(message: string): ErrorKind;
|
|
@@ -53,6 +53,19 @@ const MATCHERS = [
|
|
|
53
53
|
kind: 'parse-error',
|
|
54
54
|
pattern: /Parse Error|Expected HTTP\/|Unexpected end of stream/i,
|
|
55
55
|
},
|
|
56
|
+
// `follow-redirects` (pinned at 1.16.0, `fetch-destination.ts`'s HEAD/GET
|
|
57
|
+
// pre-flight) throws exactly "Maximum number of redirects exceeded" —
|
|
58
|
+
// no cause token, no URL — when a chain never terminates within its
|
|
59
|
+
// `maxRedirects` budget. `ERR_TOO_MANY_REDIRECTS` is the equivalent
|
|
60
|
+
// Chromium net-error code, covering the same symptom surfaced through a
|
|
61
|
+
// puppeteer navigation instead of the Node HTTP client. Both mean the
|
|
62
|
+
// SAME thing: the site's own redirect configuration loops and will loop
|
|
63
|
+
// again on any future fetch, which is why this is deterministic (not a
|
|
64
|
+
// transient network condition) — see `PERMANENT_ERROR_KINDS`.
|
|
65
|
+
{
|
|
66
|
+
kind: 'redirect-loop',
|
|
67
|
+
pattern: /Maximum number of redirects exceeded|ERR_TOO_MANY_REDIRECTS/i,
|
|
68
|
+
},
|
|
56
69
|
// `client-blocked` covers Chromium's ERR_BLOCKED_* family — the browser
|
|
57
70
|
// actively decided to reject the request (ad/tracker heuristics, CSP,
|
|
58
71
|
// CORB / ORB, administrator block list, fingerprinting protection,
|
|
@@ -110,6 +123,7 @@ const MATCHERS = [
|
|
|
110
123
|
* classifyErrorKind('getaddrinfo ENOTFOUND www.example.com'); // 'dns'
|
|
111
124
|
* classifyErrorKind('gave up after 3 retries — Race 180,000ms'); // 'timeout'
|
|
112
125
|
* classifyErrorKind('Protocol error (Page.reload): Target closed'); // 'protocol'
|
|
126
|
+
* classifyErrorKind('Maximum number of redirects exceeded'); // 'redirect-loop'
|
|
113
127
|
* ```
|
|
114
128
|
*/
|
|
115
129
|
export function classifyErrorKind(message) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies that Puppeteer can resolve an installed Chrome/Chromium executable
|
|
3
|
+
* before a crawl starts.
|
|
4
|
+
*
|
|
5
|
+
* A crawl otherwise only discovers a missing browser deep inside the
|
|
6
|
+
* per-URL scrape loop (`Crawler#_launchBrowserAndScrape`), where it surfaces
|
|
7
|
+
* as one more scrape error among many — the CLI still prints "Crawl
|
|
8
|
+
* completed" and writes an archive, so a missing Chrome (a fatal
|
|
9
|
+
* precondition, not a per-page failure) is easy to miss. Calling this once,
|
|
10
|
+
* before any archive I/O begins, turns it into an immediate, actionable
|
|
11
|
+
* failure instead.
|
|
12
|
+
* @param executablePath - Explicit override, matching
|
|
13
|
+
* {@link CrawlerOptions.executablePath}. Pass `null` (or omit) to check
|
|
14
|
+
* Puppeteer's own pinned Chrome resolution instead.
|
|
15
|
+
* @throws {Error} When the resolved executable path does not exist on disk.
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { assertChromeIsInstalled } from '@nitpicker/crawler';
|
|
19
|
+
*
|
|
20
|
+
* // Throws with install instructions before any crawl work starts.
|
|
21
|
+
* await assertChromeIsInstalled();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function assertChromeIsInstalled(executablePath?: string | null): Promise<void>;
|