@skrillex1224/playwright-toolkit 2.1.164 → 2.1.166

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.js CHANGED
@@ -1387,6 +1387,27 @@ var resolveRouteByProxy = ({
1387
1387
  // src/traffic-meter.js
1388
1388
  var logger6 = createInternalLogger("TrafficMeter");
1389
1389
  var encoder = new TextEncoder();
1390
+ var MAX_DOMAIN_BUCKETS = 160;
1391
+ var MAX_REASON_BUCKETS = 64;
1392
+ var MAX_TOP_ITEMS = 12;
1393
+ var MAX_HINT_ITEMS = 8;
1394
+ var UNKNOWN_DOMAIN = "(unknown)";
1395
+ var OTHER_DOMAINS = "(other-domains)";
1396
+ var OTHER_REASONS = "(other-reasons)";
1397
+ var STATIC_RESOURCE_TYPES = /* @__PURE__ */ new Set([
1398
+ "script",
1399
+ "stylesheet",
1400
+ "image",
1401
+ "font",
1402
+ "media",
1403
+ "manifest"
1404
+ ]);
1405
+ var BACKEND_RESOURCE_TYPES = /* @__PURE__ */ new Set([
1406
+ "xhr",
1407
+ "fetch",
1408
+ "websocket",
1409
+ "eventsource"
1410
+ ]);
1390
1411
  var toSafeNumber = (value) => {
1391
1412
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
1392
1413
  return Math.round(value);
@@ -1430,9 +1451,77 @@ var createTrafficState = () => ({
1430
1451
  directUploadBytes: 0,
1431
1452
  totalDownloadBytes: 0,
1432
1453
  proxyDownloadBytes: 0,
1433
- directDownloadBytes: 0
1454
+ directDownloadBytes: 0,
1455
+ totalFailedRequests: 0,
1456
+ proxyFailedRequests: 0,
1457
+ directFailedRequests: 0,
1458
+ totalCanceledRequests: 0,
1459
+ proxyCanceledRequests: 0,
1460
+ directCanceledRequests: 0,
1461
+ orphanDataReceivedBytes: 0,
1462
+ orphanProxyDataReceivedBytes: 0,
1463
+ orphanFinishDeltaBytes: 0,
1464
+ orphanProxyFinishDeltaBytes: 0,
1465
+ domainStats: /* @__PURE__ */ new Map(),
1466
+ typeStats: /* @__PURE__ */ new Map(),
1467
+ failedReasonStats: /* @__PURE__ */ new Map()
1434
1468
  });
1435
1469
  var ensureRoute = (route) => route === "proxy" ? "proxy" : "direct";
1470
+ var normalizeResourceType = (value) => {
1471
+ const type = String(value || "").trim().toLowerCase();
1472
+ if (!type) return "other";
1473
+ if (type === "ws") return "websocket";
1474
+ return type;
1475
+ };
1476
+ var parseHostname = (url = "") => {
1477
+ try {
1478
+ const hostname = new URL(String(url || "")).hostname.toLowerCase();
1479
+ return hostname || "";
1480
+ } catch {
1481
+ return "";
1482
+ }
1483
+ };
1484
+ var normalizeDomainKey = (domain = "") => {
1485
+ const key = String(domain || "").trim().toLowerCase();
1486
+ return key || UNKNOWN_DOMAIN;
1487
+ };
1488
+ var isStaticType = (resourceType = "") => STATIC_RESOURCE_TYPES.has(normalizeResourceType(resourceType));
1489
+ var isBackendType = (resourceType = "") => BACKEND_RESOURCE_TYPES.has(normalizeResourceType(resourceType));
1490
+ var createDomainBucket = (domain) => ({
1491
+ domain,
1492
+ requests: 0,
1493
+ proxyRequests: 0,
1494
+ directRequests: 0,
1495
+ uploadBytes: 0,
1496
+ downloadBytes: 0,
1497
+ totalBytes: 0,
1498
+ proxyBytes: 0,
1499
+ directBytes: 0,
1500
+ failedRequests: 0,
1501
+ canceledRequests: 0,
1502
+ staticBytes: 0,
1503
+ backendBytes: 0
1504
+ });
1505
+ var createTypeBucket = (resourceType) => ({
1506
+ resourceType,
1507
+ requests: 0,
1508
+ proxyRequests: 0,
1509
+ directRequests: 0,
1510
+ uploadBytes: 0,
1511
+ downloadBytes: 0,
1512
+ totalBytes: 0,
1513
+ proxyBytes: 0,
1514
+ directBytes: 0,
1515
+ failedRequests: 0,
1516
+ canceledRequests: 0
1517
+ });
1518
+ var createReasonBucket = (reason) => ({
1519
+ reason,
1520
+ count: 0,
1521
+ canceledCount: 0,
1522
+ proxyCount: 0,
1523
+ directCount: 0
1524
+ });
1436
1525
  var formatBytes = (bytes = 0) => {
1437
1526
  const value = toSafeNumber(bytes);
1438
1527
  if (value <= 0) return "0B";
@@ -1479,6 +1568,190 @@ var addDownloadBytes = (state, route, bytes = 0) => {
1479
1568
  }
1480
1569
  state.directDownloadBytes += b;
1481
1570
  };
1571
+ var incrementBucketRequests = (bucket, route) => {
1572
+ if (!bucket) return;
1573
+ bucket.requests += 1;
1574
+ if (ensureRoute(route) === "proxy") {
1575
+ bucket.proxyRequests += 1;
1576
+ return;
1577
+ }
1578
+ bucket.directRequests += 1;
1579
+ };
1580
+ var incrementBucketBytes = (bucket, route, bytes = 0, direction = "download", resourceType = "other") => {
1581
+ if (!bucket) return;
1582
+ const b = toSafeNumber(bytes);
1583
+ if (b <= 0) return;
1584
+ if (direction === "upload") {
1585
+ bucket.uploadBytes += b;
1586
+ } else {
1587
+ bucket.downloadBytes += b;
1588
+ }
1589
+ bucket.totalBytes += b;
1590
+ if (ensureRoute(route) === "proxy") {
1591
+ bucket.proxyBytes += b;
1592
+ } else {
1593
+ bucket.directBytes += b;
1594
+ }
1595
+ if (bucket.domain !== void 0) {
1596
+ if (isStaticType(resourceType)) {
1597
+ bucket.staticBytes += b;
1598
+ } else if (isBackendType(resourceType)) {
1599
+ bucket.backendBytes += b;
1600
+ }
1601
+ }
1602
+ };
1603
+ var incrementBucketFailed = (bucket, canceled = false) => {
1604
+ if (!bucket) return;
1605
+ bucket.failedRequests += 1;
1606
+ if (canceled) {
1607
+ bucket.canceledRequests += 1;
1608
+ }
1609
+ };
1610
+ var ensureReasonText = (reason) => {
1611
+ const normalized = String(reason || "").trim().toLowerCase();
1612
+ return normalized || "unknown";
1613
+ };
1614
+ var pickDomainBucket = (state, domain = "") => {
1615
+ const domainKey = normalizeDomainKey(domain);
1616
+ const knownBucket = state.domainStats.get(domainKey);
1617
+ if (knownBucket) return knownBucket;
1618
+ if (state.domainStats.size < MAX_DOMAIN_BUCKETS) {
1619
+ const bucket2 = createDomainBucket(domainKey);
1620
+ state.domainStats.set(domainKey, bucket2);
1621
+ return bucket2;
1622
+ }
1623
+ const overflow = state.domainStats.get(OTHER_DOMAINS);
1624
+ if (overflow) return overflow;
1625
+ const bucket = createDomainBucket(OTHER_DOMAINS);
1626
+ state.domainStats.set(OTHER_DOMAINS, bucket);
1627
+ return bucket;
1628
+ };
1629
+ var pickTypeBucket = (state, resourceType = "other") => {
1630
+ const typeKey = normalizeResourceType(resourceType);
1631
+ const knownBucket = state.typeStats.get(typeKey);
1632
+ if (knownBucket) return knownBucket;
1633
+ const bucket = createTypeBucket(typeKey);
1634
+ state.typeStats.set(typeKey, bucket);
1635
+ return bucket;
1636
+ };
1637
+ var pickReasonBucket = (state, reason = "") => {
1638
+ const reasonKey = ensureReasonText(reason);
1639
+ const knownBucket = state.failedReasonStats.get(reasonKey);
1640
+ if (knownBucket) return knownBucket;
1641
+ if (state.failedReasonStats.size < MAX_REASON_BUCKETS) {
1642
+ const bucket2 = createReasonBucket(reasonKey);
1643
+ state.failedReasonStats.set(reasonKey, bucket2);
1644
+ return bucket2;
1645
+ }
1646
+ const overflow = state.failedReasonStats.get(OTHER_REASONS);
1647
+ if (overflow) return overflow;
1648
+ const bucket = createReasonBucket(OTHER_REASONS);
1649
+ state.failedReasonStats.set(OTHER_REASONS, bucket);
1650
+ return bucket;
1651
+ };
1652
+ var addRequestProfile = (state, route, domain, resourceType, uploadBytes = 0) => {
1653
+ const domainBucket = pickDomainBucket(state, domain);
1654
+ const typeBucket = pickTypeBucket(state, resourceType);
1655
+ incrementBucketRequests(domainBucket, route);
1656
+ incrementBucketRequests(typeBucket, route);
1657
+ incrementBucketBytes(domainBucket, route, uploadBytes, "upload", resourceType);
1658
+ incrementBucketBytes(typeBucket, route, uploadBytes, "upload", resourceType);
1659
+ };
1660
+ var addUploadProfile = (state, route, domain, resourceType, uploadBytes = 0) => {
1661
+ const domainBucket = pickDomainBucket(state, domain);
1662
+ const typeBucket = pickTypeBucket(state, resourceType);
1663
+ incrementBucketBytes(domainBucket, route, uploadBytes, "upload", resourceType);
1664
+ incrementBucketBytes(typeBucket, route, uploadBytes, "upload", resourceType);
1665
+ };
1666
+ var addDownloadProfile = (state, route, domain, resourceType, downloadBytes = 0) => {
1667
+ const domainBucket = pickDomainBucket(state, domain);
1668
+ const typeBucket = pickTypeBucket(state, resourceType);
1669
+ incrementBucketBytes(domainBucket, route, downloadBytes, "download", resourceType);
1670
+ incrementBucketBytes(typeBucket, route, downloadBytes, "download", resourceType);
1671
+ };
1672
+ var addFailedProfile = (state, route, domain, resourceType, reason = "unknown", canceled = false) => {
1673
+ const domainBucket = pickDomainBucket(state, domain);
1674
+ const typeBucket = pickTypeBucket(state, resourceType);
1675
+ const reasonBucket = pickReasonBucket(state, reason);
1676
+ incrementBucketFailed(domainBucket, canceled);
1677
+ incrementBucketFailed(typeBucket, canceled);
1678
+ reasonBucket.count += 1;
1679
+ if (ensureRoute(route) === "proxy") {
1680
+ reasonBucket.proxyCount += 1;
1681
+ } else {
1682
+ reasonBucket.directCount += 1;
1683
+ }
1684
+ if (canceled) {
1685
+ reasonBucket.canceledCount += 1;
1686
+ }
1687
+ };
1688
+ var toRoundedRatio = (numerator, denominator) => {
1689
+ if (denominator <= 0) return 0;
1690
+ return Number((numerator / denominator * 100).toFixed(2));
1691
+ };
1692
+ var sortByBytesAndRequests = (left, right) => {
1693
+ if (right.totalBytes !== left.totalBytes) return right.totalBytes - left.totalBytes;
1694
+ if (right.requests !== left.requests) return right.requests - left.requests;
1695
+ return String(left.domain || left.resourceType || "").localeCompare(String(right.domain || right.resourceType || ""));
1696
+ };
1697
+ var buildTopDomains = (state) => {
1698
+ return Array.from(state.domainStats.values()).filter((item) => item && item.totalBytes > 0).sort(sortByBytesAndRequests).slice(0, MAX_TOP_ITEMS).map((item) => ({
1699
+ domain: item.domain,
1700
+ requests: item.requests,
1701
+ proxyRequests: item.proxyRequests,
1702
+ directRequests: item.directRequests,
1703
+ uploadBytes: item.uploadBytes,
1704
+ downloadBytes: item.downloadBytes,
1705
+ totalBytes: item.totalBytes,
1706
+ proxyBytes: item.proxyBytes,
1707
+ directBytes: item.directBytes,
1708
+ failedRequests: item.failedRequests,
1709
+ canceledRequests: item.canceledRequests,
1710
+ staticBytes: item.staticBytes,
1711
+ backendBytes: item.backendBytes
1712
+ }));
1713
+ };
1714
+ var buildTopResourceTypes = (state) => {
1715
+ return Array.from(state.typeStats.values()).filter((item) => item && (item.totalBytes > 0 || item.requests > 0)).sort(sortByBytesAndRequests).slice(0, MAX_TOP_ITEMS).map((item) => ({
1716
+ resourceType: item.resourceType,
1717
+ requests: item.requests,
1718
+ proxyRequests: item.proxyRequests,
1719
+ directRequests: item.directRequests,
1720
+ uploadBytes: item.uploadBytes,
1721
+ downloadBytes: item.downloadBytes,
1722
+ totalBytes: item.totalBytes,
1723
+ proxyBytes: item.proxyBytes,
1724
+ directBytes: item.directBytes,
1725
+ failedRequests: item.failedRequests,
1726
+ canceledRequests: item.canceledRequests
1727
+ }));
1728
+ };
1729
+ var buildFailedReasons = (state) => {
1730
+ return Array.from(state.failedReasonStats.values()).filter((item) => item && item.count > 0).sort((left, right) => {
1731
+ if (right.count !== left.count) return right.count - left.count;
1732
+ return String(left.reason || "").localeCompare(String(right.reason || ""));
1733
+ }).slice(0, MAX_TOP_ITEMS).map((item) => ({
1734
+ reason: item.reason,
1735
+ count: item.count,
1736
+ canceledCount: item.canceledCount,
1737
+ proxyCount: item.proxyCount,
1738
+ directCount: item.directCount
1739
+ }));
1740
+ };
1741
+ var buildProxyByPassHints = (state) => {
1742
+ return Array.from(state.domainStats.values()).filter((item) => item && item.domain !== UNKNOWN_DOMAIN && item.domain !== OTHER_DOMAINS).filter((item) => item.proxyBytes >= 128 * 1024 && item.proxyRequests >= 2 && item.totalBytes > 0).filter((item) => item.staticBytes > item.backendBytes && toRoundedRatio(item.staticBytes, item.totalBytes) >= 80).sort((left, right) => {
1743
+ if (right.proxyBytes !== left.proxyBytes) return right.proxyBytes - left.proxyBytes;
1744
+ return right.proxyRequests - left.proxyRequests;
1745
+ }).slice(0, MAX_HINT_ITEMS).map((item) => ({
1746
+ domain: item.domain,
1747
+ proxyBytes: item.proxyBytes,
1748
+ proxyRequests: item.proxyRequests,
1749
+ totalBytes: item.totalBytes,
1750
+ staticBytes: item.staticBytes,
1751
+ backendBytes: item.backendBytes,
1752
+ staticRatioPct: toRoundedRatio(item.staticBytes, item.totalBytes)
1753
+ }));
1754
+ };
1482
1755
  var createTrafficMeter = ({
1483
1756
  enableProxy = false,
1484
1757
  byPassRules = [],
@@ -1486,7 +1759,10 @@ var createTrafficMeter = ({
1486
1759
  } = {}) => {
1487
1760
  const state = createTrafficState();
1488
1761
  const requestMap = /* @__PURE__ */ new Map();
1762
+ const orphanReceivedMap = /* @__PURE__ */ new Map();
1489
1763
  const wsRouteMap = /* @__PURE__ */ new Map();
1764
+ const attachedPages = /* @__PURE__ */ new WeakSet();
1765
+ const attachedContexts = /* @__PURE__ */ new WeakSet();
1490
1766
  const resolveRoute = (url = "") => {
1491
1767
  return resolveRouteByProxy({
1492
1768
  requestUrl: url,
@@ -1494,62 +1770,169 @@ var createTrafficMeter = ({
1494
1770
  byPassRules
1495
1771
  }).route;
1496
1772
  };
1773
+ const fallbackRoute = () => enableProxy ? "proxy" : "direct";
1497
1774
  const debugLog = (message) => {
1498
1775
  if (!debugMode) return;
1499
1776
  logger6.info(`[\u9010\u8BF7\u6C42\u8C03\u8BD5] ${message}`);
1500
1777
  };
1778
+ const addFailed = (route, canceled = false) => {
1779
+ const normalizedRoute = ensureRoute(route);
1780
+ state.totalFailedRequests += 1;
1781
+ if (normalizedRoute === "proxy") {
1782
+ state.proxyFailedRequests += 1;
1783
+ } else {
1784
+ state.directFailedRequests += 1;
1785
+ }
1786
+ if (!canceled) return;
1787
+ state.totalCanceledRequests += 1;
1788
+ if (normalizedRoute === "proxy") {
1789
+ state.proxyCanceledRequests += 1;
1790
+ } else {
1791
+ state.directCanceledRequests += 1;
1792
+ }
1793
+ };
1794
+ const finalizeByEncodedLength = (requestId, encodedDataLength, source = "finished") => {
1795
+ const safeRequestId = String(requestId || "");
1796
+ if (!safeRequestId) return;
1797
+ const requestState = requestMap.get(safeRequestId);
1798
+ const orphanReceived = toSafeNumber(orphanReceivedMap.get(safeRequestId));
1799
+ const encoded = toSafeNumber(encodedDataLength);
1800
+ if (requestState) {
1801
+ const routed2 = ensureRoute(requestState.route);
1802
+ const downloaded = toSafeNumber(requestState.downloadBytes);
1803
+ const delta2 = Math.max(0, encoded - downloaded);
1804
+ if (delta2 > 0) {
1805
+ addDownloadBytes(state, routed2, delta2);
1806
+ addDownloadProfile(state, routed2, requestState.domain, requestState.resourceType, delta2);
1807
+ requestState.downloadBytes = downloaded + delta2;
1808
+ }
1809
+ const uploadBytes = toSafeNumber(requestState.uploadBytes);
1810
+ const total = uploadBytes + toSafeNumber(requestState.downloadBytes);
1811
+ debugLog(
1812
+ `final id=${safeRequestId} source=${source} status=ok route=${routed2} type=${requestState.resourceType || "other"} upload=${formatBytes(uploadBytes)} (${uploadBytes}) download=${formatBytes(requestState.downloadBytes)} (${requestState.downloadBytes}) total=${formatBytes(total)} (${total}) url=${requestState.url || "-"}`
1813
+ );
1814
+ requestMap.delete(safeRequestId);
1815
+ orphanReceivedMap.delete(safeRequestId);
1816
+ return;
1817
+ }
1818
+ const routed = fallbackRoute();
1819
+ const delta = Math.max(0, encoded - orphanReceived);
1820
+ if (delta > 0) {
1821
+ addDownloadBytes(state, routed, delta);
1822
+ addDownloadProfile(state, routed, UNKNOWN_DOMAIN, "other", delta);
1823
+ }
1824
+ state.orphanFinishDeltaBytes += delta;
1825
+ if (routed === "proxy") {
1826
+ state.orphanProxyFinishDeltaBytes += delta;
1827
+ }
1828
+ debugLog(
1829
+ `final id=${safeRequestId} source=${source} status=orphan route=${routed} encoded=${formatBytes(encoded)} (${encoded}) delta=${formatBytes(delta)} (${delta})`
1830
+ );
1831
+ orphanReceivedMap.delete(safeRequestId);
1832
+ };
1501
1833
  const recordRequest = (params = {}) => {
1502
1834
  const requestId = String(params.requestId || "");
1503
1835
  const request = params.request && typeof params.request === "object" ? params.request : {};
1504
1836
  const url = String(request.url || "");
1505
1837
  const route = resolveRoute(url);
1506
- const resourceType = String(params.type || request.type || "other").trim().toLowerCase() || "other";
1838
+ const resourceType = normalizeResourceType(params.type || request.type || "other");
1839
+ const domain = normalizeDomainKey(parseHostname(url));
1840
+ if (requestId && requestMap.has(requestId)) {
1841
+ const redirectResponse = params.redirectResponse && typeof params.redirectResponse === "object" ? params.redirectResponse : null;
1842
+ finalizeByEncodedLength(
1843
+ requestId,
1844
+ redirectResponse ? redirectResponse.encodedDataLength : 0,
1845
+ "redirect"
1846
+ );
1847
+ }
1507
1848
  addRequests(state, route, 1);
1508
1849
  const uploadBytes = estimateRequestBytes(request);
1509
1850
  addUploadBytes(state, route, uploadBytes);
1851
+ addRequestProfile(state, route, domain, resourceType, uploadBytes);
1510
1852
  if (requestId) {
1511
1853
  requestMap.set(requestId, {
1512
- route,
1854
+ route: ensureRoute(route),
1513
1855
  resourceType,
1856
+ domain,
1514
1857
  url,
1515
- uploadBytes
1858
+ uploadBytes,
1859
+ downloadBytes: 0
1516
1860
  });
1517
1861
  }
1518
1862
  debugLog(
1519
1863
  `request id=${requestId || "-"} route=${route} type=${resourceType} upload=${formatBytes(uploadBytes)} (${uploadBytes}) method=${String(request.method || "GET")} url=${url || "-"}`
1520
1864
  );
1521
1865
  };
1522
- const recordLoadingFinished = (params = {}) => {
1866
+ const recordDataReceived = (params = {}) => {
1523
1867
  const requestId = String(params.requestId || "");
1524
- const requestState = requestId ? requestMap.get(requestId) : null;
1525
- const route = requestState?.route || "direct";
1526
- const resourceType = requestState?.resourceType || "other";
1527
- const downloadBytes = toSafeNumber(params.encodedDataLength);
1528
- addDownloadBytes(state, route, downloadBytes);
1529
- const uploadBytes = toSafeNumber(requestState?.uploadBytes);
1530
- const totalBytes = uploadBytes + downloadBytes;
1531
- debugLog(
1532
- `finish id=${requestId || "-"} route=${route} type=${resourceType} download=${formatBytes(downloadBytes)} (${downloadBytes}) total=${formatBytes(totalBytes)} (${totalBytes}) url=${requestState?.url || "-"}`
1533
- );
1534
- if (requestId) {
1535
- requestMap.delete(requestId);
1868
+ const bytes = toSafeNumber(params.encodedDataLength);
1869
+ if (bytes <= 0) return;
1870
+ if (!requestId) {
1871
+ const routed2 = fallbackRoute();
1872
+ addDownloadBytes(state, routed2, bytes);
1873
+ addDownloadProfile(state, routed2, UNKNOWN_DOMAIN, "other", bytes);
1874
+ state.orphanDataReceivedBytes += bytes;
1875
+ if (routed2 === "proxy") {
1876
+ state.orphanProxyDataReceivedBytes += bytes;
1877
+ }
1878
+ return;
1536
1879
  }
1880
+ const requestState = requestMap.get(requestId);
1881
+ if (requestState) {
1882
+ requestState.downloadBytes = toSafeNumber(requestState.downloadBytes) + bytes;
1883
+ addDownloadBytes(state, requestState.route, bytes);
1884
+ addDownloadProfile(state, requestState.route, requestState.domain, requestState.resourceType, bytes);
1885
+ return;
1886
+ }
1887
+ const prev = toSafeNumber(orphanReceivedMap.get(requestId));
1888
+ orphanReceivedMap.set(requestId, prev + bytes);
1889
+ const routed = fallbackRoute();
1890
+ addDownloadBytes(state, routed, bytes);
1891
+ addDownloadProfile(state, routed, UNKNOWN_DOMAIN, "other", bytes);
1892
+ state.orphanDataReceivedBytes += bytes;
1893
+ if (routed === "proxy") {
1894
+ state.orphanProxyDataReceivedBytes += bytes;
1895
+ }
1896
+ };
1897
+ const recordLoadingFinished = (params = {}) => {
1898
+ finalizeByEncodedLength(params.requestId, params.encodedDataLength, "loadingFinished");
1537
1899
  };
1538
1900
  const recordRequestFailed = (params = {}) => {
1539
1901
  const requestId = String(params.requestId || "");
1540
1902
  const requestState = requestId ? requestMap.get(requestId) : null;
1541
- debugLog(
1542
- `failed id=${requestId || "-"} route=${requestState?.route || "direct"} type=${requestState?.resourceType || "other"} reason=${String(params.errorText || "").trim() || "unknown"} canceled=${Boolean(params.canceled)} url=${requestState?.url || "-"}`
1543
- );
1903
+ const canceled = Boolean(params.canceled);
1904
+ const reason = ensureReasonText(params.errorText);
1905
+ const routed = ensureRoute(requestState?.route || fallbackRoute());
1906
+ const resourceType = normalizeResourceType(requestState?.resourceType || "other");
1907
+ const domain = normalizeDomainKey(requestState?.domain || "");
1908
+ if (requestState) {
1909
+ addFailed(routed, canceled);
1910
+ addFailedProfile(state, routed, domain, resourceType, reason, canceled);
1911
+ const uploadBytes = toSafeNumber(requestState.uploadBytes);
1912
+ const downloadBytes = toSafeNumber(requestState.downloadBytes);
1913
+ const totalBytes = uploadBytes + downloadBytes;
1914
+ debugLog(
1915
+ `final id=${requestId || "-"} source=loadingFailed status=failed route=${routed} type=${requestState.resourceType || "other"} upload=${formatBytes(uploadBytes)} (${uploadBytes}) download=${formatBytes(downloadBytes)} (${downloadBytes}) total=${formatBytes(totalBytes)} (${totalBytes}) canceled=${canceled} reason=${reason} url=${requestState.url || "-"}`
1916
+ );
1917
+ } else {
1918
+ const orphanDownload = toSafeNumber(orphanReceivedMap.get(requestId));
1919
+ addFailed(routed, canceled);
1920
+ addFailedProfile(state, routed, UNKNOWN_DOMAIN, "other", reason, canceled);
1921
+ debugLog(
1922
+ `final id=${requestId || "-"} source=loadingFailed status=orphan-failed route=${routed} upload=0B (0) download=${formatBytes(orphanDownload)} (${orphanDownload}) total=${formatBytes(orphanDownload)} (${orphanDownload}) canceled=${canceled} reason=${reason} url=-`
1923
+ );
1924
+ }
1544
1925
  if (requestId) {
1545
1926
  requestMap.delete(requestId);
1927
+ orphanReceivedMap.delete(requestId);
1546
1928
  }
1547
1929
  };
1548
1930
  const bindWebSocketRoute = (params = {}) => {
1549
1931
  const requestId = String(params.requestId || "");
1550
1932
  if (!requestId) return;
1551
- const route = resolveRoute(params.url || "");
1552
- wsRouteMap.set(requestId, { route, url: String(params.url || "") });
1933
+ const url = String(params.url || "");
1934
+ const route = resolveRoute(url);
1935
+ wsRouteMap.set(requestId, { route, url, domain: normalizeDomainKey(parseHostname(url)) });
1553
1936
  };
1554
1937
  const clearWebSocketRoute = (params = {}) => {
1555
1938
  const requestId = String(params.requestId || "");
@@ -1557,48 +1940,64 @@ var createTrafficMeter = ({
1557
1940
  wsRouteMap.delete(requestId);
1558
1941
  };
1559
1942
  const getWebSocketMeta = (requestId = "") => {
1560
- if (!requestId) return { route: "direct", url: "" };
1943
+ if (!requestId) return { route: fallbackRoute(), url: "", domain: UNKNOWN_DOMAIN };
1561
1944
  const meta = wsRouteMap.get(requestId);
1562
1945
  if (!meta || typeof meta !== "object") {
1563
- return { route: "direct", url: "" };
1946
+ return { route: fallbackRoute(), url: "", domain: UNKNOWN_DOMAIN };
1564
1947
  }
1565
1948
  return {
1566
1949
  route: ensureRoute(meta.route),
1567
- url: String(meta.url || "")
1950
+ url: String(meta.url || ""),
1951
+ domain: normalizeDomainKey(meta.domain)
1568
1952
  };
1569
1953
  };
1570
1954
  const recordWebSocketFrameSent = (params = {}) => {
1571
1955
  const requestId = String(params.requestId || "");
1572
- const { route, url } = getWebSocketMeta(requestId);
1956
+ const { route, url, domain } = getWebSocketMeta(requestId);
1573
1957
  const payload = params.response && typeof params.response === "object" ? params.response.payloadData : "";
1574
1958
  const bytes = byteLength(payload || "");
1575
1959
  addUploadBytes(state, route, bytes);
1960
+ addUploadProfile(state, route, domain, "websocket", bytes);
1576
1961
  debugLog(`ws-send id=${requestId || "-"} route=${route} bytes=${formatBytes(bytes)} (${bytes}) url=${url || "-"}`);
1577
1962
  };
1578
1963
  const recordWebSocketFrameReceived = (params = {}) => {
1579
1964
  const requestId = String(params.requestId || "");
1580
- const { route, url } = getWebSocketMeta(requestId);
1965
+ const { route, url, domain } = getWebSocketMeta(requestId);
1581
1966
  const payload = params.response && typeof params.response === "object" ? params.response.payloadData : "";
1582
1967
  const bytes = byteLength(payload || "");
1583
1968
  addDownloadBytes(state, route, bytes);
1969
+ addDownloadProfile(state, route, domain, "websocket", bytes);
1584
1970
  debugLog(`ws-recv id=${requestId || "-"} route=${route} bytes=${formatBytes(bytes)} (${bytes}) url=${url || "-"}`);
1585
1971
  };
1586
1972
  const attachPage = async (page) => {
1587
1973
  if (!page || typeof page.context !== "function") return;
1974
+ if (attachedPages.has(page)) return;
1975
+ attachedPages.add(page);
1588
1976
  try {
1589
1977
  const context = page.context();
1590
1978
  if (!context || typeof context.newCDPSession !== "function") {
1591
1979
  return;
1592
1980
  }
1981
+ if (!attachedContexts.has(context) && typeof context.on === "function") {
1982
+ attachedContexts.add(context);
1983
+ context.on("page", (nextPage) => {
1984
+ if (!nextPage) return;
1985
+ attachPage(nextPage).catch((error) => {
1986
+ logger6.warn(`\u5B50\u9875\u9762 CDP \u76D1\u542C\u6CE8\u518C\u5931\u8D25: ${error?.message || error}`);
1987
+ });
1988
+ });
1989
+ }
1593
1990
  const session = await context.newCDPSession(page);
1594
1991
  await session.send("Network.enable");
1595
1992
  session.on("Network.requestWillBeSent", recordRequest);
1993
+ session.on("Network.dataReceived", recordDataReceived);
1596
1994
  session.on("Network.loadingFinished", recordLoadingFinished);
1597
1995
  session.on("Network.loadingFailed", recordRequestFailed);
1598
1996
  session.on("Network.webSocketCreated", bindWebSocketRoute);
1599
1997
  session.on("Network.webSocketClosed", clearWebSocketRoute);
1600
1998
  session.on("Network.webSocketFrameSent", recordWebSocketFrameSent);
1601
1999
  session.on("Network.webSocketFrameReceived", recordWebSocketFrameReceived);
2000
+ debugLog("CDP \u76D1\u542C\u5DF2\u6CE8\u518C");
1602
2001
  } catch (error) {
1603
2002
  logger6.warn(`CDP \u76D1\u542C\u6CE8\u518C\u5931\u8D25: ${error?.message || error}`);
1604
2003
  }
@@ -1608,7 +2007,7 @@ var createTrafficMeter = ({
1608
2007
  const proxyBytes = state.proxyUploadBytes + state.proxyDownloadBytes;
1609
2008
  const directBytes = state.directUploadBytes + state.directDownloadBytes;
1610
2009
  return {
1611
- meter: "cdp",
2010
+ meter: "cdp-data-received-v3",
1612
2011
  totalRequests: state.totalRequests,
1613
2012
  proxyRequests: state.proxyRequests,
1614
2013
  directRequests: state.directRequests,
@@ -1620,12 +2019,29 @@ var createTrafficMeter = ({
1620
2019
  directDownloadBytes: state.directDownloadBytes,
1621
2020
  totalBytes,
1622
2021
  proxyBytes,
1623
- directBytes
2022
+ directBytes,
2023
+ totalFailedRequests: state.totalFailedRequests,
2024
+ proxyFailedRequests: state.proxyFailedRequests,
2025
+ directFailedRequests: state.directFailedRequests,
2026
+ totalCanceledRequests: state.totalCanceledRequests,
2027
+ proxyCanceledRequests: state.proxyCanceledRequests,
2028
+ directCanceledRequests: state.directCanceledRequests,
2029
+ orphanDataReceivedBytes: state.orphanDataReceivedBytes,
2030
+ orphanProxyDataReceivedBytes: state.orphanProxyDataReceivedBytes,
2031
+ orphanFinishDeltaBytes: state.orphanFinishDeltaBytes,
2032
+ orphanProxyFinishDeltaBytes: state.orphanProxyFinishDeltaBytes,
2033
+ openRequestCount: requestMap.size,
2034
+ orphanOpenCount: orphanReceivedMap.size,
2035
+ topDomains: buildTopDomains(state),
2036
+ topResourceTypes: buildTopResourceTypes(state),
2037
+ failedReasons: buildFailedReasons(state),
2038
+ proxyBypassHints: buildProxyByPassHints(state)
1624
2039
  };
1625
2040
  };
1626
2041
  const reset = () => {
1627
2042
  Object.assign(state, createTrafficState());
1628
2043
  requestMap.clear();
2044
+ orphanReceivedMap.clear();
1629
2045
  wsRouteMap.clear();
1630
2046
  };
1631
2047
  return {
@@ -2360,6 +2776,196 @@ var Mutation = {
2360
2776
  logger11.success("waitForStable", `DOM \u7A33\u5B9A, \u603B\u5171 ${result.mutationCount} \u6B21\u53D8\u5316${result.wasPaused ? ", \u66FE\u6682\u505C\u8BA1\u65F6" : ""}`);
2361
2777
  return result;
2362
2778
  },
2779
+ /**
2780
+ * 等待跨 root DOM 元素稳定(主文档 + iframe 内容)
2781
+ * 通过轮询快照检测变化,适配 iframe / shadow 场景
2782
+ *
2783
+ * @param {import('playwright').Page} page - Playwright page 对象
2784
+ * @param {string | string[]} selectors - 要监控的 CSS 选择器,单个或多个(建议传 iframe 选择器)
2785
+ * @param {Object} [options] - 配置选项(签名与 waitForStable 保持一致)
2786
+ * @param {number} [options.initialTimeout] - 等待元素出现的超时 (毫秒, 默认: 60000)
2787
+ * @param {number} [options.stableTime] - 无变化持续时间后 resolve (毫秒, 默认: 10000)
2788
+ * @param {number} [options.timeout] - 整体超时时间 (毫秒, 默认: 180000)
2789
+ * @param {Function} [options.onMutation] - 变化时的回调钩子
2790
+ * @returns {Promise<{ mutationCount: number, stableTime: number, wasPaused: boolean }>}
2791
+ */
2792
+ async waitForStableAcrossRoots(page, selectors, options = {}) {
2793
+ const selectorList = Array.isArray(selectors) ? selectors : [selectors];
2794
+ const selectorQuery = selectorList.join(",");
2795
+ const initialTimeout = options.initialTimeout ?? 60 * 1e3;
2796
+ const waitForStableTime = options.stableTime ?? 10 * 1e3;
2797
+ const overallTimeout = options.timeout ?? 180 * 1e3;
2798
+ const onMutation = options.onMutation;
2799
+ const pollInterval = 500;
2800
+ const sleep = (ms) => new Promise((resolve) => {
2801
+ setTimeout(resolve, ms);
2802
+ });
2803
+ const truncate = (value, max = 800) => {
2804
+ const text = String(value || "");
2805
+ if (text.length <= max) return text;
2806
+ return `${text.slice(0, max)}...`;
2807
+ };
2808
+ const buildState = async () => {
2809
+ return await page.evaluate(({ selectorList: selectorList2 }) => {
2810
+ const normalizeText = (value) => String(value || "").replace(/\s+/g, " ").trim();
2811
+ const tail = (value, max = 512) => {
2812
+ const text = String(value || "");
2813
+ if (text.length <= max) return text;
2814
+ return text.slice(text.length - max);
2815
+ };
2816
+ const safeFrameId = (frameEl) => {
2817
+ const id = String(frameEl?.id || "").trim();
2818
+ if (id) return id;
2819
+ const name = String(frameEl?.name || "").trim();
2820
+ if (name) return name;
2821
+ return "no-id";
2822
+ };
2823
+ const items = [];
2824
+ selectorList2.forEach((selector) => {
2825
+ let nodes = [];
2826
+ try {
2827
+ nodes = Array.from(document.querySelectorAll(selector));
2828
+ } catch {
2829
+ return;
2830
+ }
2831
+ nodes.forEach((node, index) => {
2832
+ const isIframe = node?.tagName === "IFRAME";
2833
+ let text = "";
2834
+ let html = "";
2835
+ let source = "main";
2836
+ let path = `${selector}[${index}]`;
2837
+ if (isIframe) {
2838
+ source = "iframe";
2839
+ path = `${selector}[${index}]::iframe(${safeFrameId(node)})`;
2840
+ try {
2841
+ const frameDoc = node.contentDocument;
2842
+ const frameRoot = frameDoc?.body || frameDoc?.documentElement;
2843
+ if (frameRoot) {
2844
+ text = normalizeText(frameRoot.innerText || frameRoot.textContent || "");
2845
+ html = normalizeText(frameRoot.innerHTML || "");
2846
+ }
2847
+ } catch {
2848
+ }
2849
+ } else {
2850
+ text = normalizeText(node?.innerText || node?.textContent || "");
2851
+ html = normalizeText(node?.innerHTML || node?.outerHTML || "");
2852
+ }
2853
+ const snapshot = text || html;
2854
+ items.push({
2855
+ selector,
2856
+ source,
2857
+ path,
2858
+ text,
2859
+ html,
2860
+ snapshot
2861
+ });
2862
+ });
2863
+ });
2864
+ const snapshotKey = items.map((item) => `${item.path}:${item.snapshot.length}:${tail(item.snapshot, 512)}`).join("||");
2865
+ const summaryText = items.map((item) => item.snapshot || item.text).join("\n").trim();
2866
+ const summaryHtml = items.map((item) => item.html).join("\n");
2867
+ const hasMatched = items.length > 0;
2868
+ const primary = items.length > 0 ? items[items.length - 1] : null;
2869
+ return {
2870
+ hasMatched,
2871
+ snapshotKey,
2872
+ text: summaryText,
2873
+ html: summaryHtml,
2874
+ snapshotLength: summaryText.length,
2875
+ itemCount: items.length,
2876
+ primaryPath: primary?.path || "",
2877
+ mutationNodes: items.map((item) => ({
2878
+ html: item.html,
2879
+ text: item.snapshot || item.text,
2880
+ mutationType: item.source
2881
+ }))
2882
+ };
2883
+ }, { selectorList });
2884
+ };
2885
+ const invokeMutationCallback = async (context) => {
2886
+ if (!onMutation) return "__CONTINUE__";
2887
+ try {
2888
+ const result = await onMutation(context);
2889
+ return result === null || result === void 0 ? "__CONTINUE__" : "__PAUSE__";
2890
+ } catch {
2891
+ return "__CONTINUE__";
2892
+ }
2893
+ };
2894
+ logger11.start(
2895
+ "waitForStableAcrossRoots",
2896
+ `\u76D1\u63A7 ${selectorList.length} \u4E2A\u9009\u62E9\u5668(\u8DE8 root), \u7A33\u5B9A\u65F6\u95F4=${waitForStableTime}ms`
2897
+ );
2898
+ if (initialTimeout > 0) {
2899
+ try {
2900
+ await page.waitForSelector(selectorQuery, { timeout: initialTimeout });
2901
+ logger11.info(`waitForStableAcrossRoots \u5DF2\u68C0\u6D4B\u5230\u5143\u7D20: ${selectorQuery}`);
2902
+ } catch (e) {
2903
+ logger11.warning(`waitForStableAcrossRoots \u521D\u59CB\u7B49\u5F85\u8D85\u65F6 (${initialTimeout}ms): ${selectorQuery}`);
2904
+ throw e;
2905
+ }
2906
+ }
2907
+ let state = await buildState();
2908
+ if (!state?.hasMatched) {
2909
+ logger11.warning("waitForStableAcrossRoots \u672A\u627E\u5230\u53EF\u76D1\u63A7\u7684\u5143\u7D20");
2910
+ return { mutationCount: 0, stableTime: 0, wasPaused: false };
2911
+ }
2912
+ let mutationCount = 0;
2913
+ let stableSince = 0;
2914
+ let isPaused = false;
2915
+ let wasPaused = false;
2916
+ let lastSnapshotKey = state.snapshotKey;
2917
+ const applyPauseSignal = (signal) => {
2918
+ const nextPaused = signal === "__PAUSE__";
2919
+ if (nextPaused) {
2920
+ if (!isPaused) wasPaused = true;
2921
+ isPaused = true;
2922
+ stableSince = 0;
2923
+ return;
2924
+ }
2925
+ isPaused = false;
2926
+ stableSince = Date.now();
2927
+ };
2928
+ const initialSignal = await invokeMutationCallback({
2929
+ mutationCount: 0,
2930
+ html: state.html || "",
2931
+ text: state.text || "",
2932
+ mutationNodes: state.mutationNodes || []
2933
+ });
2934
+ applyPauseSignal(initialSignal);
2935
+ const deadline = Date.now() + overallTimeout;
2936
+ let lastState = state;
2937
+ while (Date.now() < deadline) {
2938
+ await sleep(pollInterval);
2939
+ lastState = await buildState();
2940
+ if (!lastState?.hasMatched) {
2941
+ continue;
2942
+ }
2943
+ if (lastState.snapshotKey !== lastSnapshotKey) {
2944
+ lastSnapshotKey = lastState.snapshotKey;
2945
+ mutationCount += 1;
2946
+ logger11.info(
2947
+ `waitForStableAcrossRoots \u53D8\u5316#${mutationCount}, len=${lastState.snapshotLength}, path=${lastState.primaryPath || "unknown"}, preview="${truncate(lastState.text, 120)}"`
2948
+ );
2949
+ const signal = await invokeMutationCallback({
2950
+ mutationCount,
2951
+ html: lastState.html || "",
2952
+ text: lastState.text || "",
2953
+ mutationNodes: lastState.mutationNodes || []
2954
+ });
2955
+ applyPauseSignal(signal);
2956
+ continue;
2957
+ }
2958
+ if (!isPaused && stableSince > 0 && Date.now() - stableSince >= waitForStableTime) {
2959
+ logger11.success("waitForStableAcrossRoots", `DOM \u7A33\u5B9A, \u603B\u5171 ${mutationCount} \u6B21\u53D8\u5316${wasPaused ? ", \u66FE\u6682\u505C\u8BA1\u65F6" : ""}`);
2960
+ return {
2961
+ mutationCount,
2962
+ stableTime: waitForStableTime,
2963
+ wasPaused
2964
+ };
2965
+ }
2966
+ }
2967
+ throw new Error(`waitForStableAcrossRoots \u8D85\u65F6 (${overallTimeout}ms), \u5DF2\u68C0\u6D4B\u5230 ${mutationCount} \u6B21\u53D8\u5316, isPaused=${isPaused}`);
2968
+ },
2363
2969
  /**
2364
2970
  * 创建一个持续监控 DOM 变化的监控器(默认仅监听新增 DOM)
2365
2971
  *