@ainyc/canonry 1.36.0 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/index.html CHANGED
@@ -12,8 +12,8 @@
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-Du_w835k.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-CborW-lk.css">
15
+ <script type="module" crossorigin src="./assets/index-DR4zYsnw.js"></script>
16
+ <link rel="stylesheet" crossorigin href="./assets/index-BZX6FEZ4.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
@@ -1055,11 +1055,13 @@ var ga4TrafficSnapshotDtoSchema = z11.object({
1055
1055
  organicSessions: z11.number(),
1056
1056
  users: z11.number()
1057
1057
  });
1058
+ var ga4SourceDimensionSchema = z11.enum(["session", "first_user", "manual_utm"]);
1058
1059
  var ga4AiReferralDtoSchema = z11.object({
1059
1060
  source: z11.string(),
1060
1061
  medium: z11.string(),
1061
1062
  sessions: z11.number(),
1062
- users: z11.number()
1063
+ users: z11.number(),
1064
+ sourceDimension: ga4SourceDimensionSchema
1063
1065
  });
1064
1066
  var ga4TrafficSummaryDtoSchema = z11.object({
1065
1067
  totalSessions: z11.number(),
@@ -1072,6 +1074,10 @@ var ga4TrafficSummaryDtoSchema = z11.object({
1072
1074
  users: z11.number()
1073
1075
  })),
1074
1076
  aiReferrals: z11.array(ga4AiReferralDtoSchema),
1077
+ /** Deduped AI session total: MAX(sessions) per date+source+medium across attribution dimensions, then summed. */
1078
+ aiSessionsDeduped: z11.number(),
1079
+ /** Deduped AI user total: MAX(users) per date+source+medium across attribution dimensions, then summed. */
1080
+ aiUsersDeduped: z11.number(),
1075
1081
  lastSyncedAt: z11.string().nullable()
1076
1082
  });
1077
1083
  var ga4AiReferralHistoryEntrySchema = z11.object({
@@ -1079,7 +1085,9 @@ var ga4AiReferralHistoryEntrySchema = z11.object({
1079
1085
  source: z11.string(),
1080
1086
  medium: z11.string(),
1081
1087
  sessions: z11.number(),
1082
- users: z11.number()
1088
+ users: z11.number(),
1089
+ /** Which GA4 dimension this row came from: session (sessionSource), first_user (firstUserSource), or manual_utm (utm_source parameter) */
1090
+ sourceDimension: ga4SourceDimensionSchema
1083
1091
  });
1084
1092
  var ga4SessionHistoryEntrySchema = z11.object({
1085
1093
  date: z11.string(),
@@ -1487,13 +1495,15 @@ var gaAiReferrals = sqliteTable("ga_ai_referrals", {
1487
1495
  date: text("date").notNull(),
1488
1496
  source: text("source").notNull(),
1489
1497
  medium: text("medium").notNull(),
1498
+ /** Which GA4 dimension produced this row: 'session' | 'first_user' | 'manual_utm' */
1499
+ sourceDimension: text("source_dimension").notNull().default("session"),
1490
1500
  sessions: integer("sessions").notNull().default(0),
1491
1501
  users: integer("users").notNull().default(0),
1492
1502
  syncedAt: text("synced_at").notNull()
1493
1503
  }, (table) => [
1494
1504
  index("idx_ga_ai_ref_project_date").on(table.projectId, table.date),
1495
1505
  index("idx_ga_ai_ref_source").on(table.source),
1496
- uniqueIndex("idx_ga_ai_ref_unique").on(table.projectId, table.date, table.source, table.medium)
1506
+ uniqueIndex("idx_ga_ai_ref_unique_v2").on(table.projectId, table.date, table.source, table.medium, table.sourceDimension)
1497
1507
  ]);
1498
1508
  var gaTrafficSummaries = sqliteTable("ga_traffic_summaries", {
1499
1509
  id: text("id").primaryKey(),
@@ -1860,7 +1870,13 @@ var MIGRATIONS = [
1860
1870
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_schedules_project ON schedules(project_id)`,
1861
1871
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_usage_scope_period_metric ON usage_counters(scope, period, metric)`,
1862
1872
  `ALTER TABLE projects ADD COLUMN config_source TEXT NOT NULL DEFAULT 'cli'`,
1863
- `ALTER TABLE projects ADD COLUMN config_revision INTEGER NOT NULL DEFAULT 1`
1873
+ `ALTER TABLE projects ADD COLUMN config_revision INTEGER NOT NULL DEFAULT 1`,
1874
+ // v20: Track which GA4 dimension produced each AI referral row
1875
+ // Values: 'session' (sessionSource), 'first_user' (firstUserSource), 'manual_utm' (manualSource/utm_source)
1876
+ `ALTER TABLE ga_ai_referrals ADD COLUMN source_dimension TEXT NOT NULL DEFAULT 'session'`,
1877
+ // Replace old unique index with one that includes source_dimension
1878
+ `DROP INDEX IF EXISTS idx_ga_ai_ref_unique`,
1879
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v2 ON ga_ai_referrals(project_id, date, source, medium, source_dimension)`
1864
1880
  ];
1865
1881
  function migrate(db) {
1866
1882
  const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -6634,7 +6650,10 @@ var AI_REFERRAL_SOURCE_FILTERS = [
6634
6650
  { matchType: "CONTAINS", value: "openai" },
6635
6651
  { matchType: "CONTAINS", value: "claude" },
6636
6652
  { matchType: "CONTAINS", value: "anthropic" },
6637
- { matchType: "CONTAINS", value: "copilot" }
6653
+ { matchType: "CONTAINS", value: "copilot" },
6654
+ { matchType: "CONTAINS", value: "phind" },
6655
+ { matchType: "EXACT", value: "you.com" },
6656
+ { matchType: "CONTAINS", value: "meta.ai" }
6638
6657
  ];
6639
6658
  async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
6640
6659
  const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
@@ -6773,52 +6792,75 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
6773
6792
  ga4Log("info", "fetch-ai-referrals.start", { propertyId, days: syncDays });
6774
6793
  const PAGE_SIZE = 1e3;
6775
6794
  const rows = [];
6776
- let offset = 0;
6777
- while (true) {
6778
- const request = {
6779
- dateRanges: [{ startDate: formatDate(startDate), endDate: formatDate(endDate) }],
6780
- dimensions: [
6781
- { name: "date" },
6782
- { name: "sessionSource" },
6783
- { name: "sessionMedium" }
6784
- ],
6785
- metrics: [
6786
- { name: "sessions" },
6787
- { name: "totalUsers" }
6788
- ],
6789
- dimensionFilter: {
6790
- orGroup: {
6791
- expressions: AI_REFERRAL_SOURCE_FILTERS.map(({ matchType, value }) => ({
6792
- filter: {
6793
- fieldName: "sessionSource",
6794
- stringFilter: { matchType, value }
6795
- }
6796
- }))
6797
- }
6798
- },
6799
- limit: PAGE_SIZE,
6800
- offset
6801
- };
6802
- const response = await runReport(accessToken, propertyId, request);
6803
- const pageRows = (response.rows ?? []).map((row) => ({
6804
- date: row.dimensionValues[0].value,
6805
- source: row.dimensionValues[1].value,
6806
- medium: row.dimensionValues[2].value,
6807
- sessions: parseInt(row.metricValues[0].value, 10) || 0,
6808
- users: parseInt(row.metricValues[1].value, 10) || 0
6809
- }));
6810
- rows.push(...pageRows);
6811
- const totalRows = response.rowCount ?? 0;
6812
- offset += pageRows.length;
6813
- if (pageRows.length < PAGE_SIZE || offset >= totalRows) break;
6795
+ const dimensionPairs = [
6796
+ ["sessionSource", "sessionMedium", "session"],
6797
+ ["firstUserSource", "firstUserMedium", "first_user"],
6798
+ ["manualSource", "manualMedium", "manual_utm"]
6799
+ ];
6800
+ for (const [sourceDim, mediumDim, dimLabel] of dimensionPairs) {
6801
+ let offset = 0;
6802
+ while (true) {
6803
+ const request = {
6804
+ dateRanges: [{ startDate: formatDate(startDate), endDate: formatDate(endDate) }],
6805
+ dimensions: [
6806
+ { name: "date" },
6807
+ { name: sourceDim },
6808
+ { name: mediumDim }
6809
+ ],
6810
+ metrics: [
6811
+ { name: "sessions" },
6812
+ { name: "totalUsers" }
6813
+ ],
6814
+ dimensionFilter: {
6815
+ orGroup: {
6816
+ expressions: AI_REFERRAL_SOURCE_FILTERS.map(({ matchType, value }) => ({
6817
+ filter: {
6818
+ fieldName: sourceDim,
6819
+ stringFilter: { matchType, value }
6820
+ }
6821
+ }))
6822
+ }
6823
+ },
6824
+ limit: PAGE_SIZE,
6825
+ offset
6826
+ };
6827
+ const response = await runReport(accessToken, propertyId, request);
6828
+ const pageRows = (response.rows ?? []).map((row) => ({
6829
+ date: row.dimensionValues[0].value,
6830
+ source: row.dimensionValues[1].value,
6831
+ medium: row.dimensionValues[2].value,
6832
+ sessions: parseInt(row.metricValues[0].value, 10) || 0,
6833
+ users: parseInt(row.metricValues[1].value, 10) || 0,
6834
+ sourceDimension: dimLabel
6835
+ }));
6836
+ rows.push(...pageRows);
6837
+ const totalRows = response.rowCount ?? 0;
6838
+ offset += pageRows.length;
6839
+ if (pageRows.length < PAGE_SIZE || offset >= totalRows) break;
6840
+ }
6814
6841
  }
6842
+ const deduped = /* @__PURE__ */ new Map();
6815
6843
  for (const row of rows) {
6844
+ const key = `${row.date}::${row.source}::${row.medium}::${row.sourceDimension}`;
6845
+ const existing = deduped.get(key);
6846
+ if (!existing) {
6847
+ deduped.set(key, row);
6848
+ } else {
6849
+ deduped.set(key, {
6850
+ ...existing,
6851
+ sessions: Math.max(existing.sessions, row.sessions),
6852
+ users: Math.max(existing.users, row.users)
6853
+ });
6854
+ }
6855
+ }
6856
+ const dedupedRows = [...deduped.values()];
6857
+ for (const row of dedupedRows) {
6816
6858
  if (row.date.length === 8 && !row.date.includes("-")) {
6817
6859
  row.date = `${row.date.slice(0, 4)}-${row.date.slice(4, 6)}-${row.date.slice(6, 8)}`;
6818
6860
  }
6819
6861
  }
6820
- ga4Log("info", "fetch-ai-referrals.done", { propertyId, rowCount: rows.length });
6821
- return rows;
6862
+ ga4Log("info", "fetch-ai-referrals.done", { propertyId, rowCount: dedupedRows.length });
6863
+ return dedupedRows;
6822
6864
  }
6823
6865
 
6824
6866
  // ../api-routes/src/google.ts
@@ -8444,6 +8486,7 @@ async function ga4Routes(app, opts) {
8444
8486
  date: row.date,
8445
8487
  source: row.source,
8446
8488
  medium: row.medium,
8489
+ sourceDimension: row.sourceDimension,
8447
8490
  sessions: row.sessions,
8448
8491
  users: row.users,
8449
8492
  syncedAt: now
@@ -8495,9 +8538,23 @@ async function ga4Routes(app, opts) {
8495
8538
  const aiReferrals = app.db.select({
8496
8539
  source: gaAiReferrals.source,
8497
8540
  medium: gaAiReferrals.medium,
8541
+ sourceDimension: gaAiReferrals.sourceDimension,
8498
8542
  sessions: sql4`SUM(${gaAiReferrals.sessions})`,
8499
8543
  users: sql4`SUM(${gaAiReferrals.users})`
8500
- }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
8544
+ }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
8545
+ const aiDeduped = app.db.select({
8546
+ sessions: sql4`SUM(max_sessions)`,
8547
+ users: sql4`SUM(max_users)`
8548
+ }).from(
8549
+ sql4`(
8550
+ SELECT date, source, medium,
8551
+ MAX(sessions) AS max_sessions,
8552
+ MAX(users) AS max_users
8553
+ FROM ga_ai_referrals
8554
+ WHERE project_id = ${project.id}
8555
+ GROUP BY date, source, medium
8556
+ )`
8557
+ ).get();
8501
8558
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).orderBy(desc6(gaTrafficSummaries.syncedAt)).limit(1).get();
8502
8559
  return {
8503
8560
  totalSessions: summary?.totalSessions ?? 0,
@@ -8512,9 +8569,12 @@ async function ga4Routes(app, opts) {
8512
8569
  aiReferrals: aiReferrals.map((r) => ({
8513
8570
  source: r.source,
8514
8571
  medium: r.medium,
8572
+ sourceDimension: r.sourceDimension,
8515
8573
  sessions: r.sessions ?? 0,
8516
8574
  users: r.users ?? 0
8517
8575
  })),
8576
+ aiSessionsDeduped: aiDeduped?.sessions ?? 0,
8577
+ aiUsersDeduped: aiDeduped?.users ?? 0,
8518
8578
  lastSyncedAt: latestSync?.syncedAt ?? null
8519
8579
  };
8520
8580
  });
@@ -8525,6 +8585,7 @@ async function ga4Routes(app, opts) {
8525
8585
  date: gaAiReferrals.date,
8526
8586
  source: gaAiReferrals.source,
8527
8587
  medium: gaAiReferrals.medium,
8588
+ sourceDimension: gaAiReferrals.sourceDimension,
8528
8589
  sessions: gaAiReferrals.sessions,
8529
8590
  users: gaAiReferrals.users
8530
8591
  }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).orderBy(gaAiReferrals.date).all();
package/dist/cli.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  setGoogleAuthConfig,
27
27
  showFirstRunNotice,
28
28
  trackEvent
29
- } from "./chunk-PZKK53EX.js";
29
+ } from "./chunk-5MOWJJND.js";
30
30
 
31
31
  // src/cli.ts
32
32
  import { pathToFileURL } from "url";
@@ -1493,14 +1493,20 @@ async function gaTraffic(project, opts) {
1493
1493
  console.log(` Total Sessions: ${result.totalSessions}`);
1494
1494
  console.log(` Organic Sessions: ${result.totalOrganicSessions}`);
1495
1495
  console.log(` Total Users: ${result.totalUsers}`);
1496
+ if (result.aiSessionsDeduped > 0) {
1497
+ const share = result.totalSessions > 0 ? Math.round(result.aiSessionsDeduped / result.totalSessions * 100) : 0;
1498
+ console.log(` AI Sessions (deduped): ${result.aiSessionsDeduped} (${share}% of total)`);
1499
+ }
1496
1500
  console.log();
1497
1501
  if (result.aiReferrals.length > 0) {
1502
+ const attrWidth = 12;
1498
1503
  console.log(" AI REFERRAL SOURCES");
1499
- console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
1500
- console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
1504
+ console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
1505
+ console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
1501
1506
  for (const ref of result.aiReferrals) {
1507
+ const dimLabel = ref.sourceDimension === "first_user" ? "first-visit" : ref.sourceDimension === "manual_utm" ? "utm" : "session";
1502
1508
  console.log(
1503
- ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${String(ref.sessions).padEnd(10)}${String(ref.users).padEnd(8)}`
1509
+ ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${dimLabel.padEnd(attrWidth)} ${String(ref.sessions).padEnd(10)}${String(ref.users).padEnd(8)}`
1504
1510
  );
1505
1511
  }
1506
1512
  console.log();
@@ -1535,13 +1541,15 @@ async function gaAiReferralHistory(project, format) {
1535
1541
  }
1536
1542
  const dateWidth = 12;
1537
1543
  const sourceWidth = Math.min(30, Math.max(10, ...result.map((r) => r.source.length)));
1544
+ const attrWidth = 12;
1538
1545
  console.log(`GA4 AI Referral History for "${project}":
1539
1546
  `);
1540
- console.log(` ${"DATE".padEnd(dateWidth)} ${"SOURCE".padEnd(sourceWidth)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
1541
- console.log(` ${"\u2500".repeat(dateWidth)} ${"\u2500".repeat(sourceWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
1547
+ console.log(` ${"DATE".padEnd(dateWidth)} ${"SOURCE".padEnd(sourceWidth)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
1548
+ console.log(` ${"\u2500".repeat(dateWidth)} ${"\u2500".repeat(sourceWidth)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
1542
1549
  for (const row of result) {
1550
+ const dimLabel = row.sourceDimension === "first_user" ? "first-visit" : row.sourceDimension === "manual_utm" ? "utm" : "session";
1543
1551
  console.log(
1544
- ` ${row.date.padEnd(dateWidth)} ${row.source.padEnd(sourceWidth)} ${String(row.sessions).padEnd(10)}${String(row.users).padEnd(8)}`
1552
+ ` ${row.date.padEnd(dateWidth)} ${row.source.padEnd(sourceWidth)} ${dimLabel.padEnd(attrWidth)} ${String(row.sessions).padEnd(10)}${String(row.users).padEnd(8)}`
1545
1553
  );
1546
1554
  }
1547
1555
  }
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createServer,
3
3
  loadConfig
4
- } from "./chunk-PZKK53EX.js";
4
+ } from "./chunk-5MOWJJND.js";
5
5
  export {
6
6
  createServer,
7
7
  loadConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "1.36.0",
3
+ "version": "1.37.0",
4
4
  "type": "module",
5
5
  "description": "The ultimate open-source AEO monitoring tool - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -57,16 +57,16 @@
57
57
  "@ainyc/canonry-api-routes": "0.0.0",
58
58
  "@ainyc/canonry-config": "0.0.0",
59
59
  "@ainyc/canonry-contracts": "0.0.0",
60
- "@ainyc/canonry-integration-bing": "0.0.0",
61
60
  "@ainyc/canonry-db": "0.0.0",
61
+ "@ainyc/canonry-integration-bing": "0.0.0",
62
62
  "@ainyc/canonry-integration-google": "0.0.0",
63
63
  "@ainyc/canonry-integration-wordpress": "0.0.0",
64
64
  "@ainyc/canonry-provider-cdp": "0.0.0",
65
65
  "@ainyc/canonry-provider-claude": "0.0.0",
66
- "@ainyc/canonry-provider-openai": "0.0.0",
67
66
  "@ainyc/canonry-provider-gemini": "0.0.0",
68
- "@ainyc/canonry-provider-perplexity": "0.0.0",
69
- "@ainyc/canonry-provider-local": "0.0.0"
67
+ "@ainyc/canonry-provider-openai": "0.0.0",
68
+ "@ainyc/canonry-provider-local": "0.0.0",
69
+ "@ainyc/canonry-provider-perplexity": "0.0.0"
70
70
  },
71
71
  "scripts": {
72
72
  "build": "tsup && tsx build-web.ts",