@ainyc/canonry 4.115.1 → 4.116.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/assets/assets/{BacklinksPage-yUb8oJuq.js → BacklinksPage-Dy6Xc-LT.js} +1 -1
  2. package/assets/assets/{ChartPrimitives-CdQM3YDG.js → ChartPrimitives-CHJh5M8x.js} +1 -1
  3. package/assets/assets/ProjectPage-Bjfa4CI5.js +7 -0
  4. package/assets/assets/{RunRow-BU_x7EcP.js → RunRow-DmvQnWPf.js} +1 -1
  5. package/assets/assets/{RunsPage-CFIBWaO7.js → RunsPage-BxkxQEKE.js} +1 -1
  6. package/assets/assets/{SettingsPage-PyMryUad.js → SettingsPage-DCyqHqDh.js} +1 -1
  7. package/assets/assets/{TrafficPage-471ypAdQ.js → TrafficPage-BT_ae5aO.js} +1 -1
  8. package/assets/assets/{TrafficSourceDetailPage-CI7TEQge.js → TrafficSourceDetailPage-Cps-tSHn.js} +1 -1
  9. package/assets/assets/{arrow-left-D2N9u-0i.js → arrow-left-D8vdqhx4.js} +1 -1
  10. package/assets/assets/{extract-error-message-CAdsqKp3.js → extract-error-message-BOY5txUA.js} +1 -1
  11. package/assets/assets/{index-CoL5HENE.js → index-DxjY63VC.js} +63 -63
  12. package/assets/assets/{trash-2-UgQrN5Np.js → trash-2-ap_Fa4zA.js} +1 -1
  13. package/assets/index.html +1 -1
  14. package/dist/{chunk-B2SNXXAJ.js → chunk-GV665WOU.js} +4 -4
  15. package/dist/{chunk-6LOKMEL3.js → chunk-IQPMK36Y.js} +1285 -1233
  16. package/dist/{chunk-CW4JISBL.js → chunk-RVH6QPTA.js} +1 -1
  17. package/dist/{chunk-Z3OGDQHB.js → chunk-WEAXJ25Y.js} +162 -34
  18. package/dist/cli.js +9 -5
  19. package/dist/index.js +4 -4
  20. package/dist/{intelligence-service-4AWB2V7U.js → intelligence-service-HEN5HLUM.js} +2 -2
  21. package/dist/mcp.js +2 -2
  22. package/package.json +12 -12
  23. package/assets/assets/ProjectPage-CHj9fvTs.js +0 -7
@@ -25,7 +25,7 @@ import {
25
25
  trafficConnectVercelRequestSchema,
26
26
  trafficConnectWordpressRequestSchema,
27
27
  trafficEventKindSchema
28
- } from "./chunk-6LOKMEL3.js";
28
+ } from "./chunk-IQPMK36Y.js";
29
29
 
30
30
  // src/config.ts
31
31
  import fs from "fs";
@@ -2,6 +2,7 @@ import {
2
2
  AGENT_PROVIDER_IDS,
3
3
  AI_ENGINE_DOMAINS,
4
4
  AI_PROVIDER_INFRA_DOMAINS,
5
+ AiReferralTrafficClasses,
5
6
  AppError,
6
7
  BacklinkSources,
7
8
  CcReleaseSyncStatuses,
@@ -20,7 +21,6 @@ import {
20
21
  DiscoveryBuckets,
21
22
  DiscoveryCompetitorTypes,
22
23
  DiscoverySessionStatuses,
23
- GA4AiReferralTrafficClasses,
24
24
  RunKinds,
25
25
  RunStatuses,
26
26
  RunTriggers,
@@ -50,6 +50,7 @@ import {
50
50
  adsSyncResponseSchema,
51
51
  agentProvidersResponseDtoSchema,
52
52
  aggregateHarvestedQueries,
53
+ aiReferralClassCounts,
53
54
  apiKeyDtoSchema,
54
55
  apiKeyListDtoSchema,
55
56
  applyHarvestSemanticNovelty,
@@ -87,8 +88,8 @@ import {
87
88
  citationStateSchema,
88
89
  citationStateToCited,
89
90
  citationVisibilityResponseSchema,
91
+ classifyAiReferralTrafficClass,
90
92
  classifyCitedSurface,
91
- classifyGa4AiReferralTrafficClass,
92
93
  classifySkillFile,
93
94
  classifySurfaceFromCategory,
94
95
  classifyTrafficPath,
@@ -132,6 +133,7 @@ import {
132
133
  filterBrandedSeedCandidates,
133
134
  findDuplicateLocationLabels,
134
135
  forbidden,
136
+ formatAiReferralClassSummary,
135
137
  formatAverageDelta,
136
138
  formatDate,
137
139
  formatDateRange,
@@ -275,7 +277,7 @@ import {
275
277
  wordpressSchemaDeployResultDtoSchema,
276
278
  wordpressSchemaStatusResultDtoSchema,
277
279
  wordpressStatusDtoSchema
278
- } from "./chunk-6LOKMEL3.js";
280
+ } from "./chunk-IQPMK36Y.js";
279
281
 
280
282
  // src/intelligence-service.ts
281
283
  import { eq as eq38, desc as desc18, asc as asc6, and as and30, ne as ne5, or as or6, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
@@ -1100,6 +1102,17 @@ var aiReferralEventsHourly = sqliteTable("ai_referral_events_hourly", {
1100
1102
  landingPathNormalized: text("landing_path_normalized").notNull(),
1101
1103
  status: integer("status").notNull(),
1102
1104
  sessionsOrHits: integer("sessions_or_hits").notNull().default(0),
1105
+ /**
1106
+ * Paid/organic splits the MEASURE, not the primary key. The paid marker lives
1107
+ * in the request's UTM tags, which `landing_path_normalized` strips, so one
1108
+ * bucket can legitimately hold both kinds and the class is NOT functionally
1109
+ * determined by the key. Unclassified arrivals are the residual
1110
+ * `sessions_or_hits - paid - organic`, which leaves rows written before the
1111
+ * ingest classifier shipped (both columns 0) visibly unknown rather than
1112
+ * silently organic — the regression migration v95 shipped on the GA4 side.
1113
+ */
1114
+ paidSessionsOrHits: integer("paid_sessions_or_hits").notNull().default(0),
1115
+ organicSessionsOrHits: integer("organic_sessions_or_hits").notNull().default(0),
1103
1116
  usersEstimated: integer("users_estimated"),
1104
1117
  createdAt: text("created_at").notNull(),
1105
1118
  updatedAt: text("updated_at").notNull()
@@ -3512,7 +3525,7 @@ var MIGRATION_VERSIONS = [
3512
3525
  //
3513
3526
  // Every row is re-classified, not only the "unclassified" ones: v95's
3514
3527
  // default is indistinguishable from a genuinely-organic classification, so
3515
- // there is no way to tell the two apart. `classifyGa4AiReferralTrafficClass`
3528
+ // there is no way to tell the two apart. `classifyAiReferralTrafficClass`
3516
3529
  // is pure and deterministic, so re-running it over already-correct rows
3517
3530
  // writes nothing, which is what makes this version safe to replay.
3518
3531
  version: 96,
@@ -3521,6 +3534,27 @@ var MIGRATION_VERSIONS = [
3521
3534
  run: (tx) => {
3522
3535
  backfillGaAiReferralTrafficClass(tx);
3523
3536
  }
3537
+ },
3538
+ {
3539
+ // Splits the server-side AI-referral MEASURE by traffic class rather than
3540
+ // adding a class column. Paid-ness lives in the request's UTM tags, which
3541
+ // `landing_path_normalized` strips, so one hourly bucket can legitimately
3542
+ // hold both paid and organic arrivals: a class column outside the 8-column
3543
+ // primary key would silently stamp a mixed bucket with one label, and
3544
+ // SQLite cannot extend a composite key with ADD COLUMN anyway.
3545
+ //
3546
+ // Deliberately NO backfill. The discriminator was never persisted — the
3547
+ // request's query string is dropped before the hourly bucket and before
3548
+ // `raw_event_samples` — so no honest class exists for historical rows.
3549
+ // Leaving both counters at 0 surfaces their whole `sessions_or_hits` as the
3550
+ // unknown residual. An older binary writing rows after a downgrade lands in
3551
+ // that residual too, which is the truth about those rows.
3552
+ version: 97,
3553
+ name: "server-side-ai-referral-traffic-class",
3554
+ statements: [
3555
+ `ALTER TABLE ai_referral_events_hourly ADD COLUMN paid_sessions_or_hits INTEGER NOT NULL DEFAULT 0`,
3556
+ `ALTER TABLE ai_referral_events_hourly ADD COLUMN organic_sessions_or_hits INTEGER NOT NULL DEFAULT 0`
3557
+ ]
3524
3558
  }
3525
3559
  ];
3526
3560
  function rebuildBacklinkTableWithSource(tx, table) {
@@ -3615,7 +3649,7 @@ function backfillGaAiReferralTrafficClass(tx) {
3615
3649
  ));
3616
3650
  let updated = 0;
3617
3651
  for (const row of rows) {
3618
- const next = classifyGa4AiReferralTrafficClass({
3652
+ const next = classifyAiReferralTrafficClass({
3619
3653
  source: row.source,
3620
3654
  medium: row.medium,
3621
3655
  channelGroup: row.channel_group,
@@ -11272,6 +11306,7 @@ function renderServerActivity(report, audience) {
11272
11306
  if (!copy) return "";
11273
11307
  return `<span class="tone-${deltaTone(d.deltaPct)}">${escapeHtml(copy)}</span>`;
11274
11308
  };
11309
+ const referralSubtitle = [formatDelta(sa.referralArrivals, "sessions"), escapeHtml(sa.referralArrivalsClassSummary)].filter(Boolean).join(" \xB7 ");
11275
11310
  if (isClient) {
11276
11311
  const crawlerRequests = {
11277
11312
  current: sa.verifiedCrawlerHits.current + sa.unverifiedCrawlerHits.current,
@@ -11310,7 +11345,7 @@ function renderServerActivity(report, audience) {
11310
11345
  <div class="metric">
11311
11346
  <div class="label">AI referral sessions</div>
11312
11347
  <div class="value">${formatNumber(sa.referralArrivals.current)}</div>
11313
- <div class="subtitle">${formatDelta(sa.referralArrivals, "sessions")}</div>
11348
+ <div class="subtitle">${referralSubtitle}</div>
11314
11349
  </div>
11315
11350
  </div>
11316
11351
  ${clientOperatorRows ? `<div class="chart-card"><h3>By AI tool</h3>
@@ -11379,7 +11414,7 @@ function renderServerActivity(report, audience) {
11379
11414
  <div class="metric">
11380
11415
  <div class="label">AI-referral sessions (${windowLabel})</div>
11381
11416
  <div class="value">${formatNumber(sa.referralArrivals.current)}</div>
11382
- <div class="subtitle">${formatDelta(sa.referralArrivals, "sessions")}</div>
11417
+ <div class="subtitle">${referralSubtitle}</div>
11383
11418
  </div>
11384
11419
  </div>
11385
11420
  ${trendChart}
@@ -11905,7 +11940,7 @@ function safeNum(value) {
11905
11940
  return 0;
11906
11941
  }
11907
11942
  function isPaidAiTrafficClass(value) {
11908
- return value === GA4AiReferralTrafficClasses.paid;
11943
+ return value === AiReferralTrafficClasses.paid;
11909
11944
  }
11910
11945
  function categorizeQuery(query, projectBrandNames, canonicalDomain) {
11911
11946
  return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain, projectBrandNames));
@@ -12298,16 +12333,21 @@ function buildServerActivity(db, projectId, windowDays) {
12298
12333
  )
12299
12334
  ).get()?.total ?? 0
12300
12335
  );
12301
- const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
12302
- db.select({ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
12336
+ const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => {
12337
+ const row = db.select({
12338
+ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
12339
+ paid: sql7`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
12340
+ organic: sql7`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`
12341
+ }).from(aiReferralEventsHourly).where(
12303
12342
  and12(
12304
12343
  eq15(aiReferralEventsHourly.projectId, projectId),
12305
12344
  nonSubresourceReferralPathCondition(),
12306
12345
  gte2(aiReferralEventsHourly.tsHour, windowStartIso),
12307
12346
  exclusiveEnd ? lt(aiReferralEventsHourly.tsHour, windowEndIso) : lte(aiReferralEventsHourly.tsHour, windowEndIso)
12308
12347
  )
12309
- ).get()?.total ?? 0
12310
- );
12348
+ ).get();
12349
+ return aiReferralClassCounts(Number(row?.total ?? 0), Number(row?.paid ?? 0), Number(row?.organic ?? 0));
12350
+ };
12311
12351
  const sumUserFetches = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
12312
12352
  db.select({ total: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
12313
12353
  and12(
@@ -12505,7 +12545,7 @@ function buildServerActivity(db, projectId, windowDays) {
12505
12545
  return {
12506
12546
  windowStart: headlineStart,
12507
12547
  windowEnd: headlineEnd,
12508
- hasData: verifiedCurrent + unverifiedCurrent + userFetchCurrent + referralCurrent + verifiedPrior + unverifiedPrior + userFetchPrior + referralPrior > 0 || byOperator.length > 0 || topCrawledPaths.length > 0 || referralProducts.length > 0,
12548
+ hasData: verifiedCurrent + unverifiedCurrent + userFetchCurrent + referralCurrent.total + verifiedPrior + unverifiedPrior + userFetchPrior + referralPrior.total > 0 || byOperator.length > 0 || topCrawledPaths.length > 0 || referralProducts.length > 0,
12509
12549
  verifiedCrawlerHits: {
12510
12550
  current: verifiedCurrent,
12511
12551
  prior: verifiedPrior,
@@ -12522,10 +12562,28 @@ function buildServerActivity(db, projectId, windowDays) {
12522
12562
  deltaPct: deltaPercent(userFetchCurrent, userFetchPrior)
12523
12563
  },
12524
12564
  referralArrivals: {
12525
- current: referralCurrent,
12526
- prior: referralPrior,
12527
- deltaPct: deltaPercent(referralCurrent, referralPrior)
12565
+ current: referralCurrent.total,
12566
+ prior: referralPrior.total,
12567
+ deltaPct: deltaPercent(referralCurrent.total, referralPrior.total)
12568
+ },
12569
+ referralArrivalsByClass: {
12570
+ paid: {
12571
+ current: referralCurrent.paid,
12572
+ prior: referralPrior.paid,
12573
+ deltaPct: deltaPercent(referralCurrent.paid, referralPrior.paid)
12574
+ },
12575
+ organic: {
12576
+ current: referralCurrent.organic,
12577
+ prior: referralPrior.organic,
12578
+ deltaPct: deltaPercent(referralCurrent.organic, referralPrior.organic)
12579
+ },
12580
+ unclassified: {
12581
+ current: referralCurrent.unknown,
12582
+ prior: referralPrior.unknown,
12583
+ deltaPct: deltaPercent(referralCurrent.unknown, referralPrior.unknown)
12584
+ }
12528
12585
  },
12586
+ referralArrivalsClassSummary: formatAiReferralClassSummary(referralCurrent),
12529
12587
  byOperator,
12530
12588
  topCrawledPaths,
12531
12589
  referralProducts,
@@ -20551,7 +20609,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
20551
20609
  date: row.dimensionValues[0].value,
20552
20610
  source,
20553
20611
  medium,
20554
- trafficClass: classifyGa4AiReferralTrafficClass({ source, medium, channelGroup, landingPage }),
20612
+ trafficClass: classifyAiReferralTrafficClass({ source, medium, channelGroup, landingPage }),
20555
20613
  channelGroup,
20556
20614
  landingPage,
20557
20615
  sessions: parseInt(row.metricValues[0].value, 10) || 0,
@@ -23527,7 +23585,7 @@ function pickWinningDimension(rows, tupleKey) {
23527
23585
  return [...winners.values()].sort((a, b) => (b.sessions ?? 0) - (a.sessions ?? 0));
23528
23586
  }
23529
23587
  function normalizeAiTrafficClass(value) {
23530
- return value === GA4AiReferralTrafficClasses.paid ? GA4AiReferralTrafficClasses.paid : GA4AiReferralTrafficClasses.organic;
23588
+ return value === AiReferralTrafficClasses.paid ? AiReferralTrafficClasses.paid : AiReferralTrafficClasses.organic;
23531
23589
  }
23532
23590
  function emptyAiCounts() {
23533
23591
  return { sessions: 0, users: 0 };
@@ -23540,7 +23598,7 @@ function summarizeAiReferralCounts(rows) {
23540
23598
  const paidBySession = emptyAiCounts();
23541
23599
  const organicBySession = emptyAiCounts();
23542
23600
  for (const row of rows) {
23543
- const isPaid = normalizeAiTrafficClass(row.trafficClass) === GA4AiReferralTrafficClasses.paid;
23601
+ const isPaid = normalizeAiTrafficClass(row.trafficClass) === AiReferralTrafficClasses.paid;
23544
23602
  const sessions = row.sessions ?? 0;
23545
23603
  const users = row.users ?? 0;
23546
23604
  const key = `${row.date}\0${row.source}\0${row.medium}`;
@@ -23872,7 +23930,7 @@ async function ga4Routes(app, opts) {
23872
23930
  date: row.date,
23873
23931
  source: row.source,
23874
23932
  medium: row.medium,
23875
- trafficClass: row.trafficClass ?? classifyGa4AiReferralTrafficClass({
23933
+ trafficClass: row.trafficClass ?? classifyAiReferralTrafficClass({
23876
23934
  source: row.source,
23877
23935
  medium: row.medium,
23878
23936
  channelGroup: row.channelGroup,
@@ -29650,6 +29708,33 @@ function utmSourceFromUrl(value) {
29650
29708
  return null;
29651
29709
  }
29652
29710
  }
29711
+ function paramsFromQuery(queryString) {
29712
+ if (!queryString) return null;
29713
+ const params = new URLSearchParams(queryString);
29714
+ return [...params.keys()].length > 0 ? params : null;
29715
+ }
29716
+ function paramsFromUrl(value) {
29717
+ if (!value) return null;
29718
+ try {
29719
+ return paramsFromQuery(new URL(value).search.replace(/^\?/, ""));
29720
+ } catch {
29721
+ return null;
29722
+ }
29723
+ }
29724
+ function classifyAiReferralTrafficClassFromEvent(event) {
29725
+ for (const params of [paramsFromQuery(event.queryString), paramsFromUrl(event.referer)]) {
29726
+ if (!params) continue;
29727
+ const trafficClass = classifyAiReferralTrafficClass({
29728
+ source: params.get("utm_source"),
29729
+ medium: params.get("utm_medium"),
29730
+ channelGroup: null,
29731
+ // Carries utm_campaign / utm_content / utm_term to the shared rules.
29732
+ landingPage: `/?${params.toString()}`
29733
+ });
29734
+ if (trafficClass === AiReferralTrafficClasses.paid) return AiReferralTrafficClasses.paid;
29735
+ }
29736
+ return AiReferralTrafficClasses.organic;
29737
+ }
29653
29738
  function isSelfTraffic(event) {
29654
29739
  const userAgent = event.userAgent?.trim();
29655
29740
  if (!userAgent) return false;
@@ -29693,6 +29778,7 @@ function classifyAiUserFetch(event) {
29693
29778
  return null;
29694
29779
  }
29695
29780
  function classifyAiReferral(event) {
29781
+ const trafficClass = classifyAiReferralTrafficClassFromEvent(event);
29696
29782
  const refererHost = hostFromUrl(event.referer);
29697
29783
  if (refererHost) {
29698
29784
  const rule = DEFAULT_AI_REFERRER_RULES.find((candidate) => hostMatches(refererHost, candidate.domain));
@@ -29701,7 +29787,8 @@ function classifyAiReferral(event) {
29701
29787
  operator: rule.operator,
29702
29788
  product: rule.product,
29703
29789
  sourceDomain: refererHost,
29704
- evidenceType: "referer"
29790
+ evidenceType: "referer",
29791
+ trafficClass
29705
29792
  };
29706
29793
  }
29707
29794
  }
@@ -29713,7 +29800,8 @@ function classifyAiReferral(event) {
29713
29800
  operator: rule.operator,
29714
29801
  product: rule.product,
29715
29802
  sourceDomain: utmSource,
29716
- evidenceType: "utm"
29803
+ evidenceType: "utm",
29804
+ trafficClass
29717
29805
  };
29718
29806
  }
29719
29807
  }
@@ -29725,7 +29813,8 @@ function classifyAiReferral(event) {
29725
29813
  operator: rule.operator,
29726
29814
  product: rule.product,
29727
29815
  sourceDomain: refererUtmSource,
29728
- evidenceType: "referer-utm"
29816
+ evidenceType: "referer-utm",
29817
+ trafficClass
29729
29818
  };
29730
29819
  }
29731
29820
  }
@@ -29828,8 +29917,10 @@ function evidenceRank(evidenceType) {
29828
29917
  return 1;
29829
29918
  }
29830
29919
  }
29831
- function strongerReferralEvidence(current, next) {
29832
- return evidenceRank(next.evidenceType) > evidenceRank(current.evidenceType) ? next : current;
29920
+ function mergeReferralSession(current, next) {
29921
+ const winner = evidenceRank(next.evidenceType) > evidenceRank(current.evidenceType) ? next : current;
29922
+ const paid = current.trafficClass === AiReferralTrafficClasses.paid || next.trafficClass === AiReferralTrafficClasses.paid;
29923
+ return { ...winner, trafficClass: paid ? AiReferralTrafficClasses.paid : winner.trafficClass };
29833
29924
  }
29834
29925
  function sortCrawlerBuckets(a, b) {
29835
29926
  return a.tsHour.localeCompare(b.tsHour) || a.botId.localeCompare(b.botId) || a.pathNormalized.localeCompare(b.pathNormalized) || String(a.status).localeCompare(String(b.status));
@@ -29862,6 +29953,8 @@ function buildTrafficProbeReport(events, options = {}) {
29862
29953
  let crawlerHits = 0;
29863
29954
  let aiUserFetchHits = 0;
29864
29955
  let aiReferralHits = 0;
29956
+ let aiReferralPaidSessions = 0;
29957
+ let aiReferralOrganicSessions = 0;
29865
29958
  let unknownHits = 0;
29866
29959
  const samples = [];
29867
29960
  for (const event of includedEvents) {
@@ -29943,11 +30036,12 @@ function buildTrafficProbeReport(events, options = {}) {
29943
30036
  sourceDomain: aiReferral.sourceDomain,
29944
30037
  evidenceType: aiReferral.evidenceType,
29945
30038
  landingPathNormalized,
29946
- status: event.status
30039
+ status: event.status,
30040
+ trafficClass: aiReferral.trafficClass
29947
30041
  };
29948
30042
  const key = aiReferralSessionKey(event, aiReferral, landingPathNormalized, aiReferralSessionWindowMs);
29949
30043
  const existing = aiReferralSessions.get(key);
29950
- aiReferralSessions.set(key, existing ? strongerReferralEvidence(existing, session) : session);
30044
+ aiReferralSessions.set(key, existing ? mergeReferralSession(existing, session) : session);
29951
30045
  }
29952
30046
  }
29953
30047
  if (!crawler && !aiUserFetch && !aiReferral) unknownHits += 1;
@@ -29975,13 +30069,21 @@ function buildTrafficProbeReport(events, options = {}) {
29975
30069
  session.landingPathNormalized,
29976
30070
  session.status ?? "null"
29977
30071
  ].join(" ");
30072
+ const { trafficClass, ...bucketFields } = session;
30073
+ const paid = trafficClass === AiReferralTrafficClasses.paid;
30074
+ if (paid) aiReferralPaidSessions += 1;
30075
+ else aiReferralOrganicSessions += 1;
29978
30076
  const existing = aiReferralBuckets.get(key);
29979
30077
  if (existing) {
29980
30078
  existing.hits += 1;
30079
+ if (paid) existing.paidHits += 1;
30080
+ else existing.organicHits += 1;
29981
30081
  } else {
29982
30082
  aiReferralBuckets.set(key, {
29983
- ...session,
29984
- hits: 1
30083
+ ...bucketFields,
30084
+ hits: 1,
30085
+ paidHits: paid ? 1 : 0,
30086
+ organicHits: paid ? 0 : 1
29985
30087
  });
29986
30088
  }
29987
30089
  incrementBucket(topAiReferrers, session.sourceDomain, {
@@ -30000,6 +30102,8 @@ function buildTrafficProbeReport(events, options = {}) {
30000
30102
  crawlerHits,
30001
30103
  aiUserFetchHits,
30002
30104
  aiReferralSessions: aiReferralSessions.size,
30105
+ aiReferralPaidSessions,
30106
+ aiReferralOrganicSessions,
30003
30107
  aiReferralHits,
30004
30108
  unknownHits
30005
30109
  },
@@ -30761,6 +30865,8 @@ async function runBackfillTask(options) {
30761
30865
  landingPathNormalized: bucket.landingPathNormalized,
30762
30866
  status: bucket.status ?? 0,
30763
30867
  sessionsOrHits: bucket.hits,
30868
+ paidSessionsOrHits: bucket.paidHits,
30869
+ organicSessionsOrHits: bucket.organicHits,
30764
30870
  usersEstimated: null,
30765
30871
  createdAt: finishedAt,
30766
30872
  updatedAt: finishedAt
@@ -31508,6 +31614,8 @@ async function trafficRoutes(app, opts) {
31508
31614
  landingPathNormalized: bucket.landingPathNormalized,
31509
31615
  status,
31510
31616
  sessionsOrHits: bucket.hits,
31617
+ paidSessionsOrHits: bucket.paidHits,
31618
+ organicSessionsOrHits: bucket.organicHits,
31511
31619
  usersEstimated: null,
31512
31620
  createdAt: finishedAt,
31513
31621
  updatedAt: finishedAt
@@ -31522,8 +31630,13 @@ async function trafficRoutes(app, opts) {
31522
31630
  aiReferralEventsHourly.landingPathNormalized,
31523
31631
  aiReferralEventsHourly.status
31524
31632
  ],
31633
+ // All three counters accumulate. The class splits the measure, so a
31634
+ // colliding bucket adds its paid and organic arrivals to the ones
31635
+ // already there rather than overwriting a single label.
31525
31636
  set: {
31526
31637
  sessionsOrHits: sql12`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
31638
+ paidSessionsOrHits: sql12`${aiReferralEventsHourly.paidSessionsOrHits} + ${bucket.paidHits}`,
31639
+ organicSessionsOrHits: sql12`${aiReferralEventsHourly.organicSessionsOrHits} + ${bucket.organicHits}`,
31527
31640
  updatedAt: finishedAt
31528
31641
  }
31529
31642
  }).run();
@@ -31975,7 +32088,7 @@ async function trafficRoutes(app, opts) {
31975
32088
  let crawlerTotal = 0;
31976
32089
  let crawlerSegments = { content: 0, sitemap: 0, robots: 0, asset: 0, other: 0 };
31977
32090
  let aiUserFetchTotal = 0;
31978
- let aiReferralTotal = 0;
32091
+ let aiReferralCounts = aiReferralClassCounts(0, 0, 0);
31979
32092
  if (kind === "all" || kind === TrafficEventKinds.crawler) {
31980
32093
  const crawlerFilters = [
31981
32094
  eq28(crawlerEventsHourly.projectId, project.id),
@@ -32041,10 +32154,19 @@ async function trafficRoutes(app, opts) {
32041
32154
  ];
32042
32155
  if (sourceIdParam) aiFilters.push(eq28(aiReferralEventsHourly.sourceId, sourceIdParam));
32043
32156
  const aiWhere = and24(...aiFilters);
32044
- const total = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
32045
- aiReferralTotal = Number(total?.total ?? 0);
32157
+ const total = app.db.select({
32158
+ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
32159
+ paid: sql12`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
32160
+ organic: sql12`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`
32161
+ }).from(aiReferralEventsHourly).where(aiWhere).get();
32162
+ aiReferralCounts = aiReferralClassCounts(
32163
+ Number(total?.total ?? 0),
32164
+ Number(total?.paid ?? 0),
32165
+ Number(total?.organic ?? 0)
32166
+ );
32046
32167
  const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc15(aiReferralEventsHourly.tsHour)).limit(limit).all();
32047
32168
  for (const r of rows) {
32169
+ const counts2 = aiReferralClassCounts(r.sessionsOrHits, r.paidSessionsOrHits, r.organicSessionsOrHits);
32048
32170
  events.push({
32049
32171
  kind: TrafficEventKinds["ai-referral"],
32050
32172
  sourceId: r.sourceId,
@@ -32055,7 +32177,10 @@ async function trafficRoutes(app, opts) {
32055
32177
  evidenceType: r.evidenceType,
32056
32178
  landingPathNormalized: r.landingPathNormalized,
32057
32179
  status: r.status,
32058
- hits: r.sessionsOrHits
32180
+ hits: counts2.total,
32181
+ paidHits: counts2.paid,
32182
+ organicHits: counts2.organic,
32183
+ unknownHits: counts2.unknown
32059
32184
  });
32060
32185
  }
32061
32186
  }
@@ -32070,7 +32195,10 @@ async function trafficRoutes(app, opts) {
32070
32195
  crawlerInfraHits: sumInfraHits(crawlerSegments),
32071
32196
  crawlerSegments,
32072
32197
  aiUserFetchHits: aiUserFetchTotal,
32073
- aiReferralHits: aiReferralTotal
32198
+ aiReferralHits: aiReferralCounts.total,
32199
+ aiReferralPaidHits: aiReferralCounts.paid,
32200
+ aiReferralOrganicHits: aiReferralCounts.organic,
32201
+ aiReferralUnknownHits: aiReferralCounts.unknown
32074
32202
  },
32075
32203
  events: trimmed
32076
32204
  };
package/dist/cli.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  setTelemetrySource,
30
30
  showFirstRunNotice,
31
31
  trackEvent
32
- } from "./chunk-B2SNXXAJ.js";
32
+ } from "./chunk-GV665WOU.js";
33
33
  import {
34
34
  CliError,
35
35
  EXIT_SYSTEM_ERROR,
@@ -46,7 +46,7 @@ import {
46
46
  saveConfig,
47
47
  saveConfigPatch,
48
48
  usageError
49
- } from "./chunk-CW4JISBL.js";
49
+ } from "./chunk-RVH6QPTA.js";
50
50
  import {
51
51
  apiKeys,
52
52
  createClient,
@@ -54,7 +54,7 @@ import {
54
54
  projects,
55
55
  queries,
56
56
  renderReportHtml
57
- } from "./chunk-Z3OGDQHB.js";
57
+ } from "./chunk-WEAXJ25Y.js";
58
58
  import {
59
59
  BacklinkSources,
60
60
  CcReleaseSyncStatuses,
@@ -80,7 +80,7 @@ import {
80
80
  providerQuotaPolicySchema,
81
81
  resolveProviderInput,
82
82
  winnabilityClassSchema
83
- } from "./chunk-6LOKMEL3.js";
83
+ } from "./chunk-IQPMK36Y.js";
84
84
 
85
85
  // src/cli.ts
86
86
  import { pathToFileURL } from "url";
@@ -4698,7 +4698,8 @@ function formatEventLine(event) {
4698
4698
  event.evidenceType,
4699
4699
  event.sourceDomain,
4700
4700
  event.landingPathNormalized,
4701
- `${event.hits} hits`
4701
+ `${event.hits} hits`,
4702
+ `(paid ${event.paidHits} \xB7 organic ${event.organicHits} \xB7 unclassified ${event.unknownHits})`
4702
4703
  ].join(" ");
4703
4704
  }
4704
4705
  }
@@ -4743,6 +4744,9 @@ async function trafficEvents(project, opts) {
4743
4744
  console.log(` Crawler hits total (window): ${result.totals.crawlerHits}`);
4744
4745
  console.log(` AI user-fetch hits (window): ${result.totals.aiUserFetchHits}`);
4745
4746
  console.log(` AI referral sessions (window): ${result.totals.aiReferralHits}`);
4747
+ console.log(` paid: ${result.totals.aiReferralPaidHits}`);
4748
+ console.log(` organic: ${result.totals.aiReferralOrganicHits}`);
4749
+ console.log(` unclassified: ${result.totals.aiReferralUnknownHits} (ingested before paid/organic classification)`);
4746
4750
  console.log("");
4747
4751
  if (result.events.length === 0) {
4748
4752
  console.log("No events in this window.");
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-B2SNXXAJ.js";
3
+ } from "./chunk-GV665WOU.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-CW4JISBL.js";
7
- import "./chunk-Z3OGDQHB.js";
8
- import "./chunk-6LOKMEL3.js";
6
+ } from "./chunk-RVH6QPTA.js";
7
+ import "./chunk-WEAXJ25Y.js";
8
+ import "./chunk-IQPMK36Y.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-Z3OGDQHB.js";
4
- import "./chunk-6LOKMEL3.js";
3
+ } from "./chunk-WEAXJ25Y.js";
4
+ import "./chunk-IQPMK36Y.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -3,10 +3,10 @@ import {
3
3
  PACKAGE_VERSION,
4
4
  canonryMcpTools,
5
5
  createApiClient
6
- } from "./chunk-CW4JISBL.js";
6
+ } from "./chunk-RVH6QPTA.js";
7
7
  import {
8
8
  isReadOnlyKey
9
- } from "./chunk-6LOKMEL3.js";
9
+ } from "./chunk-IQPMK36Y.js";
10
10
 
11
11
  // src/mcp/cli.ts
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "4.115.1",
3
+ "version": "4.116.0",
4
4
  "type": "module",
5
5
  "description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -65,27 +65,27 @@
65
65
  "@types/node-cron": "^3.0.11",
66
66
  "tsup": "^8.5.1",
67
67
  "tsx": "^4.19.0",
68
- "@ainyc/canonry-api-client": "0.0.0",
69
- "@ainyc/canonry-config": "0.0.0",
70
- "@ainyc/canonry-contracts": "0.0.0",
71
68
  "@ainyc/canonry-api-routes": "0.0.0",
69
+ "@ainyc/canonry-config": "0.0.0",
70
+ "@ainyc/canonry-api-client": "0.0.0",
72
71
  "@ainyc/canonry-db": "0.0.0",
73
72
  "@ainyc/canonry-integration-bing": "0.0.0",
73
+ "@ainyc/canonry-contracts": "0.0.0",
74
+ "@ainyc/canonry-integration-openai-ads": "0.0.0",
74
75
  "@ainyc/canonry-integration-cloud-run": "0.0.0",
75
- "@ainyc/canonry-integration-commoncrawl": "0.0.0",
76
76
  "@ainyc/canonry-integration-google": "0.0.0",
77
- "@ainyc/canonry-integration-openai-ads": "0.0.0",
78
77
  "@ainyc/canonry-integration-google-business-profile": "0.0.0",
79
- "@ainyc/canonry-integration-google-places": "0.0.0",
78
+ "@ainyc/canonry-integration-commoncrawl": "0.0.0",
80
79
  "@ainyc/canonry-integration-traffic": "0.0.0",
80
+ "@ainyc/canonry-integration-wordpress": "0.0.0",
81
81
  "@ainyc/canonry-intelligence": "0.0.0",
82
- "@ainyc/canonry-provider-claude": "0.0.0",
83
82
  "@ainyc/canonry-provider-cdp": "0.0.0",
84
- "@ainyc/canonry-integration-wordpress": "0.0.0",
85
- "@ainyc/canonry-provider-gemini": "0.0.0",
83
+ "@ainyc/canonry-provider-claude": "0.0.0",
84
+ "@ainyc/canonry-integration-google-places": "0.0.0",
85
+ "@ainyc/canonry-provider-local": "0.0.0",
86
86
  "@ainyc/canonry-provider-openai": "0.0.0",
87
- "@ainyc/canonry-provider-perplexity": "0.0.0",
88
- "@ainyc/canonry-provider-local": "0.0.0"
87
+ "@ainyc/canonry-provider-gemini": "0.0.0",
88
+ "@ainyc/canonry-provider-perplexity": "0.0.0"
89
89
  },
90
90
  "scripts": {
91
91
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",