mcp-scraper 0.48.0 → 0.50.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.
Files changed (59) hide show
  1. package/README.md +2 -2
  2. package/dist/{analytics-repository-WWXVU3VW.js → analytics-repository-IKE53S3E.js} +10 -2
  3. package/dist/bin/api-server.cjs +1214 -328
  4. package/dist/bin/api-server.cjs.map +1 -1
  5. package/dist/bin/api-server.js +4 -4
  6. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  7. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  8. package/dist/bin/mcp-scraper-cli.js +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs +3 -3
  10. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-install.js +2 -2
  12. package/dist/bin/mcp-stdio-server.cjs +75 -8
  13. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  14. package/dist/bin/mcp-stdio-server.js +5 -5
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +3 -3
  17. package/dist/{chunk-MFNGUM4L.js → chunk-2XTYLJQK.js} +15 -1
  18. package/dist/chunk-2XTYLJQK.js.map +1 -0
  19. package/dist/{chunk-PTABZABJ.js → chunk-44LNKZTE.js} +76 -8
  20. package/dist/chunk-44LNKZTE.js.map +1 -0
  21. package/dist/{chunk-YW2LXLDU.js → chunk-5BEPOFKG.js} +160 -15
  22. package/dist/chunk-5BEPOFKG.js.map +1 -0
  23. package/dist/chunk-C7NN57WH.js +7 -0
  24. package/dist/chunk-C7NN57WH.js.map +1 -0
  25. package/dist/{chunk-MFE6RMIA.js → chunk-GOZIG6HD.js} +2 -2
  26. package/dist/{chunk-R66PJOZW.js → chunk-NNZXZTJ2.js} +2 -2
  27. package/dist/{chunk-QAH7WMYC.js → chunk-QH63IZTX.js} +3 -3
  28. package/dist/chunk-QH63IZTX.js.map +1 -0
  29. package/dist/{chunk-6DPIE262.js → chunk-QHVFKFME.js} +2 -2
  30. package/dist/{chunk-PODASLGT.js → chunk-QXAY44SA.js} +2 -2
  31. package/dist/{chunk-ZEMHL36J.js → chunk-YMSZO62N.js} +2 -2
  32. package/dist/{db-A4YUPV4Z.js → db-UPSS5BGM.js} +2 -2
  33. package/dist/{extract-bundle-2HONVC5T.js → extract-bundle-RCTNANCH.js} +3 -3
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.js +3 -3
  36. package/dist/{location-data-repository-YWFJGNIP.js → location-data-repository-UUSHH5M2.js} +3 -3
  37. package/dist/{server-EVTCL3KB.js → server-3ND2EXYO.js} +882 -242
  38. package/dist/server-3ND2EXYO.js.map +1 -0
  39. package/dist/{site-extract-repository-STNOI5OH.js → site-extract-repository-3VQAALNR.js} +3 -3
  40. package/dist/{worker-4MHZS3OB.js → worker-KQN673JF.js} +5 -5
  41. package/package.json +1 -1
  42. package/dist/chunk-IAXQIHNS.js +0 -7
  43. package/dist/chunk-IAXQIHNS.js.map +0 -1
  44. package/dist/chunk-MFNGUM4L.js.map +0 -1
  45. package/dist/chunk-PTABZABJ.js.map +0 -1
  46. package/dist/chunk-QAH7WMYC.js.map +0 -1
  47. package/dist/chunk-YW2LXLDU.js.map +0 -1
  48. package/dist/server-EVTCL3KB.js.map +0 -1
  49. /package/dist/{analytics-repository-WWXVU3VW.js.map → analytics-repository-IKE53S3E.js.map} +0 -0
  50. /package/dist/{chunk-MFE6RMIA.js.map → chunk-GOZIG6HD.js.map} +0 -0
  51. /package/dist/{chunk-R66PJOZW.js.map → chunk-NNZXZTJ2.js.map} +0 -0
  52. /package/dist/{chunk-6DPIE262.js.map → chunk-QHVFKFME.js.map} +0 -0
  53. /package/dist/{chunk-PODASLGT.js.map → chunk-QXAY44SA.js.map} +0 -0
  54. /package/dist/{chunk-ZEMHL36J.js.map → chunk-YMSZO62N.js.map} +0 -0
  55. /package/dist/{db-A4YUPV4Z.js.map → db-UPSS5BGM.js.map} +0 -0
  56. /package/dist/{extract-bundle-2HONVC5T.js.map → extract-bundle-RCTNANCH.js.map} +0 -0
  57. /package/dist/{location-data-repository-YWFJGNIP.js.map → location-data-repository-UUSHH5M2.js.map} +0 -0
  58. /package/dist/{site-extract-repository-STNOI5OH.js.map → site-extract-repository-3VQAALNR.js.map} +0 -0
  59. /package/dist/{worker-4MHZS3OB.js.map → worker-KQN673JF.js.map} +0 -0
@@ -459,6 +459,12 @@ async function migrateAnalytics() {
459
459
  await client.query(
460
460
  `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS utm_content text`
461
461
  );
462
+ await client.query(
463
+ `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS engaged_ms integer`
464
+ );
465
+ await client.query(
466
+ `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS scroll_depth smallint`
467
+ );
462
468
  await client.query(
463
469
  `ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL`
464
470
  );
@@ -984,6 +990,14 @@ async function setAnalyticsPixelDomainState(input) {
984
990
  404
985
991
  );
986
992
  }
993
+ var MAX_ENGAGED_MS = 30 * 60 * 1e3;
994
+ var ENGAGED_SESSION_MS = 1e4;
995
+ function boundedEngagedMs(value) {
996
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.min(Math.trunc(value), MAX_ENGAGED_MS) : null;
997
+ }
998
+ function boundedScrollDepth(value) {
999
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.min(Math.trunc(value), 100) : null;
1000
+ }
987
1001
  var blockedPropertyName = /(email|phone|password|token|secret|authorization|cookie|name|address|form|body)/i;
988
1002
  function normalizeAnalyticsPath(value) {
989
1003
  if (!value) return "/";
@@ -1172,10 +1186,11 @@ async function ingestAnalyticsEvents(input) {
1172
1186
  id, event_id, site_id, pixel_id, hostname, visitor_id, session_id, event_name,
1173
1187
  occurred_at, path, canonical_url, title, referrer, source, medium, campaign,
1174
1188
  device_class, properties, country_code, region_code, click_ids, person_id,
1175
- channel_family, platform, campaign_id, ad_set_id, ad_id, creative_id, placement, utm_term, utm_content
1189
+ channel_family, platform, campaign_id, ad_set_id, ad_id, creative_id, placement, utm_term, utm_content,
1190
+ engaged_ms, scroll_depth
1176
1191
  ) VALUES (
1177
1192
  $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18::jsonb, $19, $20, $21::jsonb, $22,
1178
- $23, $24, $25, $26, $27, $28, $29, $30, $31
1193
+ $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33
1179
1194
  ) ON CONFLICT(site_id, event_id) DO NOTHING`,
1180
1195
  [
1181
1196
  randomUUID(),
@@ -1208,7 +1223,9 @@ async function ingestAnalyticsEvents(input) {
1208
1223
  attribution.creativeId,
1209
1224
  attribution.placement,
1210
1225
  attribution.term,
1211
- attribution.content
1226
+ attribution.content,
1227
+ boundedEngagedMs(event.engagedMs),
1228
+ boundedScrollDepth(event.scrollDepth)
1212
1229
  ]
1213
1230
  );
1214
1231
  if (inserted.rowCount) accepted += 1;
@@ -1726,10 +1743,37 @@ async function analyticsChannelBreakdown(siteId, userId, filters) {
1726
1743
  dataFreshThrough: (/* @__PURE__ */ new Date()).toISOString()
1727
1744
  };
1728
1745
  }
1729
- async function analyticsContent(siteId, userId, filters, page) {
1746
+ var ANALYTICS_CONTENT_SORTS = [
1747
+ "path",
1748
+ "visitors",
1749
+ "sessions",
1750
+ "pageviews",
1751
+ "entrances",
1752
+ "exits",
1753
+ "engagement_events",
1754
+ "conversions",
1755
+ "avg_seconds_on_page",
1756
+ "avg_scroll_depth",
1757
+ "bounce_rate",
1758
+ "exit_rate",
1759
+ "engagement_rate",
1760
+ "pageviews_per_session"
1761
+ ];
1762
+ function normalizeContentOptions(options) {
1763
+ const sort = ANALYTICS_CONTENT_SORTS.includes(options?.sort) ? options.sort : "pageviews";
1764
+ return {
1765
+ sort,
1766
+ direction: options?.direction === "asc" ? "asc" : "desc",
1767
+ search: options?.search?.trim().slice(0, 300) || null
1768
+ };
1769
+ }
1770
+ async function analyticsContent(siteId, userId, filters, page, options) {
1730
1771
  const db = getAnalyticsPool();
1731
1772
  await accessFor(db, siteId, userId);
1732
1773
  const query = eventFilterSql(siteId, filters);
1774
+ const table = normalizeContentOptions(options);
1775
+ const searchParam = `$${query.values.length + 1}`;
1776
+ const order = table.sort === "path" ? `metrics.path ${table.direction === "asc" ? "ASC" : "DESC"}` : `${table.sort} ${table.direction === "asc" ? "ASC" : "DESC"} NULLS LAST, metrics.path ASC`;
1733
1777
  const result = await db.query(
1734
1778
  `WITH filtered AS (
1735
1779
  SELECT e.* FROM analytics_events e WHERE ${query.where}
@@ -1737,7 +1781,7 @@ async function analyticsContent(siteId, userId, filters, page) {
1737
1781
  SELECT path, count(DISTINCT visitor_id)::int AS visitors,
1738
1782
  count(DISTINCT session_id)::int AS sessions,
1739
1783
  count(*) FILTER (WHERE event_name = 'page_view')::int AS pageviews,
1740
- count(*) FILTER (WHERE event_name <> 'page_view')::int AS engagement_events,
1784
+ count(*) FILTER (WHERE event_name NOT IN ('page_view', 'page_engagement'))::int AS engagement_events,
1741
1785
  count(*)::int AS events
1742
1786
  FROM filtered GROUP BY path
1743
1787
  ), session_bounds AS (
@@ -1745,35 +1789,98 @@ async function analyticsContent(siteId, userId, filters, page) {
1745
1789
  (array_agg(path ORDER BY occurred_at ASC, id ASC))[1] AS entrance_path,
1746
1790
  (array_agg(path ORDER BY occurred_at DESC, id DESC))[1] AS exit_path
1747
1791
  FROM filtered WHERE session_id IS NOT NULL GROUP BY session_id
1792
+ ), session_pageviews AS (
1793
+ SELECT session_id, count(*) FILTER (WHERE event_name = 'page_view')::int AS pageviews
1794
+ FROM filtered WHERE session_id IS NOT NULL GROUP BY session_id
1795
+ ), page_dwell AS (
1796
+ SELECT session_id, path,
1797
+ sum(engaged_ms)::bigint AS engaged_ms,
1798
+ max(scroll_depth)::int AS scroll_depth
1799
+ FROM filtered
1800
+ WHERE event_name = 'page_engagement' AND session_id IS NOT NULL
1801
+ GROUP BY session_id, path
1802
+ ), dwell_metrics AS (
1803
+ SELECT path,
1804
+ avg(engaged_ms) FILTER (WHERE engaged_ms IS NOT NULL) AS avg_engaged_ms,
1805
+ avg(scroll_depth) FILTER (WHERE scroll_depth IS NOT NULL) AS avg_scroll,
1806
+ count(*) FILTER (WHERE engaged_ms IS NOT NULL)::int AS measured_pageviews
1807
+ FROM page_dwell GROUP BY path
1748
1808
  ), entrances AS (
1749
1809
  SELECT entrance_path AS path, count(*)::int AS entrances FROM session_bounds GROUP BY entrance_path
1750
1810
  ), exits AS (
1751
1811
  SELECT exit_path AS path, count(*)::int AS exits FROM session_bounds GROUP BY exit_path
1812
+ ), bounces AS (
1813
+ SELECT b.entrance_path AS path, count(*)::int AS bounces
1814
+ FROM session_bounds b JOIN session_pageviews p USING(session_id)
1815
+ WHERE p.pageviews <= 1
1816
+ GROUP BY b.entrance_path
1817
+ ), converted_sessions AS (
1818
+ SELECT DISTINCT c.session_id
1819
+ FROM analytics_conversions c
1820
+ WHERE c.site_id = $1 AND c.occurred_at >= $2::timestamptz AND c.occurred_at < $3::timestamptz
1821
+ AND c.session_id IS NOT NULL
1822
+ ), path_sessions AS (
1823
+ SELECT DISTINCT path, session_id FROM filtered WHERE session_id IS NOT NULL
1824
+ ), engaged AS (
1825
+ SELECT ps.path,
1826
+ count(*) FILTER (
1827
+ WHERE sp.pageviews >= 2
1828
+ OR COALESCE(pd.engaged_ms, 0) >= ${ENGAGED_SESSION_MS}
1829
+ OR cs.session_id IS NOT NULL
1830
+ )::int AS engaged_sessions
1831
+ FROM path_sessions ps
1832
+ JOIN session_pageviews sp USING(session_id)
1833
+ LEFT JOIN page_dwell pd ON pd.session_id = ps.session_id AND pd.path = ps.path
1834
+ LEFT JOIN converted_sessions cs ON cs.session_id = ps.session_id
1835
+ GROUP BY ps.path
1752
1836
  ), assisted AS (
1753
1837
  SELECT f.path, count(DISTINCT c.id)::int AS conversions
1754
1838
  FROM analytics_conversions c JOIN filtered f ON f.session_id = c.session_id
1755
1839
  WHERE c.site_id = $1 AND c.occurred_at >= $2::timestamptz AND c.occurred_at < $3::timestamptz
1756
1840
  GROUP BY f.path
1757
1841
  )
1758
- SELECT metrics.*, COALESCE(entrances.entrances, 0)::int AS entrances,
1842
+ SELECT metrics.*,
1843
+ COALESCE(entrances.entrances, 0)::int AS entrances,
1759
1844
  COALESCE(exits.exits, 0)::int AS exits,
1760
- COALESCE(assisted.conversions, 0)::int AS conversions
1845
+ COALESCE(assisted.conversions, 0)::int AS conversions,
1846
+ COALESCE(dwell.measured_pageviews, 0)::int AS measured_pageviews,
1847
+ CASE WHEN dwell.avg_engaged_ms IS NULL THEN NULL
1848
+ ELSE round((dwell.avg_engaged_ms / 1000.0)::numeric, 1) END AS avg_seconds_on_page,
1849
+ CASE WHEN dwell.avg_scroll IS NULL THEN NULL
1850
+ ELSE round(dwell.avg_scroll::numeric, 1) END AS avg_scroll_depth,
1851
+ CASE WHEN COALESCE(entrances.entrances, 0) = 0 THEN NULL
1852
+ ELSE round(COALESCE(bounces.bounces, 0)::numeric * 100 / entrances.entrances, 1) END AS bounce_rate,
1853
+ CASE WHEN metrics.pageviews = 0 THEN NULL
1854
+ ELSE round(COALESCE(exits.exits, 0)::numeric * 100 / metrics.pageviews, 1) END AS exit_rate,
1855
+ CASE WHEN metrics.sessions = 0 THEN NULL
1856
+ ELSE round(COALESCE(engaged.engaged_sessions, 0)::numeric * 100 / metrics.sessions, 1) END AS engagement_rate,
1857
+ CASE WHEN metrics.sessions = 0 THEN NULL
1858
+ ELSE round(metrics.pageviews::numeric / metrics.sessions, 2) END AS pageviews_per_session
1761
1859
  FROM page_metrics metrics
1762
1860
  LEFT JOIN entrances USING(path)
1763
1861
  LEFT JOIN exits USING(path)
1764
1862
  LEFT JOIN assisted USING(path)
1765
- ORDER BY metrics.pageviews DESC, metrics.events DESC, metrics.path ASC
1863
+ LEFT JOIN bounces USING(path)
1864
+ LEFT JOIN engaged USING(path)
1865
+ LEFT JOIN dwell_metrics dwell USING(path)
1866
+ WHERE ${searchParam}::text IS NULL OR metrics.path ILIKE '%' || ${searchParam} || '%'
1867
+ ORDER BY ${order}
1766
1868
  LIMIT 1000`,
1767
- query.values
1869
+ [...query.values, table.search]
1768
1870
  );
1769
1871
  const paged = paginateRows(
1770
1872
  result.rows,
1771
1873
  page,
1772
- pageFingerprint("content", siteId, filters)
1874
+ pageFingerprint(
1875
+ `content:${table.sort}:${table.direction}:${table.search ?? ""}`,
1876
+ siteId,
1877
+ filters
1878
+ )
1773
1879
  );
1774
1880
  return {
1775
1881
  content: paged.items,
1776
1882
  pageInfo: paged.pageInfo,
1883
+ table,
1777
1884
  dataFreshThrough: (/* @__PURE__ */ new Date()).toISOString()
1778
1885
  };
1779
1886
  }
@@ -2612,7 +2719,7 @@ async function refreshAnalyticsDailyRollups(input) {
2612
2719
  try {
2613
2720
  await client.query("BEGIN");
2614
2721
  await client.query(
2615
- `DELETE FROM analytics_rollups_daily WHERE bucket >= $1::date AND bucket < $2::date`,
2722
+ `DELETE FROM analytics_rollups_daily WHERE bucket >= $1::date AND bucket <= $2::date`,
2616
2723
  [input.start, input.end]
2617
2724
  );
2618
2725
  const inserted = await client.query(
@@ -2629,7 +2736,14 @@ async function refreshAnalyticsDailyRollups(input) {
2629
2736
  now()
2630
2737
  FROM analytics_events
2631
2738
  WHERE occurred_at >= $1::timestamptz AND occurred_at < $2::timestamptz
2632
- GROUP BY 1, 2, 3, 4, 5, 6, 7`,
2739
+ GROUP BY 1, 2, 3, 4, 5, 6, 7
2740
+ ON CONFLICT (site_id, pixel_id, hostname, bucket, source, medium, campaign)
2741
+ DO UPDATE SET
2742
+ pageviews = EXCLUDED.pageviews,
2743
+ events = EXCLUDED.events,
2744
+ visitors = EXCLUDED.visitors,
2745
+ sessions = EXCLUDED.sessions,
2746
+ refreshed_at = EXCLUDED.refreshed_at`,
2633
2747
  [input.start, input.end]
2634
2748
  );
2635
2749
  await client.query("COMMIT");
@@ -2641,9 +2755,17 @@ async function refreshAnalyticsDailyRollups(input) {
2641
2755
  return { runId, rows };
2642
2756
  } catch (error) {
2643
2757
  await client.query("ROLLBACK");
2758
+ const pgCode = error?.code;
2759
+ const detail = error instanceof Error ? error.message : String(error);
2760
+ console.error(JSON.stringify({
2761
+ event: "analytics_rollup_failed",
2762
+ runId,
2763
+ pgCode: pgCode ?? null,
2764
+ message: detail.slice(0, 400)
2765
+ }));
2644
2766
  await db.query(
2645
- `UPDATE analytics_rollup_runs SET status = 'failed', completed_at = now(), error_code = 'rollup_failed' WHERE id = $1`,
2646
- [runId]
2767
+ `UPDATE analytics_rollup_runs SET status = 'failed', completed_at = now(), error_code = $2 WHERE id = $1`,
2768
+ [runId, (pgCode ? `pg_${pgCode}` : "rollup_failed").slice(0, 60)]
2647
2769
  );
2648
2770
  throw error;
2649
2771
  } finally {
@@ -2696,6 +2818,24 @@ function rowsToCsv(rows) {
2696
2818
  )
2697
2819
  ].join("\n");
2698
2820
  }
2821
+ function markdownHeading(column) {
2822
+ return column.split("_").map((word, index) => index ? word : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2823
+ }
2824
+ function markdownCell(value) {
2825
+ if (value === null || value === void 0) return "\u2014";
2826
+ return String(value).replaceAll("|", "\\|").replaceAll("\n", " ");
2827
+ }
2828
+ function rowsToMarkdown(rows) {
2829
+ const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
2830
+ if (!columns.length) return ["No rows matched this window and filter set."];
2831
+ return [
2832
+ `| ${columns.map(markdownHeading).join(" | ")} |`,
2833
+ `| ${columns.map(() => "---").join(" | ")} |`,
2834
+ ...rows.map(
2835
+ (row) => `| ${columns.map((column) => markdownCell(row[column])).join(" | ")} |`
2836
+ )
2837
+ ];
2838
+ }
2699
2839
  async function createAnalyticsExport(input) {
2700
2840
  const site = (await listAnalyticsSites(input.userId)).find(
2701
2841
  (candidate) => candidate.id === input.siteId
@@ -2763,6 +2903,7 @@ async function createAnalyticsExport(input) {
2763
2903
  `- Verified conversions: ${overview.conversions}`,
2764
2904
  `- Verified revenue: ${(overview.revenueMinor / 100).toFixed(2)}`,
2765
2905
  "",
2906
+ ...input.report === "overview" ? [] : [`## ${markdownHeading(input.report)} rows`, "", ...rowsToMarkdown(rows), ""],
2766
2907
  `Generated ${(/* @__PURE__ */ new Date()).toISOString()} from the governed ${input.report} report contract.`
2767
2908
  ].join("\n");
2768
2909
  }
@@ -2820,6 +2961,8 @@ export {
2820
2961
  listAnalyticsPixels,
2821
2962
  updateAnalyticsPixel,
2822
2963
  setAnalyticsPixelDomainState,
2964
+ MAX_ENGAGED_MS,
2965
+ ENGAGED_SESSION_MS,
2823
2966
  normalizeAnalyticsPath,
2824
2967
  normalizeAnalyticsUrl,
2825
2968
  sanitizeAnalyticsProperties,
@@ -2829,6 +2972,8 @@ export {
2829
2972
  analyticsTimeseries,
2830
2973
  analyticsAcquisition,
2831
2974
  analyticsChannelBreakdown,
2975
+ ANALYTICS_CONTENT_SORTS,
2976
+ normalizeContentOptions,
2832
2977
  analyticsContent,
2833
2978
  analyticsEventCounts,
2834
2979
  analyticsConversions,
@@ -2862,4 +3007,4 @@ export {
2862
3007
  refreshAnalyticsDailyRollupsIfDue,
2863
3008
  createAnalyticsExport
2864
3009
  };
2865
- //# sourceMappingURL=chunk-YW2LXLDU.js.map
3010
+ //# sourceMappingURL=chunk-5BEPOFKG.js.map