@canonry/canonry 4.123.0 → 4.125.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 (24) hide show
  1. package/assets/assets/{AuditHistoryPanel-BVQMDIhQ.js → AuditHistoryPanel-C-KSU-dr.js} +1 -1
  2. package/assets/assets/{BacklinksPage-nrHT9VMG.js → BacklinksPage-BH4njwCh.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-_8qLsQzI.js → ChartPrimitives-BhB9Kxzc.js} +1 -1
  4. package/assets/assets/{HistoryPage-DqsKKdyy.js → HistoryPage-BQVNx0eu.js} +1 -1
  5. package/assets/assets/{ProjectPage-GyKoQUv_.js → ProjectPage-DnkNE1AW.js} +1 -1
  6. package/assets/assets/{RunRow-BRZZcSjQ.js → RunRow-70N3oGz8.js} +1 -1
  7. package/assets/assets/{RunsPage-24aI8DKF.js → RunsPage-CtuKRP0o.js} +1 -1
  8. package/assets/assets/{SettingsPage-BaNeTG1i.js → SettingsPage-B1ZGnDz8.js} +1 -1
  9. package/assets/assets/{TrafficPage-ByvcLSjv.js → TrafficPage-BOGbXHd9.js} +1 -1
  10. package/assets/assets/{TrafficSourceDetailPage-B-siTXVg.js → TrafficSourceDetailPage-5jj7n7W1.js} +1 -1
  11. package/assets/assets/{arrow-left-CBh1cZNH.js → arrow-left-gOf0lyu3.js} +1 -1
  12. package/assets/assets/{extract-error-message-DJZCPudR.js → extract-error-message-CZKcwJhv.js} +1 -1
  13. package/assets/assets/{index-mzLganDP.js → index-B_CDPjOg.js} +61 -61
  14. package/assets/assets/{trash-2-Bh-Yvkgc.js → trash-2-BnFgG3Wq.js} +1 -1
  15. package/assets/index.html +1 -1
  16. package/dist/{chunk-JYBNBK25.js → chunk-DN5H2ZN7.js} +42 -7
  17. package/dist/{chunk-MMFSI3OG.js → chunk-RFLNTI7Y.js} +28 -2
  18. package/dist/{chunk-HJIIJI7K.js → chunk-WA57WG6W.js} +1 -1
  19. package/dist/{chunk-4AZ7VES6.js → chunk-YPRUBNUK.js} +256 -20
  20. package/dist/cli.js +11 -5
  21. package/dist/index.js +4 -4
  22. package/dist/{intelligence-service-K35EN43I.js → intelligence-service-WJ2ZR7GF.js} +2 -2
  23. package/dist/mcp.js +2 -2
  24. package/package.json +10 -10
@@ -1 +1 @@
1
- import{c}from"./index-mzLganDP.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
1
+ import{c}from"./index-B_CDPjOg.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
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-mzLganDP.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-B_CDPjOg.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/vendor-tanstack-Be8JdV5M.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/vendor-radix-CQ77EO2y.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/vendor-recharts-DSxTZhaH.js">
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-HJIIJI7K.js";
13
+ } from "./chunk-WA57WG6W.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -115,7 +115,7 @@ import {
115
115
  siteAuditPages,
116
116
  siteAuditSnapshots,
117
117
  usageCounters
118
- } from "./chunk-4AZ7VES6.js";
118
+ } from "./chunk-YPRUBNUK.js";
119
119
  import {
120
120
  AGENT_MEMORY_VALUE_MAX_BYTES,
121
121
  AGENT_PROVIDER_IDS,
@@ -185,7 +185,7 @@ import {
185
185
  validationError,
186
186
  winnabilityClassLabel,
187
187
  withRetry
188
- } from "./chunk-MMFSI3OG.js";
188
+ } from "./chunk-RFLNTI7Y.js";
189
189
 
190
190
  // src/telemetry.ts
191
191
  import crypto from "crypto";
@@ -4612,7 +4612,10 @@ async function fetchAllPages(apiKey, path10, queryPairs) {
4612
4612
  );
4613
4613
  }
4614
4614
  function insightsPairs(opts) {
4615
- return (opts?.fields ?? []).map((field) => `fields[]=${encodeURIComponent(field)}`);
4615
+ return [
4616
+ ...(opts?.fields ?? []).map((field) => `fields[]=${encodeURIComponent(field)}`),
4617
+ ...(opts?.timeRanges ?? []).map((range) => `time_ranges[]=${encodeURIComponent(JSON.stringify(range))}`)
4618
+ ];
4616
4619
  }
4617
4620
  async function getAdAccount(apiKey) {
4618
4621
  validateApiKey(apiKey);
@@ -4793,6 +4796,31 @@ function removeOpenAiAdsConnection(config, projectName) {
4793
4796
  var log4 = createLogger("AdsSync");
4794
4797
  var CAMPAIGN_INSIGHT_FIELDS = ["campaign.impressions", "campaign.clicks", "campaign.spend", "campaign.conversions", "metadata.readable_time"];
4795
4798
  var AD_GROUP_INSIGHT_FIELDS = ["ad_group.impressions", "ad_group.clicks", "ad_group.spend", "ad_group.conversions", "metadata.readable_time"];
4799
+ var INSIGHTS_LOOKBACK_MS = 90 * 24 * 60 * 60 * 1e3;
4800
+ function accountHour(date, timezone) {
4801
+ const parts = new Intl.DateTimeFormat("en-CA", {
4802
+ timeZone: timezone,
4803
+ year: "numeric",
4804
+ month: "2-digit",
4805
+ day: "2-digit",
4806
+ hour: "2-digit",
4807
+ hourCycle: "h23"
4808
+ }).formatToParts(date);
4809
+ const value = (type) => {
4810
+ const part = parts.find((candidate) => candidate.type === type)?.value;
4811
+ if (!part) throw new Error(`OpenAI Ads account timezone did not produce a ${type}`);
4812
+ return part;
4813
+ };
4814
+ return `${value("year")}-${value("month")}-${value("day")}T${value("hour")}`;
4815
+ }
4816
+ function trailingAdsInsightHourRange(now, timezone) {
4817
+ return {
4818
+ type: "hour_range",
4819
+ since: accountHour(new Date(now.getTime() - INSIGHTS_LOOKBACK_MS), timezone),
4820
+ until: accountHour(now, timezone),
4821
+ timezone
4822
+ };
4823
+ }
4796
4824
  function toInsightUpserts(level, entityId, rows) {
4797
4825
  const upserts = [];
4798
4826
  for (const row of rows) {
@@ -4830,6 +4858,7 @@ async function executeAdsSync(db, runId, projectId, opts) {
4830
4858
  log4.info("sync.start", { runId, projectId, adAccountId: connRow.adAccountId });
4831
4859
  const account = await getAdAccount(apiKey);
4832
4860
  const campaigns = await listCampaigns(apiKey);
4861
+ const insightTimeRanges = [trailingAdsInsightHourRange(/* @__PURE__ */ new Date(), account.timezone)];
4833
4862
  const errors = /* @__PURE__ */ new Map();
4834
4863
  const adGroupsByCampaign = /* @__PURE__ */ new Map();
4835
4864
  const adsByGroup = /* @__PURE__ */ new Map();
@@ -4839,12 +4868,18 @@ async function executeAdsSync(db, runId, projectId, opts) {
4839
4868
  try {
4840
4869
  const [adGroups, campaignInsights] = await Promise.all([
4841
4870
  listAdGroups(apiKey, campaign.id),
4842
- getCampaignInsights(apiKey, campaign.id, { fields: CAMPAIGN_INSIGHT_FIELDS })
4871
+ getCampaignInsights(apiKey, campaign.id, {
4872
+ fields: CAMPAIGN_INSIGHT_FIELDS,
4873
+ timeRanges: insightTimeRanges
4874
+ })
4843
4875
  ]);
4844
4876
  const groupResults = await Promise.all(adGroups.map(async (group) => ({
4845
4877
  group,
4846
4878
  ads: await listAds(apiKey, group.id),
4847
- insights: await getAdGroupInsights(apiKey, group.id, { fields: AD_GROUP_INSIGHT_FIELDS })
4879
+ insights: await getAdGroupInsights(apiKey, group.id, {
4880
+ fields: AD_GROUP_INSIGHT_FIELDS,
4881
+ timeRanges: insightTimeRanges
4882
+ })
4848
4883
  })));
4849
4884
  syncedCampaigns.push(campaign);
4850
4885
  adGroupsByCampaign.set(campaign.id, adGroups);
@@ -6901,7 +6936,7 @@ function readStoredGroundingSources(rawResponse) {
6901
6936
  return result;
6902
6937
  }
6903
6938
  async function backfillInsightsCommand(project, opts) {
6904
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-K35EN43I.js");
6939
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-WJ2ZR7GF.js");
6905
6940
  const config = loadConfig();
6906
6941
  const db = createClient(config.database);
6907
6942
  migrate(db);
@@ -867,6 +867,10 @@ function formatWindowCountDelta(d, countLabel, windowLabel) {
867
867
  const sign = d.deltaAbs > 0 ? "+" : "";
868
868
  return `${sign}${formatNumber(Math.round(d.deltaAbs))} ${countLabel} ${windowLabel}`;
869
869
  }
870
+ function compactDateToIso(value) {
871
+ if (value.length !== 8 || !/^\d{8}$/.test(value)) return value;
872
+ return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
873
+ }
870
874
 
871
875
  // ../contracts/src/model-pointers.ts
872
876
  var modelPointerEventKindSchema = z7.enum(["introduced", "repointed", "retired"]);
@@ -3908,7 +3912,18 @@ var ga4SessionHistoryEntrySchema = z24.object({
3908
3912
  date: z24.string(),
3909
3913
  sessions: z24.number(),
3910
3914
  organicSessions: z24.number(),
3911
- users: z24.number()
3915
+ /**
3916
+ * Unique visitors for the day. Deduplicated by GA when `usersSource` is
3917
+ * `deduplicated`; a landing-page sum (which overcounts multi-page visitors)
3918
+ * for days synced before per-day totals were captured.
3919
+ */
3920
+ users: z24.number(),
3921
+ /**
3922
+ * How `users` was derived. `deduplicated` matches the GA UI's active users;
3923
+ * `landing-page-sum` is the legacy overcount kept so historical days still
3924
+ * render. Never compare the two across a series without saying which is which.
3925
+ */
3926
+ usersSource: z24.enum(["deduplicated", "landing-page-sum"])
3912
3927
  });
3913
3928
 
3914
3929
  // ../contracts/src/answer-visibility.ts
@@ -6833,10 +6848,19 @@ var adsOperationStepDtoSchema = z35.discriminatedUnion("state", [
6833
6848
  adsOperationStepRollbackFailedDtoSchema,
6834
6849
  adsOperationStepUnknownDtoSchema
6835
6850
  ]);
6851
+ var AdsActivationVersionPolicies = {
6852
+ exact: "exact",
6853
+ refreshSemanticallyUnchanged: "refresh_semantically_unchanged"
6854
+ };
6855
+ var adsActivationVersionPolicySchema = z35.enum([
6856
+ AdsActivationVersionPolicies.exact,
6857
+ AdsActivationVersionPolicies.refreshSemanticallyUnchanged
6858
+ ]);
6836
6859
  var adsActivationGrantCreateRequestSchema = z35.object({
6837
6860
  manifest: adsActivationManifestSchema,
6838
6861
  executorApiKeyId: adsEntityIdSchema,
6839
- expiresAt: adsActivationIsoTimestampSchema
6862
+ expiresAt: adsActivationIsoTimestampSchema,
6863
+ versionPolicy: adsActivationVersionPolicySchema.default(AdsActivationVersionPolicies.exact)
6840
6864
  }).strict();
6841
6865
  var adsActivationGrantResponseSchema = z35.object({
6842
6866
  grant: adsActivationGrantDtoSchema
@@ -7140,6 +7164,7 @@ export {
7140
7164
  formatDeltaCopy,
7141
7165
  formatAverageDelta,
7142
7166
  formatWindowCountDelta,
7167
+ compactDateToIso,
7143
7168
  evaluateModelPointerExposure,
7144
7169
  buildModelChangeNotice,
7145
7170
  googleConnectionDtoSchema,
@@ -7425,6 +7450,7 @@ export {
7425
7450
  adsOperationResponseSchema,
7426
7451
  adsActivationGrantDtoSchema,
7427
7452
  adsOperationStepDtoSchema,
7453
+ AdsActivationVersionPolicies,
7428
7454
  adsActivationGrantCreateRequestSchema,
7429
7455
  adsActivationGrantResponseSchema,
7430
7456
  adsActivationGrantRevokeRequestSchema,
@@ -37,7 +37,7 @@ import {
37
37
  trafficConnectVercelRequestSchema,
38
38
  trafficConnectWordpressRequestSchema,
39
39
  trafficEventKindSchema
40
- } from "./chunk-MMFSI3OG.js";
40
+ } from "./chunk-RFLNTI7Y.js";
41
41
 
42
42
  // src/config.ts
43
43
  import fs from "fs";
@@ -7,6 +7,7 @@ import {
7
7
  AI_PROVIDER_INFRA_DOMAINS,
8
8
  AdsActivationEntityTypes,
9
9
  AdsActivationGrantStates,
10
+ AdsActivationVersionPolicies,
10
11
  AdsAdGroupBillingEventTypes,
11
12
  AdsCampaignBiddingTypes,
12
13
  AdsEntityStatuses,
@@ -141,6 +142,7 @@ import {
141
142
  classifyTrafficPath,
142
143
  clusterByCosine,
143
144
  coerceSkillManifest,
145
+ compactDateToIso,
144
146
  competitorBatchRequestSchema,
145
147
  competitorDtoSchema,
146
148
  computeDedupSimilarityStats,
@@ -334,7 +336,7 @@ import {
334
336
  wordpressSchemaDeployResultDtoSchema,
335
337
  wordpressSchemaStatusResultDtoSchema,
336
338
  wordpressStatusDtoSchema
337
- } from "./chunk-MMFSI3OG.js";
339
+ } from "./chunk-RFLNTI7Y.js";
338
340
 
339
341
  // src/intelligence-service.ts
340
342
  import { eq as eq41, desc as desc18, asc as asc9, and as and32, ne as ne6, or as or8, inArray as inArray15, gte as gte10, lte as lte6 } from "drizzle-orm";
@@ -378,6 +380,7 @@ __export(schema_exports, {
378
380
  domainClassifications: () => domainClassifications,
379
381
  gaAiReferrals: () => gaAiReferrals,
380
382
  gaConnections: () => gaConnections,
383
+ gaDailyTotals: () => gaDailyTotals,
381
384
  gaSocialReferrals: () => gaSocialReferrals,
382
385
  gaTrafficSnapshots: () => gaTrafficSnapshots,
383
386
  gaTrafficSummaries: () => gaTrafficSummaries,
@@ -823,6 +826,20 @@ var gaTrafficSnapshots = sqliteTable("ga_traffic_snapshots", {
823
826
  index("idx_ga_traffic_page_normalized").on(table.projectId, table.date, table.landingPageNormalized),
824
827
  index("idx_ga_traffic_run").on(table.syncRunId)
825
828
  ]);
829
+ var gaDailyTotals = sqliteTable("ga_daily_totals", {
830
+ id: text("id").primaryKey(),
831
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
832
+ date: text("date").notNull(),
833
+ sessions: integer("sessions").notNull().default(0),
834
+ users: integer("users").notNull().default(0),
835
+ syncedAt: text("synced_at").notNull(),
836
+ syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" }),
837
+ createdAt: text("created_at").notNull()
838
+ }, (table) => [
839
+ uniqueIndex("idx_ga_daily_totals_project_date").on(table.projectId, table.date),
840
+ index("idx_ga_daily_totals_project").on(table.projectId),
841
+ index("idx_ga_daily_totals_run").on(table.syncRunId)
842
+ ]);
826
843
  var gaAiReferrals = sqliteTable("ga_ai_referrals", {
827
844
  id: text("id").primaryKey(),
828
845
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
@@ -3929,6 +3946,31 @@ var MIGRATION_VERSIONS = [
3929
3946
  run: (tx) => {
3930
3947
  backfillQuerySnapshotServedModel(tx);
3931
3948
  }
3949
+ },
3950
+ {
3951
+ // `users` is not additive across landing pages — one visitor who lands on
3952
+ // three pages is one user but contributes to three `ga_traffic_snapshots`
3953
+ // rows. Summing that dimensioned table overcounts the day. This table
3954
+ // stores the same day fetched with NO landing-page dimension, so GA does
3955
+ // the dedup and the stored number matches the GA UI. Same shape and
3956
+ // rationale as `gsc_daily_totals`.
3957
+ version: 106,
3958
+ name: "ga-daily-totals",
3959
+ statements: [
3960
+ `CREATE TABLE IF NOT EXISTS ga_daily_totals (
3961
+ id TEXT PRIMARY KEY,
3962
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3963
+ date TEXT NOT NULL,
3964
+ sessions INTEGER NOT NULL DEFAULT 0,
3965
+ users INTEGER NOT NULL DEFAULT 0,
3966
+ synced_at TEXT NOT NULL,
3967
+ sync_run_id TEXT REFERENCES runs(id) ON DELETE CASCADE,
3968
+ created_at TEXT NOT NULL
3969
+ )`,
3970
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_daily_totals_project_date ON ga_daily_totals(project_id, date)`,
3971
+ `CREATE INDEX IF NOT EXISTS idx_ga_daily_totals_project ON ga_daily_totals(project_id)`,
3972
+ `CREATE INDEX IF NOT EXISTS idx_ga_daily_totals_run ON ga_daily_totals(sync_run_id)`
3973
+ ]
3932
3974
  }
3933
3975
  ];
3934
3976
  function rebuildBacklinkTableWithSource(tx, table) {
@@ -18582,7 +18624,13 @@ var routeCatalog = [
18582
18624
  properties: {
18583
18625
  manifest: adsActivationManifestSchema2,
18584
18626
  executorApiKeyId: adsEntityIdSchema,
18585
- expiresAt: { type: "string", format: "date-time" }
18627
+ expiresAt: { type: "string", format: "date-time" },
18628
+ versionPolicy: {
18629
+ type: "string",
18630
+ enum: ["exact", "refresh_semantically_unchanged"],
18631
+ default: "exact",
18632
+ description: "Exact rejects any provider version drift. refresh_semantically_unchanged rebinds versions only when the live paused tree still matches its durable create receipts field-for-field."
18633
+ }
18586
18634
  }
18587
18635
  }
18588
18636
  }
@@ -22475,9 +22523,7 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
22475
22523
  row.directSessions = directMap.get(key) ?? 0;
22476
22524
  }
22477
22525
  for (const row of rows) {
22478
- if (row.date.length === 8 && !row.date.includes("-")) {
22479
- row.date = `${row.date.slice(0, 4)}-${row.date.slice(4, 6)}-${row.date.slice(6, 8)}`;
22480
- }
22526
+ row.date = compactDateToIso(row.date);
22481
22527
  }
22482
22528
  ga4Log("info", "fetch-traffic.done", { propertyId, rowCount: rows.length });
22483
22529
  return rows;
@@ -22544,6 +22590,28 @@ async function fetchAggregateSummary(accessToken, propertyId, days) {
22544
22590
  ga4Log("info", "fetch-aggregate.done", { propertyId, ...summary });
22545
22591
  return summary;
22546
22592
  }
22593
+ async function fetchDailyTotals(accessToken, propertyId, days) {
22594
+ validateAccessToken2(accessToken);
22595
+ validatePropertyId(propertyId);
22596
+ const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
22597
+ const endDate = /* @__PURE__ */ new Date();
22598
+ const startDate = /* @__PURE__ */ new Date();
22599
+ startDate.setDate(startDate.getDate() - syncDays);
22600
+ ga4Log("info", "fetch-daily-totals.start", { propertyId, days: syncDays });
22601
+ const res = await runReport(accessToken, propertyId, {
22602
+ dateRanges: [{ startDate: formatDate2(startDate), endDate: formatDate2(endDate) }],
22603
+ dimensions: [{ name: GA4_DIMENSIONS.date }],
22604
+ metrics: [{ name: GA4_METRICS.sessions }, { name: GA4_METRICS.totalUsers }],
22605
+ limit: syncDays + 1
22606
+ });
22607
+ const rows = (res.rows ?? []).map((row) => ({
22608
+ date: compactDateToIso(row.dimensionValues[0]?.value ?? ""),
22609
+ sessions: parseInt(row.metricValues[0]?.value ?? "0", 10) || 0,
22610
+ users: parseInt(row.metricValues[1]?.value ?? "0", 10) || 0
22611
+ })).filter((row) => row.date.length > 0);
22612
+ ga4Log("info", "fetch-daily-totals.done", { propertyId, days: syncDays, rows: rows.length });
22613
+ return rows;
22614
+ }
22547
22615
  var WINDOW_DAYS = { "7d": 7, "30d": 30, "90d": 90 };
22548
22616
  async function fetchWindowSummary(accessToken, propertyId, windowKey) {
22549
22617
  validateAccessToken2(accessToken);
@@ -22691,9 +22759,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
22691
22759
  }
22692
22760
  const dedupedRows = [...deduped.values()];
22693
22761
  for (const row of dedupedRows) {
22694
- if (row.date.length === 8 && !row.date.includes("-")) {
22695
- row.date = `${row.date.slice(0, 4)}-${row.date.slice(4, 6)}-${row.date.slice(6, 8)}`;
22696
- }
22762
+ row.date = compactDateToIso(row.date);
22697
22763
  }
22698
22764
  ga4Log("info", "fetch-ai-referrals.done", { propertyId, rowCount: dedupedRows.length });
22699
22765
  return dedupedRows;
@@ -22754,9 +22820,7 @@ async function fetchSocialReferrals(accessToken, propertyId, days) {
22754
22820
  if (pageRows.length < PAGE_SIZE || offset >= totalRows) break;
22755
22821
  }
22756
22822
  for (const row of rows) {
22757
- if (row.date.length === 8 && !row.date.includes("-")) {
22758
- row.date = `${row.date.slice(0, 4)}-${row.date.slice(4, 6)}-${row.date.slice(6, 8)}`;
22759
- }
22823
+ row.date = compactDateToIso(row.date);
22760
22824
  }
22761
22825
  ga4Log("info", "fetch-social-referrals.done", { propertyId, rowCount: rows.length });
22762
22826
  return rows;
@@ -24891,6 +24955,81 @@ async function preflightAdsActivationApproval(provider, request) {
24891
24955
  }
24892
24956
  return { manifest, manifestHash: hashAdsActivationManifest(manifest) };
24893
24957
  }
24958
+ async function refreshSemanticallyUnchangedAdsActivationManifest(provider, request) {
24959
+ const manifest = canonicalManifest(request.manifest);
24960
+ await validateAccount(provider, request.adAccountId);
24961
+ await validateExactDescendants(provider, manifest, {
24962
+ entityType: AdsActivationEntityTypes.campaign,
24963
+ entityId: manifest.campaign.id,
24964
+ parentId: null,
24965
+ expectedUpdatedAt: manifest.campaign.expectedUpdatedAt
24966
+ });
24967
+ const versions = /* @__PURE__ */ new Map();
24968
+ for (const target of activationTargets(manifest)) {
24969
+ let entity;
24970
+ try {
24971
+ entity = await readTarget(provider, target);
24972
+ } catch {
24973
+ throw targetError(
24974
+ AdsActivationErrorCodes.providerReadFailed,
24975
+ "An ads entity could not be verified for version refresh",
24976
+ 502,
24977
+ target
24978
+ );
24979
+ }
24980
+ validateIdentity(target, entity);
24981
+ validateAdApproval(target, entity);
24982
+ if (entity.status !== PROVIDER_PAUSED) {
24983
+ throw targetError(AdsActivationErrorCodes.entityNotPaused, "Every approved ads entity must still be paused", 409, target);
24984
+ }
24985
+ if (!Number.isInteger(entity.updatedAt) || entity.updatedAt === null || entity.updatedAt < 0) {
24986
+ throw targetError(
24987
+ AdsActivationErrorCodes.providerReadFailed,
24988
+ "An ads entity did not report a usable provider version",
24989
+ 502,
24990
+ target
24991
+ );
24992
+ }
24993
+ if (!request.semanticallyMatchesMaterialization(target.entityType, target.entityId, entity)) {
24994
+ throw targetError(
24995
+ AdsActivationErrorCodes.entityMismatch,
24996
+ "An ads entity no longer matches its materialized approved fields",
24997
+ 409,
24998
+ target
24999
+ );
25000
+ }
25001
+ versions.set(`${target.entityType}:${target.entityId}`, entity.updatedAt);
25002
+ }
25003
+ const version = (entityType, entityId) => {
25004
+ const current = versions.get(`${entityType}:${entityId}`);
25005
+ if (current === void 0) {
25006
+ throw new AdsActivationError(
25007
+ AdsActivationErrorCodes.persistenceFailed,
25008
+ "A refreshed ads entity version is missing",
25009
+ 500
25010
+ );
25011
+ }
25012
+ return current;
25013
+ };
25014
+ const refreshed = {
25015
+ campaign: {
25016
+ id: manifest.campaign.id,
25017
+ expectedUpdatedAt: version(AdsActivationEntityTypes.campaign, manifest.campaign.id),
25018
+ adGroups: manifest.campaign.adGroups.map((group) => ({
25019
+ id: group.id,
25020
+ expectedUpdatedAt: version(AdsActivationEntityTypes.ad_group, group.id),
25021
+ ads: group.ads.map((ad) => ({
25022
+ id: ad.id,
25023
+ expectedUpdatedAt: version(AdsActivationEntityTypes.ad, ad.id)
25024
+ }))
25025
+ }))
25026
+ }
25027
+ };
25028
+ return preflightAdsActivationApproval(provider, {
25029
+ adAccountId: request.adAccountId,
25030
+ manifest: refreshed
25031
+ });
25032
+ }
24894
25033
  function stepTarget(manifest, step) {
24895
25034
  const target = activationTargets(manifest).find(
24896
25035
  (candidate) => candidate.entityType === step.entityType && candidate.entityId === step.entityId
@@ -26462,7 +26601,11 @@ function registerAdsActivationRoutes(app, opts) {
26462
26601
  const runtime = await opts.resolveRuntime(project);
26463
26602
  let preflight;
26464
26603
  try {
26465
- preflight = await preflightAdsActivationApproval(runtime.provider, {
26604
+ preflight = body.versionPolicy === AdsActivationVersionPolicies.refreshSemanticallyUnchanged ? await refreshSemanticallyUnchangedAdsActivationManifest(runtime.provider, {
26605
+ adAccountId: runtime.adAccountId,
26606
+ manifest: body.manifest,
26607
+ semanticallyMatchesMaterialization: runtime.semanticallyMatchesMaterialization ?? (() => false)
26608
+ }) : await preflightAdsActivationApproval(runtime.provider, {
26466
26609
  adAccountId: runtime.adAccountId,
26467
26610
  manifest: body.manifest
26468
26611
  });
@@ -26903,6 +27046,44 @@ function entityMatchesReconcileFields(entity, desired, fingerprint) {
26903
27046
  const candidateFingerprint = requestHash(selected);
26904
27047
  return fingerprint === null ? candidateFingerprint === requestHash(desired) : candidateFingerprint === fingerprint;
26905
27048
  }
27049
+ function semanticallyMatchesMaterializedEntity(app, input) {
27050
+ const createKind = (() => {
27051
+ switch (input.entityType) {
27052
+ case AdsActivationEntityTypes.campaign:
27053
+ return AdsOperationKinds.campaign_create;
27054
+ case AdsActivationEntityTypes.ad_group:
27055
+ return AdsOperationKinds.ad_group_create;
27056
+ case AdsActivationEntityTypes.ad:
27057
+ return AdsOperationKinds.ad_create;
27058
+ }
27059
+ })();
27060
+ const updateKind = (() => {
27061
+ switch (input.entityType) {
27062
+ case AdsActivationEntityTypes.campaign:
27063
+ return AdsOperationKinds.campaign_update;
27064
+ case AdsActivationEntityTypes.ad_group:
27065
+ return AdsOperationKinds.ad_group_update;
27066
+ case AdsActivationEntityTypes.ad:
27067
+ return AdsOperationKinds.ad_update;
27068
+ }
27069
+ })();
27070
+ const rows = app.db.select().from(adsOperations).where(and20(
27071
+ eq26(adsOperations.projectId, input.projectId),
27072
+ eq26(adsOperations.entityType, input.entityType),
27073
+ eq26(adsOperations.entityId, input.entityId),
27074
+ inArray12(adsOperations.kind, [createKind, updateKind])
27075
+ )).orderBy(asc6(adsOperations.createdAt), asc6(adsOperations.id)).all();
27076
+ if (rows.some((row) => row.state === AdsOperationStates.pending || row.state === AdsOperationStates.reconciling || row.state === AdsOperationStates.unknown || row.kind === updateKind && row.state === AdsOperationStates.succeeded)) {
27077
+ return false;
27078
+ }
27079
+ const creates = rows.filter((row) => row.kind === createKind && row.state === AdsOperationStates.succeeded);
27080
+ if (creates.length !== 1) return false;
27081
+ const create = creates[0];
27082
+ if (create.adAccountId !== input.adAccountId || create.reconcileStrategy !== AdsReconcileStrategies.create_fingerprint || !create.reconcileFields) {
27083
+ return false;
27084
+ }
27085
+ return entityMatchesReconcileFields(input.entity, create.reconcileFields, null);
27086
+ }
26906
27087
  function reconcileFieldsWithoutParent(fields) {
26907
27088
  const { campaignId: _campaignId, adGroupId: _adGroupId, ...withoutParent } = fields;
26908
27089
  return withoutParent;
@@ -27686,6 +27867,16 @@ async function adsRoutes(app, opts) {
27686
27867
  }
27687
27868
  return {
27688
27869
  adAccountId,
27870
+ semanticallyMatchesMaterialization: (entityType, entityId, entity) => {
27871
+ if (entity.status === null || entity.updatedAt === null) return false;
27872
+ return semanticallyMatchesMaterializedEntity(app, {
27873
+ projectId: project.id,
27874
+ adAccountId,
27875
+ entityType,
27876
+ entityId,
27877
+ entity: { ...entity, status: entity.status, updatedAt: entity.updatedAt }
27878
+ });
27879
+ },
27689
27880
  provider: {
27690
27881
  getAccount: refreshAccount,
27691
27882
  getCampaign: (id) => operator.getCampaign(apiKey, id),
@@ -29234,6 +29425,23 @@ async function cdpRoutes(app, opts) {
29234
29425
  // ../api-routes/src/ga.ts
29235
29426
  import crypto24 from "crypto";
29236
29427
  import { eq as eq29, desc as desc13, and as and23, sql as sql12 } from "drizzle-orm";
29428
+
29429
+ // ../api-routes/src/ga-session-history.ts
29430
+ function buildSessionHistory(landingPageDays, dailyTotals) {
29431
+ const totalsByDate = new Map(dailyTotals.map((row) => [row.date, row.users]));
29432
+ return landingPageDays.map((day) => {
29433
+ const deduplicated = totalsByDate.get(day.date);
29434
+ return {
29435
+ date: day.date,
29436
+ sessions: day.sessions,
29437
+ organicSessions: day.organicSessions,
29438
+ users: deduplicated ?? day.users,
29439
+ usersSource: deduplicated === void 0 ? "landing-page-sum" : "deduplicated"
29440
+ };
29441
+ });
29442
+ }
29443
+
29444
+ // ../api-routes/src/ga.ts
29237
29445
  function gaLog(level, action, ctx) {
29238
29446
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
29239
29447
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -29512,6 +29720,7 @@ async function ga4Routes(app, opts) {
29512
29720
  }
29513
29721
  app.db.delete(gaTrafficSnapshots).where(eq29(gaTrafficSnapshots.projectId, project.id)).run();
29514
29722
  app.db.delete(gaTrafficSummaries).where(eq29(gaTrafficSummaries.projectId, project.id)).run();
29723
+ app.db.delete(gaDailyTotals).where(eq29(gaDailyTotals.projectId, project.id)).run();
29515
29724
  app.db.delete(gaAiReferrals).where(eq29(gaAiReferrals.projectId, project.id)).run();
29516
29725
  app.db.delete(gaSocialReferrals).where(eq29(gaSocialReferrals.projectId, project.id)).run();
29517
29726
  const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
@@ -29576,7 +29785,8 @@ async function ga4Routes(app, opts) {
29576
29785
  const WINDOW_KEYS = ["7d", "30d", "90d"];
29577
29786
  const fetches = [
29578
29787
  fetchAggregateSummary(accessToken, propertyId, days),
29579
- ...WINDOW_KEYS.map((w) => fetchWindowSummary(accessToken, propertyId, w))
29788
+ ...WINDOW_KEYS.map((w) => fetchWindowSummary(accessToken, propertyId, w)),
29789
+ fetchDailyTotals(accessToken, propertyId, days)
29580
29790
  ];
29581
29791
  if (syncTraffic) fetches.push(fetchTrafficByLandingPage(accessToken, propertyId, days));
29582
29792
  if (syncAi) fetches.push(fetchAiReferrals(accessToken, propertyId, days));
@@ -29584,7 +29794,8 @@ async function ga4Routes(app, opts) {
29584
29794
  const results = await Promise.all(fetches);
29585
29795
  const summary = results[0];
29586
29796
  const windowSummaries = results.slice(1, 1 + WINDOW_KEYS.length);
29587
- let idx = 1 + WINDOW_KEYS.length;
29797
+ const dailyTotals = results[1 + WINDOW_KEYS.length];
29798
+ let idx = 2 + WINDOW_KEYS.length;
29588
29799
  if (syncTraffic) {
29589
29800
  rows = results[idx++];
29590
29801
  }
@@ -29620,6 +29831,25 @@ async function ga4Routes(app, opts) {
29620
29831
  }).run();
29621
29832
  }
29622
29833
  }
29834
+ tx.delete(gaDailyTotals).where(
29835
+ and23(
29836
+ eq29(gaDailyTotals.projectId, project.id),
29837
+ sql12`${gaDailyTotals.date} >= ${summary.periodStart}`,
29838
+ sql12`${gaDailyTotals.date} <= ${summary.periodEnd}`
29839
+ )
29840
+ ).run();
29841
+ for (const row of dailyTotals) {
29842
+ tx.insert(gaDailyTotals).values({
29843
+ id: crypto24.randomUUID(),
29844
+ projectId: project.id,
29845
+ date: row.date,
29846
+ sessions: row.sessions,
29847
+ users: row.users,
29848
+ syncedAt: now,
29849
+ syncRunId: runId,
29850
+ createdAt: now
29851
+ }).run();
29852
+ }
29623
29853
  if (syncAi) {
29624
29854
  tx.delete(gaAiReferrals).where(
29625
29855
  and23(
@@ -30116,12 +30346,18 @@ async function ga4Routes(app, opts) {
30116
30346
  organicSessions: sql12`SUM(${gaTrafficSnapshots.organicSessions})`,
30117
30347
  users: sql12`SUM(${gaTrafficSnapshots.users})`
30118
30348
  }).from(gaTrafficSnapshots).where(and23(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
30119
- return rows.map((r) => ({
30120
- date: r.date,
30121
- sessions: r.sessions ?? 0,
30122
- organicSessions: r.organicSessions ?? 0,
30123
- users: r.users ?? 0
30124
- }));
30349
+ const totalConditions = [eq29(gaDailyTotals.projectId, project.id)];
30350
+ if (cutoffDate) totalConditions.push(sql12`${gaDailyTotals.date} >= ${cutoffDate}`);
30351
+ const totals = app.db.select({ date: gaDailyTotals.date, users: gaDailyTotals.users }).from(gaDailyTotals).where(and23(...totalConditions)).all();
30352
+ return buildSessionHistory(
30353
+ rows.map((r) => ({
30354
+ date: r.date,
30355
+ sessions: r.sessions ?? 0,
30356
+ organicSessions: r.organicSessions ?? 0,
30357
+ users: r.users ?? 0
30358
+ })),
30359
+ totals
30360
+ );
30125
30361
  });
30126
30362
  app.get("/projects/:name/ga/coverage", async (request, _reply) => {
30127
30363
  const project = resolveProject(app.db, request.params.name);
package/dist/cli.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  setTelemetrySource,
30
30
  showFirstRunNotice,
31
31
  trackEvent
32
- } from "./chunk-JYBNBK25.js";
32
+ } from "./chunk-DN5H2ZN7.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-HJIIJI7K.js";
49
+ } from "./chunk-WA57WG6W.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-4AZ7VES6.js";
57
+ } from "./chunk-YPRUBNUK.js";
58
58
  import {
59
59
  AdsOperationKinds,
60
60
  AdsOperationStates,
@@ -95,7 +95,7 @@ import {
95
95
  providerQuotaPolicySchema,
96
96
  resolveProviderInput,
97
97
  winnabilityClassSchema
98
- } from "./chunk-MMFSI3OG.js";
98
+ } from "./chunk-RFLNTI7Y.js";
99
99
 
100
100
  // src/cli.ts
101
101
  import { pathToFileURL } from "url";
@@ -3763,10 +3763,16 @@ async function gaSessionHistory(project, opts) {
3763
3763
  console.log(` ${"DATE".padEnd(dateWidth)} ${"SESSIONS".padEnd(10)}${"ORGANIC".padEnd(10)}${"USERS".padEnd(8)}`);
3764
3764
  console.log(` ${"\u2500".repeat(dateWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
3765
3765
  for (const row of result) {
3766
+ const users = row.usersSource === "deduplicated" ? String(row.users) : `${row.users}*`;
3766
3767
  console.log(
3767
- ` ${row.date.padEnd(dateWidth)} ${String(row.sessions).padEnd(10)}${String(row.organicSessions).padEnd(10)}${String(row.users).padEnd(8)}`
3768
+ ` ${row.date.padEnd(dateWidth)} ${String(row.sessions).padEnd(10)}${String(row.organicSessions).padEnd(10)}${users.padEnd(8)}`
3768
3769
  );
3769
3770
  }
3771
+ if (result.some((row) => row.usersSource !== "deduplicated")) {
3772
+ console.log("\n * Visitor count is approximate \u2014 it adds up each page a visitor landed on,");
3773
+ console.log(" so someone who read several pages is counted more than once. Run");
3774
+ console.log(` "canonry ga sync ${project}" to replace these with exact counts.`);
3775
+ }
3770
3776
  }
3771
3777
  async function gaCoverage(project, format) {
3772
3778
  const client = getClient6();