@ainyc/canonry 4.13.1 → 4.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-BofSsfDl.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-C3thP3DI.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="./assets/index-D0EPNRDs.css">
17
17
  </head>
18
18
  <body>
@@ -12,7 +12,7 @@ import {
12
12
  queryGenerateRequestSchema,
13
13
  runTriggerRequestSchema,
14
14
  scheduleUpsertRequestSchema
15
- } from "./chunk-YDGT5CAY.js";
15
+ } from "./chunk-6QTH5NS5.js";
16
16
 
17
17
  // src/config.ts
18
18
  import fs from "fs";
@@ -1239,6 +1239,18 @@ var ga4SocialReferralDtoSchema = z12.object({
1239
1239
  /** GA4 default channel group (e.g. 'Organic Social', 'Paid Social') */
1240
1240
  channelGroup: z12.string()
1241
1241
  });
1242
+ var ga4ChannelBucketDtoSchema = z12.object({
1243
+ sessions: z12.number(),
1244
+ sharePct: z12.number(),
1245
+ sharePctDisplay: z12.string()
1246
+ });
1247
+ var ga4ChannelBreakdownDtoSchema = z12.object({
1248
+ organic: ga4ChannelBucketDtoSchema,
1249
+ social: ga4ChannelBucketDtoSchema,
1250
+ direct: ga4ChannelBucketDtoSchema,
1251
+ ai: ga4ChannelBucketDtoSchema,
1252
+ other: ga4ChannelBucketDtoSchema
1253
+ });
1242
1254
  var ga4TrafficSummaryDtoSchema = z12.object({
1243
1255
  totalSessions: z12.number(),
1244
1256
  totalOrganicSessions: z12.number(),
@@ -1259,20 +1271,22 @@ var ga4TrafficSummaryDtoSchema = z12.object({
1259
1271
  aiSessionsDeduped: z12.number(),
1260
1272
  /** Deduped AI user total: MAX(users) per date+source+medium across attribution dimensions, then summed. */
1261
1273
  aiUsersDeduped: z12.number(),
1262
- /** AI sessions whose CURRENT sessionSource matched an AI engine. Disjoint from Direct/Organic/Social safe for the channel breakdown. */
1274
+ /** AI sessions whose CURRENT sessionSource matched an AI engine. Can overlap with raw Organic/Social/Direct totals; `channelBreakdown` removes those overlaps for display. */
1263
1275
  aiSessionsBySession: z12.number(),
1264
- /** AI users whose CURRENT sessionSource matched an AI engine. Disjoint from Direct/Organic/Social — safe for the channel breakdown. */
1276
+ /** AI users whose CURRENT sessionSource matched an AI engine. Can overlap with raw Organic/Social/Direct totals. */
1265
1277
  aiUsersBySession: z12.number(),
1266
1278
  socialReferrals: z12.array(ga4SocialReferralDtoSchema),
1267
1279
  /** Total social sessions (session-scoped, no cross-dimension dedup needed). */
1268
1280
  socialSessions: z12.number(),
1269
1281
  /** Total social users (session-scoped, no cross-dimension dedup needed). */
1270
1282
  socialUsers: z12.number(),
1283
+ /** Five disjoint buckets used for the channel breakdown. Known AI session-source matches are removed from their native GA4 bucket before shares are computed. */
1284
+ channelBreakdown: ga4ChannelBreakdownDtoSchema,
1271
1285
  /** Organic sessions as a percentage of total sessions (0–100, rounded). */
1272
1286
  organicSharePct: z12.number(),
1273
1287
  /** Deduped AI sessions as a percentage of total sessions (0–100, rounded). Cross-cutting: can overlap with Direct/Organic/Social. */
1274
1288
  aiSharePct: z12.number(),
1275
- /** Session-source-only AI sessions as a percentage of total sessions (0–100, rounded). Disjoint from Direct/Organic/Social. */
1289
+ /** Session-source-only AI sessions as a percentage of total sessions (0–100, rounded). Can overlap with raw Organic/Social/Direct totals. */
1276
1290
  aiSharePctBySession: z12.number(),
1277
1291
  /** Direct-channel sessions as a percentage of total sessions (0–100, rounded). */
1278
1292
  directSharePct: z12.number(),
@@ -1288,6 +1302,12 @@ var ga4TrafficSummaryDtoSchema = z12.object({
1288
1302
  directSharePctDisplay: z12.string(),
1289
1303
  /** Display string for socialSharePct: 'X%', '<1%' for non-zero shares that round below 1, or '—' when sessions exist but total is unknown (partial sync). */
1290
1304
  socialSharePctDisplay: z12.string(),
1305
+ /** Sessions not covered by Organic, Social, Direct, or AI (session) channels — e.g. Referral, Email, Paid Search, Display. Always non-negative; clamped to 0 when the four disjoint channels sum above total (rounding edge). */
1306
+ otherSessions: z12.number(),
1307
+ /** Other sessions as a percentage of total sessions (0–100, rounded). */
1308
+ otherSharePct: z12.number(),
1309
+ /** Display string for otherSharePct: 'X%', '<1%' for non-zero shares that round below 1, or '—' when sessions exist but total is unknown (partial sync). */
1310
+ otherSharePctDisplay: z12.string(),
1291
1311
  lastSyncedAt: z12.string().nullable()
1292
1312
  });
1293
1313
  var ga4AiReferralHistoryEntrySchema = z12.object({
@@ -2232,6 +2252,48 @@ var trafficSyncResponseSchema = z20.object({
2232
2252
  windowEnd: z20.string()
2233
2253
  });
2234
2254
 
2255
+ // ../contracts/src/formatting.ts
2256
+ function formatRatio(value) {
2257
+ if (!Number.isFinite(value) || value === 0) return "0%";
2258
+ return `${(value * 100).toFixed(1)}%`;
2259
+ }
2260
+ function formatNumber(value) {
2261
+ if (!Number.isFinite(value)) return "\u2014";
2262
+ if (Math.abs(value) >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
2263
+ if (Math.abs(value) >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
2264
+ return value.toLocaleString("en-US");
2265
+ }
2266
+ function formatDate(iso) {
2267
+ if (!iso) return "\u2014";
2268
+ try {
2269
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
2270
+ const options = { month: "short", day: "numeric", year: "numeric" };
2271
+ const d = dateOnly && dateOnly[1] && dateOnly[2] && dateOnly[3] ? new Date(Date.UTC(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]))) : new Date(iso);
2272
+ if (Number.isNaN(d.getTime())) return iso;
2273
+ return d.toLocaleDateString("en-US", dateOnly ? { ...options, timeZone: "UTC" } : options);
2274
+ } catch {
2275
+ return iso;
2276
+ }
2277
+ }
2278
+ function formatIsoDate(iso) {
2279
+ if (!iso) return "\u2014";
2280
+ try {
2281
+ const d = new Date(iso);
2282
+ if (Number.isNaN(d.getTime())) return iso;
2283
+ const yyyy = d.getUTCFullYear();
2284
+ const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
2285
+ const dd = String(d.getUTCDate()).padStart(2, "0");
2286
+ return `${yyyy}-${mm}-${dd}`;
2287
+ } catch {
2288
+ return iso;
2289
+ }
2290
+ }
2291
+ function formatDateRange(start, end) {
2292
+ if (!start && !end) return "";
2293
+ if (start && end) return `${formatDate(start)} \u2192 ${formatDate(end)}`;
2294
+ return formatDate(start || end);
2295
+ }
2296
+
2235
2297
  export {
2236
2298
  __export,
2237
2299
  providerQuotaPolicySchema,
@@ -2326,5 +2388,10 @@ export {
2326
2388
  TrafficEvidenceKinds,
2327
2389
  TrafficEventConfidences,
2328
2390
  TrafficSourceStatuses,
2329
- TrafficSourceAuthModes
2391
+ TrafficSourceAuthModes,
2392
+ formatRatio,
2393
+ formatNumber,
2394
+ formatDate,
2395
+ formatIsoDate,
2396
+ formatDateRange
2330
2397
  };
@@ -8,7 +8,7 @@ import {
8
8
  categoryLabel,
9
9
  determineAnswerMentioned,
10
10
  normalizeProjectDomain
11
- } from "./chunk-YDGT5CAY.js";
11
+ } from "./chunk-6QTH5NS5.js";
12
12
 
13
13
  // src/intelligence-service.ts
14
14
  import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
@@ -358,6 +358,8 @@ var gaAiReferrals = sqliteTable("ga_ai_referrals", {
358
358
  medium: text("medium").notNull(),
359
359
  /** Which GA4 dimension produced this row: 'session' | 'first_user' | 'manual_utm' */
360
360
  sourceDimension: text("source_dimension").notNull().default("session"),
361
+ /** GA4 default channel group for the session (e.g. 'Referral', 'Organic Social'). */
362
+ channelGroup: text("channel_group").notNull().default("(not set)"),
361
363
  landingPage: text("landing_page").notNull().default("(not set)"),
362
364
  landingPageNormalized: text("landing_page_normalized"),
363
365
  sessions: integer("sessions").notNull().default(0),
@@ -368,7 +370,7 @@ var gaAiReferrals = sqliteTable("ga_ai_referrals", {
368
370
  index("idx_ga_ai_ref_project_date").on(table.projectId, table.date),
369
371
  index("idx_ga_ai_ref_source").on(table.source),
370
372
  index("idx_ga_ai_ref_landing_page").on(table.projectId, table.date, table.landingPageNormalized),
371
- uniqueIndex("idx_ga_ai_ref_unique_v3").on(table.projectId, table.date, table.source, table.medium, table.sourceDimension, table.landingPage),
373
+ uniqueIndex("idx_ga_ai_ref_unique_v4").on(table.projectId, table.date, table.source, table.medium, table.sourceDimension, table.channelGroup, table.landingPage),
372
374
  index("idx_ga_ai_ref_run").on(table.syncRunId)
373
375
  ]);
374
376
  var gaSocialReferrals = sqliteTable("ga_social_referrals", {
@@ -1570,6 +1572,20 @@ var MIGRATION_VERSIONS = [
1570
1572
  `CREATE INDEX IF NOT EXISTS idx_raw_event_samples_source_ts ON raw_event_samples(source_id, ts)`,
1571
1573
  `CREATE INDEX IF NOT EXISTS idx_raw_event_samples_event_type ON raw_event_samples(event_type)`
1572
1574
  ]
1575
+ },
1576
+ {
1577
+ version: 50,
1578
+ name: "ga-ai-referral-channel-group",
1579
+ statements: [],
1580
+ run: (tx) => {
1581
+ if (!tableExists(tx, "ga_ai_referrals")) return;
1582
+ if (!columnExists(tx, "ga_ai_referrals", "channel_group")) {
1583
+ tx.run(sql.raw(`ALTER TABLE ga_ai_referrals ADD COLUMN channel_group TEXT NOT NULL DEFAULT '(not set)'`));
1584
+ }
1585
+ tx.run(sql.raw(`DROP INDEX IF EXISTS idx_ga_ai_ref_unique_v3`));
1586
+ tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v4
1587
+ ON ga_ai_referrals(project_id, date, source, medium, source_dimension, channel_group, landing_page)`));
1588
+ }
1573
1589
  }
1574
1590
  ];
1575
1591
  function isDuplicateColumnError(err) {
@@ -4,7 +4,7 @@ import {
4
4
  configExists,
5
5
  loadConfig,
6
6
  saveConfigPatch
7
- } from "./chunk-LNRDWAG3.js";
7
+ } from "./chunk-5NYG5EC7.js";
8
8
  import {
9
9
  DEFAULT_RUN_HISTORY_LIMIT,
10
10
  IntelligenceService,
@@ -65,7 +65,7 @@ import {
65
65
  schedules,
66
66
  trafficSources,
67
67
  usageCounters
68
- } from "./chunk-DCE3B6KD.js";
68
+ } from "./chunk-7HBZCGRL.js";
69
69
  import {
70
70
  AGENT_MEMORY_VALUE_MAX_BYTES,
71
71
  AGENT_PROVIDER_IDS,
@@ -108,6 +108,11 @@ import {
108
108
  emptyCitationVisibility,
109
109
  extractAnswerMentions,
110
110
  findDuplicateLocationLabels,
111
+ formatDate,
112
+ formatDateRange,
113
+ formatIsoDate,
114
+ formatNumber,
115
+ formatRatio,
111
116
  getProviderLocationHandling,
112
117
  hasLocationLabel,
113
118
  internalError,
@@ -146,7 +151,7 @@ import {
146
151
  visibilityStateFromAnswerMentioned,
147
152
  windowCutoff,
148
153
  wordpressEnvSchema
149
- } from "./chunk-YDGT5CAY.js";
154
+ } from "./chunk-6QTH5NS5.js";
150
155
 
151
156
  // src/telemetry.ts
152
157
  import crypto from "crypto";
@@ -2584,16 +2589,6 @@ var COLORS = {
2584
2589
  function escapeHtml(value) {
2585
2590
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2586
2591
  }
2587
- function formatRatio(value) {
2588
- if (!Number.isFinite(value) || value === 0) return "0%";
2589
- return `${(value * 100).toFixed(1)}%`;
2590
- }
2591
- function formatNumber(value) {
2592
- if (!Number.isFinite(value)) return "\u2014";
2593
- if (Math.abs(value) >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
2594
- if (Math.abs(value) >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
2595
- return value.toLocaleString("en-US");
2596
- }
2597
2592
  function summarizeQueryParams(params) {
2598
2593
  const keys = Array.from(params.keys());
2599
2594
  const total = keys.length;
@@ -2636,23 +2631,6 @@ function formatLandingPageHtml(raw) {
2636
2631
  if (!summary) return pathHtml;
2637
2632
  return `${pathHtml}<span class="page-query" title="${escapeHtml(value)}">${escapeHtml(summary)}</span>`;
2638
2633
  }
2639
- function formatDate(iso) {
2640
- if (!iso) return "\u2014";
2641
- try {
2642
- const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
2643
- const options = { month: "short", day: "numeric", year: "numeric" };
2644
- const d = dateOnly && dateOnly[1] && dateOnly[2] && dateOnly[3] ? new Date(Date.UTC(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]))) : new Date(iso);
2645
- if (Number.isNaN(d.getTime())) return iso;
2646
- return d.toLocaleDateString("en-US", dateOnly ? { ...options, timeZone: "UTC" } : options);
2647
- } catch {
2648
- return iso;
2649
- }
2650
- }
2651
- function formatDateRange(start, end) {
2652
- if (!start && !end) return "";
2653
- if (start && end) return `${formatDate(start)} \u2192 ${formatDate(end)}`;
2654
- return formatDate(start || end);
2655
- }
2656
2634
  function gscDateRange(report) {
2657
2635
  const summary = report.executiveSummary.gsc;
2658
2636
  const gsc = report.gsc;
@@ -3461,8 +3439,32 @@ table.report-table td .badge {
3461
3439
  .client-bar-row { grid-template-columns: 100px 1fr 100px; gap: 10px; }
3462
3440
  }
3463
3441
  @media print {
3464
- body { background: white; color: black; }
3465
- section.report-section { break-inside: avoid; }
3442
+ @page { margin: 0.5in; }
3443
+ html, body {
3444
+ background: ${COLORS.bg};
3445
+ color: ${COLORS.text};
3446
+ -webkit-print-color-adjust: exact;
3447
+ print-color-adjust: exact;
3448
+ }
3449
+ .container { max-width: none; padding: 0; }
3450
+ section.report-section,
3451
+ .executive-hero,
3452
+ .headline-card,
3453
+ .hero-proof,
3454
+ .client-hero,
3455
+ .client-metric-tile,
3456
+ .client-card,
3457
+ .client-note,
3458
+ .chart-card,
3459
+ .action-card,
3460
+ .insight-card,
3461
+ .source-bar-row,
3462
+ .client-bar-row,
3463
+ tr,
3464
+ table { break-inside: avoid; }
3465
+ h1, h2, h3, .eyebrow { break-after: avoid; }
3466
+ .footer { margin-top: 32px; }
3467
+ .footer a { color: ${COLORS.text}; }
3466
3468
  }
3467
3469
  `;
3468
3470
  function section(opts, body) {
@@ -4634,7 +4636,7 @@ function renderReportHtml(report, opts = {}) {
4634
4636
  <div class="subtitle">${escapeHtml(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml(report.meta.project.country)} / ${escapeHtml(report.meta.project.language.toUpperCase())}${renderHeaderLocationFragment(report.meta.location)} \xB7 Generated ${formatDate(report.meta.generatedAt)}</div>
4635
4637
  </header>
4636
4638
  ${sections}
4637
- <footer class="footer">Generated by canonry \xB7 ${escapeHtml(report.meta.generatedAt)}</footer>
4639
+ <footer class="footer">Generated by <a href="https://canonry.ai">canonry</a> \xB7 ${escapeHtml(formatIsoDate(report.meta.generatedAt))}</footer>
4638
4640
  </div>
4639
4641
  <script type="application/json" id="canonry-report-data">${json}</script>
4640
4642
  </body>
@@ -11025,6 +11027,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
11025
11027
  { name: "date" },
11026
11028
  { name: sourceDim },
11027
11029
  { name: mediumDim },
11030
+ { name: "sessionDefaultChannelGroup" },
11028
11031
  { name: "landingPagePlusQueryString" }
11029
11032
  ],
11030
11033
  metrics: [
@@ -11050,7 +11053,8 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
11050
11053
  date: row.dimensionValues[0].value,
11051
11054
  source: row.dimensionValues[1].value,
11052
11055
  medium: row.dimensionValues[2].value,
11053
- landingPage: row.dimensionValues[3]?.value ?? "(not set)",
11056
+ channelGroup: row.dimensionValues[3]?.value ?? "(not set)",
11057
+ landingPage: row.dimensionValues[4]?.value ?? "(not set)",
11054
11058
  sessions: parseInt(row.metricValues[0].value, 10) || 0,
11055
11059
  users: parseInt(row.metricValues[1].value, 10) || 0,
11056
11060
  sourceDimension: dimLabel
@@ -11063,7 +11067,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
11063
11067
  }
11064
11068
  const deduped = /* @__PURE__ */ new Map();
11065
11069
  for (const row of rows) {
11066
- const key = `${row.date}::${row.source}::${row.medium}::${row.sourceDimension}::${row.landingPage}`;
11070
+ const key = `${row.date}::${row.source}::${row.medium}::${row.sourceDimension}::${row.channelGroup}::${row.landingPage}`;
11067
11071
  const existing = deduped.get(key);
11068
11072
  if (!existing) {
11069
11073
  deduped.set(key, row);
@@ -12657,6 +12661,37 @@ function formatSharePct(numerator, total) {
12657
12661
  if (rounded === 0) return "<1%";
12658
12662
  return `${rounded}%`;
12659
12663
  }
12664
+ var SOCIAL_CHANNEL_GROUPS2 = /* @__PURE__ */ new Set(["Organic Social", "Paid Social"]);
12665
+ function buildChannelBreakdown(input) {
12666
+ const aiSessions = [...input.aiSessionsByChannelGroup.values()].reduce((sum, sessions) => sum + sessions, 0);
12667
+ const aiOrganicOverlap = Math.min(input.organicSessions, input.aiSessionsByChannelGroup.get("Organic Search") ?? 0);
12668
+ const aiSocialOverlap = Math.min(
12669
+ input.socialSessions,
12670
+ [...input.aiSessionsByChannelGroup.entries()].filter(([channelGroup]) => SOCIAL_CHANNEL_GROUPS2.has(channelGroup)).reduce((sum, [, sessions]) => sum + sessions, 0)
12671
+ );
12672
+ const aiDirectOverlap = Math.min(input.directSessions, input.aiSessionsByChannelGroup.get("Direct") ?? 0);
12673
+ const organicSessions = Math.max(0, input.organicSessions - aiOrganicOverlap);
12674
+ const socialSessions = Math.max(0, input.socialSessions - aiSocialOverlap);
12675
+ const directSessions = Math.max(0, input.directSessions - aiDirectOverlap);
12676
+ const coveredSessions = organicSessions + socialSessions + directSessions + aiSessions;
12677
+ const otherSessions = Math.max(0, input.totalSessions - coveredSessions);
12678
+ const bucket = (sessions) => ({
12679
+ sessions,
12680
+ sharePct: input.totalSessions > 0 ? Math.round(sessions / input.totalSessions * 100) : 0,
12681
+ sharePctDisplay: formatSharePct(sessions, input.totalSessions)
12682
+ });
12683
+ return {
12684
+ organic: bucket(organicSessions),
12685
+ social: bucket(socialSessions),
12686
+ direct: bucket(directSessions),
12687
+ ai: bucket(aiSessions),
12688
+ other: {
12689
+ sessions: otherSessions,
12690
+ sharePct: input.totalSessions > 0 ? Math.round(otherSessions / input.totalSessions * 100) : 0,
12691
+ sharePctDisplay: input.totalSessions <= 0 && coveredSessions > 0 ? "\u2014" : formatSharePct(otherSessions, input.totalSessions)
12692
+ }
12693
+ };
12694
+ }
12660
12695
  function pickWinningDimension(rows, tupleKey) {
12661
12696
  const winners = /* @__PURE__ */ new Map();
12662
12697
  for (const row of rows) {
@@ -12945,6 +12980,7 @@ async function ga4Routes(app, opts) {
12945
12980
  source: row.source,
12946
12981
  medium: row.medium,
12947
12982
  sourceDimension: row.sourceDimension,
12983
+ channelGroup: row.channelGroup,
12948
12984
  landingPage: row.landingPage,
12949
12985
  landingPageNormalized: normalizeUrlPath(row.landingPage),
12950
12986
  sessions: row.sessions,
@@ -13136,10 +13172,18 @@ async function ga4Routes(app, opts) {
13136
13172
  GROUP BY date, source, medium
13137
13173
  )`
13138
13174
  ).get();
13139
- const aiBySession = app.db.select({
13175
+ const aiBySessionRows = app.db.select({
13176
+ channelGroup: gaAiReferrals.channelGroup,
13140
13177
  sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
13141
13178
  users: sql5`COALESCE(SUM(${gaAiReferrals.users}), 0)`
13142
- }).from(gaAiReferrals).where(and9(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).get();
13179
+ }).from(gaAiReferrals).where(and9(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
13180
+ const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
13181
+ let aiBySessionUsers = 0;
13182
+ for (const row of aiBySessionRows) {
13183
+ aiSessionsByChannelGroup.set(row.channelGroup, row.sessions ?? 0);
13184
+ aiBySessionUsers += row.users ?? 0;
13185
+ }
13186
+ const aiBySessionSessions = [...aiSessionsByChannelGroup.values()].reduce((sum, sessions) => sum + sessions, 0);
13143
13187
  const socialReferrals = app.db.select({
13144
13188
  source: gaSocialReferrals.source,
13145
13189
  medium: gaSocialReferrals.medium,
@@ -13154,9 +13198,18 @@ async function ga4Routes(app, opts) {
13154
13198
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
13155
13199
  const total = summaryRow?.totalSessions ?? 0;
13156
13200
  const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
13201
+ const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
13202
+ const socialSessions = socialTotals?.sessions ?? 0;
13203
+ const channelBreakdown = buildChannelBreakdown({
13204
+ totalSessions: total,
13205
+ organicSessions: totalOrganicSessions,
13206
+ socialSessions,
13207
+ directSessions: totalDirectSessions,
13208
+ aiSessionsByChannelGroup
13209
+ });
13157
13210
  return {
13158
13211
  totalSessions: total,
13159
- totalOrganicSessions: summaryRow?.totalOrganicSessions ?? 0,
13212
+ totalOrganicSessions,
13160
13213
  totalDirectSessions,
13161
13214
  totalUsers: summaryRow?.totalUsers ?? 0,
13162
13215
  topPages: rows.map((r) => ({
@@ -13183,8 +13236,8 @@ async function ga4Routes(app, opts) {
13183
13236
  })),
13184
13237
  aiSessionsDeduped: aiDeduped?.sessions ?? 0,
13185
13238
  aiUsersDeduped: aiDeduped?.users ?? 0,
13186
- aiSessionsBySession: aiBySession?.sessions ?? 0,
13187
- aiUsersBySession: aiBySession?.users ?? 0,
13239
+ aiSessionsBySession: aiBySessionSessions,
13240
+ aiUsersBySession: aiBySessionUsers,
13188
13241
  socialReferrals: socialReferrals.map((r) => ({
13189
13242
  source: r.source,
13190
13243
  medium: r.medium,
@@ -13192,18 +13245,22 @@ async function ga4Routes(app, opts) {
13192
13245
  sessions: r.sessions ?? 0,
13193
13246
  users: r.users ?? 0
13194
13247
  })),
13195
- socialSessions: socialTotals?.sessions ?? 0,
13248
+ socialSessions,
13196
13249
  socialUsers: socialTotals?.users ?? 0,
13197
- organicSharePct: total > 0 ? Math.round((summaryRow?.totalOrganicSessions ?? 0) / total * 100) : 0,
13250
+ channelBreakdown,
13251
+ organicSharePct: total > 0 ? Math.round(totalOrganicSessions / total * 100) : 0,
13198
13252
  aiSharePct: total > 0 ? Math.round((aiDeduped?.sessions ?? 0) / total * 100) : 0,
13199
- aiSharePctBySession: total > 0 ? Math.round((aiBySession?.sessions ?? 0) / total * 100) : 0,
13253
+ aiSharePctBySession: total > 0 ? Math.round(aiBySessionSessions / total * 100) : 0,
13200
13254
  directSharePct: total > 0 ? Math.round(totalDirectSessions / total * 100) : 0,
13201
- socialSharePct: total > 0 ? Math.round((socialTotals?.sessions ?? 0) / total * 100) : 0,
13202
- organicSharePctDisplay: formatSharePct(summaryRow?.totalOrganicSessions ?? 0, total),
13255
+ socialSharePct: total > 0 ? Math.round(socialSessions / total * 100) : 0,
13256
+ otherSessions: channelBreakdown.other.sessions,
13257
+ otherSharePct: channelBreakdown.other.sharePct,
13258
+ otherSharePctDisplay: channelBreakdown.other.sharePctDisplay,
13259
+ organicSharePctDisplay: formatSharePct(totalOrganicSessions, total),
13203
13260
  aiSharePctDisplay: formatSharePct(aiDeduped?.sessions ?? 0, total),
13204
- aiSharePctBySessionDisplay: formatSharePct(aiBySession?.sessions ?? 0, total),
13261
+ aiSharePctBySessionDisplay: formatSharePct(aiBySessionSessions, total),
13205
13262
  directSharePctDisplay: formatSharePct(totalDirectSessions, total),
13206
- socialSharePctDisplay: formatSharePct(socialTotals?.sessions ?? 0, total),
13263
+ socialSharePctDisplay: formatSharePct(socialSessions, total),
13207
13264
  lastSyncedAt: latestSync?.syncedAt ?? null,
13208
13265
  periodStart: (() => {
13209
13266
  const start = cutoffDate ?? summaryMeta?.periodStart ?? null;
package/dist/cli.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  setGoogleAuthConfig,
19
19
  showFirstRunNotice,
20
20
  trackEvent
21
- } from "./chunk-RIGQFQJJ.js";
21
+ } from "./chunk-FRDVC2XF.js";
22
22
  import {
23
23
  CliError,
24
24
  EXIT_SYSTEM_ERROR,
@@ -33,7 +33,7 @@ import {
33
33
  saveConfig,
34
34
  saveConfigPatch,
35
35
  usageError
36
- } from "./chunk-LNRDWAG3.js";
36
+ } from "./chunk-5NYG5EC7.js";
37
37
  import {
38
38
  apiKeys,
39
39
  competitors,
@@ -45,7 +45,7 @@ import {
45
45
  projects,
46
46
  querySnapshots,
47
47
  runs
48
- } from "./chunk-DCE3B6KD.js";
48
+ } from "./chunk-7HBZCGRL.js";
49
49
  import {
50
50
  CcReleaseSyncStatuses,
51
51
  CheckScopes,
@@ -64,7 +64,7 @@ import {
64
64
  providerQuotaPolicySchema,
65
65
  resolveProviderInput,
66
66
  skillsClientSchema
67
- } from "./chunk-YDGT5CAY.js";
67
+ } from "./chunk-6QTH5NS5.js";
68
68
 
69
69
  // src/cli.ts
70
70
  import { pathToFileURL } from "url";
@@ -580,7 +580,7 @@ function readStoredGroundingSources(rawResponse) {
580
580
  return result;
581
581
  }
582
582
  async function backfillInsightsCommand(project, opts) {
583
- const { IntelligenceService } = await import("./intelligence-service-NT24OLLA.js");
583
+ const { IntelligenceService } = await import("./intelligence-service-BCKXIKIL.js");
584
584
  const config = loadConfig();
585
585
  const db = createClient(config.database);
586
586
  migrate(db);
@@ -2378,6 +2378,10 @@ async function gaAttribution(project, opts) {
2378
2378
  aiSharePctBySessionDisplay: traffic.aiSharePctBySessionDisplay,
2379
2379
  socialSharePctDisplay: traffic.socialSharePctDisplay,
2380
2380
  directSharePctDisplay: traffic.directSharePctDisplay,
2381
+ otherSessions: traffic.otherSessions,
2382
+ otherSharePct: traffic.otherSharePct,
2383
+ otherSharePctDisplay: traffic.otherSharePctDisplay,
2384
+ channelBreakdown: traffic.channelBreakdown,
2381
2385
  aiReferrals: traffic.aiReferrals,
2382
2386
  aiReferralLandingPages: traffic.aiReferralLandingPages,
2383
2387
  socialReferrals: traffic.socialReferrals,
@@ -2395,15 +2399,11 @@ async function gaAttribution(project, opts) {
2395
2399
  console.log(` Total Users: ${traffic.totalUsers}`);
2396
2400
  console.log();
2397
2401
  console.log(" CHANNEL BREAKDOWN 7d trend 30d trend");
2398
- console.log(` Organic Search: ${String(traffic.totalOrganicSessions).padEnd(6)} (${traffic.organicSharePctDisplay.padStart(4)}) ${fmtTrend(trend.organic.trend7dPct).padEnd(12)} ${fmtTrend(trend.organic.trend30dPct)}`);
2399
- console.log(` Social: ${String(traffic.socialSessions).padEnd(6)} (${traffic.socialSharePctDisplay.padStart(4)}) ${fmtTrend(trend.social.trend7dPct).padEnd(12)} ${fmtTrend(trend.social.trend30dPct)}`);
2400
- console.log(` Direct: ${String(traffic.totalDirectSessions).padEnd(6)} (${traffic.directSharePctDisplay.padStart(4)}) ${fmtTrend(trend.direct.trend7dPct).padEnd(12)} ${fmtTrend(trend.direct.trend30dPct)}`);
2401
- console.log(` AI Referrals: ${String(traffic.aiSessionsBySession).padEnd(6)} (${traffic.aiSharePctBySessionDisplay.padStart(4)}) ${fmtTrend(trend.ai.trend7dPct).padEnd(12)} ${fmtTrend(trend.ai.trend30dPct)} (lower bound \u2014 sessionSource only; referrer-stripped traffic falls under Direct)`);
2402
- const otherSessions2 = traffic.totalSessions - traffic.totalOrganicSessions - traffic.aiSessionsBySession - traffic.socialSessions - traffic.totalDirectSessions;
2403
- if (otherSessions2 > 0) {
2404
- const otherPct = traffic.totalSessions > 0 ? Math.round(otherSessions2 / traffic.totalSessions * 100) : 0;
2405
- console.log(` Other: ${String(otherSessions2).padEnd(6)} (${String(otherPct).padStart(2)}%)`);
2406
- }
2402
+ console.log(` Organic Search: ${String(traffic.channelBreakdown.organic.sessions).padEnd(6)} (${traffic.channelBreakdown.organic.sharePctDisplay.padStart(4)}) ${fmtTrend(trend.organic.trend7dPct).padEnd(12)} ${fmtTrend(trend.organic.trend30dPct)}`);
2403
+ console.log(` Social: ${String(traffic.channelBreakdown.social.sessions).padEnd(6)} (${traffic.channelBreakdown.social.sharePctDisplay.padStart(4)}) ${fmtTrend(trend.social.trend7dPct).padEnd(12)} ${fmtTrend(trend.social.trend30dPct)}`);
2404
+ console.log(` Direct: ${String(traffic.channelBreakdown.direct.sessions).padEnd(6)} (${traffic.channelBreakdown.direct.sharePctDisplay.padStart(4)}) ${fmtTrend(trend.direct.trend7dPct).padEnd(12)} ${fmtTrend(trend.direct.trend30dPct)}`);
2405
+ console.log(` AI Referrals: ${String(traffic.channelBreakdown.ai.sessions).padEnd(6)} (${traffic.channelBreakdown.ai.sharePctDisplay.padStart(4)}) ${fmtTrend(trend.ai.trend7dPct).padEnd(12)} ${fmtTrend(trend.ai.trend30dPct)} (lower bound \u2014 sessionSource only; referrer-stripped traffic falls under Direct)`);
2406
+ console.log(` Other: ${String(traffic.channelBreakdown.other.sessions).padEnd(6)} (${traffic.channelBreakdown.other.sharePctDisplay.padStart(4)})`);
2407
2407
  console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
2408
2408
  console.log(` Total: ${String(traffic.totalSessions).padEnd(6)} ${fmtTrend(trend.total.trend7dPct).padEnd(12)} ${fmtTrend(trend.total.trend30dPct)}`);
2409
2409
  if (trend.aiBiggestMover) {
@@ -2446,6 +2446,10 @@ async function gaAttribution(project, opts) {
2446
2446
  aiSharePctBySessionDisplay: traffic.aiSharePctBySessionDisplay,
2447
2447
  socialSharePctDisplay: traffic.socialSharePctDisplay,
2448
2448
  directSharePctDisplay: traffic.directSharePctDisplay,
2449
+ otherSessions: traffic.otherSessions,
2450
+ otherSharePct: traffic.otherSharePct,
2451
+ otherSharePctDisplay: traffic.otherSharePctDisplay,
2452
+ channelBreakdown: traffic.channelBreakdown,
2449
2453
  aiReferrals: traffic.aiReferrals,
2450
2454
  aiReferralLandingPages: traffic.aiReferralLandingPages,
2451
2455
  socialReferrals: traffic.socialReferrals,
@@ -2464,15 +2468,11 @@ async function gaAttribution(project, opts) {
2464
2468
  console.log(` Total Users: ${traffic.totalUsers}`);
2465
2469
  console.log();
2466
2470
  console.log(" CHANNEL BREAKDOWN");
2467
- console.log(` Organic Search: ${traffic.totalOrganicSessions} sessions (${traffic.organicSharePctDisplay})`);
2468
- console.log(` Social: ${traffic.socialSessions} sessions (${traffic.socialSharePctDisplay})`);
2469
- console.log(` Direct: ${traffic.totalDirectSessions} sessions (${traffic.directSharePctDisplay})`);
2470
- console.log(` AI Referrals: ${traffic.aiSessionsBySession} sessions (${traffic.aiSharePctBySessionDisplay}) (lower bound \u2014 sessionSource only; referrer-stripped traffic falls under Direct)`);
2471
- const otherSessions = traffic.totalSessions - traffic.totalOrganicSessions - traffic.aiSessionsBySession - traffic.socialSessions - traffic.totalDirectSessions;
2472
- if (otherSessions > 0) {
2473
- const otherPct = traffic.totalSessions > 0 ? Math.round(otherSessions / traffic.totalSessions * 100) : 0;
2474
- console.log(` Other: ${otherSessions} sessions (${otherPct}%)`);
2475
- }
2471
+ console.log(` Organic Search: ${traffic.channelBreakdown.organic.sessions} sessions (${traffic.channelBreakdown.organic.sharePctDisplay})`);
2472
+ console.log(` Social: ${traffic.channelBreakdown.social.sessions} sessions (${traffic.channelBreakdown.social.sharePctDisplay})`);
2473
+ console.log(` Direct: ${traffic.channelBreakdown.direct.sessions} sessions (${traffic.channelBreakdown.direct.sharePctDisplay})`);
2474
+ console.log(` AI Referrals: ${traffic.channelBreakdown.ai.sessions} sessions (${traffic.channelBreakdown.ai.sharePctDisplay}) (lower bound \u2014 sessionSource only; referrer-stripped traffic falls under Direct)`);
2475
+ console.log(` Other: ${traffic.channelBreakdown.other.sessions} sessions (${traffic.channelBreakdown.other.sharePctDisplay})`);
2476
2476
  if (traffic.aiReferrals.length > 0) {
2477
2477
  console.log();
2478
2478
  console.log(" AI SOURCES");
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-RIGQFQJJ.js";
3
+ } from "./chunk-FRDVC2XF.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-LNRDWAG3.js";
7
- import "./chunk-DCE3B6KD.js";
8
- import "./chunk-YDGT5CAY.js";
6
+ } from "./chunk-5NYG5EC7.js";
7
+ import "./chunk-7HBZCGRL.js";
8
+ import "./chunk-6QTH5NS5.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-DCE3B6KD.js";
4
- import "./chunk-YDGT5CAY.js";
3
+ } from "./chunk-7HBZCGRL.js";
4
+ import "./chunk-6QTH5NS5.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  CliError,
3
3
  canonryMcpTools,
4
4
  createApiClient
5
- } from "./chunk-LNRDWAG3.js";
6
- import "./chunk-YDGT5CAY.js";
5
+ } from "./chunk-5NYG5EC7.js";
6
+ import "./chunk-6QTH5NS5.js";
7
7
 
8
8
  // src/mcp/cli.ts
9
9
  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.13.1",
3
+ "version": "4.13.3",
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",
@@ -59,22 +59,22 @@
59
59
  "@types/node-cron": "^3.0.11",
60
60
  "tsup": "^8.5.1",
61
61
  "tsx": "^4.19.0",
62
- "@ainyc/canonry-api-routes": "0.0.0",
63
62
  "@ainyc/canonry-contracts": "0.0.0",
63
+ "@ainyc/canonry-api-routes": "0.0.0",
64
64
  "@ainyc/canonry-db": "0.0.0",
65
+ "@ainyc/canonry-intelligence": "0.0.0",
65
66
  "@ainyc/canonry-config": "0.0.0",
66
67
  "@ainyc/canonry-integration-bing": "0.0.0",
67
- "@ainyc/canonry-intelligence": "0.0.0",
68
68
  "@ainyc/canonry-integration-cloud-run": "0.0.0",
69
69
  "@ainyc/canonry-integration-google": "0.0.0",
70
+ "@ainyc/canonry-integration-commoncrawl": "0.0.0",
70
71
  "@ainyc/canonry-integration-traffic": "0.0.0",
71
- "@ainyc/canonry-provider-cdp": "0.0.0",
72
72
  "@ainyc/canonry-integration-wordpress": "0.0.0",
73
- "@ainyc/canonry-provider-claude": "0.0.0",
74
73
  "@ainyc/canonry-provider-gemini": "0.0.0",
74
+ "@ainyc/canonry-provider-cdp": "0.0.0",
75
75
  "@ainyc/canonry-provider-local": "0.0.0",
76
- "@ainyc/canonry-integration-commoncrawl": "0.0.0",
77
76
  "@ainyc/canonry-provider-openai": "0.0.0",
77
+ "@ainyc/canonry-provider-claude": "0.0.0",
78
78
  "@ainyc/canonry-provider-perplexity": "0.0.0"
79
79
  },
80
80
  "scripts": {