@ainyc/canonry 4.23.0 → 4.23.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/assets/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-qGDEhNJu.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-BFfB9cRq.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="./assets/index-rPok6yk8.css">
17
17
  </head>
18
18
  <body>
@@ -1752,7 +1752,7 @@ var canonryMcpTools = [
1752
1752
  defineTool({
1753
1753
  name: "canonry_traffic_source_get",
1754
1754
  title: "Get traffic source detail",
1755
- description: "Get one traffic source plus 24h totals (crawler hits, AI-referral hits, raw event sample count) and the latest traffic-sync run summary. Use to confirm a source is healthy and observing traffic before drilling into events.",
1755
+ description: "Get one traffic source plus 24h totals (crawler hits, AI-referral sessions, raw event sample count) and the latest traffic-sync run summary. Use to confirm a source is healthy and observing traffic before drilling into events.",
1756
1756
  access: "read",
1757
1757
  tier: "traffic",
1758
1758
  inputSchema: trafficSourceIdInputSchema,
@@ -1763,7 +1763,7 @@ var canonryMcpTools = [
1763
1763
  defineTool({
1764
1764
  name: "canonry_traffic_status",
1765
1765
  title: "Traffic status (all sources)",
1766
- description: "Single-call composite returning every non-archived traffic source plus its last-24h totals (crawler hits, AI-referral hits, sample count) and latest source-scoped traffic-sync run. Same per-entry shape as canonry_traffic_source_get, but one call covers all sources \u2014 prefer this over a list+per-source fan-out.",
1766
+ description: "Single-call composite returning every non-archived traffic source plus its last-24h totals (crawler hits, AI-referral sessions, sample count) and latest source-scoped traffic-sync run. Same per-entry shape as canonry_traffic_source_get, but one call covers all sources \u2014 prefer this over a list+per-source fan-out.",
1767
1767
  access: "read",
1768
1768
  tier: "traffic",
1769
1769
  inputSchema: projectInputSchema,
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-VOSBGXXG.js";
8
+ } from "./chunk-6EJ54OX7.js";
9
9
  import {
10
10
  DEFAULT_RUN_HISTORY_LIMIT,
11
11
  IntelligenceService,
@@ -4304,33 +4304,44 @@ function renderServerActivity(report, audience) {
4304
4304
  return `<span class="tone-${deltaTone(d.deltaPct)}">${escapeHtml(copy)}</span>`;
4305
4305
  };
4306
4306
  if (isClient) {
4307
- const clientOperators = sa.byOperator.filter((o) => o.verifiedHits > 0 || o.referralArrivals > 0).slice(0, 5);
4307
+ const crawlerRequests = {
4308
+ current: sa.verifiedCrawlerHits.current + sa.unverifiedCrawlerHits.current,
4309
+ prior: sa.verifiedCrawlerHits.prior + sa.unverifiedCrawlerHits.prior,
4310
+ deltaPct: deltaPercent(
4311
+ sa.verifiedCrawlerHits.current + sa.unverifiedCrawlerHits.current,
4312
+ sa.verifiedCrawlerHits.prior + sa.unverifiedCrawlerHits.prior
4313
+ )
4314
+ };
4315
+ const crawlerTrustSummary = `${formatNumber(sa.verifiedCrawlerHits.current)} verified \xB7 ${formatNumber(sa.unverifiedCrawlerHits.current)} unverified`;
4316
+ const crawlerDelta = formatDelta(crawlerRequests, "requests");
4317
+ const crawlerSubtitle = crawlerDelta ? `${escapeHtml(crawlerTrustSummary)} \xB7 ${crawlerDelta}` : escapeHtml(crawlerTrustSummary);
4318
+ const clientOperators = sa.byOperator.filter((o) => o.verifiedHits > 0 || o.unverifiedHits > 0 || o.referralArrivals > 0).slice(0, 5);
4308
4319
  const clientOperatorRows = clientOperators.map((o) => `
4309
4320
  <tr>
4310
4321
  <td>${escapeHtml(o.operator)}</td>
4311
- <td class="numeric">${formatNumber(o.verifiedHits)}</td>
4322
+ <td class="numeric">${formatNumber(o.verifiedHits + o.unverifiedHits)}</td>
4312
4323
  <td class="numeric">${formatNumber(o.referralArrivals)}</td>
4313
4324
  </tr>`).join("");
4314
4325
  return section(
4315
4326
  serverActivityHeading("client", true),
4316
4327
  `<div class="metric-grid">
4317
4328
  <div class="metric">
4318
- <div class="label">AI bots visited your site</div>
4319
- <div class="value">${formatNumber(sa.verifiedCrawlerHits.current)}</div>
4320
- <div class="subtitle">${formatDelta(sa.verifiedCrawlerHits, "crawls")}</div>
4329
+ <div class="label">AI bot requests observed</div>
4330
+ <div class="value">${formatNumber(crawlerRequests.current)}</div>
4331
+ <div class="subtitle">${crawlerSubtitle}</div>
4321
4332
  </div>
4322
4333
  <div class="metric">
4323
- <div class="label">People clicked through from AI</div>
4334
+ <div class="label">AI referral sessions</div>
4324
4335
  <div class="value">${formatNumber(sa.referralArrivals.current)}</div>
4325
- <div class="subtitle">${formatDelta(sa.referralArrivals, "arrivals")}</div>
4336
+ <div class="subtitle">${formatDelta(sa.referralArrivals, "sessions")}</div>
4326
4337
  </div>
4327
4338
  </div>
4328
4339
  ${clientOperatorRows ? `<div class="chart-card"><h3>By AI tool</h3>
4329
4340
  <table class="report-table">
4330
- <thead><tr><th>AI tool</th><th class="numeric">Bot visits (7d)</th><th class="numeric">Click-throughs</th></tr></thead>
4341
+ <thead><tr><th>AI tool</th><th class="numeric">Bot requests (7d)</th><th class="numeric">Referral sessions</th></tr></thead>
4331
4342
  <tbody>${clientOperatorRows}</tbody>
4332
4343
  </table>
4333
- <p class="meta">Verified visits only. We confirm each bot via reverse-DNS so the numbers above can't be inflated by anyone faking a user agent.</p>
4344
+ <p class="meta">Verified requests are reverse-DNS confirmed. Unverified requests are user-agent claims shown separately in agency diagnostics.</p>
4334
4345
  </div>` : ""}`
4335
4346
  );
4336
4347
  }
@@ -4378,16 +4389,21 @@ function renderServerActivity(report, audience) {
4378
4389
  <div class="subtitle">${formatDelta(sa.verifiedCrawlerHits, "hits")}</div>
4379
4390
  </div>
4380
4391
  <div class="metric">
4381
- <div class="label">AI-referral arrivals (7d)</div>
4392
+ <div class="label">Unverified crawler hits (7d)</div>
4393
+ <div class="value">${formatNumber(sa.unverifiedCrawlerHits.current)}</div>
4394
+ <div class="subtitle">${formatDelta(sa.unverifiedCrawlerHits, "hits")}</div>
4395
+ </div>
4396
+ <div class="metric">
4397
+ <div class="label">AI-referral sessions (7d)</div>
4382
4398
  <div class="value">${formatNumber(sa.referralArrivals.current)}</div>
4383
- <div class="subtitle">${formatDelta(sa.referralArrivals, "arrivals")}</div>
4399
+ <div class="subtitle">${formatDelta(sa.referralArrivals, "sessions")}</div>
4384
4400
  </div>
4385
4401
  </div>
4386
4402
  ${trendChart}
4387
4403
  ${operatorRows ? `<div class="chart-card"><h3>Per AI operator</h3>
4388
4404
  <p class="meta">Verified means rDNS-confirmed. Unverified bots claim the user-agent but couldn't be verified \u2014 could be the real bot or an imitator.</p>
4389
4405
  <table class="report-table">
4390
- <thead><tr><th>Operator</th><th class="numeric">Verified hits</th><th class="numeric">Unverified</th><th class="numeric">Referral arrivals</th><th class="numeric">7d delta</th></tr></thead>
4406
+ <thead><tr><th>Operator</th><th class="numeric">Verified hits</th><th class="numeric">Unverified</th><th class="numeric">Referral sessions</th><th class="numeric">7d delta</th></tr></thead>
4391
4407
  <tbody>${operatorRows}</tbody>
4392
4408
  </table>
4393
4409
  </div>` : ""}
@@ -4398,16 +4414,16 @@ function renderServerActivity(report, audience) {
4398
4414
  <tbody>${pathRows}</tbody>
4399
4415
  </table>
4400
4416
  </div>` : ""}
4401
- ${referralProductRows ? `<div class="chart-card"><h3>Click-throughs by AI product</h3>
4417
+ ${referralProductRows ? `<div class="chart-card"><h3>AI-referral sessions by product</h3>
4402
4418
  <p class="meta">Where humans landed coming from each AI product (chatgpt.com, claude.ai, \u2026).</p>
4403
4419
  <table class="report-table">
4404
- <thead><tr><th>Product</th><th class="numeric">Arrivals</th><th class="numeric">Distinct landing paths</th></tr></thead>
4420
+ <thead><tr><th>Product</th><th class="numeric">Sessions</th><th class="numeric">Distinct landing paths</th></tr></thead>
4405
4421
  <tbody>${referralProductRows}</tbody>
4406
4422
  </table>
4407
4423
  </div>` : ""}
4408
4424
  ${referralLandingRows ? `<div class="chart-card"><h3>Top AI-referral landing paths</h3>
4409
4425
  <table class="report-table">
4410
- <thead><tr><th>Path</th><th class="numeric">Arrivals</th><th class="numeric">Distinct products</th></tr></thead>
4426
+ <thead><tr><th>Path</th><th class="numeric">Sessions</th><th class="numeric">Distinct products</th></tr></thead>
4411
4427
  <tbody>${referralLandingRows}</tbody>
4412
4428
  </table>
4413
4429
  </div>` : ""}`
@@ -5075,28 +5091,39 @@ function buildCandidateQueries(opts) {
5075
5091
  });
5076
5092
  }
5077
5093
  function aggregateGscByQuery(rows) {
5078
- const byQuery = /* @__PURE__ */ new Map();
5094
+ const accumulators = /* @__PURE__ */ new Map();
5079
5095
  for (const r of rows) {
5080
- const existing = byQuery.get(r.query);
5081
- const candidate = {
5082
- // GSC stores `page` as a full URL for url-prefix properties; normalize to
5083
- // a path so it can be joined against `gaTrafficByPage` (which is keyed by
5084
- // path) and so `ourBestPage.url` / `targetRef` stay consistent regardless
5085
- // of whether the page is sourced from GSC or from inventory.
5086
- page: extractPath(r.page),
5087
- position: Number(r.position) || 0,
5088
- impressions: r.impressions,
5089
- clicks: r.clicks,
5090
- ctr: Number(r.ctr) || 0
5091
- };
5096
+ const page = extractPath(r.page);
5097
+ const position = Number(r.position) || 0;
5098
+ const existing = accumulators.get(r.query);
5092
5099
  if (!existing) {
5093
- byQuery.set(r.query, candidate);
5100
+ accumulators.set(r.query, {
5101
+ bestPage: page,
5102
+ bestPageImpressions: r.impressions,
5103
+ totalClicks: r.clicks,
5104
+ totalImpressions: r.impressions,
5105
+ weightedPositionSum: position * r.impressions
5106
+ });
5094
5107
  continue;
5095
5108
  }
5096
- if (candidate.impressions > existing.impressions) {
5097
- byQuery.set(r.query, candidate);
5109
+ existing.totalClicks += r.clicks;
5110
+ existing.totalImpressions += r.impressions;
5111
+ existing.weightedPositionSum += position * r.impressions;
5112
+ if (r.impressions > existing.bestPageImpressions) {
5113
+ existing.bestPage = page;
5114
+ existing.bestPageImpressions = r.impressions;
5098
5115
  }
5099
5116
  }
5117
+ const byQuery = /* @__PURE__ */ new Map();
5118
+ for (const [query, acc] of accumulators) {
5119
+ byQuery.set(query, {
5120
+ page: acc.bestPage,
5121
+ position: acc.totalImpressions > 0 ? acc.weightedPositionSum / acc.totalImpressions : 0,
5122
+ impressions: acc.totalImpressions,
5123
+ clicks: acc.totalClicks,
5124
+ ctr: acc.totalImpressions > 0 ? acc.totalClicks / acc.totalImpressions : 0
5125
+ });
5126
+ }
5100
5127
  return byQuery;
5101
5128
  }
5102
5129
  function aggregateCandidate(opts) {
@@ -5503,6 +5530,29 @@ function buildAiReferrals(db, projectId) {
5503
5530
  const topLandingPages = [...pageAgg.entries()].map(([page, data]) => ({ page, sessions: data.sessions, users: data.users })).sort((a, b) => b.sessions - a.sessions).slice(0, TOP_AI_REFERRAL_PAGES_LIMIT);
5504
5531
  return { totalSessions: total, totalUsers, bySource, trend, topLandingPages };
5505
5532
  }
5533
+ function nonSubresourceReferralPathCondition() {
5534
+ return sql3`
5535
+ LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/_next/static/%'
5536
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/assets/%'
5537
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/static/%'
5538
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/favicon.%'
5539
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.avif'
5540
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.css'
5541
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.gif'
5542
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.ico'
5543
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.jpeg'
5544
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.jpg'
5545
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.js'
5546
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.map'
5547
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.mjs'
5548
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.otf'
5549
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.png'
5550
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.svg'
5551
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.webmanifest'
5552
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.woff'
5553
+ AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.woff2'
5554
+ `;
5555
+ }
5506
5556
  function buildServerActivity(db, projectId) {
5507
5557
  const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
5508
5558
  and5(
@@ -5529,10 +5579,21 @@ function buildServerActivity(db, projectId) {
5529
5579
  )
5530
5580
  ).get()?.total ?? 0
5531
5581
  );
5582
+ const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5583
+ db.select({ total: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
5584
+ and5(
5585
+ eq13(crawlerEventsHourly.projectId, projectId),
5586
+ ne(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5587
+ gte(crawlerEventsHourly.tsHour, windowStartIso),
5588
+ exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
5589
+ )
5590
+ ).get()?.total ?? 0
5591
+ );
5532
5592
  const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5533
5593
  db.select({ total: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
5534
5594
  and5(
5535
5595
  eq13(aiReferralEventsHourly.projectId, projectId),
5596
+ nonSubresourceReferralPathCondition(),
5536
5597
  gte(aiReferralEventsHourly.tsHour, windowStartIso),
5537
5598
  exclusiveEnd ? lt(aiReferralEventsHourly.tsHour, windowEndIso) : lte(aiReferralEventsHourly.tsHour, windowEndIso)
5538
5599
  )
@@ -5540,6 +5601,8 @@ function buildServerActivity(db, projectId) {
5540
5601
  );
5541
5602
  const verifiedCurrent = sumVerifiedCrawlers(headlineStart, headlineEnd);
5542
5603
  const verifiedPrior = sumVerifiedCrawlers(priorStart, headlineStart, true);
5604
+ const unverifiedCurrent = sumUnverifiedCrawlers(headlineStart, headlineEnd);
5605
+ const unverifiedPrior = sumUnverifiedCrawlers(priorStart, headlineStart, true);
5543
5606
  const referralCurrent = sumReferrals(headlineStart, headlineEnd);
5544
5607
  const referralPrior = sumReferrals(priorStart, headlineStart, true);
5545
5608
  const crawlerByOperatorRows = db.select({
@@ -5570,6 +5633,7 @@ function buildServerActivity(db, projectId) {
5570
5633
  }).from(aiReferralEventsHourly).where(
5571
5634
  and5(
5572
5635
  eq13(aiReferralEventsHourly.projectId, projectId),
5636
+ nonSubresourceReferralPathCondition(),
5573
5637
  gte(aiReferralEventsHourly.tsHour, headlineStart),
5574
5638
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
5575
5639
  )
@@ -5601,7 +5665,7 @@ function buildServerActivity(db, projectId) {
5601
5665
  referralArrivals: v.referrals,
5602
5666
  deltaPct: deltaPercent(v.verified, v.prior)
5603
5667
  })).sort(
5604
- (a, b) => b.verifiedHits - a.verifiedHits || b.referralArrivals - a.referralArrivals
5668
+ (a, b) => b.verifiedHits - a.verifiedHits || b.unverifiedHits - a.unverifiedHits || b.referralArrivals - a.referralArrivals
5605
5669
  );
5606
5670
  const topPathsRows = db.select({
5607
5671
  path: crawlerEventsHourly.pathNormalized,
@@ -5627,6 +5691,7 @@ function buildServerActivity(db, projectId) {
5627
5691
  }).from(aiReferralEventsHourly).where(
5628
5692
  and5(
5629
5693
  eq13(aiReferralEventsHourly.projectId, projectId),
5694
+ nonSubresourceReferralPathCondition(),
5630
5695
  gte(aiReferralEventsHourly.tsHour, headlineStart),
5631
5696
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
5632
5697
  )
@@ -5643,6 +5708,7 @@ function buildServerActivity(db, projectId) {
5643
5708
  }).from(aiReferralEventsHourly).where(
5644
5709
  and5(
5645
5710
  eq13(aiReferralEventsHourly.projectId, projectId),
5711
+ nonSubresourceReferralPathCondition(),
5646
5712
  gte(aiReferralEventsHourly.tsHour, headlineStart),
5647
5713
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
5648
5714
  )
@@ -5669,6 +5735,7 @@ function buildServerActivity(db, projectId) {
5669
5735
  }).from(aiReferralEventsHourly).where(
5670
5736
  and5(
5671
5737
  eq13(aiReferralEventsHourly.projectId, projectId),
5738
+ nonSubresourceReferralPathCondition(),
5672
5739
  gte(aiReferralEventsHourly.tsHour, trendStart),
5673
5740
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
5674
5741
  )
@@ -5688,12 +5755,17 @@ function buildServerActivity(db, projectId) {
5688
5755
  return {
5689
5756
  windowStart: headlineStart,
5690
5757
  windowEnd: headlineEnd,
5691
- hasData: verifiedCurrent + referralCurrent + verifiedPrior + referralPrior > 0 || byOperator.length > 0 || topCrawledPaths.length > 0 || referralProducts.length > 0,
5758
+ hasData: verifiedCurrent + unverifiedCurrent + referralCurrent + verifiedPrior + unverifiedPrior + referralPrior > 0 || byOperator.length > 0 || topCrawledPaths.length > 0 || referralProducts.length > 0,
5692
5759
  verifiedCrawlerHits: {
5693
5760
  current: verifiedCurrent,
5694
5761
  prior: verifiedPrior,
5695
5762
  deltaPct: deltaPercent(verifiedCurrent, verifiedPrior)
5696
5763
  },
5764
+ unverifiedCrawlerHits: {
5765
+ current: unverifiedCurrent,
5766
+ prior: unverifiedPrior,
5767
+ deltaPct: deltaPercent(unverifiedCurrent, unverifiedPrior)
5768
+ },
5697
5769
  referralArrivals: {
5698
5770
  current: referralCurrent,
5699
5771
  prior: referralPrior,
@@ -8657,6 +8729,7 @@ var routeCatalog = [
8657
8729
  { name: "query", in: "query", description: "Filter by search query.", schema: stringSchema },
8658
8730
  { name: "page", in: "query", description: "Filter by page URL.", schema: stringSchema },
8659
8731
  limitQueryParameter,
8732
+ offsetQueryParameter,
8660
8733
  analyticsWindowParameter
8661
8734
  ],
8662
8735
  responses: {
@@ -10043,7 +10116,7 @@ var routeCatalog = [
10043
10116
  method: "post",
10044
10117
  path: "/api/v1/projects/{name}/traffic/sources/{id}/sync",
10045
10118
  summary: "Trigger a sync run for a traffic source",
10046
- description: "Pulls request logs from the configured Cloud Run service for the lookback window, classifies crawler / AI-referral hits, and upserts hourly buckets and a bounded sample tail.",
10119
+ description: "Pulls request logs from the configured Cloud Run service for the lookback window, classifies crawler hits / AI-referral sessions, and upserts hourly buckets and a bounded sample tail.",
10047
10120
  tags: ["traffic"],
10048
10121
  parameters: [
10049
10122
  nameParameter,
@@ -10138,7 +10211,7 @@ var routeCatalog = [
10138
10211
  {
10139
10212
  method: "get",
10140
10213
  path: "/api/v1/projects/{name}/traffic/events",
10141
- summary: "List rolled-up crawler and AI-referral hits within a window",
10214
+ summary: "List rolled-up crawler hits and AI-referral sessions within a window",
10142
10215
  description: "Returns hourly rollup rows from `crawler_events_hourly` and `ai_referral_events_hourly`. Defaults to the last 24h. Totals reflect the full window; the `events` array is capped by `limit` (default 500, max 5000).",
10143
10216
  tags: ["traffic"],
10144
10217
  parameters: [
@@ -11998,7 +12071,7 @@ async function googleRoutes(app, opts) {
11998
12071
  });
11999
12072
  app.get("/projects/:name/google/gsc/performance", async (request) => {
12000
12073
  const project = resolveProject(app.db, request.params.name);
12001
- const { startDate, endDate, query, page, limit } = request.query;
12074
+ const { startDate, endDate, query, page, limit, offset } = request.query;
12002
12075
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
12003
12076
  const conditions = [eq18(gscSearchData.projectId, project.id)];
12004
12077
  if (startDate) conditions.push(sql5`${gscSearchData.date} >= ${startDate}`);
@@ -12006,7 +12079,9 @@ async function googleRoutes(app, opts) {
12006
12079
  if (endDate) conditions.push(sql5`${gscSearchData.date} <= ${endDate}`);
12007
12080
  if (query) conditions.push(sql5`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
12008
12081
  if (page) conditions.push(sql5`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
12009
- const rows = app.db.select().from(gscSearchData).where(and8(...conditions)).orderBy(desc8(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
12082
+ const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
12083
+ const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
12084
+ const rows = app.db.select().from(gscSearchData).where(and8(...conditions)).orderBy(desc8(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
12010
12085
  return rows.map((r) => ({
12011
12086
  date: r.date,
12012
12087
  query: r.query,
@@ -16898,9 +16973,21 @@ function classifyAiReferral(event) {
16898
16973
 
16899
16974
  // ../integration-traffic/src/rollup.ts
16900
16975
  var DEFAULT_SAMPLE_LIMIT = 25;
16976
+ var DEFAULT_AI_REFERRAL_SESSION_WINDOW_MS = 6e4;
16901
16977
  var UUID_SEGMENT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16902
16978
  var LONG_HEX_SEGMENT = /^[0-9a-f]{16,}$/i;
16903
16979
  var NUMERIC_SEGMENT = /^\d+$/;
16980
+ var ASSET_EXTENSION_PATTERN = /\.(?:avif|bmp|css|gif|ico|jpe?g|js|json|map|mjs|mp4|otf|png|svg|webm|webmanifest|woff2?|xml)$/i;
16981
+ var ASSET_PATH_PREFIXES = [
16982
+ "/_next/static/",
16983
+ "/assets/",
16984
+ "/build/",
16985
+ "/dist/",
16986
+ "/fonts/",
16987
+ "/images/",
16988
+ "/img/",
16989
+ "/static/"
16990
+ ];
16904
16991
  function normalizeTrafficPathPattern(path15) {
16905
16992
  const cleanPath = path15.trim() || "/";
16906
16993
  const pathOnly = cleanPath.split("?")[0] || "/";
@@ -16920,6 +17007,69 @@ function hourBucket(value) {
16920
17007
  date.setUTCMinutes(0, 0, 0);
16921
17008
  return date.toISOString();
16922
17009
  }
17010
+ function sessionWindowBucket(value, windowMs) {
17011
+ const date = new Date(value);
17012
+ if (Number.isNaN(date.getTime())) return value;
17013
+ return new Date(Math.floor(date.getTime() / windowMs) * windowMs).toISOString();
17014
+ }
17015
+ function normalizeHost2(host) {
17016
+ if (!host) return null;
17017
+ return host.trim().toLowerCase().replace(/^www\./, "") || null;
17018
+ }
17019
+ function sameHost(a, b) {
17020
+ const normalizedA = normalizeHost2(a);
17021
+ const normalizedB = normalizeHost2(b);
17022
+ return !!normalizedA && !!normalizedB && normalizedA === normalizedB;
17023
+ }
17024
+ function pathFromSameOriginReferer(event) {
17025
+ if (!event.referer) return null;
17026
+ try {
17027
+ const refererUrl = new URL(event.referer);
17028
+ if (!sameHost(refererUrl.hostname, event.host)) return null;
17029
+ return refererUrl.pathname || "/";
17030
+ } catch {
17031
+ return null;
17032
+ }
17033
+ }
17034
+ function resolveAiReferralLandingPath(event, evidenceType) {
17035
+ if (evidenceType === "referer-utm") {
17036
+ const refererPath = pathFromSameOriginReferer(event);
17037
+ if (refererPath) return normalizeTrafficPathPattern(refererPath);
17038
+ }
17039
+ return normalizeTrafficPathPattern(event.path);
17040
+ }
17041
+ function isLikelySubresourcePath(path15) {
17042
+ const cleanPath = path15.split("?")[0] || "/";
17043
+ return ASSET_PATH_PREFIXES.some((prefix) => cleanPath.startsWith(prefix)) || ASSET_EXTENSION_PATTERN.test(cleanPath);
17044
+ }
17045
+ function actorKey(event) {
17046
+ const remoteIp = event.remoteIp?.trim();
17047
+ const userAgent = event.userAgent?.trim();
17048
+ if (remoteIp || userAgent) return `${remoteIp ?? "unknown-ip"} ${userAgent ?? "unknown-ua"}`;
17049
+ return `event:${event.eventId}`;
17050
+ }
17051
+ function aiReferralSessionKey(event, aiReferral, landingPathNormalized, windowMs) {
17052
+ return [
17053
+ hourBucket(event.observedAt),
17054
+ sessionWindowBucket(event.observedAt, windowMs),
17055
+ actorKey(event),
17056
+ aiReferral.sourceDomain,
17057
+ landingPathNormalized
17058
+ ].join(" ");
17059
+ }
17060
+ function evidenceRank(evidenceType) {
17061
+ switch (evidenceType) {
17062
+ case "referer":
17063
+ return 3;
17064
+ case "utm":
17065
+ return 2;
17066
+ case "referer-utm":
17067
+ return 1;
17068
+ }
17069
+ }
17070
+ function strongerReferralEvidence(current, next) {
17071
+ return evidenceRank(next.evidenceType) > evidenceRank(current.evidenceType) ? next : current;
17072
+ }
16923
17073
  function sortCrawlerBuckets(a, b) {
16924
17074
  return a.tsHour.localeCompare(b.tsHour) || a.botId.localeCompare(b.botId) || a.pathNormalized.localeCompare(b.pathNormalized) || String(a.status).localeCompare(String(b.status));
16925
17075
  }
@@ -16931,8 +17081,11 @@ function topEntries(map, limit) {
16931
17081
  }
16932
17082
  function buildTrafficProbeReport(events, options = {}) {
16933
17083
  const sampleLimit = options.sampleLimit ?? DEFAULT_SAMPLE_LIMIT;
17084
+ const configuredSessionWindowMs = options.aiReferralSessionWindowMs ?? DEFAULT_AI_REFERRAL_SESSION_WINDOW_MS;
17085
+ const aiReferralSessionWindowMs = configuredSessionWindowMs > 0 ? configuredSessionWindowMs : DEFAULT_AI_REFERRAL_SESSION_WINDOW_MS;
16934
17086
  const crawlerBuckets = /* @__PURE__ */ new Map();
16935
17087
  const aiReferralBuckets = /* @__PURE__ */ new Map();
17088
+ const aiReferralSessions = /* @__PURE__ */ new Map();
16936
17089
  const topBots = /* @__PURE__ */ new Map();
16937
17090
  const topCrawlerPaths = /* @__PURE__ */ new Map();
16938
17091
  const topAiReferrers = /* @__PURE__ */ new Map();
@@ -16979,34 +17132,21 @@ function buildTrafficProbeReport(events, options = {}) {
16979
17132
  }
16980
17133
  if (aiReferral) {
16981
17134
  aiReferralHits += 1;
16982
- const key = [
16983
- tsHour,
16984
- aiReferral.product,
16985
- aiReferral.sourceDomain,
16986
- aiReferral.evidenceType,
16987
- pathNormalized,
16988
- event.status ?? "null"
16989
- ].join(" ");
16990
- const existing = aiReferralBuckets.get(key);
16991
- if (existing) {
16992
- existing.hits += 1;
16993
- } else {
16994
- aiReferralBuckets.set(key, {
17135
+ const landingPathNormalized = resolveAiReferralLandingPath(event, aiReferral.evidenceType);
17136
+ if (!isLikelySubresourcePath(landingPathNormalized)) {
17137
+ const session = {
16995
17138
  tsHour,
16996
17139
  operator: aiReferral.operator,
16997
17140
  product: aiReferral.product,
16998
17141
  sourceDomain: aiReferral.sourceDomain,
16999
17142
  evidenceType: aiReferral.evidenceType,
17000
- landingPathNormalized: pathNormalized,
17001
- status: event.status,
17002
- hits: 1
17003
- });
17143
+ landingPathNormalized,
17144
+ status: event.status
17145
+ };
17146
+ const key = aiReferralSessionKey(event, aiReferral, landingPathNormalized, aiReferralSessionWindowMs);
17147
+ const existing = aiReferralSessions.get(key);
17148
+ aiReferralSessions.set(key, existing ? strongerReferralEvidence(existing, session) : session);
17004
17149
  }
17005
- incrementBucket(topAiReferrers, aiReferral.sourceDomain, {
17006
- sourceDomain: aiReferral.sourceDomain,
17007
- product: aiReferral.product
17008
- });
17009
- incrementBucket(topAiReferralLandingPaths, pathNormalized, { landingPathNormalized: pathNormalized });
17010
17150
  }
17011
17151
  if (!crawler && !aiReferral) unknownHits += 1;
17012
17152
  samples.push({
@@ -17023,11 +17163,38 @@ function buildTrafficProbeReport(events, options = {}) {
17023
17163
  });
17024
17164
  if (samples.length > sampleLimit) samples.shift();
17025
17165
  }
17166
+ for (const session of aiReferralSessions.values()) {
17167
+ const key = [
17168
+ session.tsHour,
17169
+ session.product,
17170
+ session.sourceDomain,
17171
+ session.evidenceType,
17172
+ session.landingPathNormalized,
17173
+ session.status ?? "null"
17174
+ ].join(" ");
17175
+ const existing = aiReferralBuckets.get(key);
17176
+ if (existing) {
17177
+ existing.hits += 1;
17178
+ } else {
17179
+ aiReferralBuckets.set(key, {
17180
+ ...session,
17181
+ hits: 1
17182
+ });
17183
+ }
17184
+ incrementBucket(topAiReferrers, session.sourceDomain, {
17185
+ sourceDomain: session.sourceDomain,
17186
+ product: session.product
17187
+ });
17188
+ incrementBucket(topAiReferralLandingPaths, session.landingPathNormalized, {
17189
+ landingPathNormalized: session.landingPathNormalized
17190
+ });
17191
+ }
17026
17192
  return {
17027
17193
  generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
17028
17194
  totals: {
17029
17195
  normalizedEvents: events.length,
17030
17196
  crawlerHits,
17197
+ aiReferralSessions: aiReferralSessions.size,
17031
17198
  aiReferralHits,
17032
17199
  unknownHits
17033
17200
  },
@@ -18742,7 +18909,7 @@ var sourceConnectedCheck = {
18742
18909
  status: CheckStatuses.skipped,
18743
18910
  code: "traffic.source.none",
18744
18911
  summary: "No server-side traffic source connected \u2014 server-log AI visibility data unavailable for this project.",
18745
- remediation: "Connect a traffic source via `canonry traffic connect <type> <project>` to surface crawler hits and AI-referral arrivals from your server logs.",
18912
+ remediation: "Connect a traffic source via `canonry traffic connect <type> <project>` to surface crawler hits and AI-referral sessions from your server logs.",
18746
18913
  details: { sourceCount: 0 }
18747
18914
  };
18748
18915
  }
@@ -18836,7 +19003,7 @@ var recentDataCheck = {
18836
19003
  return {
18837
19004
  status: CheckStatuses.warn,
18838
19005
  code: "traffic.recent-data.stale",
18839
- summary: `No crawler hits or AI-referral arrivals in the last ${RECENT_DATA_WARN_DAYS} days, though older data exists.`,
19006
+ summary: `No crawler hits or AI-referral sessions in the last ${RECENT_DATA_WARN_DAYS} days, though older data exists.`,
18840
19007
  remediation: lastSyncedAt ? `Last sync: ${lastSyncedAt}. Run \`canonry traffic sync <project>\` to refresh, or check the source connection.` : "Run `canonry traffic sync <project>` to pull recent events.",
18841
19008
  details: { lastSyncedAt, sourceCount: sources.length }
18842
19009
  };
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  setTelemetrySource,
21
21
  showFirstRunNotice,
22
22
  trackEvent
23
- } from "./chunk-CLVZF3X7.js";
23
+ } from "./chunk-E5PZ23OS.js";
24
24
  import {
25
25
  CliError,
26
26
  EXIT_SYSTEM_ERROR,
@@ -36,7 +36,7 @@ import {
36
36
  saveConfig,
37
37
  saveConfigPatch,
38
38
  usageError
39
- } from "./chunk-VOSBGXXG.js";
39
+ } from "./chunk-6EJ54OX7.js";
40
40
  import {
41
41
  apiKeys,
42
42
  competitors,
@@ -2987,7 +2987,7 @@ async function trafficStatus(project, opts) {
2987
2987
  console.log(` Last synced: ${d.lastSyncedAt ?? "never"}`);
2988
2988
  if (d.lastError) console.log(` Last error: ${d.lastError}`);
2989
2989
  console.log(` 24h crawler: ${d.totals24h.crawlerHits} hits`);
2990
- console.log(` 24h AI referral: ${d.totals24h.aiReferralHits} hits`);
2990
+ console.log(` 24h AI referral: ${d.totals24h.aiReferralHits} sessions`);
2991
2991
  console.log(` 24h samples: ${d.totals24h.sampleCount}`);
2992
2992
  if (d.latestRun) {
2993
2993
  console.log(` Latest run: ${d.latestRun.runId} (${d.latestRun.status})`);
@@ -3050,13 +3050,13 @@ async function trafficEvents(project, opts) {
3050
3050
  }
3051
3051
  console.log(`Traffic events for "${project}" ${result.windowStart} \u2192 ${result.windowEnd}`);
3052
3052
  console.log(` Crawler hits (window): ${result.totals.crawlerHits}`);
3053
- console.log(` AI referral hits (window): ${result.totals.aiReferralHits}`);
3053
+ console.log(` AI referral sessions (window): ${result.totals.aiReferralHits}`);
3054
3054
  console.log("");
3055
3055
  if (result.events.length === 0) {
3056
3056
  console.log("No events in this window.");
3057
3057
  return;
3058
3058
  }
3059
- console.log(" TS_HOUR KIND IDENTITY EVIDENCE/STATUS PATH HITS");
3059
+ console.log(" TS_HOUR KIND IDENTITY EVIDENCE/STATUS PATH COUNT");
3060
3060
  for (const event of result.events) {
3061
3061
  console.log(` ${formatEventLine(event)}`);
3062
3062
  }
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-CLVZF3X7.js";
3
+ } from "./chunk-E5PZ23OS.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-VOSBGXXG.js";
6
+ } from "./chunk-6EJ54OX7.js";
7
7
  import "./chunk-OYYFXKRK.js";
8
8
  import "./chunk-EUGCQSFC.js";
9
9
  export {
package/dist/mcp.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  CliError,
3
3
  canonryMcpTools,
4
4
  createApiClient
5
- } from "./chunk-VOSBGXXG.js";
5
+ } from "./chunk-6EJ54OX7.js";
6
6
  import "./chunk-EUGCQSFC.js";
7
7
 
8
8
  // src/mcp/cli.ts
@@ -137,7 +137,7 @@ var CANONRY_MCP_TOOLKITS = [
137
137
  name: "traffic",
138
138
  title: "Server-side traffic ingestion",
139
139
  description: "Connect Cloud Run traffic sources, trigger syncs, and read crawler / AI-referral hourly rollups straight from server logs (no GA dependency).",
140
- whenToLoad: "Load when you need server-log evidence of crawler hits or AI-referral arrivals (e.g. confirming GPTBot or ChatGPT-User on a page), or when wiring up / syncing a Cloud Run traffic source."
140
+ whenToLoad: "Load when you need server-log evidence of crawler hits or AI-referral sessions (e.g. confirming GPTBot or ChatGPT-User on a page), or when wiring up / syncing a Cloud Run traffic source."
141
141
  },
142
142
  {
143
143
  name: "agent",