@nitpicker/crawler 0.14.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 +136 -10
- package/lib/archive/archive.js +147 -9
- package/lib/archive/body-hash/compute-body-hash.d.ts +22 -0
- package/lib/archive/body-hash/compute-body-hash.js +31 -0
- package/lib/archive/body-hash/extract-body.d.ts +18 -0
- package/lib/archive/body-hash/extract-body.js +29 -0
- package/lib/archive/body-hash/mask-dynamic-ids.d.ts +33 -0
- package/lib/archive/body-hash/mask-dynamic-ids.js +44 -0
- package/lib/archive/body-hash/normalize-url-like-strings.d.ts +16 -0
- package/lib/archive/body-hash/normalize-url-like-strings.js +19 -0
- package/lib/archive/cache/clear-archive-cache-entry.d.ts +19 -0
- package/lib/archive/cache/clear-archive-cache-entry.js +25 -0
- package/lib/archive/cache/clear-archive-cache-root.d.ts +26 -0
- package/lib/archive/cache/clear-archive-cache-root.js +32 -0
- package/lib/archive/cache/list-archive-cache-entries.d.ts +23 -0
- package/lib/archive/cache/list-archive-cache-entries.js +125 -0
- package/lib/archive/cache/path-exists.d.ts +9 -0
- package/lib/archive/cache/path-exists.js +18 -0
- package/lib/archive/cache/types.d.ts +33 -0
- package/lib/archive/cache/types.js +1 -0
- package/lib/archive/create-adjunct-tables.d.ts +14 -0
- package/lib/archive/create-adjunct-tables.js +169 -0
- package/lib/archive/create-entity-tables.js +21 -1
- package/lib/archive/database.d.ts +67 -2
- package/lib/archive/database.js +92 -3
- package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +1 -0
- package/lib/archive/db-ops/_shared/compress-payload.d.ts +26 -0
- package/lib/archive/db-ops/_shared/compress-payload.js +30 -0
- package/lib/archive/db-ops/_shared/create-write-ref-caches.js +1 -0
- package/lib/archive/db-ops/_shared/types.d.ts +2 -0
- package/lib/archive/db-ops/_shared/upsert-json-ref.js +3 -3
- package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +12 -3
- package/lib/archive/db-ops/analysis/replace-page-templates.js +35 -3
- package/lib/archive/db-ops/analysis/types.d.ts +61 -0
- package/lib/archive/db-ops/analysis/types.js +1 -0
- package/lib/archive/db-ops/console-logs/compute-console-log-hash.d.ts +19 -0
- package/lib/archive/db-ops/console-logs/compute-console-log-hash.js +30 -0
- package/lib/archive/db-ops/console-logs/replace-console-logs.d.ts +34 -0
- package/lib/archive/db-ops/console-logs/replace-console-logs.js +123 -0
- package/lib/archive/db-ops/console-logs/stringify-console-log-args.d.ts +16 -0
- package/lib/archive/db-ops/console-logs/stringify-console-log-args.js +26 -0
- package/lib/archive/db-ops/console-logs/types.d.ts +38 -0
- package/lib/archive/db-ops/console-logs/types.js +1 -0
- package/lib/archive/db-ops/console-logs/upsert-console-log-item.d.ts +29 -0
- package/lib/archive/db-ops/console-logs/upsert-console-log-item.js +52 -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/db-ops/errors/list-dns-burned-host-candidates.d.ts +11 -0
- package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +21 -4
- package/lib/archive/db-ops/inventory/record-inventory-run.js +1 -0
- package/lib/archive/db-ops/lifecycle/init.d.ts +11 -2
- package/lib/archive/db-ops/lifecycle/init.js +21 -2
- package/lib/archive/db-ops/outages/close-network-outage.d.ts +12 -0
- package/lib/archive/db-ops/outages/close-network-outage.js +15 -0
- package/lib/archive/db-ops/outages/close-stale-open-network-outages.d.ts +26 -0
- package/lib/archive/db-ops/outages/close-stale-open-network-outages.js +42 -0
- package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.d.ts +26 -0
- package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.js +31 -0
- package/lib/archive/db-ops/outages/insert-network-outage.d.ts +14 -0
- package/lib/archive/db-ops/outages/insert-network-outage.js +29 -0
- package/lib/archive/db-ops/outages/list-network-outages.d.ts +28 -0
- package/lib/archive/db-ops/outages/list-network-outages.js +42 -0
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +13 -0
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +35 -7
- package/lib/archive/db-ops/pages/write/update-page.js +12 -0
- package/lib/archive/db-ops/pages/write/write-page-html-blob.js +6 -6
- package/lib/archive/filesystem/output-binary.d.ts +12 -0
- package/lib/archive/filesystem/output-binary.js +17 -0
- package/lib/archive/get-failed-page-messages.d.ts +20 -8
- package/lib/archive/get-failed-page-messages.js +29 -15
- package/lib/archive/migrate-content-items-alias-of-id.d.ts +47 -0
- package/lib/archive/migrate-content-items-alias-of-id.js +58 -0
- package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +11 -0
- package/lib/archive/migrate-inventory-runs-invalid-skipped.js +24 -0
- package/lib/archive/migrate-page-meta-body-hash.d.ts +39 -0
- package/lib/archive/migrate-page-meta-body-hash.js +52 -0
- package/lib/archive/migrate-page-meta-console-error-count.d.ts +22 -0
- package/lib/archive/migrate-page-meta-console-error-count.js +34 -0
- package/lib/archive/page.d.ts +1 -1
- package/lib/archive/types.d.ts +60 -2
- package/lib/archive/url-alias/compute-tier-a-alias-key.d.ts +31 -0
- package/lib/archive/url-alias/compute-tier-a-alias-key.js +36 -0
- package/lib/archive/url-alias/compute-tier-b-alias-key.d.ts +19 -0
- package/lib/archive/url-alias/compute-tier-b-alias-key.js +30 -0
- package/lib/archive/url-alias/format-alias-key.d.ts +13 -0
- package/lib/archive/url-alias/format-alias-key.js +14 -0
- package/lib/archive/url-alias/parse-alias-key-parts.d.ts +15 -0
- package/lib/archive/url-alias/parse-alias-key-parts.js +40 -0
- package/lib/archive/url-alias/types.d.ts +7 -0
- package/lib/archive/url-alias/types.js +1 -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/choose-probe-host.d.ts +24 -0
- package/lib/crawler/choose-probe-host.js +38 -0
- package/lib/crawler/clear-dns-burned-host-cache.js +2 -0
- package/lib/crawler/crawler.d.ts +12 -0
- package/lib/crawler/crawler.js +496 -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/dns-burned-host-burn-timestamps.d.ts +17 -0
- package/lib/crawler/dns-burned-host-burn-timestamps.js +17 -0
- package/lib/crawler/evict-network-classified-destination-cache-entries.d.ts +26 -0
- package/lib/crawler/evict-network-classified-destination-cache-entries.js +34 -0
- package/lib/crawler/evict-outage-tainted-dns-burns.d.ts +23 -0
- package/lib/crawler/evict-outage-tainted-dns-burns.js +26 -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/network-gate.d.ts +49 -0
- package/lib/crawler/network-gate.js +78 -0
- package/lib/crawler/network-outage-detector.d.ts +51 -0
- package/lib/crawler/network-outage-detector.js +81 -0
- package/lib/crawler/network-outage-summary-counter.d.ts +21 -0
- package/lib/crawler/network-outage-summary-counter.js +18 -0
- package/lib/crawler/probe-network.d.ts +26 -0
- package/lib/crawler/probe-network.js +21 -0
- package/lib/crawler/types.d.ts +185 -1
- package/lib/crawler-orchestrator.d.ts +74 -18
- package/lib/crawler-orchestrator.js +267 -27
- package/lib/crawler.d.ts +22 -0
- package/lib/crawler.js +18 -0
- package/lib/is-within-outage-window.d.ts +49 -0
- package/lib/is-within-outage-window.js +33 -0
- package/lib/network-related-error-kinds.d.ts +23 -0
- package/lib/network-related-error-kinds.js +28 -0
- package/lib/permanent-error-kinds.d.ts +9 -4
- package/lib/permanent-error-kinds.js +10 -4
- package/lib/types.d.ts +11 -2
- package/lib/utils/array/each-splitted.d.ts +1 -1
- package/lib/utils/compute-file-sha256.d.ts +17 -17
- package/lib/utils/compute-file-sha256.js +18 -47
- package/package.json +3 -3
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds the `content_items.alias_of_id` column to archives created before
|
|
3
|
+
* this feature, then ensures its index exists.
|
|
4
|
+
*
|
|
5
|
+
* `content_items` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
|
|
6
|
+
* {@link import('./create-entity-tables.js').createEntityTables}, which
|
|
7
|
+
* self-heals a *missing table* on every `initSchema` call but is a no-op
|
|
8
|
+
* against an *existing* table — adding a column to the DDL string never
|
|
9
|
+
* reaches an archive whose `content_items` predates this change. This
|
|
10
|
+
* mirrors {@link import('./migrate-page-meta-body-hash.js').migratePageMetaBodyHash}'s
|
|
11
|
+
* catch-up: a `hasColumn`-guarded `ALTER TABLE` for the one column
|
|
12
|
+
* `CREATE TABLE IF NOT EXISTS` cannot retrofit.
|
|
13
|
+
*
|
|
14
|
+
* Uses a raw `ALTER TABLE` (not the knex schema builder) so the retrofitted
|
|
15
|
+
* column's `REFERENCES content_items(id) DEFERRABLE INITIALLY DEFERRED`
|
|
16
|
+
* constraint matches the fresh-archive DDL bit-for-bit — the same
|
|
17
|
+
* self-referencing shape as `redirect_dest_id` (see
|
|
18
|
+
* `create-entity-tables.ts`'s JSDoc on why that FK must be deferred: a
|
|
19
|
+
* lower-id row can reference a higher-id row within the same write).
|
|
20
|
+
*
|
|
21
|
+
* The index is created here — unconditionally, after the column-add guard,
|
|
22
|
+
* not inside `createEntityTables`'s DDL — for both a fresh archive (where
|
|
23
|
+
* `alias_of_id` already exists from the DDL, so only the index still needs
|
|
24
|
+
* creating) and a legacy archive (where the column is added just above,
|
|
25
|
+
* then the index follows in the same call). `createEntityTables` runs
|
|
26
|
+
* unconditionally on every archive open, including legacy archives that
|
|
27
|
+
* still lack `alias_of_id` at that point; an unconditional
|
|
28
|
+
* `CREATE INDEX ... alias_of_id` there would fail with `no such column`
|
|
29
|
+
* before this migration ever runs. This function is the one place
|
|
30
|
+
* guaranteed to run only after the column is confirmed present, for both
|
|
31
|
+
* archive kinds.
|
|
32
|
+
*
|
|
33
|
+
* Only adds the column and its index — it does not compute values for
|
|
34
|
+
* existing rows (they stay `NULL`). That computation runs separately, from
|
|
35
|
+
* `backfillAliasOfId` during a viewer-read-model build, since it requires
|
|
36
|
+
* comparing every page's title and (for the trailing-slash tier) its
|
|
37
|
+
* `body_hash` against every other page.
|
|
38
|
+
*
|
|
39
|
+
* Idempotent: adding the column is a no-op once it exists (the index
|
|
40
|
+
* creation always runs, but `IF NOT EXISTS` makes repeat runs a no-op too).
|
|
41
|
+
* Guards on `content_items`'s existence defensively, though by the time this
|
|
42
|
+
* runs (after `initSchema`, itself after `assertCompatibleVersion` rejects
|
|
43
|
+
* pre-0.13 archives) the table is always present.
|
|
44
|
+
* @param instance - The Knex query builder instance connected to the database.
|
|
45
|
+
*/
|
|
46
|
+
export async function migrateContentItemsAliasOfId(instance) {
|
|
47
|
+
const hasContentItems = await instance.schema.hasTable('content_items');
|
|
48
|
+
if (!hasContentItems) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const hasColumn = await instance.schema.hasColumn('content_items', 'alias_of_id');
|
|
52
|
+
if (!hasColumn) {
|
|
53
|
+
await instance.raw('ALTER TABLE content_items ADD COLUMN alias_of_id INTEGER REFERENCES content_items(id) DEFERRABLE INITIALLY DEFERRED');
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.error('[migrate] content_items.alias_of_id column added');
|
|
56
|
+
}
|
|
57
|
+
await instance.raw('CREATE INDEX IF NOT EXISTS idx_content_items_alias_of_id ON content_items(alias_of_id)');
|
|
58
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Adds the `inventory_runs.invalid_skipped` column to archives created
|
|
4
|
+
* before it existed. `CREATE TABLE IF NOT EXISTS` (used for `inventory_runs`
|
|
5
|
+
* itself) cannot retrofit a new column onto an already-existing table, so
|
|
6
|
+
* this lightweight, `hasColumn`-guarded `ALTER TABLE` runs on every
|
|
7
|
+
* `initSchema` call — idempotent, and self-healing for archives whose
|
|
8
|
+
* provisioning crashed partway through.
|
|
9
|
+
* @param instance - The Knex query builder instance connected to the database.
|
|
10
|
+
*/
|
|
11
|
+
export declare function migrateInventoryRunsInvalidSkipped(instance: Knex): Promise<void>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds the `inventory_runs.invalid_skipped` column to archives created
|
|
3
|
+
* before it existed. `CREATE TABLE IF NOT EXISTS` (used for `inventory_runs`
|
|
4
|
+
* itself) cannot retrofit a new column onto an already-existing table, so
|
|
5
|
+
* this lightweight, `hasColumn`-guarded `ALTER TABLE` runs on every
|
|
6
|
+
* `initSchema` call — idempotent, and self-healing for archives whose
|
|
7
|
+
* provisioning crashed partway through.
|
|
8
|
+
* @param instance - The Knex query builder instance connected to the database.
|
|
9
|
+
*/
|
|
10
|
+
export async function migrateInventoryRunsInvalidSkipped(instance) {
|
|
11
|
+
const hasTable = await instance.schema.hasTable('inventory_runs');
|
|
12
|
+
if (!hasTable) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const hasColumn = await instance.schema.hasColumn('inventory_runs', 'invalid_skipped');
|
|
16
|
+
if (hasColumn) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
await instance.schema.table('inventory_runs', (t) => {
|
|
20
|
+
t.integer('invalid_skipped');
|
|
21
|
+
});
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
console.error('[migrate] inventory_runs.invalid_skipped column added');
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Adds the `page_meta.body_hash` column to archives created before this
|
|
4
|
+
* feature, then ensures its index exists.
|
|
5
|
+
*
|
|
6
|
+
* `page_meta` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
|
|
7
|
+
* {@link import('./create-entity-tables.js').createEntityTables}, which
|
|
8
|
+
* self-heals a *missing table* on every `initSchema` call but is a no-op
|
|
9
|
+
* against an *existing* table — adding a column to the DDL string never
|
|
10
|
+
* reaches an archive whose `page_meta` predates this change. This mirrors
|
|
11
|
+
* {@link import('./migrate-main-contents-columns.js').migrateMainContentsColumns}'s
|
|
12
|
+
* catch-up: a `hasColumn`-guarded `ALTER TABLE` for the one column
|
|
13
|
+
* `CREATE TABLE IF NOT EXISTS` cannot retrofit.
|
|
14
|
+
*
|
|
15
|
+
* The index is created here — unconditionally, after the column-add guard,
|
|
16
|
+
* not inside `createEntityTables`'s DDL — for both a fresh archive (where
|
|
17
|
+
* `body_hash` already exists from the DDL, so only the index still needs
|
|
18
|
+
* creating) and a legacy archive (where the column is added just above,
|
|
19
|
+
* then the index follows in the same call). `createEntityTables` runs
|
|
20
|
+
* unconditionally on every archive open, including legacy archives that
|
|
21
|
+
* still lack `body_hash` at that point; an unconditional
|
|
22
|
+
* `CREATE INDEX ... body_hash` there would fail with `no such column` before
|
|
23
|
+
* this migration ever runs. This function is the one place guaranteed to
|
|
24
|
+
* run only after the column is confirmed present, for both archive kinds.
|
|
25
|
+
*
|
|
26
|
+
* Only adds the column and its index — it does not backfill values for
|
|
27
|
+
* existing rows (they stay `NULL`). That backfill runs separately, from
|
|
28
|
+
* `backfillBodyHashFromHtmlBlobs` during a viewer-read-model build, since it
|
|
29
|
+
* requires decompressing every page's stored HTML and is too heavy to run on
|
|
30
|
+
* every archive open.
|
|
31
|
+
*
|
|
32
|
+
* Idempotent: adding the column is a no-op once it exists (the index
|
|
33
|
+
* creation always runs, but `IF NOT EXISTS` makes repeat runs a no-op too).
|
|
34
|
+
* Guards on `page_meta`'s existence defensively, though by the time this
|
|
35
|
+
* runs (after `initSchema`, itself after `assertCompatibleVersion` rejects
|
|
36
|
+
* pre-0.13 archives) the table is always present.
|
|
37
|
+
* @param instance - The Knex query builder instance connected to the database.
|
|
38
|
+
*/
|
|
39
|
+
export declare function migratePageMetaBodyHash(instance: Knex): Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds the `page_meta.body_hash` column to archives created before this
|
|
3
|
+
* feature, then ensures its index exists.
|
|
4
|
+
*
|
|
5
|
+
* `page_meta` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
|
|
6
|
+
* {@link import('./create-entity-tables.js').createEntityTables}, which
|
|
7
|
+
* self-heals a *missing table* on every `initSchema` call but is a no-op
|
|
8
|
+
* against an *existing* table — adding a column to the DDL string never
|
|
9
|
+
* reaches an archive whose `page_meta` predates this change. This mirrors
|
|
10
|
+
* {@link import('./migrate-main-contents-columns.js').migrateMainContentsColumns}'s
|
|
11
|
+
* catch-up: a `hasColumn`-guarded `ALTER TABLE` for the one column
|
|
12
|
+
* `CREATE TABLE IF NOT EXISTS` cannot retrofit.
|
|
13
|
+
*
|
|
14
|
+
* The index is created here — unconditionally, after the column-add guard,
|
|
15
|
+
* not inside `createEntityTables`'s DDL — for both a fresh archive (where
|
|
16
|
+
* `body_hash` already exists from the DDL, so only the index still needs
|
|
17
|
+
* creating) and a legacy archive (where the column is added just above,
|
|
18
|
+
* then the index follows in the same call). `createEntityTables` runs
|
|
19
|
+
* unconditionally on every archive open, including legacy archives that
|
|
20
|
+
* still lack `body_hash` at that point; an unconditional
|
|
21
|
+
* `CREATE INDEX ... body_hash` there would fail with `no such column` before
|
|
22
|
+
* this migration ever runs. This function is the one place guaranteed to
|
|
23
|
+
* run only after the column is confirmed present, for both archive kinds.
|
|
24
|
+
*
|
|
25
|
+
* Only adds the column and its index — it does not backfill values for
|
|
26
|
+
* existing rows (they stay `NULL`). That backfill runs separately, from
|
|
27
|
+
* `backfillBodyHashFromHtmlBlobs` during a viewer-read-model build, since it
|
|
28
|
+
* requires decompressing every page's stored HTML and is too heavy to run on
|
|
29
|
+
* every archive open.
|
|
30
|
+
*
|
|
31
|
+
* Idempotent: adding the column is a no-op once it exists (the index
|
|
32
|
+
* creation always runs, but `IF NOT EXISTS` makes repeat runs a no-op too).
|
|
33
|
+
* Guards on `page_meta`'s existence defensively, though by the time this
|
|
34
|
+
* runs (after `initSchema`, itself after `assertCompatibleVersion` rejects
|
|
35
|
+
* pre-0.13 archives) the table is always present.
|
|
36
|
+
* @param instance - The Knex query builder instance connected to the database.
|
|
37
|
+
*/
|
|
38
|
+
export async function migratePageMetaBodyHash(instance) {
|
|
39
|
+
const hasPageMeta = await instance.schema.hasTable('page_meta');
|
|
40
|
+
if (!hasPageMeta) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const hasColumn = await instance.schema.hasColumn('page_meta', 'body_hash');
|
|
44
|
+
if (!hasColumn) {
|
|
45
|
+
await instance.schema.table('page_meta', (t) => {
|
|
46
|
+
t.binary('body_hash');
|
|
47
|
+
});
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.error('[migrate] page_meta.body_hash column added');
|
|
50
|
+
}
|
|
51
|
+
await instance.raw('CREATE INDEX IF NOT EXISTS idx_page_meta_body_hash ON page_meta(body_hash)');
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Adds the `page_meta.console_error_count` column to archives created
|
|
4
|
+
* before this feature (issue #228).
|
|
5
|
+
*
|
|
6
|
+
* `page_meta` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
|
|
7
|
+
* {@link import('./create-entity-tables.js').createEntityTables}, which
|
|
8
|
+
* self-heals a *missing table* on every `initSchema` call but is a no-op
|
|
9
|
+
* against an *existing* table — adding a column to the DDL string never
|
|
10
|
+
* reaches an archive whose `page_meta` predates this change. Same
|
|
11
|
+
* catch-up shape as {@link import('./migrate-page-meta-body-hash.js').migratePageMetaBodyHash}.
|
|
12
|
+
*
|
|
13
|
+
* Unlike `body_hash`, there is no separate backfill step: a page that has
|
|
14
|
+
* never been re-scraped since this feature shipped has no
|
|
15
|
+
* `page_console_logs` rows either, so `0` (the value `replaceConsoleLogs`
|
|
16
|
+
* writes for a page with no error/pageerror entries) is already the
|
|
17
|
+
* correct value, not a placeholder pending backfill.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent: adding the column is a no-op once it exists.
|
|
20
|
+
* @param instance - The Knex query builder instance connected to the database.
|
|
21
|
+
*/
|
|
22
|
+
export declare function migratePageMetaConsoleErrorCount(instance: Knex): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds the `page_meta.console_error_count` column to archives created
|
|
3
|
+
* before this feature (issue #228).
|
|
4
|
+
*
|
|
5
|
+
* `page_meta` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
|
|
6
|
+
* {@link import('./create-entity-tables.js').createEntityTables}, which
|
|
7
|
+
* self-heals a *missing table* on every `initSchema` call but is a no-op
|
|
8
|
+
* against an *existing* table — adding a column to the DDL string never
|
|
9
|
+
* reaches an archive whose `page_meta` predates this change. Same
|
|
10
|
+
* catch-up shape as {@link import('./migrate-page-meta-body-hash.js').migratePageMetaBodyHash}.
|
|
11
|
+
*
|
|
12
|
+
* Unlike `body_hash`, there is no separate backfill step: a page that has
|
|
13
|
+
* never been re-scraped since this feature shipped has no
|
|
14
|
+
* `page_console_logs` rows either, so `0` (the value `replaceConsoleLogs`
|
|
15
|
+
* writes for a page with no error/pageerror entries) is already the
|
|
16
|
+
* correct value, not a placeholder pending backfill.
|
|
17
|
+
*
|
|
18
|
+
* Idempotent: adding the column is a no-op once it exists.
|
|
19
|
+
* @param instance - The Knex query builder instance connected to the database.
|
|
20
|
+
*/
|
|
21
|
+
export async function migratePageMetaConsoleErrorCount(instance) {
|
|
22
|
+
const hasPageMeta = await instance.schema.hasTable('page_meta');
|
|
23
|
+
if (!hasPageMeta) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const hasColumn = await instance.schema.hasColumn('page_meta', 'console_error_count');
|
|
27
|
+
if (!hasColumn) {
|
|
28
|
+
await instance.schema.table('page_meta', (t) => {
|
|
29
|
+
t.integer('console_error_count');
|
|
30
|
+
});
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.error('[migrate] page_meta.console_error_count column added');
|
|
33
|
+
}
|
|
34
|
+
}
|
package/lib/archive/page.d.ts
CHANGED
|
@@ -406,6 +406,7 @@ export default class Page {
|
|
|
406
406
|
isInternalPage: boolean;
|
|
407
407
|
getAnchors: Anchor[];
|
|
408
408
|
getReferrers: Referrer[];
|
|
409
|
+
dir: string | number | null;
|
|
409
410
|
title: string | number | null;
|
|
410
411
|
main_content_word_count: string | number | null;
|
|
411
412
|
main_content_node_name: string | number | null;
|
|
@@ -425,7 +426,6 @@ export default class Page {
|
|
|
425
426
|
scroll_height_desktop: string | number | null;
|
|
426
427
|
scroll_height_mobile: string | number | null;
|
|
427
428
|
lang: string | number | null;
|
|
428
|
-
dir: string | number | null;
|
|
429
429
|
charset: string | number | null;
|
|
430
430
|
baseHref: string | number | null;
|
|
431
431
|
viewport_raw: string | number | null;
|
package/lib/archive/types.d.ts
CHANGED
|
@@ -128,9 +128,9 @@ export interface InventoryRunMeta {
|
|
|
128
128
|
ran_at: string;
|
|
129
129
|
/** Human-readable identifier (e.g. `'prod-2026-06-21'`). `null` when the caller did not supply one. */
|
|
130
130
|
list_label?: string | null;
|
|
131
|
-
/** SHA-256 hex digest of the source file. `null`
|
|
131
|
+
/** SHA-256 hex digest of the source file. `null` for programmatic callers that built the URL list in-memory (no source file to hash). */
|
|
132
132
|
source_file_sha256?: string | null;
|
|
133
|
-
/** Number of
|
|
133
|
+
/** Number of valid URLs in the input list, after the CLI has warned-and-dropped unparseable-URL lines but before scope filtering. */
|
|
134
134
|
total_lines?: number | null;
|
|
135
135
|
/** Number of new HTML seeds inserted as `content_items` rows by this run. */
|
|
136
136
|
new_pages?: number | null;
|
|
@@ -138,9 +138,67 @@ export interface InventoryRunMeta {
|
|
|
138
138
|
new_resources?: number | null;
|
|
139
139
|
/** Number of input URLs dropped because they fell outside the archived scope. */
|
|
140
140
|
scope_skipped?: number | null;
|
|
141
|
+
/** Number of source-file lines dropped by the CLI for failing URL validation, before this row's `total_lines` was counted. `null` for programmatic callers that built the URL list in-memory (no source file to have invalid lines). */
|
|
142
|
+
invalid_skipped?: number | null;
|
|
141
143
|
/** Free-form text for backfill annotations or operator notes. */
|
|
142
144
|
notes?: string | null;
|
|
143
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* A row in `network_outages` — one detected operator-network outage.
|
|
148
|
+
*
|
|
149
|
+
* Append-only except `ended_at`: it is written once, `NULL`, when the
|
|
150
|
+
* outage is first detected, and updated exactly once when a recovery probe
|
|
151
|
+
* succeeds. A row can also be left `ended_at = NULL` forever if the crawl
|
|
152
|
+
* process is killed mid-outage — readers must resolve this via a clamp
|
|
153
|
+
* (see `is-within-outage-window.ts` and the `db-ops/outages/` writer that
|
|
154
|
+
* closes stale-open rows on the next writer session), never by treating
|
|
155
|
+
* `NULL` as an unbounded window.
|
|
156
|
+
*/
|
|
157
|
+
export interface NetworkOutageRow {
|
|
158
|
+
id: number;
|
|
159
|
+
/** Epoch ms, backdated to the earliest error still inside the detector's sliding window at trigger time. */
|
|
160
|
+
started_at: number;
|
|
161
|
+
/** Epoch ms the sliding window actually crossed both thresholds. */
|
|
162
|
+
detected_at: number;
|
|
163
|
+
/** Epoch ms the recovery probe first succeeded, or `null` while still open / if the session crashed before recovery. */
|
|
164
|
+
ended_at: number | null;
|
|
165
|
+
/** Hostname the recovery probe targeted, or `null` if none was available (see `choose-probe-host.ts`). */
|
|
166
|
+
probe_host: string | null;
|
|
167
|
+
/** Error count in the detector's window at trigger time. */
|
|
168
|
+
trigger_error_count: number;
|
|
169
|
+
/** Distinct host count in the detector's window at trigger time. */
|
|
170
|
+
trigger_host_count: number;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Fields required to record a newly-detected outage via
|
|
174
|
+
* `Database.insertNetworkOutage`. camelCase (unlike {@link NetworkOutageRow}
|
|
175
|
+
* / {@link InventoryRunMeta}) because callers build this directly from
|
|
176
|
+
* `NetworkOutageDetector`'s camelCase `OutageSuspect` plus a probe host —
|
|
177
|
+
* the db-op does the camelCase → snake_case column mapping on write.
|
|
178
|
+
*/
|
|
179
|
+
export interface InsertNetworkOutageParams {
|
|
180
|
+
/** Backdated to the earliest error still inside the detector's window at trigger time. */
|
|
181
|
+
startedAt: number;
|
|
182
|
+
/** When the sliding window actually crossed both thresholds. */
|
|
183
|
+
detectedAt: number;
|
|
184
|
+
/** Hostname the recovery probe will target, or `null` if none was available. */
|
|
185
|
+
probeHost: string | null;
|
|
186
|
+
triggerErrorCount: number;
|
|
187
|
+
triggerHostCount: number;
|
|
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
|
+
}
|
|
144
202
|
/**
|
|
145
203
|
* Filter type for querying pages from the database.
|
|
146
204
|
*
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes the Tier A alias key for `url`: two URLs share a Tier A key when
|
|
3
|
+
* they differ only by scheme (`http` vs `https` are folded together),
|
|
4
|
+
* host letter-casing, or a `/index.{ext}` path suffix — differences that
|
|
5
|
+
* are safe to treat as "the same resource" from the URL string alone, with
|
|
6
|
+
* no need to compare rendered content.
|
|
7
|
+
*
|
|
8
|
+
* This is a canonical-key function, not a pairwise comparator: grouping
|
|
9
|
+
* rows by exact string equality of this function's output is definitionally
|
|
10
|
+
* an equivalence relation (reflexive, symmetric, transitive), unlike
|
|
11
|
+
* `compareUrlSortKeys`'s pairwise "roughly equal" comparison (see
|
|
12
|
+
* ARCHITECTURE.md's "URL natural-sort comparator は推移律を保証しない" —
|
|
13
|
+
* that non-transitivity problem is inherent to pairwise comparators, and
|
|
14
|
+
* does not apply to a canonical-key partition like this one).
|
|
15
|
+
*
|
|
16
|
+
* Deliberately does not touch the query string or fragment: a query-string
|
|
17
|
+
* difference is not addressed by this feature (out of scope — see the
|
|
18
|
+
* `body_hash` masking, a separate content-hash-based mechanism, for
|
|
19
|
+
* detecting meaningless-parameter duplicates), and fragments are
|
|
20
|
+
* client-side only and never affect server-rendered content.
|
|
21
|
+
* @param url - The URL string to compute a key for.
|
|
22
|
+
* @returns The Tier A key, or `null` if `url` is not a parseable http(s) URL.
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* computeTierAAliasKey('http://Example.com/about/index.html');
|
|
26
|
+
* // 'example.com/about/'
|
|
27
|
+
* computeTierAAliasKey('https://example.com/about/');
|
|
28
|
+
* // 'example.com/about/' -- same key: scheme and /index.html are folded
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function computeTierAAliasKey(url: string): string | null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { formatAliasKey } from './format-alias-key.js';
|
|
2
|
+
import { parseAliasKeyParts } from './parse-alias-key-parts.js';
|
|
3
|
+
/**
|
|
4
|
+
* Computes the Tier A alias key for `url`: two URLs share a Tier A key when
|
|
5
|
+
* they differ only by scheme (`http` vs `https` are folded together),
|
|
6
|
+
* host letter-casing, or a `/index.{ext}` path suffix — differences that
|
|
7
|
+
* are safe to treat as "the same resource" from the URL string alone, with
|
|
8
|
+
* no need to compare rendered content.
|
|
9
|
+
*
|
|
10
|
+
* This is a canonical-key function, not a pairwise comparator: grouping
|
|
11
|
+
* rows by exact string equality of this function's output is definitionally
|
|
12
|
+
* an equivalence relation (reflexive, symmetric, transitive), unlike
|
|
13
|
+
* `compareUrlSortKeys`'s pairwise "roughly equal" comparison (see
|
|
14
|
+
* ARCHITECTURE.md's "URL natural-sort comparator は推移律を保証しない" —
|
|
15
|
+
* that non-transitivity problem is inherent to pairwise comparators, and
|
|
16
|
+
* does not apply to a canonical-key partition like this one).
|
|
17
|
+
*
|
|
18
|
+
* Deliberately does not touch the query string or fragment: a query-string
|
|
19
|
+
* difference is not addressed by this feature (out of scope — see the
|
|
20
|
+
* `body_hash` masking, a separate content-hash-based mechanism, for
|
|
21
|
+
* detecting meaningless-parameter duplicates), and fragments are
|
|
22
|
+
* client-side only and never affect server-rendered content.
|
|
23
|
+
* @param url - The URL string to compute a key for.
|
|
24
|
+
* @returns The Tier A key, or `null` if `url` is not a parseable http(s) URL.
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* computeTierAAliasKey('http://Example.com/about/index.html');
|
|
28
|
+
* // 'example.com/about/'
|
|
29
|
+
* computeTierAAliasKey('https://example.com/about/');
|
|
30
|
+
* // 'example.com/about/' -- same key: scheme and /index.html are folded
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function computeTierAAliasKey(url) {
|
|
34
|
+
const parts = parseAliasKeyParts(url);
|
|
35
|
+
return parts ? formatAliasKey(parts) : null;
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes the Tier B alias key for `url`: like `computeTierAAliasKey`,
|
|
3
|
+
* but additionally strips one trailing `/` from the path (the bare root
|
|
4
|
+
* path `/` is left alone). Two URLs sharing a Tier B key differ only by
|
|
5
|
+
* trailing-slash presence — a resource-identity signal considerably weaker
|
|
6
|
+
* than Tier A's (a web server can legitimately serve different content at
|
|
7
|
+
* `/foo` and `/foo/`), so callers must additionally require a matching
|
|
8
|
+
* `page_meta.body_hash` before treating a Tier B match as the same page.
|
|
9
|
+
* @param url - The URL string to compute a key for.
|
|
10
|
+
* @returns The Tier B key, or `null` if `url` is not a parseable http(s) URL.
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* computeTierBAliasKey('https://example.com/foo');
|
|
14
|
+
* computeTierBAliasKey('https://example.com/foo/');
|
|
15
|
+
* // both: 'example.com/foo' -- same key, but the caller must still confirm
|
|
16
|
+
* // a matching body_hash before merging these as the same page.
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function computeTierBAliasKey(url: string): string | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { formatAliasKey } from './format-alias-key.js';
|
|
2
|
+
import { parseAliasKeyParts } from './parse-alias-key-parts.js';
|
|
3
|
+
/**
|
|
4
|
+
* Computes the Tier B alias key for `url`: like `computeTierAAliasKey`,
|
|
5
|
+
* but additionally strips one trailing `/` from the path (the bare root
|
|
6
|
+
* path `/` is left alone). Two URLs sharing a Tier B key differ only by
|
|
7
|
+
* trailing-slash presence — a resource-identity signal considerably weaker
|
|
8
|
+
* than Tier A's (a web server can legitimately serve different content at
|
|
9
|
+
* `/foo` and `/foo/`), so callers must additionally require a matching
|
|
10
|
+
* `page_meta.body_hash` before treating a Tier B match as the same page.
|
|
11
|
+
* @param url - The URL string to compute a key for.
|
|
12
|
+
* @returns The Tier B key, or `null` if `url` is not a parseable http(s) URL.
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* computeTierBAliasKey('https://example.com/foo');
|
|
16
|
+
* computeTierBAliasKey('https://example.com/foo/');
|
|
17
|
+
* // both: 'example.com/foo' -- same key, but the caller must still confirm
|
|
18
|
+
* // a matching body_hash before merging these as the same page.
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function computeTierBAliasKey(url) {
|
|
22
|
+
const parts = parseAliasKeyParts(url);
|
|
23
|
+
if (!parts) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const path = parts.path.length > 1 && parts.path.endsWith('/')
|
|
27
|
+
? parts.path.slice(0, -1)
|
|
28
|
+
: parts.path;
|
|
29
|
+
return formatAliasKey({ ...parts, path });
|
|
30
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AliasKeyParts } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Formats parsed alias-key parts into a single string key. `host`/`port`
|
|
4
|
+
* never contain `/` or `?`, and `path`/`search` always start with those
|
|
5
|
+
* characters respectively (or `search` is empty), so concatenation without
|
|
6
|
+
* an explicit delimiter is unambiguous.
|
|
7
|
+
* @param parts - The parts to format.
|
|
8
|
+
* @returns The formatted key string.
|
|
9
|
+
* @example
|
|
10
|
+
* formatAliasKey({ host: 'example.com', port: '', path: '/about/', search: '' });
|
|
11
|
+
* // 'example.com/about/'
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatAliasKey(parts: AliasKeyParts): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats parsed alias-key parts into a single string key. `host`/`port`
|
|
3
|
+
* never contain `/` or `?`, and `path`/`search` always start with those
|
|
4
|
+
* characters respectively (or `search` is empty), so concatenation without
|
|
5
|
+
* an explicit delimiter is unambiguous.
|
|
6
|
+
* @param parts - The parts to format.
|
|
7
|
+
* @returns The formatted key string.
|
|
8
|
+
* @example
|
|
9
|
+
* formatAliasKey({ host: 'example.com', port: '', path: '/about/', search: '' });
|
|
10
|
+
* // 'example.com/about/'
|
|
11
|
+
*/
|
|
12
|
+
export function formatAliasKey(parts) {
|
|
13
|
+
return `${parts.host}${parts.port ? `:${parts.port}` : ''}${parts.path}${parts.search}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AliasKeyParts } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parses `url` and extracts the components Tier A / Tier B keys are built
|
|
4
|
+
* from. Returns `null` for anything that isn't a parseable `http`/`https`
|
|
5
|
+
* URL — such a row is defensively excluded from alias candidacy entirely
|
|
6
|
+
* (a non-http(s) URL should not occur for anything the crawler itself
|
|
7
|
+
* stored, since every `url_refs.url` was already parsed once during
|
|
8
|
+
* crawling).
|
|
9
|
+
* @param url - The URL string to parse.
|
|
10
|
+
* @returns The extracted parts, or `null` if unparseable / not http(s).
|
|
11
|
+
* @example
|
|
12
|
+
* parseAliasKeyParts('https://Example.com/about/index.html');
|
|
13
|
+
* // { host: 'example.com', port: '', path: '/about/', search: '' }
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseAliasKeyParts(url: string): AliasKeyParts | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const INDEX_SUFFIX_PATTERN = /\/index\.\w+$/i;
|
|
2
|
+
/**
|
|
3
|
+
* Parses `url` and extracts the components Tier A / Tier B keys are built
|
|
4
|
+
* from. Returns `null` for anything that isn't a parseable `http`/`https`
|
|
5
|
+
* URL — such a row is defensively excluded from alias candidacy entirely
|
|
6
|
+
* (a non-http(s) URL should not occur for anything the crawler itself
|
|
7
|
+
* stored, since every `url_refs.url` was already parsed once during
|
|
8
|
+
* crawling).
|
|
9
|
+
* @param url - The URL string to parse.
|
|
10
|
+
* @returns The extracted parts, or `null` if unparseable / not http(s).
|
|
11
|
+
* @example
|
|
12
|
+
* parseAliasKeyParts('https://Example.com/about/index.html');
|
|
13
|
+
* // { host: 'example.com', port: '', path: '/about/', search: '' }
|
|
14
|
+
*/
|
|
15
|
+
export function parseAliasKeyParts(url) {
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = new URL(url);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
// WHATWG URL already lowercases `hostname` during parsing; the
|
|
28
|
+
// explicit `.toLowerCase()` documents that intent rather than
|
|
29
|
+
// relying on it silently.
|
|
30
|
+
host: parsed.hostname.toLowerCase(),
|
|
31
|
+
// Empty string when the port is the scheme's default (or
|
|
32
|
+
// unspecified) — this is why `http://example.com/` and
|
|
33
|
+
// `https://example.com/` fold to the same key below (both have
|
|
34
|
+
// `port === ''`) while an explicit non-default port on either
|
|
35
|
+
// scheme is preserved and kept distinct.
|
|
36
|
+
port: parsed.port,
|
|
37
|
+
path: parsed.pathname.replace(INDEX_SUFFIX_PATTERN, '/'),
|
|
38
|
+
search: parsed.search,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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>;
|