@ainyc/canonry 4.23.0 → 4.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/canonry-setup/references/server-side-traffic.md +54 -7
- package/assets/assets/index-BFfB9cRq.js +302 -0
- package/assets/index.html +1 -1
- package/dist/{chunk-VOSBGXXG.js → chunk-6EJ54OX7.js} +2 -2
- package/dist/{chunk-CLVZF3X7.js → chunk-E5PZ23OS.js} +229 -62
- package/dist/cli.js +5 -5
- package/dist/index.js +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +6 -6
- package/assets/assets/index-qGDEhNJu.js +0 -302
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-
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
4319
|
-
<div class="value">${formatNumber(
|
|
4320
|
-
<div class="subtitle">${
|
|
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">
|
|
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, "
|
|
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
|
|
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
|
|
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">
|
|
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, "
|
|
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
|
|
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>
|
|
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">
|
|
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">
|
|
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
|
|
5094
|
+
const accumulators = /* @__PURE__ */ new Map();
|
|
5079
5095
|
for (const r of rows) {
|
|
5080
|
-
const
|
|
5081
|
-
const
|
|
5082
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5097
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
16983
|
-
|
|
16984
|
-
|
|
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
|
|
17001
|
-
status: event.status
|
|
17002
|
-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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}
|
|
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
|
|
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
|
|
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
package/dist/mcp.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
CliError,
|
|
3
3
|
canonryMcpTools,
|
|
4
4
|
createApiClient
|
|
5
|
-
} from "./chunk-
|
|
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
|
|
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",
|