@ultimat3/scraping 19.1.2 → 19.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/scraping",
3
- "version": "19.1.2",
3
+ "version": "19.2.0",
4
4
  "description": "Browser automation as a job: scrape() returns a JobHandle",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,9 +30,9 @@
30
30
  "test": "bun test"
31
31
  },
32
32
  "dependencies": {
33
- "@ultimat3/core": "19.1.2",
34
- "@ultimat3/jobs": "19.1.2",
35
- "@ultimat3/schema": "19.1.2",
36
- "@ultimat3/storage": "19.1.2"
33
+ "@ultimat3/core": "19.2.0",
34
+ "@ultimat3/jobs": "19.2.0",
35
+ "@ultimat3/schema": "19.2.0",
36
+ "@ultimat3/storage": "19.2.0"
37
37
  }
38
38
  }
@@ -36,13 +36,64 @@ export const cdpAttachFailed = (cdpUrl: string, thrown: unknown): ScrapeError =>
36
36
  meta: { cdpUrl },
37
37
  });
38
38
 
39
+ /**
40
+ * What separates "there is no browser" from "the browser stopped answering". Both arrive at the
41
+ * same `catch` around `launch()` (`driver-cdp.ts`), and only one of them is worth a second attempt.
42
+ *
43
+ * Matched on the LAUNCHER'S OWN WORDS rather than on a flag this function could be passed, because
44
+ * the launch site hands it one `unknown` and nothing else. Every entry below is a message a browser
45
+ * library or the kernel emits for a browser that is not there, and none of them is producible by a
46
+ * page that merely went away mid-run — which is what keeps the split honest at `cdp-target.ts`'s
47
+ * call site, where the browser demonstrably existed.
48
+ *
49
+ * Lowercase, and compared against a lowercased render, so a library re-capitalising its own message
50
+ * does not silently turn a permanent misconfiguration back into five retries.
51
+ */
52
+ const BROWSER_ABSENT_SIGNS: readonly string[] = [
53
+ // puppeteer-core ships no browser and has no default: this is its whole message when nothing was
54
+ // configured — ``An `executablePath` or `channel` must be specified for `puppeteer-core```.
55
+ 'must be specified',
56
+ // `spawn /usr/bin/google-chrome ENOENT` — an executablePath naming a file that is not there,
57
+ // which is what a CHROME_PATH left over from another machine is.
58
+ 'enoent',
59
+ // The binary is there and this uid may not execute it. Attempt 2 runs as the same user.
60
+ 'eacces',
61
+ // The two spellings puppeteer has shipped for "I looked for a browser and found none".
62
+ 'could not find chrome',
63
+ 'could not find browser',
64
+ 'was not found at the configured executablepath',
65
+ ];
66
+
67
+ const browserIsAbsent = (thrown: unknown): boolean => {
68
+ const text = renderThrowable(thrown).toLowerCase();
69
+ return BROWSER_ABSENT_SIGNS.some((sign) => text.includes(sign));
70
+ };
71
+
72
+ /**
73
+ * A launch or a page call that failed. TWO codes, because the two failures have opposite answers.
74
+ *
75
+ * `X_SCRAPE_BROWSER_UNREACHABLE` is transport and stays retryable: a browser that was there is
76
+ * gone, and attempt 2 may find it back.
77
+ *
78
+ * `X_SCRAPE_BROWSER_MISSING` is a misconfiguration and is terminal. Its fix used to read "raise
79
+ * watchdog: { idleMs } on the scrape() definition … otherwise re-run once the browser host is
80
+ * back", which on the `x shot` path names a definition that does not exist and an act that never
81
+ * helps — while the retry table spent five browser launches proving it.
82
+ */
39
83
  export const browserUnreachable = (driver: string, thrown: unknown): ScrapeError =>
40
- new ScrapeError({
41
- code: 'X_SCRAPE_BROWSER_UNREACHABLE',
42
- cause: `the ${driver} browser stopped answering: ${renderThrowable(thrown)}`,
43
- fix: 'raise watchdog: { idleMs } on the scrape() definition if the site is genuinely slow, otherwise re-run once the browser host is back',
44
- meta: { driver },
45
- });
84
+ browserIsAbsent(thrown)
85
+ ? new ScrapeError({
86
+ code: 'X_SCRAPE_BROWSER_MISSING',
87
+ cause: `the ${driver} driver has no browser to launch: ${renderThrowable(thrown)}`,
88
+ fix: 'install a Chrome or Chromium and name it — localBrowser({ launcher, executablePath: env.CHROME_PATH }) — or attach to one somebody else is running with remoteBrowser({ cdpUrl: env.SCRAPE_CDP_URL })',
89
+ meta: { driver },
90
+ })
91
+ : new ScrapeError({
92
+ code: 'X_SCRAPE_BROWSER_UNREACHABLE',
93
+ cause: `the ${driver} browser stopped answering: ${renderThrowable(thrown)}`,
94
+ fix: 'raise watchdog: { idleMs } on the scrape() definition if the site is genuinely slow, otherwise re-run once the browser host is back',
95
+ meta: { driver },
96
+ });
46
97
 
47
98
  export const profileLocked = (profileDir: string): ScrapeError =>
48
99
  new ScrapeError({
package/src/errors.ts CHANGED
@@ -11,6 +11,7 @@ export const SCRAPE_OWNED_ERROR_CODES = [
11
11
  'X_SCRAPE_DRIVER_UNKNOWN',
12
12
  'X_SCRAPE_CDP_ATTACH_FAILED',
13
13
  'X_SCRAPE_BROWSER_UNREACHABLE',
14
+ 'X_SCRAPE_BROWSER_MISSING',
14
15
  'X_SCRAPE_PROFILE_LOCKED',
15
16
  'X_SCRAPE_HOST_BLOCKED',
16
17
  'X_SCRAPE_SELECTOR_MISSING',
@@ -59,6 +60,7 @@ export const SCRAPE_ERROR_TITLES: Readonly<Record<ScrapeOwnedErrorCode, string>>
59
60
  X_SCRAPE_DRIVER_UNKNOWN: 'no browser driver is installed for this run',
60
61
  X_SCRAPE_CDP_ATTACH_FAILED: 'the CDP endpoint refused the attach',
61
62
  X_SCRAPE_BROWSER_UNREACHABLE: 'the browser went away mid-run',
63
+ X_SCRAPE_BROWSER_MISSING: 'there is no browser on this machine for the driver to launch',
62
64
  X_SCRAPE_PROFILE_LOCKED: 'another process holds this browser profile',
63
65
  X_SCRAPE_HOST_BLOCKED: 'the page asked for a host allowHosts does not list',
64
66
  X_SCRAPE_SELECTOR_MISSING: 'the selector never appeared inside its window',
@@ -106,6 +108,10 @@ registerErrorCodes(
106
108
  */
107
109
  export const SCRAPE_ERROR_RETRY = {
108
110
  X_SCRAPE_CDP_ATTACH_FAILED: 'retryable',
111
+ // Retryable, and it now means ONLY what its title says: a browser that was there and stopped
112
+ // answering. It used to be raised for a launch that failed because there was no browser to
113
+ // launch at all, which the same run repeated five times — `X_SCRAPE_BROWSER_MISSING` below is
114
+ // that half, split out and terminal.
109
115
  X_SCRAPE_BROWSER_UNREACHABLE: 'retryable',
110
116
  X_SCRAPE_TIMEOUT: 'retryable',
111
117
  X_SCRAPE_WEDGED: 'retryable',
@@ -124,6 +130,14 @@ export const SCRAPE_ERROR_RETRY = {
124
130
  // Everything below is terminal, and each one is listed rather than left to the default so that
125
131
  // deleting a line is a visible decision.
126
132
  X_SCRAPE_DRIVER_UNKNOWN: 'terminal',
133
+ // TERMINAL where its sibling `X_SCRAPE_BROWSER_UNREACHABLE` is retryable, and the difference IS
134
+ // the reason the two codes are separate. "The browser host went away" is transport — attempt 2
135
+ // may find it back. "There is no browser binary on this box, or none was configured" is a
136
+ // MISCONFIGURATION: attempt 2 spawns the same absent path, five times, on a five-attempt policy,
137
+ // and on an authenticated target that is five arrivals at a login for no chance of a different
138
+ // answer. Measured on `x shot`, where there is no `scrape()` definition to raise a watchdog on
139
+ // and re-running never helps. The repair is an install or an `executablePath`, so a human decides.
140
+ X_SCRAPE_BROWSER_MISSING: 'terminal',
127
141
  X_SCRAPE_PROFILE_LOCKED: 'terminal',
128
142
  X_SCRAPE_HOST_BLOCKED: 'terminal',
129
143
  X_SCRAPE_SELECTOR_MISSING: 'terminal',