@canonry/canonry 4.134.0 → 4.135.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 (27) hide show
  1. package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +3 -2
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +34 -6
  3. package/assets/assets/{AuditHistoryPanel-DBUWbfNk.js → AuditHistoryPanel-BS0dmlrQ.js} +1 -1
  4. package/assets/assets/{BacklinksPage-BFm0VdUO.js → BacklinksPage-CPpWLVfa.js} +1 -1
  5. package/assets/assets/{ChartPrimitives-oV_nduTH.js → ChartPrimitives-Dv--2i87.js} +1 -1
  6. package/assets/assets/{HistoryPage-IF2FMem3.js → HistoryPage-BO4Fv-9q.js} +1 -1
  7. package/assets/assets/ProjectPage-Di_nDqtH.js +8 -0
  8. package/assets/assets/{RunRow-De5zdch5.js → RunRow-BE8zdGJ8.js} +1 -1
  9. package/assets/assets/{RunsPage-xtRF78r8.js → RunsPage-CbGanhaz.js} +1 -1
  10. package/assets/assets/{SettingsPage-B_GAxaWu.js → SettingsPage-odOpsOuf.js} +1 -1
  11. package/assets/assets/{TrafficPage-E8I5ineJ.js → TrafficPage-DK7oc0DK.js} +1 -1
  12. package/assets/assets/{TrafficSourceDetailPage-DGk2MqSp.js → TrafficSourceDetailPage-CEFjQVuF.js} +1 -1
  13. package/assets/assets/{arrow-left-D-4QMxi9.js → arrow-left-DgzLEzZB.js} +1 -1
  14. package/assets/assets/{extract-error-message-DuqIFU8Z.js → extract-error-message-vlMjpZb2.js} +1 -1
  15. package/assets/assets/{index-De1MKbc7.js → index-BMlTgq7D.js} +72 -72
  16. package/assets/assets/{trash-2-BVh__r6G.js → trash-2-DX33Ys-K.js} +1 -1
  17. package/assets/index.html +1 -1
  18. package/dist/{chunk-TKXWTDV6.js → chunk-3PFRVEWP.js} +99 -31
  19. package/dist/{chunk-ZUQPHGEN.js → chunk-PAR6P5H6.js} +35 -3
  20. package/dist/{chunk-PC7AYXND.js → chunk-PXEEWJRV.js} +74 -20
  21. package/dist/{chunk-LIXWDIDR.js → chunk-TPVMHYHD.js} +188 -97
  22. package/dist/cli.js +63 -26
  23. package/dist/index.js +4 -4
  24. package/dist/{intelligence-service-4XOFUN5E.js → intelligence-service-BMEJ5GFZ.js} +2 -2
  25. package/dist/mcp.js +2 -2
  26. package/package.json +9 -9
  27. package/assets/assets/ProjectPage-ClwxX9Ga.js +0 -8
@@ -209,6 +209,7 @@ import {
209
209
  formatNumber,
210
210
  formatRatio,
211
211
  formatWindowCountDelta,
212
+ ga4AiReferralDailyDtoSchema,
212
213
  ga4AiReferralHistoryEntrySchema,
213
214
  ga4SessionHistoryEntrySchema,
214
215
  ga4SocialReferralHistoryEntrySchema,
@@ -364,7 +365,7 @@ import {
364
365
  wordpressSchemaDeployResultDtoSchema,
365
366
  wordpressSchemaStatusResultDtoSchema,
366
367
  wordpressStatusDtoSchema
367
- } from "./chunk-PC7AYXND.js";
368
+ } from "./chunk-PXEEWJRV.js";
368
369
 
369
370
  // src/intelligence-service.ts
370
371
  import { eq as eq44, desc as desc21, asc as asc9, and as and35, ne as ne6, or as or10, inArray as inArray15, gte as gte12, lte as lte8 } from "drizzle-orm";
@@ -17462,6 +17463,7 @@ var SCHEMA_TABLE = {
17462
17463
  ResearchRunDetailDto: researchRunDetailSchema,
17463
17464
  ResearchRunListDto: researchRunListSchema,
17464
17465
  DoctorReportDto: doctorReportSchema,
17466
+ GA4AiReferralDailyDto: ga4AiReferralDailyDtoSchema,
17465
17467
  GA4AiReferralHistoryEntry: ga4AiReferralHistoryEntrySchema,
17466
17468
  GA4MeasurementAnalysisDto: gaMeasurementAnalysisDtoSchema,
17467
17469
  GA4SessionHistoryEntry: ga4SessionHistoryEntrySchema,
@@ -21205,7 +21207,8 @@ var routeCatalog = [
21205
21207
  {
21206
21208
  method: "get",
21207
21209
  path: "/api/v1/projects/{name}/ga/ai-referral-history",
21208
- summary: "Get AI referral sessions per day grouped by source and landing page",
21210
+ summary: "Get raw AI referral detail rows per day, landing page, and attribution dimension",
21211
+ description: "Detail rows, not totals. One row per landing page per attribution dimension, so a single day of one source is many rows and each is commonly worth one session. Use /ga/ai-referral-daily for per-date or per-source session counts.",
21209
21212
  tags: ["ga4"],
21210
21213
  parameters: [nameParameter, analyticsWindowParameter],
21211
21214
  responses: {
@@ -21214,6 +21217,19 @@ var routeCatalog = [
21214
21217
  404: errorResponse("Project not found.")
21215
21218
  }
21216
21219
  },
21220
+ {
21221
+ method: "get",
21222
+ path: "/api/v1/projects/{name}/ga/ai-referral-daily",
21223
+ summary: "Get AI referral sessions per day and per source",
21224
+ description: "Sums landing pages within one attribution dimension and never across dimensions, so totalSessions equals the aiSessionsDeduped reported by /ga/traffic for the same window. Sessions only: GA counts users as a distinct count at the grain requested, so an AI-referral user total cannot be summed from these rows and no un-dimensioned AI-referral fetch exists to supply one.",
21225
+ tags: ["ga4"],
21226
+ parameters: [nameParameter, analyticsWindowParameter],
21227
+ responses: {
21228
+ 200: jsonResponse("AI referral daily series returned.", "GA4AiReferralDailyDto"),
21229
+ 400: errorResponse("GA4 is not connected."),
21230
+ 404: errorResponse("Project not found.")
21231
+ }
21232
+ },
21217
21233
  {
21218
21234
  method: "get",
21219
21235
  path: "/api/v1/projects/{name}/ga/social-referral-history",
@@ -28709,11 +28725,17 @@ function storedSnapshotProvenance(input) {
28709
28725
  sourceSync
28710
28726
  };
28711
28727
  }
28712
- function historicalCampaignRollups(rows) {
28728
+ function adsAccountToday(timezone, nowIso) {
28729
+ return formatIsoDateInTimeZone(nowIso, timezone ?? "UTC");
28730
+ }
28731
+ function inProgressWindowDate(newestDate, accountToday) {
28732
+ return newestDate !== null && newestDate === accountToday ? accountToday : null;
28733
+ }
28734
+ function historicalCampaignRollups(rows, accountToday) {
28713
28735
  if (rows.length === 0) {
28714
28736
  return {
28715
28737
  status: AdsHistoricalCampaignRollupStatuses.unavailable,
28716
- window: { from: null, to: null },
28738
+ window: { from: null, to: null, inProgressDate: null },
28717
28739
  totals: null
28718
28740
  };
28719
28741
  }
@@ -28733,7 +28755,7 @@ function historicalCampaignRollups(rows) {
28733
28755
  }
28734
28756
  return {
28735
28757
  status: AdsHistoricalCampaignRollupStatuses.reported,
28736
- window: { from: fromDate, to: toDate },
28758
+ window: { from: fromDate, to: toDate, inProgressDate: inProgressWindowDate(toDate, accountToday) },
28737
28759
  totals: {
28738
28760
  impressions,
28739
28761
  clicks,
@@ -30490,6 +30512,8 @@ async function adsRoutes(app, opts) {
30490
30512
  if (from) conditions.push(gte8(adsInsightsDaily.date, from));
30491
30513
  if (to) conditions.push(lte6(adsInsightsDaily.date, to));
30492
30514
  const rows = app.db.select().from(adsInsightsDaily).where(and22(...conditions)).orderBy(asc6(adsInsightsDaily.date)).all();
30515
+ const conn = app.db.select().from(adsConnections).where(eq28(adsConnections.projectId, project.id)).get();
30516
+ const accountToday = adsAccountToday(conn?.timezone, (/* @__PURE__ */ new Date()).toISOString());
30493
30517
  const dtoRows = rows.map((row) => ({
30494
30518
  level: row.level,
30495
30519
  entityId: row.entityId,
@@ -30499,9 +30523,9 @@ async function adsRoutes(app, opts) {
30499
30523
  spendMicros: row.spendMicros,
30500
30524
  conversions: row.conversions,
30501
30525
  ctr: adsCtr(row.clicks, row.impressions),
30502
- cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
30526
+ cpcMicros: adsCpcMicros(row.spendMicros, row.clicks),
30527
+ inProgress: row.date === accountToday
30503
30528
  }));
30504
- const conn = app.db.select().from(adsConnections).where(eq28(adsConnections.projectId, project.id)).get();
30505
30529
  const response = { rows: dtoRows, currencyCode: conn?.currencyCode ?? null };
30506
30530
  return response;
30507
30531
  });
@@ -30531,7 +30555,7 @@ async function adsRoutes(app, opts) {
30531
30555
  sourceSync,
30532
30556
  latestAdsSync
30533
30557
  });
30534
- const rollups = historicalCampaignRollups(app.db.select({
30558
+ const rollupRows = app.db.select({
30535
30559
  date: adsInsightsDaily.date,
30536
30560
  impressions: adsInsightsDaily.impressions,
30537
30561
  clicks: adsInsightsDaily.clicks,
@@ -30540,7 +30564,11 @@ async function adsRoutes(app, opts) {
30540
30564
  }).from(adsInsightsDaily).where(and22(
30541
30565
  eq28(adsInsightsDaily.projectId, project.id),
30542
30566
  eq28(adsInsightsDaily.level, AdsInsightLevels.campaign)
30543
- )).all());
30567
+ )).all();
30568
+ const rollups = historicalCampaignRollups(
30569
+ rollupRows,
30570
+ adsAccountToday(connection?.timezone, (/* @__PURE__ */ new Date()).toISOString())
30571
+ );
30544
30572
  const adsByGroup = /* @__PURE__ */ new Map();
30545
30573
  for (const ad of adRows) {
30546
30574
  const list = adsByGroup.get(ad.adGroupId) ?? [];
@@ -30846,7 +30874,9 @@ async function adsRoutes(app, opts) {
30846
30874
  maxReaderCalls: ADS_LIVE_MAX_READER_CALLS,
30847
30875
  readerCalls,
30848
30876
  maxPagesPerReaderCall: liveDeliveryMaxPagesPerReaderCall,
30849
- maxUpstreamHttpRequests: ADS_LIVE_MAX_READER_CALLS * liveDeliveryMaxPagesPerReaderCall,
30877
+ // The +1 is the single-page in-progress-day request an insight
30878
+ // reader call makes alongside its paginating ranged walk.
30879
+ maxUpstreamHttpRequests: ADS_LIVE_MAX_READER_CALLS * (liveDeliveryMaxPagesPerReaderCall + 1),
30850
30880
  truncated
30851
30881
  },
30852
30882
  entities,
@@ -30890,7 +30920,14 @@ async function adsRoutes(app, opts) {
30890
30920
  campaignCount,
30891
30921
  adGroupCount,
30892
30922
  adCount,
30893
- window: { from: fromDate, to: toDate },
30923
+ window: {
30924
+ from: fromDate,
30925
+ to: toDate,
30926
+ inProgressDate: inProgressWindowDate(
30927
+ toDate,
30928
+ adsAccountToday(row?.timezone, (/* @__PURE__ */ new Date()).toISOString())
30929
+ )
30930
+ },
30894
30931
  totals: {
30895
30932
  impressions,
30896
30933
  clicks,
@@ -31802,6 +31839,118 @@ function buildSessionHistory(landingPageDays, dailyTotals) {
31802
31839
  });
31803
31840
  }
31804
31841
 
31842
+ // ../api-routes/src/ga-ai-referral-aggregation.ts
31843
+ function normalizeAiTrafficClass(value) {
31844
+ return value === AiReferralTrafficClasses.paid ? AiReferralTrafficClasses.paid : AiReferralTrafficClasses.organic;
31845
+ }
31846
+ function emptyAiCounts() {
31847
+ return { sessions: 0 };
31848
+ }
31849
+ function resolveWinningDimensions(rows) {
31850
+ const groups = /* @__PURE__ */ new Map();
31851
+ for (const row of rows) {
31852
+ const key = `${row.date}\0${row.source}\0${row.medium}`;
31853
+ let group = groups.get(key);
31854
+ if (!group) {
31855
+ group = { date: row.date, source: row.source, medium: row.medium, byDimension: /* @__PURE__ */ new Map() };
31856
+ groups.set(key, group);
31857
+ }
31858
+ let dim = group.byDimension.get(row.sourceDimension);
31859
+ if (!dim) {
31860
+ dim = { paidSessions: 0, organicSessions: 0 };
31861
+ group.byDimension.set(row.sourceDimension, dim);
31862
+ }
31863
+ const sessions = row.sessions ?? 0;
31864
+ if (normalizeAiTrafficClass(row.trafficClass) === AiReferralTrafficClasses.paid) {
31865
+ dim.paidSessions += sessions;
31866
+ } else {
31867
+ dim.organicSessions += sessions;
31868
+ }
31869
+ }
31870
+ const winners = [];
31871
+ for (const group of groups.values()) {
31872
+ const dims = [...group.byDimension.values()];
31873
+ const bestSessions = dims.reduce((best, d) => d.paidSessions + d.organicSessions > best.paidSessions + best.organicSessions ? d : best);
31874
+ winners.push({
31875
+ date: group.date,
31876
+ source: group.source,
31877
+ medium: group.medium,
31878
+ paidSessions: bestSessions.paidSessions,
31879
+ organicSessions: bestSessions.organicSessions
31880
+ });
31881
+ }
31882
+ return winners;
31883
+ }
31884
+ function summarizeAiReferralCounts(rows) {
31885
+ const paidDeduped = emptyAiCounts();
31886
+ const organicDeduped = emptyAiCounts();
31887
+ const paidBySession = emptyAiCounts();
31888
+ const organicBySession = emptyAiCounts();
31889
+ const bySessionChannelGroup = /* @__PURE__ */ new Map();
31890
+ for (const winner of resolveWinningDimensions(rows)) {
31891
+ paidDeduped.sessions += winner.paidSessions;
31892
+ organicDeduped.sessions += winner.organicSessions;
31893
+ }
31894
+ for (const row of rows) {
31895
+ if (row.sourceDimension !== "session") continue;
31896
+ const sessions = row.sessions ?? 0;
31897
+ bySessionChannelGroup.set(
31898
+ row.channelGroup,
31899
+ (bySessionChannelGroup.get(row.channelGroup) ?? 0) + sessions
31900
+ );
31901
+ const bucket = normalizeAiTrafficClass(row.trafficClass) === AiReferralTrafficClasses.paid ? paidBySession : organicBySession;
31902
+ bucket.sessions += sessions;
31903
+ }
31904
+ return {
31905
+ paidDeduped,
31906
+ organicDeduped,
31907
+ paidBySession,
31908
+ organicBySession,
31909
+ bySessionChannelGroup,
31910
+ deduped: { sessions: paidDeduped.sessions + organicDeduped.sessions },
31911
+ bySession: { sessions: paidBySession.sessions + organicBySession.sessions }
31912
+ };
31913
+ }
31914
+ function buildAiReferralDailySeries(rows) {
31915
+ const byDate = /* @__PURE__ */ new Map();
31916
+ const sourceTotals = /* @__PURE__ */ new Map();
31917
+ let totalSessions = 0;
31918
+ let totalPaidSessions = 0;
31919
+ let totalOrganicSessions = 0;
31920
+ for (const winner of resolveWinningDimensions(rows)) {
31921
+ const sessions = winner.paidSessions + winner.organicSessions;
31922
+ let bySource = byDate.get(winner.date);
31923
+ if (!bySource) {
31924
+ bySource = /* @__PURE__ */ new Map();
31925
+ byDate.set(winner.date, bySource);
31926
+ }
31927
+ let bucket = bySource.get(winner.source);
31928
+ if (!bucket) {
31929
+ bucket = { source: winner.source, sessions: 0, paidSessions: 0, organicSessions: 0 };
31930
+ bySource.set(winner.source, bucket);
31931
+ }
31932
+ bucket.sessions += sessions;
31933
+ bucket.paidSessions += winner.paidSessions;
31934
+ bucket.organicSessions += winner.organicSessions;
31935
+ sourceTotals.set(winner.source, (sourceTotals.get(winner.source) ?? 0) + sessions);
31936
+ totalSessions += sessions;
31937
+ totalPaidSessions += winner.paidSessions;
31938
+ totalOrganicSessions += winner.organicSessions;
31939
+ }
31940
+ const days = [...byDate.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, bySource]) => {
31941
+ const sources2 = [...bySource.values()].sort((a, b) => b.sessions - a.sessions || a.source.localeCompare(b.source));
31942
+ return {
31943
+ date,
31944
+ sessions: sources2.reduce((sum, s) => sum + s.sessions, 0),
31945
+ paidSessions: sources2.reduce((sum, s) => sum + s.paidSessions, 0),
31946
+ organicSessions: sources2.reduce((sum, s) => sum + s.organicSessions, 0),
31947
+ bySource: sources2
31948
+ };
31949
+ });
31950
+ const sources = [...sourceTotals.entries()].sort(([aSource, aSessions], [bSource, bSessions]) => bSessions - aSessions || aSource.localeCompare(bSource)).map(([source]) => source);
31951
+ return { days, sources, totalSessions, totalPaidSessions, totalOrganicSessions };
31952
+ }
31953
+
31805
31954
  // ../api-routes/src/ga.ts
31806
31955
  function gaLog(level, action, ctx) {
31807
31956
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
@@ -31858,12 +32007,6 @@ function pickWinningDimension(rows, tupleKey) {
31858
32007
  }
31859
32008
  return [...winners.values()].sort((a, b) => (b.sessions ?? 0) - (a.sessions ?? 0));
31860
32009
  }
31861
- function normalizeAiTrafficClass(value) {
31862
- return value === AiReferralTrafficClasses.paid ? AiReferralTrafficClasses.paid : AiReferralTrafficClasses.organic;
31863
- }
31864
- function emptyAiCounts() {
31865
- return { sessions: 0, users: 0 };
31866
- }
31867
32010
  function persistAcquisitionMeasurement(db, input) {
31868
32011
  const { startDate, endDate } = input.report;
31869
32012
  db.transaction((tx) => {
@@ -31992,70 +32135,6 @@ function persistMeasurementError(db, projectId, component, message, updatedAt) {
31992
32135
  }
31993
32136
  }).run();
31994
32137
  }
31995
- function summarizeAiReferralCounts(rows) {
31996
- const dedupeGroups = /* @__PURE__ */ new Map();
31997
- const bySessionChannelGroup = /* @__PURE__ */ new Map();
31998
- const paidDeduped = emptyAiCounts();
31999
- const organicDeduped = emptyAiCounts();
32000
- const paidBySession = emptyAiCounts();
32001
- const organicBySession = emptyAiCounts();
32002
- for (const row of rows) {
32003
- const isPaid = normalizeAiTrafficClass(row.trafficClass) === AiReferralTrafficClasses.paid;
32004
- const sessions = row.sessions ?? 0;
32005
- const users = row.users ?? 0;
32006
- const key = `${row.date}\0${row.source}\0${row.medium}`;
32007
- let byDimension = dedupeGroups.get(key);
32008
- if (!byDimension) {
32009
- byDimension = /* @__PURE__ */ new Map();
32010
- dedupeGroups.set(key, byDimension);
32011
- }
32012
- let dim = byDimension.get(row.sourceDimension);
32013
- if (!dim) {
32014
- dim = { paidSessions: 0, organicSessions: 0, paidUsers: 0, organicUsers: 0 };
32015
- byDimension.set(row.sourceDimension, dim);
32016
- }
32017
- if (isPaid) {
32018
- dim.paidSessions += sessions;
32019
- dim.paidUsers += users;
32020
- } else {
32021
- dim.organicSessions += sessions;
32022
- dim.organicUsers += users;
32023
- }
32024
- if (row.sourceDimension === "session") {
32025
- bySessionChannelGroup.set(
32026
- row.channelGroup,
32027
- (bySessionChannelGroup.get(row.channelGroup) ?? 0) + sessions
32028
- );
32029
- const sessionBucket = isPaid ? paidBySession : organicBySession;
32030
- sessionBucket.sessions += sessions;
32031
- sessionBucket.users += users;
32032
- }
32033
- }
32034
- for (const byDimension of dedupeGroups.values()) {
32035
- const dims = [...byDimension.values()];
32036
- const bestSessions = dims.reduce((best, d) => d.paidSessions + d.organicSessions > best.paidSessions + best.organicSessions ? d : best);
32037
- const bestUsers = dims.reduce((best, d) => d.paidUsers + d.organicUsers > best.paidUsers + best.organicUsers ? d : best);
32038
- paidDeduped.sessions += bestSessions.paidSessions;
32039
- organicDeduped.sessions += bestSessions.organicSessions;
32040
- paidDeduped.users += bestUsers.paidUsers;
32041
- organicDeduped.users += bestUsers.organicUsers;
32042
- }
32043
- return {
32044
- paidDeduped,
32045
- organicDeduped,
32046
- paidBySession,
32047
- organicBySession,
32048
- bySessionChannelGroup,
32049
- deduped: {
32050
- sessions: paidDeduped.sessions + organicDeduped.sessions,
32051
- users: paidDeduped.users + organicDeduped.users
32052
- },
32053
- bySession: {
32054
- sessions: paidBySession.sessions + organicBySession.sessions,
32055
- users: paidBySession.users + organicBySession.users
32056
- }
32057
- };
32058
- }
32059
32138
  async function refreshOAuthTokenIfNeeded(googleStore, authConfig, canonicalDomain, oauthConn) {
32060
32139
  const expiresAt = oauthConn.tokenExpiresAt ? new Date(oauthConn.tokenExpiresAt).getTime() : 0;
32061
32140
  const fiveMinutes = 5 * 60 * 1e3;
@@ -32590,8 +32669,7 @@ async function ga4Routes(app, opts) {
32590
32669
  medium: gaAiReferrals.medium,
32591
32670
  trafficClass: gaAiReferrals.trafficClass,
32592
32671
  sourceDimension: gaAiReferrals.sourceDimension,
32593
- sessions: sql15`SUM(${gaAiReferrals.sessions})`,
32594
- users: sql15`SUM(${gaAiReferrals.users})`
32672
+ sessions: sql15`SUM(${gaAiReferrals.sessions})`
32595
32673
  }).from(gaAiReferrals).where(and25(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.trafficClass, gaAiReferrals.sourceDimension).all();
32596
32674
  const aiReferralLandingPageRows = app.db.select({
32597
32675
  source: gaAiReferrals.source,
@@ -32599,8 +32677,7 @@ async function ga4Routes(app, opts) {
32599
32677
  trafficClass: gaAiReferrals.trafficClass,
32600
32678
  sourceDimension: gaAiReferrals.sourceDimension,
32601
32679
  landingPage: sql15`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
32602
- sessions: sql15`SUM(${gaAiReferrals.sessions})`,
32603
- users: sql15`SUM(${gaAiReferrals.users})`
32680
+ sessions: sql15`SUM(${gaAiReferrals.sessions})`
32604
32681
  }).from(gaAiReferrals).where(and25(...aiConditions)).groupBy(
32605
32682
  gaAiReferrals.source,
32606
32683
  gaAiReferrals.medium,
@@ -32615,8 +32692,7 @@ async function ga4Routes(app, opts) {
32615
32692
  trafficClass: gaAiReferrals.trafficClass,
32616
32693
  sourceDimension: gaAiReferrals.sourceDimension,
32617
32694
  channelGroup: gaAiReferrals.channelGroup,
32618
- sessions: gaAiReferrals.sessions,
32619
- users: gaAiReferrals.users
32695
+ sessions: gaAiReferrals.sessions
32620
32696
  }).from(gaAiReferrals).where(and25(...aiConditions)).all();
32621
32697
  const aiReferrals = pickWinningDimension(
32622
32698
  aiReferralRows,
@@ -32662,13 +32738,18 @@ async function ga4Routes(app, opts) {
32662
32738
  directSessions: r.directSessions ?? 0,
32663
32739
  users: r.users ?? 0
32664
32740
  })),
32741
+ // No `users` on either referral row type, and no `ai*Users*` totals: GA
32742
+ // reports users as a COUNT DISTINCT at the grain requested, so summing
32743
+ // the stored column across landing pages (and, for these rows, across
32744
+ // every date in the window) counted one visitor once per row. Withdrawn
32745
+ // in 4.135.0; see `ga-ai-referral-aggregation.ts` for why no correct
32746
+ // figure can be computed or fetched.
32665
32747
  aiReferrals: aiReferrals.map((r) => ({
32666
32748
  source: r.source,
32667
32749
  medium: r.medium,
32668
32750
  trafficClass: normalizeAiTrafficClass(r.trafficClass),
32669
32751
  sourceDimension: r.sourceDimension,
32670
- sessions: r.sessions ?? 0,
32671
- users: r.users ?? 0
32752
+ sessions: r.sessions ?? 0
32672
32753
  })),
32673
32754
  aiReferralLandingPages: aiReferralLandingPages.map((r) => ({
32674
32755
  source: r.source,
@@ -32676,21 +32757,14 @@ async function ga4Routes(app, opts) {
32676
32757
  trafficClass: normalizeAiTrafficClass(r.trafficClass),
32677
32758
  sourceDimension: r.sourceDimension,
32678
32759
  landingPage: r.landingPage,
32679
- sessions: r.sessions ?? 0,
32680
- users: r.users ?? 0
32760
+ sessions: r.sessions ?? 0
32681
32761
  })),
32682
32762
  aiSessionsDeduped: aiSummary.deduped.sessions,
32683
- aiUsersDeduped: aiSummary.deduped.users,
32684
32763
  paidAiSessionsDeduped: aiSummary.paidDeduped.sessions,
32685
- paidAiUsersDeduped: aiSummary.paidDeduped.users,
32686
32764
  organicAiSessionsDeduped: aiSummary.organicDeduped.sessions,
32687
- organicAiUsersDeduped: aiSummary.organicDeduped.users,
32688
32765
  aiSessionsBySession: aiSummary.bySession.sessions,
32689
- aiUsersBySession: aiSummary.bySession.users,
32690
32766
  paidAiSessionsBySession: aiSummary.paidBySession.sessions,
32691
- paidAiUsersBySession: aiSummary.paidBySession.users,
32692
32767
  organicAiSessionsBySession: aiSummary.organicBySession.sessions,
32693
- organicAiUsersBySession: aiSummary.organicBySession.users,
32694
32768
  socialReferrals: socialReferrals.map((r) => ({
32695
32769
  source: r.source,
32696
32770
  medium: r.medium,
@@ -32757,6 +32831,23 @@ async function ga4Routes(app, opts) {
32757
32831
  ).orderBy(gaAiReferrals.date).all();
32758
32832
  return rows;
32759
32833
  });
32834
+ app.get("/projects/:name/ga/ai-referral-daily", async (request, _reply) => {
32835
+ const project = resolveProject(app.db, request.params.name);
32836
+ requireGa4Connection(opts, project.name, project.canonicalDomain);
32837
+ const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
32838
+ const conditions = [eq31(gaAiReferrals.projectId, project.id)];
32839
+ if (cutoffDate) conditions.push(sql15`${gaAiReferrals.date} >= ${cutoffDate}`);
32840
+ const rows = app.db.select({
32841
+ date: gaAiReferrals.date,
32842
+ source: gaAiReferrals.source,
32843
+ medium: gaAiReferrals.medium,
32844
+ trafficClass: gaAiReferrals.trafficClass,
32845
+ sourceDimension: gaAiReferrals.sourceDimension,
32846
+ channelGroup: gaAiReferrals.channelGroup,
32847
+ sessions: gaAiReferrals.sessions
32848
+ }).from(gaAiReferrals).where(and25(...conditions)).all();
32849
+ return buildAiReferralDailySeries(rows);
32850
+ });
32760
32851
  app.get("/projects/:name/ga/social-referral-history", async (request, _reply) => {
32761
32852
  const project = resolveProject(app.db, request.params.name);
32762
32853
  requireGa4Connection(opts, project.name, project.canonicalDomain);
package/dist/cli.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  setTelemetrySource,
30
30
  showFirstRunNotice,
31
31
  trackEvent
32
- } from "./chunk-TKXWTDV6.js";
32
+ } from "./chunk-3PFRVEWP.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-ZUQPHGEN.js";
49
+ } from "./chunk-PAR6P5H6.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-LIXWDIDR.js";
57
+ } from "./chunk-TPVMHYHD.js";
58
58
  import {
59
59
  AdsDeliverySnapshotStatuses,
60
60
  AdsHistoricalCampaignRollupStatuses,
@@ -100,7 +100,7 @@ import {
100
100
  providerQuotaPolicySchema,
101
101
  resolveProviderInput,
102
102
  winnabilityClassSchema
103
- } from "./chunk-PC7AYXND.js";
103
+ } from "./chunk-PXEEWJRV.js";
104
104
 
105
105
  // src/cli.ts
106
106
  import { pathToFileURL } from "url";
@@ -3012,6 +3012,9 @@ function readRequest(inputPath, schema) {
3012
3012
  });
3013
3013
  }
3014
3014
  }
3015
+ function partialDayNote(inProgressDate) {
3016
+ return inProgressDate === null ? "" : ` (${inProgressDate} still filling)`;
3017
+ }
3015
3018
  function printOperationDetails(operation) {
3016
3019
  console.log(`Operation: ${operation.operationKey}`);
3017
3020
  if (operation.entityId) console.log(`Entity: ${operation.entityType ?? "unknown"} ${operation.entityId}`);
@@ -3366,8 +3369,9 @@ async function adsInsights(project, opts) {
3366
3369
  console.log("DATE LEVEL ENTITY IMPR CLICKS SPEND CPC");
3367
3370
  for (const row of result.rows) {
3368
3371
  const cpc = row.cpcMicros != null ? formatMicros(row.cpcMicros, currency) : "\u2014";
3372
+ const partial = row.inProgress ? " (still filling)" : "";
3369
3373
  console.log(
3370
- `${row.date} ${row.level.padEnd(9)} ${row.entityId.padEnd(36).slice(0, 36)} ${String(row.impressions).padStart(6)} ${String(row.clicks).padStart(6)} ${formatMicros(row.spendMicros, currency).padStart(9)} ${cpc}`
3374
+ `${row.date} ${row.level.padEnd(9)} ${row.entityId.padEnd(36).slice(0, 36)} ${String(row.impressions).padStart(6)} ${String(row.clicks).padStart(6)} ${formatMicros(row.spendMicros, currency).padStart(9)} ${cpc}${partial}`
3371
3375
  );
3372
3376
  }
3373
3377
  }
@@ -3384,7 +3388,7 @@ async function adsSummary(project, opts) {
3384
3388
  }
3385
3389
  console.log(`Account: ${result.displayName ?? "unknown"} (${result.currencyCode ?? "?"})`);
3386
3390
  console.log(`Structure: ${result.campaignCount} campaigns / ${result.adGroupCount} ad groups / ${result.adCount} ads`);
3387
- console.log(`Window: ${result.window.from ?? "\u2014"} \u2192 ${result.window.to ?? "\u2014"}`);
3391
+ console.log(`Window: ${result.window.from ?? "\u2014"} \u2192 ${result.window.to ?? "\u2014"}${partialDayNote(result.window.inProgressDate)}`);
3388
3392
  console.log(`Impressions: ${result.totals.impressions}`);
3389
3393
  console.log(`Clicks: ${result.totals.clicks}${result.totals.ctr != null ? ` (CTR ${(result.totals.ctr * 100).toFixed(2)}%)` : ""}`);
3390
3394
  console.log(`Spend: ${formatMicros(result.totals.spendMicros, result.currencyCode ?? "USD")}${result.totals.cpcMicros != null ? ` (CPC ${formatMicros(result.totals.cpcMicros, result.currencyCode ?? "USD")})` : ""}`);
@@ -3402,7 +3406,8 @@ async function adsDeliveryDiagnostics(project, opts) {
3402
3406
  console.log(`Last synced: ${result.snapshot.lastSyncedAt ?? "never"}`);
3403
3407
  console.log(`Activity: ${result.assessment.state}`);
3404
3408
  if (result.historicalCampaignRollups.status === AdsHistoricalCampaignRollupStatuses.reported && result.historicalCampaignRollups.totals !== null) {
3405
- console.log(`Historical: ${result.historicalCampaignRollups.totals.impressions} impressions / ${result.historicalCampaignRollups.totals.clicks} clicks (${result.historicalCampaignRollups.window.from} \u2192 ${result.historicalCampaignRollups.window.to})`);
3409
+ const rollupWindow = result.historicalCampaignRollups.window;
3410
+ console.log(`Historical: ${result.historicalCampaignRollups.totals.impressions} impressions / ${result.historicalCampaignRollups.totals.clicks} clicks (${rollupWindow.from} \u2192 ${rollupWindow.to})${partialDayNote(rollupWindow.inProgressDate)}`);
3406
3411
  } else {
3407
3412
  console.log("Historical: no stored campaign rollups");
3408
3413
  }
@@ -3982,13 +3987,13 @@ async function gaTraffic(project, opts) {
3982
3987
  const attrWidth = 12;
3983
3988
  const classWidth = 8;
3984
3989
  console.log(" AI REFERRAL SOURCES");
3985
- console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"CLASS".padEnd(classWidth)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
3986
- console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(classWidth)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
3990
+ console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"CLASS".padEnd(classWidth)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)}`);
3991
+ console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(classWidth)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)}`);
3987
3992
  for (const ref of result.aiReferrals) {
3988
3993
  const dimLabel = ref.sourceDimension === "first_user" ? "first-visit" : ref.sourceDimension === "manual_utm" ? "utm" : "session";
3989
3994
  const classLabel = ref.trafficClass === "paid" ? "paid" : "organic";
3990
3995
  console.log(
3991
- ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${classLabel.padEnd(classWidth)} ${dimLabel.padEnd(attrWidth)} ${String(ref.sessions).padEnd(10)}${String(ref.users).padEnd(8)}`
3996
+ ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${classLabel.padEnd(classWidth)} ${dimLabel.padEnd(attrWidth)} ${String(ref.sessions).padEnd(10)}`
3992
3997
  );
3993
3998
  }
3994
3999
  console.log();
@@ -3996,14 +4001,14 @@ async function gaTraffic(project, opts) {
3996
4001
  if (result.aiReferralLandingPages.length > 0) {
3997
4002
  const attrWidth = 12;
3998
4003
  console.log(" AI REFERRAL LANDING PAGES");
3999
- console.log(` ${"LANDING PAGE".padEnd(30)} ${"SOURCE".padEnd(25)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
4000
- console.log(` ${"\u2500".repeat(30)} ${"\u2500".repeat(25)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
4004
+ console.log(` ${"LANDING PAGE".padEnd(30)} ${"SOURCE".padEnd(25)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)}`);
4005
+ console.log(` ${"\u2500".repeat(30)} ${"\u2500".repeat(25)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)}`);
4001
4006
  for (const row of result.aiReferralLandingPages) {
4002
4007
  const dimLabel = row.sourceDimension === "first_user" ? "first-visit" : row.sourceDimension === "manual_utm" ? "utm" : "session";
4003
4008
  const page = row.landingPage.length > 30 ? row.landingPage.slice(0, 27) + "..." : row.landingPage;
4004
4009
  const source = row.source.length > 25 ? row.source.slice(0, 22) + "..." : row.source;
4005
4010
  console.log(
4006
- ` ${page.padEnd(30)} ${source.padEnd(25)} ${dimLabel.padEnd(attrWidth)} ${String(row.sessions).padEnd(10)}${String(row.users).padEnd(8)}`
4011
+ ` ${page.padEnd(30)} ${source.padEnd(25)} ${dimLabel.padEnd(attrWidth)} ${String(row.sessions).padEnd(10)}`
4007
4012
  );
4008
4013
  }
4009
4014
  console.log();
@@ -4111,6 +4116,36 @@ async function gaAiReferralHistory(project, opts) {
4111
4116
  );
4112
4117
  }
4113
4118
  }
4119
+ async function gaAiReferralDaily(project, opts) {
4120
+ const client = getClient7();
4121
+ const result = await client.gaAiReferralDaily(project, opts?.window ? { window: opts.window } : void 0);
4122
+ if (opts?.format === "json") {
4123
+ console.log(JSON.stringify(result, null, 2));
4124
+ return;
4125
+ } else if (opts?.format === "jsonl") {
4126
+ emitJsonl(result.days.map((day) => ({ project, window: opts?.window, ...day })));
4127
+ return;
4128
+ }
4129
+ if (result.days.length === 0) {
4130
+ console.log(`No AI referral sessions.${opts?.window ? " Try a wider window or omit --window." : ' Run "canonry ga sync <project>" first.'}`);
4131
+ return;
4132
+ }
4133
+ const dateWidth = 12;
4134
+ const sourceWidth = Math.min(30, Math.max(10, ...result.sources.map((s) => s.length)));
4135
+ console.log(`GA4 AI Referral Sessions per Day for "${project}":
4136
+ `);
4137
+ console.log(` ${"DATE".padEnd(dateWidth)} ${"SOURCE".padEnd(sourceWidth)} ${"SESSIONS".padEnd(10)}${"PAID".padEnd(8)}${"ORGANIC".padEnd(9)}`);
4138
+ console.log(` ${"\u2500".repeat(dateWidth)} ${"\u2500".repeat(sourceWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}${"\u2500".repeat(9)}`);
4139
+ for (const day of result.days) {
4140
+ for (const entry of day.bySource) {
4141
+ console.log(
4142
+ ` ${day.date.padEnd(dateWidth)} ${entry.source.padEnd(sourceWidth)} ${String(entry.sessions).padEnd(10)}${String(entry.paidSessions).padEnd(8)}${String(entry.organicSessions).padEnd(9)}`
4143
+ );
4144
+ }
4145
+ }
4146
+ console.log(`
4147
+ Total: ${result.totalSessions} sessions (${result.totalPaidSessions} paid, ${result.totalOrganicSessions} organic)`);
4148
+ }
4114
4149
  async function gaSocialReferralHistory(project, opts) {
4115
4150
  const client = getClient7();
4116
4151
  const result = await client.gaSocialReferralHistory(project, opts?.window ? { window: opts.window } : void 0);
@@ -4269,17 +4304,11 @@ async function gaAttribution(project, opts) {
4269
4304
  totalUsers: traffic.totalUsers,
4270
4305
  organicSessions: traffic.totalOrganicSessions,
4271
4306
  aiSessions: traffic.aiSessionsDeduped,
4272
- aiUsers: traffic.aiUsersDeduped,
4273
4307
  paidAiSessions: traffic.paidAiSessionsDeduped,
4274
- paidAiUsers: traffic.paidAiUsersDeduped,
4275
4308
  organicAiSessions: traffic.organicAiSessionsDeduped,
4276
- organicAiUsers: traffic.organicAiUsersDeduped,
4277
4309
  aiSessionsBySession: traffic.aiSessionsBySession,
4278
- aiUsersBySession: traffic.aiUsersBySession,
4279
4310
  paidAiSessionsBySession: traffic.paidAiSessionsBySession,
4280
- paidAiUsersBySession: traffic.paidAiUsersBySession,
4281
4311
  organicAiSessionsBySession: traffic.organicAiSessionsBySession,
4282
- organicAiUsersBySession: traffic.organicAiUsersBySession,
4283
4312
  socialSessions: traffic.socialSessions,
4284
4313
  socialUsers: traffic.socialUsers,
4285
4314
  directSessions: traffic.totalDirectSessions,
@@ -4356,17 +4385,11 @@ async function gaAttribution(project, opts) {
4356
4385
  totalUsers: traffic.totalUsers,
4357
4386
  organicSessions: traffic.totalOrganicSessions,
4358
4387
  aiSessions: traffic.aiSessionsDeduped,
4359
- aiUsers: traffic.aiUsersDeduped,
4360
4388
  paidAiSessions: traffic.paidAiSessionsDeduped,
4361
- paidAiUsers: traffic.paidAiUsersDeduped,
4362
4389
  organicAiSessions: traffic.organicAiSessionsDeduped,
4363
- organicAiUsers: traffic.organicAiUsersDeduped,
4364
4390
  aiSessionsBySession: traffic.aiSessionsBySession,
4365
- aiUsersBySession: traffic.aiUsersBySession,
4366
4391
  paidAiSessionsBySession: traffic.paidAiSessionsBySession,
4367
- paidAiUsersBySession: traffic.paidAiUsersBySession,
4368
4392
  organicAiSessionsBySession: traffic.organicAiSessionsBySession,
4369
- organicAiUsersBySession: traffic.organicAiUsersBySession,
4370
4393
  socialSessions: traffic.socialSessions,
4371
4394
  socialUsers: traffic.socialUsers,
4372
4395
  directSessions: traffic.totalDirectSessions,
@@ -4575,6 +4598,20 @@ var GA_CLI_COMMANDS = [
4575
4598
  });
4576
4599
  }
4577
4600
  },
4601
+ {
4602
+ path: ["ga", "ai-referral-daily"],
4603
+ usage: "canonry ga ai-referral-daily <project> [--window 30d] [--format json]",
4604
+ options: {
4605
+ window: stringOption()
4606
+ },
4607
+ run: async (input) => {
4608
+ const project = requireProject(input, "ga.ai-referral-daily", "canonry ga ai-referral-daily <project> [--window 30d] [--format json]");
4609
+ await gaAiReferralDaily(project, {
4610
+ window: getString(input.values, "window"),
4611
+ format: input.format
4612
+ });
4613
+ }
4614
+ },
4578
4615
  {
4579
4616
  path: ["ga", "social-referral-history"],
4580
4617
  usage: "canonry ga social-referral-history <project> [--window 30d] [--format json]",
@@ -4638,7 +4675,7 @@ var GA_CLI_COMMANDS = [
4638
4675
  unknownSubcommand(input.positionals[0], {
4639
4676
  command: "ga",
4640
4677
  usage: "canonry ga <subcommand> <project> [args]",
4641
- available: ["connect", "disconnect", "status", "sync", "measurement-analysis", "traffic", "coverage", "ai-referral-history", "social-referral-history", "session-history", "social-referral-summary", "attribution"]
4678
+ available: ["connect", "disconnect", "status", "sync", "measurement-analysis", "traffic", "coverage", "ai-referral-history", "ai-referral-daily", "social-referral-history", "session-history", "social-referral-summary", "attribution"]
4642
4679
  });
4643
4680
  }
4644
4681
  }
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-TKXWTDV6.js";
3
+ } from "./chunk-3PFRVEWP.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-ZUQPHGEN.js";
7
- import "./chunk-LIXWDIDR.js";
8
- import "./chunk-PC7AYXND.js";
6
+ } from "./chunk-PAR6P5H6.js";
7
+ import "./chunk-TPVMHYHD.js";
8
+ import "./chunk-PXEEWJRV.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig