@d-zero/beholder 2.1.6 → 3.0.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/CHANGELOG.md CHANGED
@@ -3,6 +3,44 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [3.0.0](https://github.com/d-zero-dev/tools/compare/@d-zero/beholder@2.1.6...@d-zero/beholder@3.0.0) (2026-06-16)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **beholder:** warn loudly and tripwire-test puppeteer Page.\_client() coverage ([97a07ea](https://github.com/d-zero-dev/tools/commit/97a07ea273e90d50bfede1d68f594ddee9c33268))
11
+
12
+ - feat(beholder)!: expand meta extraction with frontmatter-keys schema and Wappalyzer tag detection ([6ee7861](https://github.com/d-zero-dev/tools/commit/6ee78617aac3fe3d5c022ccfd0df265de0c5310b))
13
+
14
+ ### Features
15
+
16
+ - **beholder:** rewrite getAnchorList with single AX tree + parallel describeNode ([#876](https://github.com/d-zero-dev/tools/issues/876)) ([7e5b089](https://github.com/d-zero-dev/tools/commit/7e5b089695bd1e605d63c6faef2e8bf927bd861f))
17
+
18
+ ### BREAKING CHANGES
19
+
20
+ - `Meta` is restructured from flat keys (`noindex`, `canonical`,
21
+ `'og:type'`, `'twitter:card'`, ...) into a nested shape backed by
22
+ `frontmatter-keys.md`. New required fields: `title`, `jsonLd`,
23
+ `speculationRules`, `originTrial`, `tags`, `others`. `getMeta(page)` now takes
24
+ a context object `getMeta(page, { url, html?, statusCode?, headers? }, timeout?)`.
25
+ Old top-level shortcuts (`canonical`, `alternate`, `noindex`, `nofollow`,
26
+ `noarchive`, `'og:*'`, `'twitter:card'`) are removed; values move to
27
+ `meta.link.canonical`, `meta.robots.*`, `meta.og.*`, `meta.twitter.*` etc.
28
+
29
+ Changes:
30
+
31
+ - New `src/meta/` module: `types.ts`, `keys.ts`, `parsers.ts`, `classify.ts`,
32
+ `id-extractors.ts`, `tag-detection.ts`, plus ambient `simple-wappalyzer.d.ts`
33
+ - Browser-side `collectHead()` serializes every `<meta>`, `<link>`, structured-data
34
+ `<script>`, `<base>`, `<iframe>` plus a curated set of `window` globals into
35
+ `RawHeadEntry[]`; Node-side `classify()` maps these to typed Meta fields
36
+ - `simple-wappalyzer` (MIT) added as a dependency for technology detection;
37
+ detected providers run through `id-extractors.ts` for real ID extraction
38
+ (GA4, GTM, UA, FB Pixel, Hotjar, Clarity, ...)
39
+ - Unknown markup is preserved under `Meta.others` (meta/property/httpEquiv/
40
+ itemprop/link/script/iframe buckets) so nothing is silently dropped
41
+ - Tests: parsers/classify/id-extractors/tag-detection units + getMeta
42
+ error/timeout fallback
43
+
6
44
  ## [2.1.6](https://github.com/d-zero-dev/tools/compare/@d-zero/beholder@2.1.5...@d-zero/beholder@2.1.6) (2026-06-15)
7
45
 
8
46
  ### Bug Fixes
@@ -19,8 +19,12 @@ import type { ElementHandle, Page } from 'puppeteer';
19
19
  * Default timeout (ms) applied to DOM evaluation operations when the caller does not
20
20
  * specify one. Bounds how long a single `page.evaluate` / property read may hang on a
21
21
  * page whose main thread is unresponsive.
22
+ *
23
+ * WHY 180s: Aligned with the upstream `Scraper#fetchData` retryable timeout (3 min) so
24
+ * a single phase does not exceed the retry budget while still tolerating large pages
25
+ * (e.g., 1000+ anchors) and slow main threads.
22
26
  */
23
- export declare const DEFAULT_DOM_EVALUATION_TIMEOUT = 30000;
27
+ export declare const DEFAULT_DOM_EVALUATION_TIMEOUT = 180000;
24
28
  /**
25
29
  * Parameters for {@link getProp}.
26
30
  * @template T - The expected type of the property value.
@@ -65,35 +69,79 @@ export declare function getImageList(page: Page, viewportWidth: number, timeout?
65
69
  * the accessible name (from the accessibility tree, falling back to `textContent`),
66
70
  * and filters out non-HTTP links.
67
71
  *
68
- * WHY this keeps per-element CDP calls (unlike {@link getMeta} / {@link getImageList}):
69
- * the accessible name comes from Chrome's computed accessibility tree
70
- * (`page.accessibility.snapshot`), which is a CDP-only feature unavailable to in-page
71
- * DOM APIs. Each {@link getProp} read is still bounded by `timeout`.
72
+ * WHY Strategy F (single AX-tree fetch + parallel `DOM.describeNode`): the old
73
+ * implementation called `page.accessibility.snapshot({ root })` per anchor, which
74
+ * triggers a CDP round-trip *and* a Chrome-side AX subtree computation (~42ms
75
+ * each). On a page with 1181 anchors that compounded to ~53s. By fetching the
76
+ * full AX tree once and using `DOM.describeNode` in parallel to map element
77
+ * handles back to AX nodes by `backendDOMNodeId`, the same data is collected in
78
+ * ~150ms on the same page — a ~350× speed-up while preserving the original
79
+ * accessible-name semantics. See issue #876 for measurements.
80
+ *
81
+ * WHY the whole operation is wrapped in `raceWithTimeout`: even with bounded
82
+ * per-CDP-call timeouts, a degenerate page (blocked main thread, thousands of
83
+ * anchors, runaway describeNode latency) could chain enough sub-timeouts to
84
+ * exceed the caller's `timeout` budget. The outer race guarantees the function
85
+ * returns within `timeout`, surfacing whatever anchors were collected so far so
86
+ * the upstream scrape phase can continue rather than tripping a retryable retry.
72
87
  * @param page - The Puppeteer page to extract anchors from.
73
88
  * @param options - Optional URL parsing options (e.g., `disableQueries`).
74
- * @param timeout - Timeout in ms per property read. Defaults to {@link DEFAULT_DOM_EVALUATION_TIMEOUT}.
89
+ * @param timeout - Total time budget in ms for the whole extraction. Defaults to {@link DEFAULT_DOM_EVALUATION_TIMEOUT}.
75
90
  * @returns An array of {@link AnchorData} objects for all HTTP(S) links found on the page.
76
91
  */
77
92
  export declare function getAnchorList(page: Page, options?: ParseURLOptions, timeout?: number): Promise<AnchorData[]>;
78
93
  /**
79
- * Extracts comprehensive meta information from the page's `<head>`.
94
+ * Required context for {@link getMeta}. Provided by the scraper from data it
95
+ * already has on hand (URL it navigated to, response status/headers it received).
96
+ *
97
+ * `html` is optional: when omitted, `getMeta` falls back to `page.content()`
98
+ * to obtain the rendered HTML for the third-party tag detection pass.
99
+ */
100
+ export type GetMetaContext = {
101
+ /** The fully resolved URL of the page (after redirects). */
102
+ readonly url: string;
103
+ /** Rendered HTML. Falls back to `page.content()` when omitted. */
104
+ readonly html?: string;
105
+ /** Response status code, surfaced to the Wappalyzer driver. */
106
+ readonly statusCode?: number;
107
+ /** Response headers; case is preserved by the caller, lowercased internally. */
108
+ readonly headers?: Record<string, string | string[] | undefined>;
109
+ /**
110
+ * When `true`, the returned `Meta` includes `_raw: RawHeadEntry[]` for
111
+ * debugging. Default `false` to keep the serialized payload small.
112
+ */
113
+ readonly includeRaw?: boolean;
114
+ };
115
+ /**
116
+ * Extracts comprehensive metadata from the page.
80
117
  *
81
- * Collects all metadata in a single `page.evaluate` call (14 CDP round-trips
82
- * collapsed into 1) wrapped in {@link raceWithTimeout}. On timeout (an unresponsive
83
- * page) a minimal `{ title: '' }` is returned rather than hanging.
118
+ * Two passes happen in parallel:
119
+ * 1. Browser-side `collectHead()` serializes every `<meta>`, `<link>`,
120
+ * relevant `<script>`, `<base>`, `<noscript>`/`<iframe>` and a curated
121
+ * set of `window` globals into a `RawHeadEntry[]`. Node-side `classify()`
122
+ * then maps those entries to typed `Meta` fields using the lookup tables
123
+ * in `./meta/keys.ts`, with unknown entries preserved in `Meta.others`.
124
+ * 2. `detectTags()` runs `simple-wappalyzer` over the page HTML to produce
125
+ * `Meta.tags` (technology detection + real-ID extraction).
84
126
  *
85
- * Collected metadata:
86
- * - `title` - The document title.
87
- * - `lang` - The `lang` attribute of the `<html>` element.
88
- * - `description` - The `<meta name="description">` content.
89
- * - `keywords` - The `<meta name="keywords">` content.
90
- * - `noindex` / `nofollow` / `noarchive` - Parsed from the `<meta name="robots">` directives.
91
- * - `canonical` - The `<link rel="canonical">` content.
92
- * - `alternate` - The `<link rel="alternate">` content.
93
- * - Open Graph tags: `og:type`, `og:title`, `og:site_name`, `og:description`, `og:url`, `og:image`.
94
- * - `twitter:card` - The Twitter Card type.
95
- * @param page - The Puppeteer page to extract meta information from.
96
- * @param timeout - Timeout in ms for the evaluation. Defaults to {@link DEFAULT_DOM_EVALUATION_TIMEOUT}.
97
- * @returns An object containing all extracted meta properties.
127
+ * The whole call is wrapped in `raceWithTimeout`. On timeout an empty `Meta`
128
+ * (with `title: ''` and empty required arrays/objects) is returned.
129
+ * @param page
130
+ * @param context
131
+ * @param timeout
132
+ * @example
133
+ * ```ts
134
+ * const meta = await getMeta(page, {
135
+ * url: 'https://example.com/',
136
+ * html: await page.content(),
137
+ * statusCode: response.status,
138
+ * headers: response.headers,
139
+ * });
140
+ * console.log(meta.title); // <title> text
141
+ * console.log(meta.og?.image); // og:image[] array
142
+ * console.log(meta.robots?.noindex); // parsed robots
143
+ * console.log(meta.tags.detected.Analytics); // Wappalyzer hits
144
+ * console.log(meta.tags.entries.find(e => e.provider === 'Google Analytics')?.id);
145
+ * ```
98
146
  */
99
- export declare function getMeta(page: Page, timeout?: number): Promise<Meta>;
147
+ export declare function getMeta(page: Page, context: GetMetaContext, timeout?: number): Promise<Meta>;