@ainyc/canonry 4.13.1 → 4.14.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,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-B6Mi9Fd1.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) {