browserclaw 0.11.6 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1573,6 +1573,7 @@ ${stderrOutput.slice(0, 2e3)}` : "";
1573
1573
  userDataDir,
1574
1574
  cdpPort,
1575
1575
  startedAt,
1576
+ launchMs: Date.now() - startedAt,
1576
1577
  proc
1577
1578
  };
1578
1579
  }
@@ -6327,6 +6328,137 @@ var CrawlPage = class {
6327
6328
  pollMs: opts?.pollMs
6328
6329
  });
6329
6330
  }
6331
+ // ── Auth Health ──────────────────────────────────────────────
6332
+ /**
6333
+ * Check whether the current page session appears authenticated.
6334
+ *
6335
+ * Evaluates one or more rules against the page state. All rules must pass
6336
+ * for `authenticated` to be `true`. Returns per-rule details for debugging.
6337
+ *
6338
+ * @param rules - Array of auth check rules (url, cookie, selector, text, textGone, fn)
6339
+ * @returns Authentication status and per-rule check details
6340
+ *
6341
+ * @example
6342
+ * ```ts
6343
+ * // Check by URL and absence of login text
6344
+ * const result = await page.isAuthenticated([
6345
+ * { url: '/dashboard' },
6346
+ * { textGone: 'Sign in' },
6347
+ * ]);
6348
+ * if (!result.authenticated) {
6349
+ * console.log('Auth failed:', result.checks.filter(c => !c.passed));
6350
+ * }
6351
+ *
6352
+ * // Check by cookie presence
6353
+ * const result = await page.isAuthenticated([{ cookie: 'session_id' }]);
6354
+ *
6355
+ * // Check with custom JS function
6356
+ * const result = await page.isAuthenticated([
6357
+ * { fn: '() => !!document.querySelector("[data-user-id]")' },
6358
+ * ]);
6359
+ * ```
6360
+ */
6361
+ async isAuthenticated(rules) {
6362
+ if (!rules.length) return { authenticated: true, checks: [] };
6363
+ const page = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6364
+ const checks = [];
6365
+ for (const rule of rules) {
6366
+ if (rule.url !== void 0) {
6367
+ const currentUrl = page.url();
6368
+ const passed = currentUrl.includes(rule.url);
6369
+ checks.push({ rule: "url", passed, detail: passed ? currentUrl : `expected "${rule.url}" in "${currentUrl}"` });
6370
+ }
6371
+ if (rule.cookie !== void 0) {
6372
+ const cookies = await page.context().cookies();
6373
+ const found = cookies.some((c) => c.name === rule.cookie && c.value !== "");
6374
+ checks.push({
6375
+ rule: "cookie",
6376
+ passed: found,
6377
+ detail: found ? `cookie "${rule.cookie}" present` : `cookie "${rule.cookie}" missing or empty`
6378
+ });
6379
+ }
6380
+ if (rule.selector !== void 0) {
6381
+ try {
6382
+ const count = await page.locator(rule.selector).count();
6383
+ const passed = count > 0;
6384
+ checks.push({
6385
+ rule: "selector",
6386
+ passed,
6387
+ detail: passed ? `"${rule.selector}" found (${String(count)})` : `"${rule.selector}" not found`
6388
+ });
6389
+ } catch (err) {
6390
+ console.warn(
6391
+ `[browserclaw] isAuthenticated selector check failed: ${err instanceof Error ? err.message : String(err)}`
6392
+ );
6393
+ checks.push({ rule: "selector", passed: false, detail: `"${rule.selector}" error during evaluation` });
6394
+ }
6395
+ }
6396
+ if (rule.text !== void 0 || rule.textGone !== void 0) {
6397
+ let bodyText = null;
6398
+ try {
6399
+ const raw = await evaluateViaPlaywright({
6400
+ cdpUrl: this.cdpUrl,
6401
+ targetId: this.targetId,
6402
+ fn: '() => { const b = document.body; return b ? b.innerText : ""; }'
6403
+ });
6404
+ bodyText = typeof raw === "string" ? raw : null;
6405
+ } catch (err) {
6406
+ console.warn(
6407
+ `[browserclaw] isAuthenticated body text fetch failed: ${err instanceof Error ? err.message : String(err)}`
6408
+ );
6409
+ }
6410
+ if (rule.text !== void 0) {
6411
+ if (bodyText === null) {
6412
+ checks.push({ rule: "text", passed: false, detail: `"${rule.text}" error during evaluation` });
6413
+ } else {
6414
+ const passed = bodyText.includes(rule.text);
6415
+ checks.push({
6416
+ rule: "text",
6417
+ passed,
6418
+ detail: passed ? `"${rule.text}" found` : `"${rule.text}" not found in page text`
6419
+ });
6420
+ }
6421
+ }
6422
+ if (rule.textGone !== void 0) {
6423
+ if (bodyText === null) {
6424
+ checks.push({ rule: "textGone", passed: false, detail: `"${rule.textGone}" error during evaluation` });
6425
+ } else {
6426
+ const passed = !bodyText.includes(rule.textGone);
6427
+ checks.push({
6428
+ rule: "textGone",
6429
+ passed,
6430
+ detail: passed ? `"${rule.textGone}" absent (good)` : `"${rule.textGone}" still present`
6431
+ });
6432
+ }
6433
+ }
6434
+ }
6435
+ if (rule.fn !== void 0) {
6436
+ try {
6437
+ const result = await evaluateViaPlaywright({
6438
+ cdpUrl: this.cdpUrl,
6439
+ targetId: this.targetId,
6440
+ fn: rule.fn
6441
+ });
6442
+ const passed = result !== null && result !== void 0 && result !== false && result !== 0 && result !== "";
6443
+ checks.push({
6444
+ rule: "fn",
6445
+ passed,
6446
+ detail: passed ? "function returned truthy" : `function returned ${JSON.stringify(result)}`
6447
+ });
6448
+ } catch (err) {
6449
+ checks.push({
6450
+ rule: "fn",
6451
+ passed: false,
6452
+ detail: `function threw: ${err instanceof Error ? err.message : String(err)}`
6453
+ });
6454
+ }
6455
+ }
6456
+ }
6457
+ return {
6458
+ authenticated: checks.length > 0 && checks.every((c) => c.passed),
6459
+ checks
6460
+ };
6461
+ }
6330
6462
  // ── Playwright Escape Hatches ─────────────────────────────────
6331
6463
  /**
6332
6464
  * Get the underlying Playwright `Page` object for this tab.
@@ -6385,9 +6517,11 @@ var BrowserClaw = class _BrowserClaw {
6385
6517
  ssrfPolicy;
6386
6518
  recordVideo;
6387
6519
  chrome;
6388
- constructor(cdpUrl, chrome, ssrfPolicy, recordVideo) {
6520
+ _telemetry;
6521
+ constructor(cdpUrl, chrome, telemetry, ssrfPolicy, recordVideo) {
6389
6522
  this.cdpUrl = cdpUrl;
6390
6523
  this.chrome = chrome;
6524
+ this._telemetry = telemetry;
6391
6525
  this.ssrfPolicy = ssrfPolicy;
6392
6526
  this.recordVideo = recordVideo;
6393
6527
  }
@@ -6416,13 +6550,21 @@ var BrowserClaw = class _BrowserClaw {
6416
6550
  * ```
6417
6551
  */
6418
6552
  static async launch(opts = {}) {
6553
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6419
6554
  const chrome = await launchChrome(opts);
6420
6555
  const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
6421
6556
  const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
6422
- const browser = new _BrowserClaw(cdpUrl, chrome, ssrfPolicy, opts.recordVideo);
6557
+ const telemetry = {
6558
+ launchMs: chrome.launchMs,
6559
+ timestamps: { startedAt, launchedAt: (/* @__PURE__ */ new Date()).toISOString() }
6560
+ };
6561
+ const browser = new _BrowserClaw(cdpUrl, chrome, telemetry, ssrfPolicy, opts.recordVideo);
6423
6562
  if (opts.url !== void 0 && opts.url !== "") {
6424
6563
  const page = await browser.currentPage();
6564
+ const navT0 = Date.now();
6425
6565
  await page.goto(opts.url);
6566
+ telemetry.navMs = Date.now() - navT0;
6567
+ telemetry.timestamps.navigatedAt = (/* @__PURE__ */ new Date()).toISOString();
6426
6568
  }
6427
6569
  return browser;
6428
6570
  }
@@ -6441,6 +6583,8 @@ var BrowserClaw = class _BrowserClaw {
6441
6583
  * ```
6442
6584
  */
6443
6585
  static async connect(cdpUrl, opts) {
6586
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6587
+ const connectT0 = Date.now();
6444
6588
  let resolvedUrl = cdpUrl;
6445
6589
  if (resolvedUrl === void 0 || resolvedUrl === "") {
6446
6590
  const discovered = await discoverChromeCdpUrl();
@@ -6456,7 +6600,11 @@ var BrowserClaw = class _BrowserClaw {
6456
6600
  }
6457
6601
  await connectBrowser(resolvedUrl, opts?.authToken);
6458
6602
  const ssrfPolicy = opts?.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
6459
- return new _BrowserClaw(resolvedUrl, null, ssrfPolicy, opts?.recordVideo);
6603
+ const telemetry = {
6604
+ connectMs: Date.now() - connectT0,
6605
+ timestamps: { startedAt, connectedAt: (/* @__PURE__ */ new Date()).toISOString() }
6606
+ };
6607
+ return new _BrowserClaw(resolvedUrl, null, telemetry, ssrfPolicy, opts?.recordVideo);
6460
6608
  }
6461
6609
  /**
6462
6610
  * Open a URL in a new tab and return the page handle.
@@ -6485,7 +6633,12 @@ var BrowserClaw = class _BrowserClaw {
6485
6633
  * @returns CrawlPage for the first/active page
6486
6634
  */
6487
6635
  async currentPage() {
6636
+ const connectT0 = Date.now();
6488
6637
  const { browser } = await connectBrowser(this.cdpUrl);
6638
+ if (this._telemetry.connectMs === void 0) {
6639
+ this._telemetry.connectMs = Date.now() - connectT0;
6640
+ this._telemetry.timestamps.connectedAt = (/* @__PURE__ */ new Date()).toISOString();
6641
+ }
6489
6642
  const pages = getAllPages(browser);
6490
6643
  if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
6491
6644
  const tid = await pageTargetId(pages[0]).catch(() => null);
@@ -6562,15 +6715,61 @@ var BrowserClaw = class _BrowserClaw {
6562
6715
  * If the browser was launched by `BrowserClaw.launch()`, the Chrome process
6563
6716
  * will be terminated. If connected via `BrowserClaw.connect()`, only the
6564
6717
  * Playwright connection is closed.
6718
+ *
6719
+ * @param exitReason - Optional structured reason for stopping (e.g. `'success'`, `'auth_failed'`, `'timeout'`)
6565
6720
  */
6566
- async stop() {
6567
- clearRecordingContext(this.cdpUrl);
6568
- await disconnectBrowser();
6569
- if (this.chrome) {
6570
- await stopChrome(this.chrome);
6571
- this.chrome = null;
6721
+ async stop(exitReason) {
6722
+ this._telemetry.timestamps.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
6723
+ if (exitReason !== void 0) this._telemetry.exitReason = exitReason;
6724
+ try {
6725
+ clearRecordingContext(this.cdpUrl);
6726
+ await disconnectBrowser();
6727
+ if (this.chrome) {
6728
+ await stopChrome(this.chrome);
6729
+ this.chrome = null;
6730
+ }
6731
+ this._telemetry.cleanupOk = true;
6732
+ } catch (err) {
6733
+ this._telemetry.cleanupOk = false;
6734
+ throw err;
6572
6735
  }
6573
6736
  }
6737
+ /**
6738
+ * Get structured telemetry for this browser session.
6739
+ *
6740
+ * Returns timing data, timestamps, and exit information collected
6741
+ * throughout the session lifecycle. Useful for diagnosing startup
6742
+ * latency, auth failures, and cleanup issues in cron/unattended runs.
6743
+ *
6744
+ * @returns Telemetry envelope with launch/connect/nav timings and exit info
6745
+ *
6746
+ * @example
6747
+ * ```ts
6748
+ * const browser = await BrowserClaw.launch({ url: 'https://example.com' });
6749
+ * const page = await browser.currentPage();
6750
+ *
6751
+ * // ... do work ...
6752
+ *
6753
+ * const auth = await page.isAuthenticated([{ cookie: 'session' }]);
6754
+ * browser.recordAuthResult(auth.authenticated);
6755
+ *
6756
+ * await browser.stop(auth.authenticated ? 'success' : 'auth_failed');
6757
+ * console.log(browser.telemetry());
6758
+ * // { launchMs: 1823, connectMs: 45, navMs: 620, authOk: true,
6759
+ * // exitReason: 'success', cleanupOk: true, timestamps: { ... } }
6760
+ * ```
6761
+ */
6762
+ telemetry() {
6763
+ return this._telemetry;
6764
+ }
6765
+ /**
6766
+ * Record the result of an authentication check in the telemetry envelope.
6767
+ *
6768
+ * @param ok - Whether authentication was successful
6769
+ */
6770
+ recordAuthResult(ok) {
6771
+ this._telemetry.authOk = ok;
6772
+ }
6574
6773
  };
6575
6774
 
6576
6775
  exports.BlockedBrowserTargetError = BlockedBrowserTargetError;