@ainyc/canonry 4.21.4 → 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/assets/index-BFfB9cRq.js +302 -0
- package/assets/index.html +1 -1
- package/dist/{chunk-VFKGHXVJ.js → chunk-6EJ54OX7.js} +26 -3
- package/dist/{chunk-3UGJUNQX.js → chunk-E5PZ23OS.js} +654 -110
- package/dist/{chunk-EY63PENL.js → chunk-EUGCQSFC.js} +12 -0
- package/dist/{chunk-GVQYROIK.js → chunk-OYYFXKRK.js} +1 -1
- package/dist/cli.js +107 -9
- package/dist/index.d.ts +17 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-5COCQKXG.js → intelligence-service-NVN2PAR7.js} +2 -2
- package/dist/mcp.js +3 -3
- package/package.json +10 -10
- package/assets/assets/index-CYfF3BeK.js +0 -302
|
@@ -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,
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
schedules,
|
|
67
67
|
trafficSources,
|
|
68
68
|
usageCounters
|
|
69
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-OYYFXKRK.js";
|
|
70
70
|
import {
|
|
71
71
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
72
72
|
AGENT_PROVIDER_IDS,
|
|
@@ -154,12 +154,13 @@ import {
|
|
|
154
154
|
serializeRunError,
|
|
155
155
|
snapshotRequestSchema,
|
|
156
156
|
summarizeCheckResults,
|
|
157
|
+
trafficConnectWordpressRequestSchema,
|
|
157
158
|
unsupportedKind,
|
|
158
159
|
validationError,
|
|
159
160
|
visibilityStateFromAnswerMentioned,
|
|
160
161
|
windowCutoff,
|
|
161
162
|
wordpressEnvSchema
|
|
162
|
-
} from "./chunk-
|
|
163
|
+
} from "./chunk-EUGCQSFC.js";
|
|
163
164
|
|
|
164
165
|
// src/telemetry.ts
|
|
165
166
|
import crypto from "crypto";
|
|
@@ -4303,33 +4304,44 @@ function renderServerActivity(report, audience) {
|
|
|
4303
4304
|
return `<span class="tone-${deltaTone(d.deltaPct)}">${escapeHtml(copy)}</span>`;
|
|
4304
4305
|
};
|
|
4305
4306
|
if (isClient) {
|
|
4306
|
-
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);
|
|
4307
4319
|
const clientOperatorRows = clientOperators.map((o) => `
|
|
4308
4320
|
<tr>
|
|
4309
4321
|
<td>${escapeHtml(o.operator)}</td>
|
|
4310
|
-
<td class="numeric">${formatNumber(o.verifiedHits)}</td>
|
|
4322
|
+
<td class="numeric">${formatNumber(o.verifiedHits + o.unverifiedHits)}</td>
|
|
4311
4323
|
<td class="numeric">${formatNumber(o.referralArrivals)}</td>
|
|
4312
4324
|
</tr>`).join("");
|
|
4313
4325
|
return section(
|
|
4314
4326
|
serverActivityHeading("client", true),
|
|
4315
4327
|
`<div class="metric-grid">
|
|
4316
4328
|
<div class="metric">
|
|
4317
|
-
<div class="label">AI
|
|
4318
|
-
<div class="value">${formatNumber(
|
|
4319
|
-
<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>
|
|
4320
4332
|
</div>
|
|
4321
4333
|
<div class="metric">
|
|
4322
|
-
<div class="label">
|
|
4334
|
+
<div class="label">AI referral sessions</div>
|
|
4323
4335
|
<div class="value">${formatNumber(sa.referralArrivals.current)}</div>
|
|
4324
|
-
<div class="subtitle">${formatDelta(sa.referralArrivals, "
|
|
4336
|
+
<div class="subtitle">${formatDelta(sa.referralArrivals, "sessions")}</div>
|
|
4325
4337
|
</div>
|
|
4326
4338
|
</div>
|
|
4327
4339
|
${clientOperatorRows ? `<div class="chart-card"><h3>By AI tool</h3>
|
|
4328
4340
|
<table class="report-table">
|
|
4329
|
-
<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>
|
|
4330
4342
|
<tbody>${clientOperatorRows}</tbody>
|
|
4331
4343
|
</table>
|
|
4332
|
-
<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>
|
|
4333
4345
|
</div>` : ""}`
|
|
4334
4346
|
);
|
|
4335
4347
|
}
|
|
@@ -4377,16 +4389,21 @@ function renderServerActivity(report, audience) {
|
|
|
4377
4389
|
<div class="subtitle">${formatDelta(sa.verifiedCrawlerHits, "hits")}</div>
|
|
4378
4390
|
</div>
|
|
4379
4391
|
<div class="metric">
|
|
4380
|
-
<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>
|
|
4381
4398
|
<div class="value">${formatNumber(sa.referralArrivals.current)}</div>
|
|
4382
|
-
<div class="subtitle">${formatDelta(sa.referralArrivals, "
|
|
4399
|
+
<div class="subtitle">${formatDelta(sa.referralArrivals, "sessions")}</div>
|
|
4383
4400
|
</div>
|
|
4384
4401
|
</div>
|
|
4385
4402
|
${trendChart}
|
|
4386
4403
|
${operatorRows ? `<div class="chart-card"><h3>Per AI operator</h3>
|
|
4387
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>
|
|
4388
4405
|
<table class="report-table">
|
|
4389
|
-
<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>
|
|
4390
4407
|
<tbody>${operatorRows}</tbody>
|
|
4391
4408
|
</table>
|
|
4392
4409
|
</div>` : ""}
|
|
@@ -4397,16 +4414,16 @@ function renderServerActivity(report, audience) {
|
|
|
4397
4414
|
<tbody>${pathRows}</tbody>
|
|
4398
4415
|
</table>
|
|
4399
4416
|
</div>` : ""}
|
|
4400
|
-
${referralProductRows ? `<div class="chart-card"><h3>
|
|
4417
|
+
${referralProductRows ? `<div class="chart-card"><h3>AI-referral sessions by product</h3>
|
|
4401
4418
|
<p class="meta">Where humans landed coming from each AI product (chatgpt.com, claude.ai, \u2026).</p>
|
|
4402
4419
|
<table class="report-table">
|
|
4403
|
-
<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>
|
|
4404
4421
|
<tbody>${referralProductRows}</tbody>
|
|
4405
4422
|
</table>
|
|
4406
4423
|
</div>` : ""}
|
|
4407
4424
|
${referralLandingRows ? `<div class="chart-card"><h3>Top AI-referral landing paths</h3>
|
|
4408
4425
|
<table class="report-table">
|
|
4409
|
-
<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>
|
|
4410
4427
|
<tbody>${referralLandingRows}</tbody>
|
|
4411
4428
|
</table>
|
|
4412
4429
|
</div>` : ""}`
|
|
@@ -5074,28 +5091,39 @@ function buildCandidateQueries(opts) {
|
|
|
5074
5091
|
});
|
|
5075
5092
|
}
|
|
5076
5093
|
function aggregateGscByQuery(rows) {
|
|
5077
|
-
const
|
|
5094
|
+
const accumulators = /* @__PURE__ */ new Map();
|
|
5078
5095
|
for (const r of rows) {
|
|
5079
|
-
const
|
|
5080
|
-
const
|
|
5081
|
-
|
|
5082
|
-
// a path so it can be joined against `gaTrafficByPage` (which is keyed by
|
|
5083
|
-
// path) and so `ourBestPage.url` / `targetRef` stay consistent regardless
|
|
5084
|
-
// of whether the page is sourced from GSC or from inventory.
|
|
5085
|
-
page: extractPath(r.page),
|
|
5086
|
-
position: Number(r.position) || 0,
|
|
5087
|
-
impressions: r.impressions,
|
|
5088
|
-
clicks: r.clicks,
|
|
5089
|
-
ctr: Number(r.ctr) || 0
|
|
5090
|
-
};
|
|
5096
|
+
const page = extractPath(r.page);
|
|
5097
|
+
const position = Number(r.position) || 0;
|
|
5098
|
+
const existing = accumulators.get(r.query);
|
|
5091
5099
|
if (!existing) {
|
|
5092
|
-
|
|
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
|
+
});
|
|
5093
5107
|
continue;
|
|
5094
5108
|
}
|
|
5095
|
-
|
|
5096
|
-
|
|
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;
|
|
5097
5115
|
}
|
|
5098
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
|
+
}
|
|
5099
5127
|
return byQuery;
|
|
5100
5128
|
}
|
|
5101
5129
|
function aggregateCandidate(opts) {
|
|
@@ -5502,6 +5530,29 @@ function buildAiReferrals(db, projectId) {
|
|
|
5502
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);
|
|
5503
5531
|
return { totalSessions: total, totalUsers, bySource, trend, topLandingPages };
|
|
5504
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
|
+
}
|
|
5505
5556
|
function buildServerActivity(db, projectId) {
|
|
5506
5557
|
const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
|
|
5507
5558
|
and5(
|
|
@@ -5528,10 +5579,21 @@ function buildServerActivity(db, projectId) {
|
|
|
5528
5579
|
)
|
|
5529
5580
|
).get()?.total ?? 0
|
|
5530
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
|
+
);
|
|
5531
5592
|
const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
5532
5593
|
db.select({ total: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
5533
5594
|
and5(
|
|
5534
5595
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5596
|
+
nonSubresourceReferralPathCondition(),
|
|
5535
5597
|
gte(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
5536
5598
|
exclusiveEnd ? lt(aiReferralEventsHourly.tsHour, windowEndIso) : lte(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
5537
5599
|
)
|
|
@@ -5539,6 +5601,8 @@ function buildServerActivity(db, projectId) {
|
|
|
5539
5601
|
);
|
|
5540
5602
|
const verifiedCurrent = sumVerifiedCrawlers(headlineStart, headlineEnd);
|
|
5541
5603
|
const verifiedPrior = sumVerifiedCrawlers(priorStart, headlineStart, true);
|
|
5604
|
+
const unverifiedCurrent = sumUnverifiedCrawlers(headlineStart, headlineEnd);
|
|
5605
|
+
const unverifiedPrior = sumUnverifiedCrawlers(priorStart, headlineStart, true);
|
|
5542
5606
|
const referralCurrent = sumReferrals(headlineStart, headlineEnd);
|
|
5543
5607
|
const referralPrior = sumReferrals(priorStart, headlineStart, true);
|
|
5544
5608
|
const crawlerByOperatorRows = db.select({
|
|
@@ -5569,6 +5633,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5569
5633
|
}).from(aiReferralEventsHourly).where(
|
|
5570
5634
|
and5(
|
|
5571
5635
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5636
|
+
nonSubresourceReferralPathCondition(),
|
|
5572
5637
|
gte(aiReferralEventsHourly.tsHour, headlineStart),
|
|
5573
5638
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
5574
5639
|
)
|
|
@@ -5600,7 +5665,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5600
5665
|
referralArrivals: v.referrals,
|
|
5601
5666
|
deltaPct: deltaPercent(v.verified, v.prior)
|
|
5602
5667
|
})).sort(
|
|
5603
|
-
(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
|
|
5604
5669
|
);
|
|
5605
5670
|
const topPathsRows = db.select({
|
|
5606
5671
|
path: crawlerEventsHourly.pathNormalized,
|
|
@@ -5626,6 +5691,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5626
5691
|
}).from(aiReferralEventsHourly).where(
|
|
5627
5692
|
and5(
|
|
5628
5693
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5694
|
+
nonSubresourceReferralPathCondition(),
|
|
5629
5695
|
gte(aiReferralEventsHourly.tsHour, headlineStart),
|
|
5630
5696
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
5631
5697
|
)
|
|
@@ -5642,6 +5708,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5642
5708
|
}).from(aiReferralEventsHourly).where(
|
|
5643
5709
|
and5(
|
|
5644
5710
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5711
|
+
nonSubresourceReferralPathCondition(),
|
|
5645
5712
|
gte(aiReferralEventsHourly.tsHour, headlineStart),
|
|
5646
5713
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
5647
5714
|
)
|
|
@@ -5668,6 +5735,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5668
5735
|
}).from(aiReferralEventsHourly).where(
|
|
5669
5736
|
and5(
|
|
5670
5737
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5738
|
+
nonSubresourceReferralPathCondition(),
|
|
5671
5739
|
gte(aiReferralEventsHourly.tsHour, trendStart),
|
|
5672
5740
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
5673
5741
|
)
|
|
@@ -5687,12 +5755,17 @@ function buildServerActivity(db, projectId) {
|
|
|
5687
5755
|
return {
|
|
5688
5756
|
windowStart: headlineStart,
|
|
5689
5757
|
windowEnd: headlineEnd,
|
|
5690
|
-
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,
|
|
5691
5759
|
verifiedCrawlerHits: {
|
|
5692
5760
|
current: verifiedCurrent,
|
|
5693
5761
|
prior: verifiedPrior,
|
|
5694
5762
|
deltaPct: deltaPercent(verifiedCurrent, verifiedPrior)
|
|
5695
5763
|
},
|
|
5764
|
+
unverifiedCrawlerHits: {
|
|
5765
|
+
current: unverifiedCurrent,
|
|
5766
|
+
prior: unverifiedPrior,
|
|
5767
|
+
deltaPct: deltaPercent(unverifiedCurrent, unverifiedPrior)
|
|
5768
|
+
},
|
|
5696
5769
|
referralArrivals: {
|
|
5697
5770
|
current: referralCurrent,
|
|
5698
5771
|
prior: referralPrior,
|
|
@@ -8656,6 +8729,7 @@ var routeCatalog = [
|
|
|
8656
8729
|
{ name: "query", in: "query", description: "Filter by search query.", schema: stringSchema },
|
|
8657
8730
|
{ name: "page", in: "query", description: "Filter by page URL.", schema: stringSchema },
|
|
8658
8731
|
limitQueryParameter,
|
|
8732
|
+
offsetQueryParameter,
|
|
8659
8733
|
analyticsWindowParameter
|
|
8660
8734
|
],
|
|
8661
8735
|
responses: {
|
|
@@ -10007,11 +10081,42 @@ var routeCatalog = [
|
|
|
10007
10081
|
404: { description: "Project not found." }
|
|
10008
10082
|
}
|
|
10009
10083
|
},
|
|
10084
|
+
{
|
|
10085
|
+
method: "post",
|
|
10086
|
+
path: "/api/v1/projects/{name}/traffic/connect/wordpress",
|
|
10087
|
+
summary: "Connect a WordPress traffic-logger source",
|
|
10088
|
+
description: "Probes the WordPress traffic-logger plugin endpoint with the supplied Application Password (single page, `limit=1`) before persisting. On success, stores the credential in `~/.canonry/config.yaml` and creates / updates the project's active WordPress `traffic_sources` row. A probe failure (HTTP 4xx/5xx, network error) surfaces as 502 with the upstream status in the message so the caller learns about a bad credential up front instead of at the first sync.",
|
|
10089
|
+
tags: ["traffic"],
|
|
10090
|
+
parameters: [nameParameter],
|
|
10091
|
+
requestBody: {
|
|
10092
|
+
required: true,
|
|
10093
|
+
content: {
|
|
10094
|
+
"application/json": {
|
|
10095
|
+
schema: {
|
|
10096
|
+
type: "object",
|
|
10097
|
+
required: ["baseUrl", "username", "applicationPassword"],
|
|
10098
|
+
properties: {
|
|
10099
|
+
baseUrl: { ...stringSchema, description: "Absolute base URL of the WordPress site (e.g. `https://example.com`)." },
|
|
10100
|
+
username: { ...stringSchema, description: "WordPress username paired with the Application Password." },
|
|
10101
|
+
applicationPassword: { ...stringSchema, description: "WordPress Application Password (raw; the server base64-encodes it for Basic auth)." },
|
|
10102
|
+
displayName: stringSchema
|
|
10103
|
+
}
|
|
10104
|
+
}
|
|
10105
|
+
}
|
|
10106
|
+
}
|
|
10107
|
+
},
|
|
10108
|
+
responses: {
|
|
10109
|
+
200: { description: "Traffic source DTO returned." },
|
|
10110
|
+
400: { description: "Invalid WordPress connection request." },
|
|
10111
|
+
404: { description: "Project not found." },
|
|
10112
|
+
502: { description: "WordPress plugin endpoint probe failed (bad credentials, unreachable host, etc.)." }
|
|
10113
|
+
}
|
|
10114
|
+
},
|
|
10010
10115
|
{
|
|
10011
10116
|
method: "post",
|
|
10012
10117
|
path: "/api/v1/projects/{name}/traffic/sources/{id}/sync",
|
|
10013
10118
|
summary: "Trigger a sync run for a traffic source",
|
|
10014
|
-
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.",
|
|
10015
10120
|
tags: ["traffic"],
|
|
10016
10121
|
parameters: [
|
|
10017
10122
|
nameParameter,
|
|
@@ -10106,7 +10211,7 @@ var routeCatalog = [
|
|
|
10106
10211
|
{
|
|
10107
10212
|
method: "get",
|
|
10108
10213
|
path: "/api/v1/projects/{name}/traffic/events",
|
|
10109
|
-
summary: "List rolled-up crawler and AI-referral
|
|
10214
|
+
summary: "List rolled-up crawler hits and AI-referral sessions within a window",
|
|
10110
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).",
|
|
10111
10216
|
tags: ["traffic"],
|
|
10112
10217
|
parameters: [
|
|
@@ -11966,7 +12071,7 @@ async function googleRoutes(app, opts) {
|
|
|
11966
12071
|
});
|
|
11967
12072
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
11968
12073
|
const project = resolveProject(app.db, request.params.name);
|
|
11969
|
-
const { startDate, endDate, query, page, limit } = request.query;
|
|
12074
|
+
const { startDate, endDate, query, page, limit, offset } = request.query;
|
|
11970
12075
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
11971
12076
|
const conditions = [eq18(gscSearchData.projectId, project.id)];
|
|
11972
12077
|
if (startDate) conditions.push(sql5`${gscSearchData.date} >= ${startDate}`);
|
|
@@ -11974,7 +12079,9 @@ async function googleRoutes(app, opts) {
|
|
|
11974
12079
|
if (endDate) conditions.push(sql5`${gscSearchData.date} <= ${endDate}`);
|
|
11975
12080
|
if (query) conditions.push(sql5`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
11976
12081
|
if (page) conditions.push(sql5`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
11977
|
-
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();
|
|
11978
12085
|
return rows.map((r) => ({
|
|
11979
12086
|
date: r.date,
|
|
11980
12087
|
query: r.query,
|
|
@@ -16779,6 +16886,12 @@ function hostMatches(host, domain) {
|
|
|
16779
16886
|
const normalizedDomain = normalizeHost(domain);
|
|
16780
16887
|
return normalizedHost === normalizedDomain || normalizedHost.endsWith(`.${normalizedDomain}`);
|
|
16781
16888
|
}
|
|
16889
|
+
function utmTokenMatchesDomain(utmSource, domain) {
|
|
16890
|
+
if (hostMatches(utmSource, domain)) return true;
|
|
16891
|
+
const normalizedUtm = normalizeHost(utmSource);
|
|
16892
|
+
const firstLabel = normalizeHost(domain).split(".")[0];
|
|
16893
|
+
return Boolean(firstLabel) && normalizedUtm === firstLabel;
|
|
16894
|
+
}
|
|
16782
16895
|
function hostFromUrl(value) {
|
|
16783
16896
|
if (!value) return null;
|
|
16784
16897
|
try {
|
|
@@ -16833,7 +16946,7 @@ function classifyAiReferral(event) {
|
|
|
16833
16946
|
}
|
|
16834
16947
|
const utmSource = utmSourceFromQuery(event.queryString);
|
|
16835
16948
|
if (utmSource) {
|
|
16836
|
-
const rule = DEFAULT_AI_REFERRER_RULES.find((candidate) =>
|
|
16949
|
+
const rule = DEFAULT_AI_REFERRER_RULES.find((candidate) => utmTokenMatchesDomain(utmSource, candidate.domain));
|
|
16837
16950
|
if (rule) {
|
|
16838
16951
|
return {
|
|
16839
16952
|
operator: rule.operator,
|
|
@@ -16845,7 +16958,7 @@ function classifyAiReferral(event) {
|
|
|
16845
16958
|
}
|
|
16846
16959
|
const refererUtmSource = utmSourceFromUrl(event.referer);
|
|
16847
16960
|
if (refererUtmSource) {
|
|
16848
|
-
const rule = DEFAULT_AI_REFERRER_RULES.find((candidate) =>
|
|
16961
|
+
const rule = DEFAULT_AI_REFERRER_RULES.find((candidate) => utmTokenMatchesDomain(refererUtmSource, candidate.domain));
|
|
16849
16962
|
if (rule) {
|
|
16850
16963
|
return {
|
|
16851
16964
|
operator: rule.operator,
|
|
@@ -16860,9 +16973,21 @@ function classifyAiReferral(event) {
|
|
|
16860
16973
|
|
|
16861
16974
|
// ../integration-traffic/src/rollup.ts
|
|
16862
16975
|
var DEFAULT_SAMPLE_LIMIT = 25;
|
|
16976
|
+
var DEFAULT_AI_REFERRAL_SESSION_WINDOW_MS = 6e4;
|
|
16863
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;
|
|
16864
16978
|
var LONG_HEX_SEGMENT = /^[0-9a-f]{16,}$/i;
|
|
16865
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
|
+
];
|
|
16866
16991
|
function normalizeTrafficPathPattern(path15) {
|
|
16867
16992
|
const cleanPath = path15.trim() || "/";
|
|
16868
16993
|
const pathOnly = cleanPath.split("?")[0] || "/";
|
|
@@ -16882,6 +17007,69 @@ function hourBucket(value) {
|
|
|
16882
17007
|
date.setUTCMinutes(0, 0, 0);
|
|
16883
17008
|
return date.toISOString();
|
|
16884
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
|
+
}
|
|
16885
17073
|
function sortCrawlerBuckets(a, b) {
|
|
16886
17074
|
return a.tsHour.localeCompare(b.tsHour) || a.botId.localeCompare(b.botId) || a.pathNormalized.localeCompare(b.pathNormalized) || String(a.status).localeCompare(String(b.status));
|
|
16887
17075
|
}
|
|
@@ -16893,8 +17081,11 @@ function topEntries(map, limit) {
|
|
|
16893
17081
|
}
|
|
16894
17082
|
function buildTrafficProbeReport(events, options = {}) {
|
|
16895
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;
|
|
16896
17086
|
const crawlerBuckets = /* @__PURE__ */ new Map();
|
|
16897
17087
|
const aiReferralBuckets = /* @__PURE__ */ new Map();
|
|
17088
|
+
const aiReferralSessions = /* @__PURE__ */ new Map();
|
|
16898
17089
|
const topBots = /* @__PURE__ */ new Map();
|
|
16899
17090
|
const topCrawlerPaths = /* @__PURE__ */ new Map();
|
|
16900
17091
|
const topAiReferrers = /* @__PURE__ */ new Map();
|
|
@@ -16941,56 +17132,69 @@ function buildTrafficProbeReport(events, options = {}) {
|
|
|
16941
17132
|
}
|
|
16942
17133
|
if (aiReferral) {
|
|
16943
17134
|
aiReferralHits += 1;
|
|
16944
|
-
const
|
|
16945
|
-
|
|
16946
|
-
|
|
16947
|
-
aiReferral.sourceDomain,
|
|
16948
|
-
aiReferral.evidenceType,
|
|
16949
|
-
pathNormalized,
|
|
16950
|
-
event.status ?? "null"
|
|
16951
|
-
].join(" ");
|
|
16952
|
-
const existing = aiReferralBuckets.get(key);
|
|
16953
|
-
if (existing) {
|
|
16954
|
-
existing.hits += 1;
|
|
16955
|
-
} else {
|
|
16956
|
-
aiReferralBuckets.set(key, {
|
|
17135
|
+
const landingPathNormalized = resolveAiReferralLandingPath(event, aiReferral.evidenceType);
|
|
17136
|
+
if (!isLikelySubresourcePath(landingPathNormalized)) {
|
|
17137
|
+
const session = {
|
|
16957
17138
|
tsHour,
|
|
16958
17139
|
operator: aiReferral.operator,
|
|
16959
17140
|
product: aiReferral.product,
|
|
16960
17141
|
sourceDomain: aiReferral.sourceDomain,
|
|
16961
17142
|
evidenceType: aiReferral.evidenceType,
|
|
16962
|
-
landingPathNormalized
|
|
16963
|
-
status: event.status
|
|
16964
|
-
|
|
16965
|
-
|
|
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);
|
|
16966
17149
|
}
|
|
16967
|
-
incrementBucket(topAiReferrers, aiReferral.sourceDomain, {
|
|
16968
|
-
sourceDomain: aiReferral.sourceDomain,
|
|
16969
|
-
product: aiReferral.product
|
|
16970
|
-
});
|
|
16971
|
-
incrementBucket(topAiReferralLandingPaths, pathNormalized, { landingPathNormalized: pathNormalized });
|
|
16972
17150
|
}
|
|
16973
17151
|
if (!crawler && !aiReferral) unknownHits += 1;
|
|
16974
|
-
|
|
16975
|
-
|
|
16976
|
-
|
|
16977
|
-
|
|
16978
|
-
|
|
16979
|
-
|
|
16980
|
-
|
|
16981
|
-
|
|
16982
|
-
|
|
16983
|
-
|
|
16984
|
-
|
|
16985
|
-
|
|
17152
|
+
samples.push({
|
|
17153
|
+
eventId: event.eventId,
|
|
17154
|
+
observedAt: event.observedAt,
|
|
17155
|
+
sourceType: event.sourceType,
|
|
17156
|
+
path: event.path,
|
|
17157
|
+
pathNormalized,
|
|
17158
|
+
status: event.status,
|
|
17159
|
+
userAgent: event.userAgent,
|
|
17160
|
+
referer: event.referer,
|
|
17161
|
+
crawler,
|
|
17162
|
+
aiReferral
|
|
17163
|
+
});
|
|
17164
|
+
if (samples.length > sampleLimit) samples.shift();
|
|
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
|
|
16986
17182
|
});
|
|
16987
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
|
+
});
|
|
16988
17191
|
}
|
|
16989
17192
|
return {
|
|
16990
17193
|
generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
16991
17194
|
totals: {
|
|
16992
17195
|
normalizedEvents: events.length,
|
|
16993
17196
|
crawlerHits,
|
|
17197
|
+
aiReferralSessions: aiReferralSessions.size,
|
|
16994
17198
|
aiReferralHits,
|
|
16995
17199
|
unknownHits
|
|
16996
17200
|
},
|
|
@@ -17009,10 +17213,155 @@ function incrementBucket(map, key, fields) {
|
|
|
17009
17213
|
else map.set(key, { fields, hits: 1 });
|
|
17010
17214
|
}
|
|
17011
17215
|
|
|
17216
|
+
// ../integration-wordpress-traffic/src/normalize.ts
|
|
17217
|
+
function trimOrNull(value) {
|
|
17218
|
+
if (value === null || value === void 0) return null;
|
|
17219
|
+
const trimmed = value.trim();
|
|
17220
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
17221
|
+
}
|
|
17222
|
+
function buildEventId2(event) {
|
|
17223
|
+
return `wordpress:${event.observed_at}:${event.id}`;
|
|
17224
|
+
}
|
|
17225
|
+
function normalizeWordpressTrafficEvent(event) {
|
|
17226
|
+
if (!event.observed_at) return null;
|
|
17227
|
+
if (typeof event.id !== "number" || !Number.isFinite(event.id)) return null;
|
|
17228
|
+
const path15 = event.path?.trim();
|
|
17229
|
+
if (!path15) return null;
|
|
17230
|
+
const queryString = trimOrNull(event.query_string);
|
|
17231
|
+
const host = trimOrNull(event.host);
|
|
17232
|
+
const requestUrl = host ? `https://${host}${path15}${queryString ? `?${queryString}` : ""}` : `${path15}${queryString ? `?${queryString}` : ""}`;
|
|
17233
|
+
return {
|
|
17234
|
+
sourceType: TrafficSourceTypes.wordpress,
|
|
17235
|
+
evidenceKind: TrafficEvidenceKinds["raw-request"],
|
|
17236
|
+
confidence: TrafficEventConfidences.observed,
|
|
17237
|
+
eventId: buildEventId2(event),
|
|
17238
|
+
observedAt: event.observed_at,
|
|
17239
|
+
method: trimOrNull(event.method),
|
|
17240
|
+
requestUrl,
|
|
17241
|
+
host,
|
|
17242
|
+
path: path15,
|
|
17243
|
+
queryString,
|
|
17244
|
+
status: typeof event.status === "number" && Number.isFinite(event.status) ? event.status : null,
|
|
17245
|
+
userAgent: trimOrNull(event.user_agent),
|
|
17246
|
+
remoteIp: trimOrNull(event.remote_ip_hash),
|
|
17247
|
+
referer: trimOrNull(event.referer),
|
|
17248
|
+
latencyMs: null,
|
|
17249
|
+
requestSizeBytes: null,
|
|
17250
|
+
responseSizeBytes: null,
|
|
17251
|
+
providerResource: {
|
|
17252
|
+
type: "wordpress_site",
|
|
17253
|
+
labels: host ? { host } : {}
|
|
17254
|
+
},
|
|
17255
|
+
providerLabels: {}
|
|
17256
|
+
};
|
|
17257
|
+
}
|
|
17258
|
+
|
|
17259
|
+
// ../integration-wordpress-traffic/src/client.ts
|
|
17260
|
+
var WORDPRESS_TRAFFIC_ENDPOINT_PATH = "/wp-json/canonry/v1/events";
|
|
17261
|
+
var DEFAULT_PAGE_SIZE2 = 500;
|
|
17262
|
+
var DEFAULT_MAX_PAGES2 = 1;
|
|
17263
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
17264
|
+
var WordpressTrafficApiError = class extends Error {
|
|
17265
|
+
constructor(message, status, body) {
|
|
17266
|
+
super(message);
|
|
17267
|
+
this.status = status;
|
|
17268
|
+
this.body = body;
|
|
17269
|
+
this.name = "WordpressTrafficApiError";
|
|
17270
|
+
}
|
|
17271
|
+
};
|
|
17272
|
+
function trimRequired(name, value) {
|
|
17273
|
+
const trimmed = value.trim();
|
|
17274
|
+
if (!trimmed) {
|
|
17275
|
+
throw new WordpressTrafficApiError(`${name} is required`, 400);
|
|
17276
|
+
}
|
|
17277
|
+
return trimmed;
|
|
17278
|
+
}
|
|
17279
|
+
function normalizePageSize2(pageSize) {
|
|
17280
|
+
if (pageSize === void 0) return DEFAULT_PAGE_SIZE2;
|
|
17281
|
+
if (!Number.isInteger(pageSize) || pageSize < 1) {
|
|
17282
|
+
throw new WordpressTrafficApiError("pageSize must be a positive integer", 400);
|
|
17283
|
+
}
|
|
17284
|
+
return pageSize;
|
|
17285
|
+
}
|
|
17286
|
+
function normalizeMaxPages2(maxPages) {
|
|
17287
|
+
if (maxPages === void 0) return DEFAULT_MAX_PAGES2;
|
|
17288
|
+
if (!Number.isInteger(maxPages) || maxPages < 1) {
|
|
17289
|
+
throw new WordpressTrafficApiError("maxPages must be a positive integer", 400);
|
|
17290
|
+
}
|
|
17291
|
+
return maxPages;
|
|
17292
|
+
}
|
|
17293
|
+
function resolveEndpoint(baseUrl) {
|
|
17294
|
+
const trimmed = trimRequired("baseUrl", baseUrl).replace(/\/+$/, "");
|
|
17295
|
+
return `${trimmed}${WORDPRESS_TRAFFIC_ENDPOINT_PATH}`;
|
|
17296
|
+
}
|
|
17297
|
+
function buildBasicAuthHeader(username, applicationPassword) {
|
|
17298
|
+
const credentials = `${trimRequired("username", username)}:${trimRequired("applicationPassword", applicationPassword)}`;
|
|
17299
|
+
return `Basic ${Buffer.from(credentials, "utf8").toString("base64")}`;
|
|
17300
|
+
}
|
|
17301
|
+
async function readErrorBody2(response) {
|
|
17302
|
+
const text = await response.text().catch(() => "");
|
|
17303
|
+
if (!text) return void 0;
|
|
17304
|
+
return text.length <= 500 ? text : `${text.slice(0, 500)}... [truncated]`;
|
|
17305
|
+
}
|
|
17306
|
+
async function listWordpressTrafficEvents(options) {
|
|
17307
|
+
const endpoint = resolveEndpoint(options.baseUrl);
|
|
17308
|
+
const authHeader = buildBasicAuthHeader(options.username, options.applicationPassword);
|
|
17309
|
+
const pageSize = normalizePageSize2(options.pageSize);
|
|
17310
|
+
const maxPages = normalizeMaxPages2(options.maxPages);
|
|
17311
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
17312
|
+
let cursor = options.cursor;
|
|
17313
|
+
let rawEntryCount = 0;
|
|
17314
|
+
let skippedEntryCount = 0;
|
|
17315
|
+
const events = [];
|
|
17316
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
17317
|
+
const url = new URL(endpoint);
|
|
17318
|
+
url.searchParams.set("limit", String(pageSize));
|
|
17319
|
+
if (cursor !== void 0 && cursor !== "") {
|
|
17320
|
+
url.searchParams.set("cursor", cursor);
|
|
17321
|
+
}
|
|
17322
|
+
const response = await fetch(url, {
|
|
17323
|
+
method: "GET",
|
|
17324
|
+
headers: {
|
|
17325
|
+
Authorization: authHeader,
|
|
17326
|
+
Accept: "application/json"
|
|
17327
|
+
},
|
|
17328
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
17329
|
+
});
|
|
17330
|
+
if (!response.ok) {
|
|
17331
|
+
const body2 = await readErrorBody2(response);
|
|
17332
|
+
throw new WordpressTrafficApiError(
|
|
17333
|
+
`WordPress traffic endpoint returned HTTP ${response.status}`,
|
|
17334
|
+
response.status,
|
|
17335
|
+
body2
|
|
17336
|
+
);
|
|
17337
|
+
}
|
|
17338
|
+
const body = await response.json();
|
|
17339
|
+
const entries = body.events ?? [];
|
|
17340
|
+
rawEntryCount += entries.length;
|
|
17341
|
+
for (const entry of entries) {
|
|
17342
|
+
const normalized = normalizeWordpressTrafficEvent(entry);
|
|
17343
|
+
if (normalized) {
|
|
17344
|
+
events.push(normalized);
|
|
17345
|
+
} else {
|
|
17346
|
+
skippedEntryCount += 1;
|
|
17347
|
+
}
|
|
17348
|
+
}
|
|
17349
|
+
cursor = body.next_cursor ?? void 0;
|
|
17350
|
+
if (!body.has_more || !cursor) break;
|
|
17351
|
+
}
|
|
17352
|
+
return {
|
|
17353
|
+
events,
|
|
17354
|
+
rawEntryCount,
|
|
17355
|
+
skippedEntryCount,
|
|
17356
|
+
nextCursor: cursor,
|
|
17357
|
+
endpoint
|
|
17358
|
+
};
|
|
17359
|
+
}
|
|
17360
|
+
|
|
17012
17361
|
// ../api-routes/src/traffic.ts
|
|
17013
17362
|
var DEFAULT_SYNC_WINDOW_MINUTES = 43200;
|
|
17014
|
-
var
|
|
17015
|
-
var
|
|
17363
|
+
var DEFAULT_PAGE_SIZE3 = 1e3;
|
|
17364
|
+
var DEFAULT_MAX_PAGES3 = 5;
|
|
17016
17365
|
var DEFAULT_SAMPLE_LIMIT2 = 100;
|
|
17017
17366
|
var MAX_TRACKED_EVENT_IDS = 1e3;
|
|
17018
17367
|
var DEFAULT_BACKFILL_DAYS = 30;
|
|
@@ -17089,7 +17438,7 @@ async function runBackfillTask(options) {
|
|
|
17089
17438
|
location,
|
|
17090
17439
|
startTime: windowStart.toISOString(),
|
|
17091
17440
|
endTime: windowEnd.toISOString(),
|
|
17092
|
-
pageSize:
|
|
17441
|
+
pageSize: DEFAULT_PAGE_SIZE3,
|
|
17093
17442
|
maxPages: BACKFILL_MAX_PAGES,
|
|
17094
17443
|
// Backfill is intentionally `firstSync: false`. We don't want desc
|
|
17095
17444
|
// ordering — the in-memory rollup builder handles any order, and the
|
|
@@ -17118,7 +17467,7 @@ async function runBackfillTask(options) {
|
|
|
17118
17467
|
const newSorted = allEvents.slice().sort((a, b) => a.observedAt < b.observedAt ? 1 : a.observedAt > b.observedAt ? -1 : 0).map((e) => e.eventId);
|
|
17119
17468
|
const newRingBuffer = newSorted.slice(0, MAX_TRACKED_EVENT_IDS);
|
|
17120
17469
|
const currentLastSyncedMs = sourceRow.lastSyncedAt ? new Date(sourceRow.lastSyncedAt).getTime() : Number.NEGATIVE_INFINITY;
|
|
17121
|
-
const nextLastSyncedAt = Math.max(currentLastSyncedMs, windowEnd.getTime()) === windowEnd.getTime() ?
|
|
17470
|
+
const nextLastSyncedAt = Math.max(currentLastSyncedMs, windowEnd.getTime()) === windowEnd.getTime() ? windowEndIso : sourceRow.lastSyncedAt;
|
|
17122
17471
|
try {
|
|
17123
17472
|
app.db.transaction((tx) => {
|
|
17124
17473
|
tx.delete(crawlerEventsHourly).where(
|
|
@@ -17219,9 +17568,10 @@ async function runBackfillTask(options) {
|
|
|
17219
17568
|
async function trafficRoutes(app, opts) {
|
|
17220
17569
|
const pullEvents = opts.pullCloudRunEvents ?? listCloudRunTrafficEvents;
|
|
17221
17570
|
const resolveAccessToken2 = opts.resolveCloudRunAccessToken ?? defaultResolveAccessToken;
|
|
17571
|
+
const pullWordpressEvents = opts.pullWordpressTrafficEvents ?? listWordpressTrafficEvents;
|
|
17222
17572
|
const syncWindowMinutes = opts.defaultSyncWindowMinutes ?? DEFAULT_SYNC_WINDOW_MINUTES;
|
|
17223
|
-
const pageSize = opts.defaultPageSize ??
|
|
17224
|
-
const maxPages = opts.defaultMaxPages ??
|
|
17573
|
+
const pageSize = opts.defaultPageSize ?? DEFAULT_PAGE_SIZE3;
|
|
17574
|
+
const maxPages = opts.defaultMaxPages ?? DEFAULT_MAX_PAGES3;
|
|
17225
17575
|
const sampleLimit = opts.defaultSampleLimit ?? DEFAULT_SAMPLE_LIMIT2;
|
|
17226
17576
|
app.post("/projects/:name/traffic/connect/cloud-run", async (request) => {
|
|
17227
17577
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -17305,6 +17655,84 @@ async function trafficRoutes(app, opts) {
|
|
|
17305
17655
|
});
|
|
17306
17656
|
return rowToDto(sourceRow);
|
|
17307
17657
|
});
|
|
17658
|
+
app.post("/projects/:name/traffic/connect/wordpress", async (request) => {
|
|
17659
|
+
const project = resolveProject(app.db, request.params.name);
|
|
17660
|
+
if (!opts.wordpressTrafficCredentialStore) {
|
|
17661
|
+
throw validationError("WordPress traffic credential storage is not configured for this deployment");
|
|
17662
|
+
}
|
|
17663
|
+
const credentialStore = opts.wordpressTrafficCredentialStore;
|
|
17664
|
+
const parsed = trafficConnectWordpressRequestSchema.safeParse(request.body ?? {});
|
|
17665
|
+
if (!parsed.success) {
|
|
17666
|
+
throw validationError(parsed.error.issues.map((i) => i.message).join("; "));
|
|
17667
|
+
}
|
|
17668
|
+
const { baseUrl, username, applicationPassword, displayName } = parsed.data;
|
|
17669
|
+
try {
|
|
17670
|
+
await pullWordpressEvents({
|
|
17671
|
+
baseUrl,
|
|
17672
|
+
username,
|
|
17673
|
+
applicationPassword,
|
|
17674
|
+
pageSize: 1,
|
|
17675
|
+
maxPages: 1
|
|
17676
|
+
});
|
|
17677
|
+
} catch (e) {
|
|
17678
|
+
if (e instanceof WordpressTrafficApiError) {
|
|
17679
|
+
throw providerError(
|
|
17680
|
+
`WordPress traffic probe failed (HTTP ${e.status}): ${e.message}${e.body ? ` \u2014 ${e.body}` : ""}`
|
|
17681
|
+
);
|
|
17682
|
+
}
|
|
17683
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
17684
|
+
throw providerError(`WordPress traffic probe failed: ${msg}`);
|
|
17685
|
+
}
|
|
17686
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17687
|
+
const existing = credentialStore.getConnection(project.name);
|
|
17688
|
+
credentialStore.upsertConnection({
|
|
17689
|
+
projectName: project.name,
|
|
17690
|
+
baseUrl,
|
|
17691
|
+
username,
|
|
17692
|
+
applicationPassword,
|
|
17693
|
+
createdAt: existing?.createdAt ?? now,
|
|
17694
|
+
updatedAt: now
|
|
17695
|
+
});
|
|
17696
|
+
const activeSource = app.db.select().from(trafficSources).where(eq23(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
|
|
17697
|
+
const config = { baseUrl, username };
|
|
17698
|
+
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
17699
|
+
let sourceRow;
|
|
17700
|
+
if (activeSource) {
|
|
17701
|
+
app.db.update(trafficSources).set({
|
|
17702
|
+
displayName: fallbackName,
|
|
17703
|
+
status: TrafficSourceStatuses.connected,
|
|
17704
|
+
lastError: null,
|
|
17705
|
+
configJson: JSON.stringify(config),
|
|
17706
|
+
updatedAt: now
|
|
17707
|
+
}).where(eq23(trafficSources.id, activeSource.id)).run();
|
|
17708
|
+
sourceRow = app.db.select().from(trafficSources).where(eq23(trafficSources.id, activeSource.id)).get();
|
|
17709
|
+
} else {
|
|
17710
|
+
const newId = crypto20.randomUUID();
|
|
17711
|
+
app.db.insert(trafficSources).values({
|
|
17712
|
+
id: newId,
|
|
17713
|
+
projectId: project.id,
|
|
17714
|
+
sourceType: TrafficSourceTypes.wordpress,
|
|
17715
|
+
displayName: fallbackName,
|
|
17716
|
+
status: TrafficSourceStatuses.connected,
|
|
17717
|
+
lastSyncedAt: null,
|
|
17718
|
+
lastCursor: null,
|
|
17719
|
+
lastError: null,
|
|
17720
|
+
archivedAt: null,
|
|
17721
|
+
configJson: JSON.stringify(config),
|
|
17722
|
+
createdAt: now,
|
|
17723
|
+
updatedAt: now
|
|
17724
|
+
}).run();
|
|
17725
|
+
sourceRow = app.db.select().from(trafficSources).where(eq23(trafficSources.id, newId)).get();
|
|
17726
|
+
}
|
|
17727
|
+
writeAuditLog(app.db, {
|
|
17728
|
+
projectId: project.id,
|
|
17729
|
+
actor: "api",
|
|
17730
|
+
action: "traffic.wordpress.connected",
|
|
17731
|
+
entityType: "traffic_source",
|
|
17732
|
+
entityId: sourceRow.id
|
|
17733
|
+
});
|
|
17734
|
+
return rowToDto(sourceRow);
|
|
17735
|
+
});
|
|
17308
17736
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
17309
17737
|
const project = resolveProject(app.db, request.params.name);
|
|
17310
17738
|
const sourceRow = app.db.select().from(trafficSources).where(eq23(trafficSources.id, request.params.id)).get();
|
|
@@ -17398,25 +17826,35 @@ async function trafficRoutes(app, opts) {
|
|
|
17398
17826
|
markFailed(msg, "PROVIDER_PULL");
|
|
17399
17827
|
throw providerError(`Cloud Run pull failed: ${msg}`);
|
|
17400
17828
|
}
|
|
17401
|
-
const seenEventIds = new Set(parseJsonColumn(sourceRow.lastEventIds, []));
|
|
17402
|
-
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
17403
|
-
const newSorted = dedupedEvents.slice().sort((a, b) => a.observedAt < b.observedAt ? 1 : a.observedAt > b.observedAt ? -1 : 0).map((e) => e.eventId);
|
|
17404
|
-
const previousIds = parseJsonColumn(sourceRow.lastEventIds, []);
|
|
17405
|
-
const merged = [];
|
|
17406
|
-
const mergedSet = /* @__PURE__ */ new Set();
|
|
17407
|
-
for (const id of [...newSorted, ...previousIds]) {
|
|
17408
|
-
if (mergedSet.has(id)) continue;
|
|
17409
|
-
mergedSet.add(id);
|
|
17410
|
-
merged.push(id);
|
|
17411
|
-
if (merged.length >= MAX_TRACKED_EVENT_IDS) break;
|
|
17412
|
-
}
|
|
17413
|
-
const nextEventIds = merged;
|
|
17414
|
-
const report = buildTrafficProbeReport(dedupedEvents, { sampleLimit });
|
|
17415
|
-
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17416
17829
|
let crawlerBucketRows = 0;
|
|
17417
17830
|
let aiReferralBucketRows = 0;
|
|
17418
17831
|
let sampleRows = 0;
|
|
17832
|
+
let finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17833
|
+
let pulledEventsCount = 0;
|
|
17834
|
+
let crawlerHitsCount = 0;
|
|
17835
|
+
let aiReferralHitsCount = 0;
|
|
17836
|
+
let unknownHitsCount = 0;
|
|
17419
17837
|
app.db.transaction((tx) => {
|
|
17838
|
+
const latestRow = tx.select().from(trafficSources).where(eq23(trafficSources.id, sourceRow.id)).get();
|
|
17839
|
+
const previousIds = parseJsonColumn(latestRow.lastEventIds, []);
|
|
17840
|
+
const seenEventIds = new Set(previousIds);
|
|
17841
|
+
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
17842
|
+
const newSorted = dedupedEvents.slice().sort((a, b) => a.observedAt < b.observedAt ? 1 : a.observedAt > b.observedAt ? -1 : 0).map((e) => e.eventId);
|
|
17843
|
+
const merged = [];
|
|
17844
|
+
const mergedSet = /* @__PURE__ */ new Set();
|
|
17845
|
+
for (const id of [...newSorted, ...previousIds]) {
|
|
17846
|
+
if (mergedSet.has(id)) continue;
|
|
17847
|
+
mergedSet.add(id);
|
|
17848
|
+
merged.push(id);
|
|
17849
|
+
if (merged.length >= MAX_TRACKED_EVENT_IDS) break;
|
|
17850
|
+
}
|
|
17851
|
+
const nextEventIds = merged;
|
|
17852
|
+
const report = buildTrafficProbeReport(dedupedEvents, { sampleLimit });
|
|
17853
|
+
finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17854
|
+
pulledEventsCount = report.totals.normalizedEvents;
|
|
17855
|
+
crawlerHitsCount = report.totals.crawlerHits;
|
|
17856
|
+
aiReferralHitsCount = report.totals.aiReferralHits;
|
|
17857
|
+
unknownHitsCount = report.totals.unknownHits;
|
|
17420
17858
|
for (const bucket of report.crawlerEventsHourly) {
|
|
17421
17859
|
const status = bucket.status ?? 0;
|
|
17422
17860
|
tx.insert(crawlerEventsHourly).values({
|
|
@@ -17515,7 +17953,11 @@ async function trafficRoutes(app, opts) {
|
|
|
17515
17953
|
}
|
|
17516
17954
|
tx.update(trafficSources).set({
|
|
17517
17955
|
status: TrafficSourceStatuses.connected,
|
|
17518
|
-
|
|
17956
|
+
// Advance to windowEnd, not finishedAt — events arriving at the
|
|
17957
|
+
// source between windowEnd and finishedAt aren't in this pull's
|
|
17958
|
+
// range. If we stored finishedAt, the next sync's clamp would skip
|
|
17959
|
+
// past them and they'd be lost.
|
|
17960
|
+
lastSyncedAt: windowEnd.toISOString(),
|
|
17519
17961
|
lastError: null,
|
|
17520
17962
|
lastEventIds: JSON.stringify(nextEventIds),
|
|
17521
17963
|
updatedAt: finishedAt
|
|
@@ -17534,9 +17976,9 @@ async function trafficRoutes(app, opts) {
|
|
|
17534
17976
|
status: "completed",
|
|
17535
17977
|
sourceType: sourceRow.sourceType,
|
|
17536
17978
|
sourceId: sourceRow.id,
|
|
17537
|
-
pulledEvents:
|
|
17538
|
-
crawlerHits:
|
|
17539
|
-
aiReferralHits:
|
|
17979
|
+
pulledEvents: pulledEventsCount,
|
|
17980
|
+
crawlerHits: crawlerHitsCount,
|
|
17981
|
+
aiReferralHits: aiReferralHitsCount,
|
|
17540
17982
|
durationMs: Date.now() - syncStartedAtMs
|
|
17541
17983
|
});
|
|
17542
17984
|
} catch {
|
|
@@ -17545,10 +17987,10 @@ async function trafficRoutes(app, opts) {
|
|
|
17545
17987
|
sourceId: sourceRow.id,
|
|
17546
17988
|
runId,
|
|
17547
17989
|
syncedAt: finishedAt,
|
|
17548
|
-
pulledEvents:
|
|
17549
|
-
crawlerHits:
|
|
17550
|
-
aiReferralHits:
|
|
17551
|
-
unknownHits:
|
|
17990
|
+
pulledEvents: pulledEventsCount,
|
|
17991
|
+
crawlerHits: crawlerHitsCount,
|
|
17992
|
+
aiReferralHits: aiReferralHitsCount,
|
|
17993
|
+
unknownHits: unknownHitsCount,
|
|
17552
17994
|
crawlerBucketRows,
|
|
17553
17995
|
aiReferralBucketRows,
|
|
17554
17996
|
sampleRows,
|
|
@@ -18467,7 +18909,7 @@ var sourceConnectedCheck = {
|
|
|
18467
18909
|
status: CheckStatuses.skipped,
|
|
18468
18910
|
code: "traffic.source.none",
|
|
18469
18911
|
summary: "No server-side traffic source connected \u2014 server-log AI visibility data unavailable for this project.",
|
|
18470
|
-
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.",
|
|
18471
18913
|
details: { sourceCount: 0 }
|
|
18472
18914
|
};
|
|
18473
18915
|
}
|
|
@@ -18548,12 +18990,20 @@ var recentDataCheck = {
|
|
|
18548
18990
|
)
|
|
18549
18991
|
).get()?.total ?? 0
|
|
18550
18992
|
);
|
|
18993
|
+
const olderReferrals = Number(
|
|
18994
|
+
ctx.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
18995
|
+
and15(
|
|
18996
|
+
eq24(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
18997
|
+
gte3(aiReferralEventsHourly.tsHour, failCutoff)
|
|
18998
|
+
)
|
|
18999
|
+
).get()?.total ?? 0
|
|
19000
|
+
);
|
|
18551
19001
|
const lastSyncedAt = sources.map((s) => s.lastSyncedAt).filter(Boolean).sort().at(-1) ?? null;
|
|
18552
|
-
if (olderCrawlers > 0 || lastSyncedAt) {
|
|
19002
|
+
if (olderCrawlers > 0 || olderReferrals > 0 || lastSyncedAt) {
|
|
18553
19003
|
return {
|
|
18554
19004
|
status: CheckStatuses.warn,
|
|
18555
19005
|
code: "traffic.recent-data.stale",
|
|
18556
|
-
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.`,
|
|
18557
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.",
|
|
18558
19008
|
details: { lastSyncedAt, sourceCount: sources.length }
|
|
18559
19009
|
};
|
|
@@ -18955,6 +19405,8 @@ async function apiRoutes(app, opts) {
|
|
|
18955
19405
|
cloudRunCredentialStore: opts.cloudRunCredentialStore,
|
|
18956
19406
|
pullCloudRunEvents: opts.pullCloudRunEvents,
|
|
18957
19407
|
resolveCloudRunAccessToken: opts.resolveCloudRunAccessToken,
|
|
19408
|
+
wordpressTrafficCredentialStore: opts.wordpressTrafficCredentialStore,
|
|
19409
|
+
pullWordpressTrafficEvents: opts.pullWordpressTrafficEvents,
|
|
18958
19410
|
onTrafficSynced: opts.onTrafficSynced
|
|
18959
19411
|
});
|
|
18960
19412
|
await api.register(backlinksRoutes, {
|
|
@@ -19021,6 +19473,50 @@ function buildTrafficSourceValidators(opts) {
|
|
|
19021
19473
|
validateScopes: () => null
|
|
19022
19474
|
};
|
|
19023
19475
|
}
|
|
19476
|
+
if (opts.wordpressTrafficCredentialStore) {
|
|
19477
|
+
const store = opts.wordpressTrafficCredentialStore;
|
|
19478
|
+
const pullEvents = opts.pullWordpressTrafficEvents ?? listWordpressTrafficEvents;
|
|
19479
|
+
validators[TrafficSourceTypes.wordpress] = {
|
|
19480
|
+
validateCredentials: async (source) => {
|
|
19481
|
+
const record = store.getConnection(source.projectName);
|
|
19482
|
+
if (!record) {
|
|
19483
|
+
return {
|
|
19484
|
+
status: CheckStatuses.fail,
|
|
19485
|
+
code: "traffic.credentials.missing",
|
|
19486
|
+
summary: `No WordPress traffic credential found in ~/.canonry/config.yaml for project "${source.projectName}".`,
|
|
19487
|
+
remediation: "Re-run `canonry traffic connect wordpress <project> --url <site> --username <user> --app-password <password>`."
|
|
19488
|
+
};
|
|
19489
|
+
}
|
|
19490
|
+
try {
|
|
19491
|
+
await pullEvents({
|
|
19492
|
+
baseUrl: record.baseUrl,
|
|
19493
|
+
username: record.username,
|
|
19494
|
+
applicationPassword: record.applicationPassword,
|
|
19495
|
+
pageSize: 1,
|
|
19496
|
+
maxPages: 1
|
|
19497
|
+
});
|
|
19498
|
+
return {
|
|
19499
|
+
status: CheckStatuses.ok,
|
|
19500
|
+
code: "traffic.credentials.resolved",
|
|
19501
|
+
summary: `WordPress endpoint responds for "${source.displayName}" (${new URL(record.baseUrl).host}).`
|
|
19502
|
+
};
|
|
19503
|
+
} catch (e) {
|
|
19504
|
+
const httpStatus = e instanceof WordpressTrafficApiError ? e.status : null;
|
|
19505
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
19506
|
+
return {
|
|
19507
|
+
status: CheckStatuses.fail,
|
|
19508
|
+
code: httpStatus === 401 || httpStatus === 403 ? "traffic.credentials.unauthorized" : "traffic.credentials.resolve-failed",
|
|
19509
|
+
summary: httpStatus ? `WordPress endpoint returned HTTP ${httpStatus}: ${msg}.` : `WordPress endpoint probe failed: ${msg}.`,
|
|
19510
|
+
remediation: "Verify the site URL is reachable and the Application Password is valid. Re-connect the source if needed."
|
|
19511
|
+
};
|
|
19512
|
+
}
|
|
19513
|
+
},
|
|
19514
|
+
// WordPress Application Passwords have no scope concept — auth is
|
|
19515
|
+
// strictly "valid credential or not". Surface a skipped result so the
|
|
19516
|
+
// framework is uniform without producing a false signal.
|
|
19517
|
+
validateScopes: () => null
|
|
19518
|
+
};
|
|
19519
|
+
}
|
|
19024
19520
|
return Object.keys(validators).length > 0 ? validators : void 0;
|
|
19025
19521
|
}
|
|
19026
19522
|
|
|
@@ -21458,8 +21954,40 @@ function removeCloudRunConnection(config, projectName) {
|
|
|
21458
21954
|
return true;
|
|
21459
21955
|
}
|
|
21460
21956
|
|
|
21461
|
-
// src/wordpress-config.ts
|
|
21957
|
+
// src/wordpress-traffic-config.ts
|
|
21462
21958
|
function ensureConnections4(config) {
|
|
21959
|
+
if (!config.wordpressTraffic) config.wordpressTraffic = {};
|
|
21960
|
+
if (!config.wordpressTraffic.connections) config.wordpressTraffic.connections = [];
|
|
21961
|
+
return config.wordpressTraffic.connections;
|
|
21962
|
+
}
|
|
21963
|
+
function getWordpressTrafficConnection(config, projectName) {
|
|
21964
|
+
return (config.wordpressTraffic?.connections ?? []).find((c) => c.projectName === projectName);
|
|
21965
|
+
}
|
|
21966
|
+
function upsertWordpressTrafficConnection(config, connection) {
|
|
21967
|
+
const connections = ensureConnections4(config);
|
|
21968
|
+
const index = connections.findIndex((c) => c.projectName === connection.projectName);
|
|
21969
|
+
if (index === -1) {
|
|
21970
|
+
connections.push(connection);
|
|
21971
|
+
return connection;
|
|
21972
|
+
}
|
|
21973
|
+
connections[index] = connection;
|
|
21974
|
+
return connection;
|
|
21975
|
+
}
|
|
21976
|
+
function removeWordpressTrafficConnection(config, projectName) {
|
|
21977
|
+
const connections = config.wordpressTraffic?.connections;
|
|
21978
|
+
if (!connections?.length) return false;
|
|
21979
|
+
const next = connections.filter((c) => c.projectName !== projectName);
|
|
21980
|
+
if (next.length === connections.length) return false;
|
|
21981
|
+
if (!config.wordpressTraffic) return false;
|
|
21982
|
+
config.wordpressTraffic.connections = next;
|
|
21983
|
+
if (next.length === 0) {
|
|
21984
|
+
delete config.wordpressTraffic;
|
|
21985
|
+
}
|
|
21986
|
+
return true;
|
|
21987
|
+
}
|
|
21988
|
+
|
|
21989
|
+
// src/wordpress-config.ts
|
|
21990
|
+
function ensureConnections5(config) {
|
|
21463
21991
|
if (!config.wordpress) config.wordpress = {};
|
|
21464
21992
|
if (!config.wordpress.connections) config.wordpress.connections = [];
|
|
21465
21993
|
return config.wordpress.connections;
|
|
@@ -21476,7 +22004,7 @@ function getWordpressConnection(config, projectName) {
|
|
|
21476
22004
|
return (config.wordpress?.connections ?? []).find((connection) => connection.projectName === projectName);
|
|
21477
22005
|
}
|
|
21478
22006
|
function upsertWordpressConnection(config, connection) {
|
|
21479
|
-
const connections =
|
|
22007
|
+
const connections = ensureConnections5(config);
|
|
21480
22008
|
const normalized = normalizeConnection(connection);
|
|
21481
22009
|
const index = connections.findIndex((entry) => entry.projectName === connection.projectName);
|
|
21482
22010
|
if (index === -1) {
|
|
@@ -25808,6 +26336,21 @@ async function createServer(opts) {
|
|
|
25808
26336
|
return removed;
|
|
25809
26337
|
}
|
|
25810
26338
|
};
|
|
26339
|
+
const wordpressTrafficCredentialStore = {
|
|
26340
|
+
getConnection: (projectName) => {
|
|
26341
|
+
return getWordpressTrafficConnection(opts.config, projectName);
|
|
26342
|
+
},
|
|
26343
|
+
upsertConnection: (record) => {
|
|
26344
|
+
const updated = upsertWordpressTrafficConnection(opts.config, record);
|
|
26345
|
+
saveConfigPatch(opts.config);
|
|
26346
|
+
return updated;
|
|
26347
|
+
},
|
|
26348
|
+
deleteConnection: (projectName) => {
|
|
26349
|
+
const removed = removeWordpressTrafficConnection(opts.config, projectName);
|
|
26350
|
+
if (removed) saveConfigPatch(opts.config);
|
|
26351
|
+
return removed;
|
|
26352
|
+
}
|
|
26353
|
+
};
|
|
25811
26354
|
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto31.randomBytes(32).toString("hex");
|
|
25812
26355
|
const googleConnectionStore = {
|
|
25813
26356
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
@@ -26152,6 +26695,7 @@ async function createServer(opts) {
|
|
|
26152
26695
|
wordpressConnectionStore,
|
|
26153
26696
|
ga4CredentialStore,
|
|
26154
26697
|
cloudRunCredentialStore,
|
|
26698
|
+
wordpressTrafficCredentialStore,
|
|
26155
26699
|
onTrafficSynced: (event) => {
|
|
26156
26700
|
trackEvent("traffic.synced", {
|
|
26157
26701
|
status: event.status,
|