@canonry/canonry 4.130.0 → 4.132.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 (25) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +7 -0
  2. package/assets/assets/{AuditHistoryPanel-Ydzb2Y_Y.js → AuditHistoryPanel-Carj_FCP.js} +1 -1
  3. package/assets/assets/{BacklinksPage-DYodEusK.js → BacklinksPage-CqOTDVIn.js} +1 -1
  4. package/assets/assets/{ChartPrimitives-D67hsnF7.js → ChartPrimitives-EuQ82l0S.js} +1 -1
  5. package/assets/assets/{HistoryPage-_h9ITL1w.js → HistoryPage-D6XQfiLX.js} +1 -1
  6. package/assets/assets/{ProjectPage-fCJKCcHE.js → ProjectPage-Dswa9Ah6.js} +1 -1
  7. package/assets/assets/{RunRow-DzUNyTE-.js → RunRow-ThryEZFR.js} +1 -1
  8. package/assets/assets/{RunsPage-FTvAjptk.js → RunsPage-RFs2WkTC.js} +1 -1
  9. package/assets/assets/{SettingsPage-CvgpYEAt.js → SettingsPage-DoUccn9s.js} +1 -1
  10. package/assets/assets/{TrafficPage-qaMW9k6o.js → TrafficPage-CVvSDZPY.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-CRCso7_A.js → TrafficSourceDetailPage-BTUQUBeS.js} +1 -1
  12. package/assets/assets/{arrow-left-C--lHdP2.js → arrow-left-BoCNqusn.js} +1 -1
  13. package/assets/assets/{extract-error-message-DVqYbKpB.js → extract-error-message-BsfOVrKi.js} +1 -1
  14. package/assets/assets/{index-CdXn0JEf.js → index-jJurRky0.js} +102 -102
  15. package/assets/assets/{trash-2-CyKRU3tR.js → trash-2-BPb4368K.js} +1 -1
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-WIALMCXM.js → chunk-5FWAPJFX.js} +45 -1
  18. package/dist/{chunk-NVRJZU6V.js → chunk-7BD2ZOPV.js} +90 -0
  19. package/dist/{chunk-5XU6WEMK.js → chunk-QIJKS5KN.js} +1050 -304
  20. package/dist/{chunk-QR5U2K3O.js → chunk-RWTOEEG3.js} +4 -4
  21. package/dist/cli.js +67 -5
  22. package/dist/index.js +4 -4
  23. package/dist/{intelligence-service-53GUE6EI.js → intelligence-service-RU2ACYPY.js} +2 -2
  24. package/dist/mcp.js +2 -2
  25. package/package.json +10 -10
@@ -200,6 +200,9 @@ import {
200
200
  ga4SocialReferralHistoryEntrySchema,
201
201
  ga4StatusDtoSchema,
202
202
  ga4SyncResponseDtoSchema,
203
+ gaMeasurementAnalysisDtoSchema,
204
+ gaMeasurementAnalysisWindowSchema,
205
+ gaMeasurementHostScopeSchema,
203
206
  gateHarvestedSearchQueries,
204
207
  gbpAccountListResponseSchema,
205
208
  gbpAttributesListResponseSchema,
@@ -344,10 +347,10 @@ import {
344
347
  wordpressSchemaDeployResultDtoSchema,
345
348
  wordpressSchemaStatusResultDtoSchema,
346
349
  wordpressStatusDtoSchema
347
- } from "./chunk-NVRJZU6V.js";
350
+ } from "./chunk-7BD2ZOPV.js";
348
351
 
349
352
  // src/intelligence-service.ts
350
- import { eq as eq42, desc as desc19, asc as asc9, and as and33, ne as ne6, or as or8, inArray as inArray15, gte as gte10, lte as lte6 } from "drizzle-orm";
353
+ import { eq as eq43, desc as desc19, asc as asc9, and as and34, ne as ne6, or as or9, inArray as inArray15, gte as gte11, lte as lte7 } from "drizzle-orm";
351
354
 
352
355
  // ../db/src/client.ts
353
356
  import { mkdirSync } from "fs";
@@ -12620,31 +12623,31 @@ function renderAiReferrals(report) {
12620
12623
  </div>`
12621
12624
  );
12622
12625
  }
12623
- function serverActivityHeading(audience, hasData, windowDays) {
12626
+ function serverActivityHeading(audience, hasData, windowDays2) {
12624
12627
  const isClient = audience === "client";
12625
12628
  return {
12626
12629
  id: "server-activity",
12627
12630
  eyebrow: isClient ? "AI engine attention" : "Section 10",
12628
12631
  title: "AI Visibility \u2014 Server-Side",
12629
- intro: isClient ? hasData ? `What AI engines actually do in your server logs over the last ${windowDays} days \u2014 the other half of citations.` : "Live telemetry from your server logs." : "What AI engines actually do in your server logs \u2014 direct evidence, complementary to citations (which measure what they say)."
12632
+ intro: isClient ? hasData ? `What AI engines actually do in your server logs over the last ${windowDays2} days \u2014 the other half of citations.` : "Live telemetry from your server logs." : "What AI engines actually do in your server logs \u2014 direct evidence, complementary to citations (which measure what they say)."
12630
12633
  };
12631
12634
  }
12632
12635
  function renderServerActivity(report, audience) {
12633
12636
  const sa = report.serverActivity;
12634
12637
  const isClient = audience === "client";
12635
- const windowDays = report.meta.periodDays;
12636
- const windowLabel = `${windowDays}d`;
12637
- const priorWindowLabel = `vs prior ${windowDays} days`;
12638
+ const windowDays2 = report.meta.periodDays;
12639
+ const windowLabel = `${windowDays2}d`;
12640
+ const priorWindowLabel = `vs prior ${windowDays2} days`;
12638
12641
  if (!sa) {
12639
12642
  if (isClient) return "";
12640
12643
  return section(
12641
- serverActivityHeading("agency", false, windowDays),
12644
+ serverActivityHeading("agency", false, windowDays2),
12642
12645
  renderEmpty("Connect a server-side traffic source to surface what AI engines do directly in your server logs \u2014 distinct from GA4 click-throughs.")
12643
12646
  );
12644
12647
  }
12645
12648
  if (!sa.hasData) {
12646
12649
  return section(
12647
- serverActivityHeading(audience, false, windowDays),
12650
+ serverActivityHeading(audience, false, windowDays2),
12648
12651
  renderEmpty(isClient ? "Your server-side traffic source is connected. Numbers will appear after the next sync." : "Source connected \u2014 collecting your first data. Numbers will appear after the next sync.")
12649
12652
  );
12650
12653
  }
@@ -12677,7 +12680,7 @@ function renderServerActivity(report, audience) {
12677
12680
  <td class="numeric">${formatNumber(o.referralArrivals)}</td>
12678
12681
  </tr>`).join("");
12679
12682
  return section(
12680
- serverActivityHeading("client", true, windowDays),
12683
+ serverActivityHeading("client", true, windowDays2),
12681
12684
  `<div class="metric-grid">
12682
12685
  <div class="metric">
12683
12686
  <div class="label">AI bot requests observed</div>
@@ -12738,10 +12741,10 @@ function renderServerActivity(report, audience) {
12738
12741
  const trendChart = sa.dailyTrend.length > 0 ? renderLineChart(
12739
12742
  sa.dailyTrend.map((d) => ({ x: d.date, y: d.verifiedCrawlerHits, label: d.date.slice(5) })),
12740
12743
  COLORS.series[1],
12741
- `Verified crawler hits over time (last ${windowDays} days)`
12744
+ `Verified crawler hits over time (last ${windowDays2} days)`
12742
12745
  ) : "";
12743
12746
  return section(
12744
- serverActivityHeading("agency", true, windowDays),
12747
+ serverActivityHeading("agency", true, windowDays2),
12745
12748
  `<div class="metric-grid">
12746
12749
  <div class="metric">
12747
12750
  <div class="label">Verified crawler hits (${windowLabel})</div>
@@ -13271,11 +13274,11 @@ var TOP_CAMPAIGN_LIMIT = 10;
13271
13274
  var INSIGHT_LOOKBACK_RUNS = 5;
13272
13275
  var SERVER_ACTIVITY_TOP_PATHS_LIMIT = 10;
13273
13276
  var GA_WINDOW_SUMMARY_KEYS = { 7: "7d", 30: "30d", 90: "90d" };
13274
- function windowStartDate(endDate, windowDays) {
13277
+ function windowStartDate(endDate, windowDays2) {
13275
13278
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(endDate);
13276
13279
  if (!m) return endDate;
13277
13280
  const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
13278
- d.setUTCDate(d.getUTCDate() - (windowDays - 1));
13281
+ d.setUTCDate(d.getUTCDate() - (windowDays2 - 1));
13279
13282
  return d.toISOString().slice(0, 10);
13280
13283
  }
13281
13284
  function safeNum(value) {
@@ -13319,14 +13322,14 @@ function loadQueryLookup(db, projectId) {
13319
13322
  for (const row of rows) byId.set(row.id, row.query);
13320
13323
  return { byId };
13321
13324
  }
13322
- function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries, windowDays) {
13325
+ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries, windowDays2) {
13323
13326
  const allRows = db.select().from(gscSearchData).where(eq16(gscSearchData.projectId, projectId)).all();
13324
13327
  const allDailyTotals = readGscDailyTotals(db, projectId, "", "9999-12-31");
13325
13328
  let maxDate = "";
13326
13329
  for (const r of allRows) if (r.date > maxDate) maxDate = r.date;
13327
13330
  for (const r of allDailyTotals) if (r.date > maxDate) maxDate = r.date;
13328
13331
  if (!maxDate) return null;
13329
- const startDate = windowStartDate(maxDate, windowDays);
13332
+ const startDate = windowStartDate(maxDate, windowDays2);
13330
13333
  const rows = allRows.filter((r) => r.date >= startDate && r.date <= maxDate);
13331
13334
  const dailyTotals = allDailyTotals.filter((r) => r.date >= startDate && r.date <= maxDate);
13332
13335
  if (rows.length === 0 && dailyTotals.length === 0) return null;
@@ -13419,8 +13422,8 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
13419
13422
  gscButNotTracked
13420
13423
  };
13421
13424
  }
13422
- function buildGaSection(db, projectId, windowDays) {
13423
- const gaWindowKey = GA_WINDOW_SUMMARY_KEYS[windowDays];
13425
+ function buildGaSection(db, projectId, windowDays2) {
13426
+ const gaWindowKey = GA_WINDOW_SUMMARY_KEYS[windowDays2];
13424
13427
  const windowSummary = gaWindowKey ? db.select().from(gaTrafficWindowSummaries).where(
13425
13428
  and12(
13426
13429
  eq16(gaTrafficWindowSummaries.projectId, projectId),
@@ -13432,7 +13435,7 @@ function buildGaSection(db, projectId, windowDays) {
13432
13435
  if (!windowSummary && !fallbackSummary && allSnapshotRows.length === 0) return null;
13433
13436
  let snapshotMaxDate = "";
13434
13437
  for (const r of allSnapshotRows) if (r.date > snapshotMaxDate) snapshotMaxDate = r.date;
13435
- const snapshotStartDate = snapshotMaxDate ? windowStartDate(snapshotMaxDate, windowDays) : "";
13438
+ const snapshotStartDate = snapshotMaxDate ? windowStartDate(snapshotMaxDate, windowDays2) : "";
13436
13439
  const snapshotRows = snapshotStartDate ? allSnapshotRows.filter((r) => r.date >= snapshotStartDate && r.date <= snapshotMaxDate) : allSnapshotRows;
13437
13440
  const totalSessions = windowSummary?.totalSessions ?? fallbackSummary?.totalSessions ?? snapshotRows.reduce((s, r) => s + r.sessions, 0);
13438
13441
  const totalUsers = windowSummary?.totalUsers ?? fallbackSummary?.totalUsers ?? snapshotRows.reduce((s, r) => s + r.users, 0);
@@ -13544,8 +13547,8 @@ function buildSocialReferrals(db, projectId) {
13544
13547
  topCampaigns
13545
13548
  };
13546
13549
  }
13547
- function resolveAiReferralWindow(db, projectId, windowDays) {
13548
- const windowKey = GA_WINDOW_SUMMARY_KEYS[windowDays];
13550
+ function resolveAiReferralWindow(db, projectId, windowDays2) {
13551
+ const windowKey = GA_WINDOW_SUMMARY_KEYS[windowDays2];
13549
13552
  const windowSummary = windowKey ? db.select({ periodStart: gaTrafficWindowSummaries.periodStart, periodEnd: gaTrafficWindowSummaries.periodEnd }).from(gaTrafficWindowSummaries).where(and12(
13550
13553
  eq16(gaTrafficWindowSummaries.projectId, projectId),
13551
13554
  eq16(gaTrafficWindowSummaries.windowKey, windowKey)
@@ -13553,10 +13556,10 @@ function resolveAiReferralWindow(db, projectId, windowDays) {
13553
13556
  if (windowSummary) return { start: windowSummary.periodStart, end: windowSummary.periodEnd };
13554
13557
  const latestSummary = db.select({ periodEnd: gaTrafficSummaries.periodEnd }).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, projectId)).orderBy(desc7(gaTrafficSummaries.syncedAt)).get();
13555
13558
  const end = latestSummary?.periodEnd ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
13556
- return { start: windowStartDate(end, windowDays), end };
13559
+ return { start: windowStartDate(end, windowDays2), end };
13557
13560
  }
13558
- function buildAiReferrals(db, projectId, windowDays) {
13559
- const window = resolveAiReferralWindow(db, projectId, windowDays);
13561
+ function buildAiReferrals(db, projectId, windowDays2) {
13562
+ const window = resolveAiReferralWindow(db, projectId, windowDays2);
13560
13563
  const rows = db.select().from(gaAiReferrals).where(and12(
13561
13564
  eq16(gaAiReferrals.projectId, projectId),
13562
13565
  sql8`${gaAiReferrals.date} >= ${window.start}`,
@@ -13658,7 +13661,7 @@ function nonSubresourceReferralPathCondition() {
13658
13661
  AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.woff2'
13659
13662
  `;
13660
13663
  }
13661
- function buildServerActivity(db, projectId, windowDays) {
13664
+ function buildServerActivity(db, projectId, windowDays2) {
13662
13665
  const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
13663
13666
  and12(
13664
13667
  eq16(trafficSources.projectId, projectId),
@@ -13668,8 +13671,8 @@ function buildServerActivity(db, projectId, windowDays) {
13668
13671
  if (sourceRows.length === 0) return null;
13669
13672
  const now = /* @__PURE__ */ new Date();
13670
13673
  const headlineEnd = now.toISOString();
13671
- const headlineStartMs = now.getTime() - windowDays * 24 * 60 * 6e4;
13672
- const priorStartMs = headlineStartMs - windowDays * 24 * 60 * 6e4;
13674
+ const headlineStartMs = now.getTime() - windowDays2 * 24 * 60 * 6e4;
13675
+ const priorStartMs = headlineStartMs - windowDays2 * 24 * 60 * 6e4;
13673
13676
  const trendStartMs = headlineStartMs;
13674
13677
  const headlineStart = new Date(headlineStartMs).toISOString();
13675
13678
  const priorStart = new Date(priorStartMs).toISOString();
@@ -16511,6 +16514,7 @@ var SCHEMA_TABLE = {
16511
16514
  ResearchRunListDto: researchRunListSchema,
16512
16515
  DoctorReportDto: doctorReportSchema,
16513
16516
  GA4AiReferralHistoryEntry: ga4AiReferralHistoryEntrySchema,
16517
+ GA4MeasurementAnalysisDto: gaMeasurementAnalysisDtoSchema,
16514
16518
  GA4SessionHistoryEntry: ga4SessionHistoryEntrySchema,
16515
16519
  GA4SocialReferralHistoryEntry: ga4SocialReferralHistoryEntrySchema,
16516
16520
  GA4StatusDto: ga4StatusDtoSchema,
@@ -20122,6 +20126,47 @@ var routeCatalog = [
20122
20126
  404: errorResponse("Project not found.")
20123
20127
  }
20124
20128
  },
20129
+ {
20130
+ method: "get",
20131
+ path: "/api/v1/projects/{name}/ga/measurement-analysis",
20132
+ summary: "Get GA4 measurement analysis",
20133
+ tags: ["ga4"],
20134
+ parameters: [
20135
+ nameParameter,
20136
+ {
20137
+ name: "window",
20138
+ in: "query",
20139
+ description: "30-day cohorts to return.",
20140
+ schema: { type: "string", enum: ["30d", "60d", "90d"], default: "90d" }
20141
+ },
20142
+ {
20143
+ name: "hostScope",
20144
+ in: "query",
20145
+ description: "Configured marketing hosts or every observed host.",
20146
+ schema: { type: "string", enum: ["marketing", "all"], default: "marketing" }
20147
+ },
20148
+ {
20149
+ name: "pathPrefix",
20150
+ in: "query",
20151
+ description: "Optional boundary-safe normalized landing-page prefix.",
20152
+ schema: stringSchema
20153
+ },
20154
+ {
20155
+ name: "limit",
20156
+ in: "query",
20157
+ description: "Maximum page and query detail rows. Native channels are never truncated.",
20158
+ schema: { type: "integer", minimum: 1, maximum: 100, default: 100 }
20159
+ }
20160
+ ],
20161
+ responses: {
20162
+ 200: jsonResponse(
20163
+ "GA4 measurement analysis returned.",
20164
+ "GA4MeasurementAnalysisDto"
20165
+ ),
20166
+ 400: errorResponse("Invalid measurement analysis filters."),
20167
+ 404: errorResponse("Project not found.")
20168
+ }
20169
+ },
20125
20170
  {
20126
20171
  method: "post",
20127
20172
  path: "/api/v1/projects/{name}/ga/sync",
@@ -22059,9 +22104,9 @@ function computeFreshness(rows, asOfDate) {
22059
22104
  const pendingDays = dataThroughDate ? Math.max(0, daysBetween(dataThroughDate, asOfDate)) : 0;
22060
22105
  return { dataThroughDate, latestStoredDate: latestStoredDate || null, pendingDays };
22061
22106
  }
22062
- function buildTimeseries(rows, freshness, windowDays = 30) {
22107
+ function buildTimeseries(rows, freshness, windowDays2 = 30) {
22063
22108
  if (rows.length === 0 || freshness.latestStoredDate === null) return [];
22064
- const windowLow = shiftDate(freshness.latestStoredDate, -(windowDays - 1));
22109
+ const windowLow = shiftDate(freshness.latestStoredDate, -(windowDays2 - 1));
22065
22110
  const metricKeys = /* @__PURE__ */ new Set();
22066
22111
  const byDate = /* @__PURE__ */ new Map();
22067
22112
  for (const row of rows) {
@@ -22551,11 +22596,14 @@ var GA4_DIMENSIONS = {
22551
22596
  firstUserSource: "firstUserSource",
22552
22597
  firstUserMedium: "firstUserMedium",
22553
22598
  sessionDefaultChannelGrouping: "sessionDefaultChannelGrouping",
22554
- sessionDefaultChannelGroup: "sessionDefaultChannelGroup"
22599
+ sessionDefaultChannelGroup: "sessionDefaultChannelGroup",
22600
+ hostName: "hostName",
22601
+ eventName: "eventName"
22555
22602
  };
22556
22603
  var GA4_METRICS = {
22557
22604
  sessions: "sessions",
22558
- totalUsers: "totalUsers"
22605
+ totalUsers: "totalUsers",
22606
+ eventCount: "eventCount"
22559
22607
  };
22560
22608
 
22561
22609
  // ../integration-google-analytics/src/types.ts
@@ -22824,6 +22872,100 @@ async function batchRunReports(accessToken, propertyId, requests) {
22824
22872
  function formatDate2(d) {
22825
22873
  return d.toISOString().slice(0, 10);
22826
22874
  }
22875
+ function measurementDateRange(days) {
22876
+ const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
22877
+ const endDate = /* @__PURE__ */ new Date();
22878
+ const startDate = /* @__PURE__ */ new Date();
22879
+ startDate.setUTCDate(startDate.getUTCDate() - syncDays);
22880
+ return { syncDays, dateRange: { startDate: formatDate2(startDate), endDate: formatDate2(endDate) } };
22881
+ }
22882
+ async function fetchPagedReport(accessToken, propertyId, baseRequest, pageSize = 1e4) {
22883
+ const rows = [];
22884
+ let offset = 0;
22885
+ for (let page = 0; page < GA4_MAX_PAGES; page++) {
22886
+ const response = await runReport(accessToken, propertyId, { ...baseRequest, limit: pageSize, offset });
22887
+ const pageRows = response.rows ?? [];
22888
+ rows.push(...pageRows);
22889
+ offset += pageRows.length;
22890
+ if (pageRows.length < pageSize || response.rowCount !== void 0 && offset >= response.rowCount) break;
22891
+ if (page === GA4_MAX_PAGES - 1) {
22892
+ throw new Error(`GA4 report truncated: page limit (${GA4_MAX_PAGES}) reached before all rows were fetched`);
22893
+ }
22894
+ }
22895
+ return rows;
22896
+ }
22897
+ async function fetchAcquisitionByChannel(accessToken, propertyId, days, options) {
22898
+ validateAccessToken2(accessToken);
22899
+ validatePropertyId(propertyId);
22900
+ const { syncDays, dateRange } = measurementDateRange(days);
22901
+ const rows = await fetchPagedReport(accessToken, propertyId, {
22902
+ dateRanges: [dateRange],
22903
+ dimensions: [
22904
+ { name: GA4_DIMENSIONS.date },
22905
+ { name: GA4_DIMENSIONS.sessionDefaultChannelGroup },
22906
+ { name: GA4_DIMENSIONS.sessionSource },
22907
+ { name: GA4_DIMENSIONS.sessionMedium },
22908
+ { name: GA4_DIMENSIONS.hostName },
22909
+ { name: GA4_DIMENSIONS.landingPagePlusQueryString }
22910
+ ],
22911
+ metrics: [{ name: GA4_METRICS.sessions }]
22912
+ }, options?.pageSize);
22913
+ ga4Log("info", "fetch-acquisition.done", { propertyId, days: syncDays, rowCount: rows.length });
22914
+ return { ...dateRange, rows: rows.map((row) => ({
22915
+ date: compactDateToIso(row.dimensionValues[0]?.value ?? ""),
22916
+ channelGroup: row.dimensionValues[1]?.value ?? "(not set)",
22917
+ source: row.dimensionValues[2]?.value ?? "(not set)",
22918
+ medium: row.dimensionValues[3]?.value ?? "(not set)",
22919
+ hostName: row.dimensionValues[4]?.value ?? "(not set)",
22920
+ landingPage: row.dimensionValues[5]?.value ?? "(not set)",
22921
+ sessions: parseInt(row.metricValues[0]?.value ?? "0", 10) || 0
22922
+ })) };
22923
+ }
22924
+ function isLandingPageDimensionIncompatibility(error) {
22925
+ return error instanceof GA4ApiError && error.status === 400 && /landingPagePlusQueryString/i.test(error.message) && /incompatib/i.test(error.message);
22926
+ }
22927
+ async function fetchLeadEvents(accessToken, propertyId, eventNames, days) {
22928
+ validateAccessToken2(accessToken);
22929
+ validatePropertyId(propertyId);
22930
+ const { dateRange } = measurementDateRange(days);
22931
+ const filter = { orGroup: { expressions: eventNames.map((value) => ({ filter: {
22932
+ fieldName: GA4_DIMENSIONS.eventName,
22933
+ stringFilter: { matchType: "EXACT", value }
22934
+ } })) } };
22935
+ const dimensions = [
22936
+ { name: GA4_DIMENSIONS.date },
22937
+ { name: GA4_DIMENSIONS.eventName },
22938
+ { name: GA4_DIMENSIONS.sessionDefaultChannelGroup },
22939
+ { name: GA4_DIMENSIONS.sessionSource },
22940
+ { name: GA4_DIMENSIONS.sessionMedium }
22941
+ ];
22942
+ const request = (landingPage) => ({
22943
+ dateRanges: [dateRange],
22944
+ dimensions: landingPage ? [...dimensions, { name: GA4_DIMENSIONS.hostName }, { name: GA4_DIMENSIONS.landingPagePlusQueryString }] : dimensions,
22945
+ metrics: [{ name: GA4_METRICS.eventCount }],
22946
+ dimensionFilter: filter
22947
+ });
22948
+ let attributionScope = "landing-page";
22949
+ let rawRows;
22950
+ try {
22951
+ rawRows = await fetchPagedReport(accessToken, propertyId, request(true));
22952
+ } catch (error) {
22953
+ if (!isLandingPageDimensionIncompatibility(error)) throw error;
22954
+ attributionScope = "channel";
22955
+ rawRows = await fetchPagedReport(accessToken, propertyId, request(false));
22956
+ }
22957
+ const rows = rawRows.map((row) => ({
22958
+ date: compactDateToIso(row.dimensionValues[0]?.value ?? ""),
22959
+ eventName: row.dimensionValues[1]?.value ?? "(not set)",
22960
+ channelGroup: row.dimensionValues[2]?.value ?? "(not set)",
22961
+ source: row.dimensionValues[3]?.value ?? "(not set)",
22962
+ medium: row.dimensionValues[4]?.value ?? "(not set)",
22963
+ hostName: attributionScope === "landing-page" ? row.dimensionValues[5]?.value ?? "(not available)" : "(not available)",
22964
+ landingPage: attributionScope === "landing-page" ? row.dimensionValues[6]?.value ?? "(not available)" : "(not available)",
22965
+ eventCount: parseInt(row.metricValues[0]?.value ?? "0", 10) || 0
22966
+ }));
22967
+ return { ...dateRange, attributionScope, rows };
22968
+ }
22827
22969
  var AI_REFERRAL_SOURCE_FILTERS = [
22828
22970
  { matchType: "CONTAINS", value: "perplexity" },
22829
22971
  { matchType: "CONTAINS", value: "gemini" },
@@ -30050,6 +30192,134 @@ function normalizeAiTrafficClass(value) {
30050
30192
  function emptyAiCounts() {
30051
30193
  return { sessions: 0, users: 0 };
30052
30194
  }
30195
+ function persistAcquisitionMeasurement(db, input) {
30196
+ const { startDate, endDate } = input.report;
30197
+ db.transaction((tx) => {
30198
+ tx.delete(gaAcquisitionDaily).where(and23(
30199
+ eq29(gaAcquisitionDaily.projectId, input.projectId),
30200
+ sql13`${gaAcquisitionDaily.date} >= ${startDate}`,
30201
+ sql13`${gaAcquisitionDaily.date} <= ${endDate}`
30202
+ )).run();
30203
+ for (const row of input.report.rows) {
30204
+ tx.insert(gaAcquisitionDaily).values({
30205
+ id: crypto24.randomUUID(),
30206
+ projectId: input.projectId,
30207
+ date: row.date,
30208
+ channelGroup: row.channelGroup,
30209
+ source: row.source,
30210
+ medium: row.medium,
30211
+ hostName: row.hostName,
30212
+ landingPage: row.landingPage,
30213
+ landingPageNormalized: normalizeUrlPath(row.landingPage),
30214
+ sessions: row.sessions,
30215
+ syncedAt: input.syncedAt,
30216
+ syncRunId: input.runId,
30217
+ createdAt: input.syncedAt
30218
+ }).run();
30219
+ }
30220
+ tx.insert(gaMeasurementSyncStates).values({
30221
+ projectId: input.projectId,
30222
+ acquisitionStatus: "ready",
30223
+ acquisitionError: null,
30224
+ acquisitionSyncedAt: input.syncedAt,
30225
+ updatedAt: input.syncedAt
30226
+ }).onConflictDoUpdate({
30227
+ target: gaMeasurementSyncStates.projectId,
30228
+ set: {
30229
+ acquisitionStatus: "ready",
30230
+ acquisitionError: null,
30231
+ acquisitionSyncedAt: input.syncedAt,
30232
+ updatedAt: input.syncedAt
30233
+ }
30234
+ }).run();
30235
+ });
30236
+ }
30237
+ function persistLeadMeasurement(db, input) {
30238
+ const { startDate, endDate } = input.report;
30239
+ db.transaction((tx) => {
30240
+ tx.delete(gaLeadEventsDaily).where(and23(
30241
+ eq29(gaLeadEventsDaily.projectId, input.projectId),
30242
+ sql13`${gaLeadEventsDaily.date} >= ${startDate}`,
30243
+ sql13`${gaLeadEventsDaily.date} <= ${endDate}`
30244
+ )).run();
30245
+ for (const row of input.report.rows) {
30246
+ tx.insert(gaLeadEventsDaily).values({
30247
+ id: crypto24.randomUUID(),
30248
+ projectId: input.projectId,
30249
+ date: row.date,
30250
+ eventName: row.eventName,
30251
+ channelGroup: row.channelGroup,
30252
+ source: row.source,
30253
+ medium: row.medium,
30254
+ hostName: row.hostName,
30255
+ landingPage: row.landingPage,
30256
+ landingPageNormalized: input.report.attributionScope === "landing-page" ? normalizeUrlPath(row.landingPage) : null,
30257
+ attributionScope: input.report.attributionScope,
30258
+ eventCount: row.eventCount,
30259
+ syncedAt: input.syncedAt,
30260
+ syncRunId: input.runId,
30261
+ createdAt: input.syncedAt
30262
+ }).run();
30263
+ }
30264
+ tx.insert(gaMeasurementSyncStates).values({
30265
+ projectId: input.projectId,
30266
+ leadStatus: "ready",
30267
+ leadError: null,
30268
+ leadSyncedAt: input.syncedAt,
30269
+ leadAttributionScope: input.report.attributionScope,
30270
+ updatedAt: input.syncedAt
30271
+ }).onConflictDoUpdate({
30272
+ target: gaMeasurementSyncStates.projectId,
30273
+ set: {
30274
+ leadStatus: "ready",
30275
+ leadError: null,
30276
+ leadSyncedAt: input.syncedAt,
30277
+ leadAttributionScope: input.report.attributionScope,
30278
+ updatedAt: input.syncedAt
30279
+ }
30280
+ }).run();
30281
+ });
30282
+ }
30283
+ function resetLeadMeasurement(db, projectId, updatedAt) {
30284
+ db.transaction((tx) => {
30285
+ tx.delete(gaLeadEventsDaily).where(eq29(gaLeadEventsDaily.projectId, projectId)).run();
30286
+ tx.insert(gaMeasurementSyncStates).values({ projectId, leadStatus: "never-synced", leadError: null, leadSyncedAt: null, leadAttributionScope: null, updatedAt }).onConflictDoUpdate({
30287
+ target: gaMeasurementSyncStates.projectId,
30288
+ set: { leadStatus: "never-synced", leadError: null, leadSyncedAt: null, leadAttributionScope: null, updatedAt }
30289
+ }).run();
30290
+ });
30291
+ }
30292
+ function persistMeasurementError(db, projectId, component, message, updatedAt) {
30293
+ if (component === "acquisition") {
30294
+ db.insert(gaMeasurementSyncStates).values({
30295
+ projectId,
30296
+ acquisitionStatus: "error",
30297
+ acquisitionError: message,
30298
+ updatedAt
30299
+ }).onConflictDoUpdate({
30300
+ target: gaMeasurementSyncStates.projectId,
30301
+ set: {
30302
+ acquisitionStatus: "error",
30303
+ acquisitionError: message,
30304
+ updatedAt
30305
+ }
30306
+ }).run();
30307
+ return;
30308
+ }
30309
+ db.insert(gaMeasurementSyncStates).values({
30310
+ projectId,
30311
+ leadStatus: "error",
30312
+ leadError: message,
30313
+ updatedAt
30314
+ }).onConflictDoUpdate({
30315
+ target: gaMeasurementSyncStates.projectId,
30316
+ set: {
30317
+ leadStatus: "error",
30318
+ leadError: message,
30319
+ updatedAt
30320
+ }
30321
+ }).run();
30322
+ }
30053
30323
  function summarizeAiReferralCounts(rows) {
30054
30324
  const dedupeGroups = /* @__PURE__ */ new Map();
30055
30325
  const bySessionChannelGroup = /* @__PURE__ */ new Map();
@@ -30313,6 +30583,10 @@ async function ga4Routes(app, opts) {
30313
30583
  const syncSummary = true;
30314
30584
  const syncAi = !only || only === "ai";
30315
30585
  const syncSocial = !only || only === "social";
30586
+ const measurementState = app.db.select().from(gaMeasurementSyncStates).where(eq29(gaMeasurementSyncStates.projectId, project.id)).get();
30587
+ const acquisitionDays = measurementState?.acquisitionSyncedAt != null ? Math.min(Math.max(1, days), 90) : 90;
30588
+ const leadDays = measurementState?.leadSyncedAt != null ? Math.min(Math.max(1, days), 90) : 90;
30589
+ const leadEventNames = project.measurement.leadEventNames;
30316
30590
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
30317
30591
  const runId = crypto24.randomUUID();
30318
30592
  app.db.insert(runs).values({
@@ -30338,6 +30612,10 @@ async function ga4Routes(app, opts) {
30338
30612
  if (syncTraffic) fetches.push(fetchTrafficByLandingPage(accessToken, propertyId, days));
30339
30613
  if (syncAi) fetches.push(fetchAiReferrals(accessToken, propertyId, days));
30340
30614
  if (syncSocial) fetches.push(fetchSocialReferrals(accessToken, propertyId, days));
30615
+ const measurementFetch = Promise.allSettled([
30616
+ fetchAcquisitionByChannel(accessToken, propertyId, acquisitionDays),
30617
+ leadEventNames.length > 0 ? fetchLeadEvents(accessToken, propertyId, leadEventNames, leadDays) : Promise.resolve(null)
30618
+ ]);
30341
30619
  const results = await Promise.all(fetches);
30342
30620
  const summary = results[0];
30343
30621
  const windowSummaries = results.slice(1, 1 + WINDOW_KEYS.length);
@@ -30483,7 +30761,74 @@ async function ga4Routes(app, opts) {
30483
30761
  }
30484
30762
  }
30485
30763
  });
30486
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq29(runs.id, runId)).run();
30764
+ const [acquisitionSettled, leadsSettled] = await measurementFetch;
30765
+ const measurementNow = (/* @__PURE__ */ new Date()).toISOString();
30766
+ const recordMeasurementFailure = (component, reason) => {
30767
+ const message = reason instanceof Error ? reason.message : String(reason);
30768
+ gaLog("warn", "measurement.component-failed", {
30769
+ projectId: project.id,
30770
+ runId,
30771
+ component,
30772
+ error: message
30773
+ });
30774
+ try {
30775
+ persistMeasurementError(app.db, project.id, component, message, measurementNow);
30776
+ } catch (stateError) {
30777
+ gaLog("error", "measurement.state-write-failed", {
30778
+ projectId: project.id,
30779
+ runId,
30780
+ component,
30781
+ error: stateError instanceof Error ? stateError.message : String(stateError)
30782
+ });
30783
+ }
30784
+ return { status: "error", days: component === "acquisition" ? acquisitionDays : leadDays, rowCount: 0, error: message };
30785
+ };
30786
+ const acquisitionResult = (() => {
30787
+ if (acquisitionSettled.status === "rejected") {
30788
+ return recordMeasurementFailure("acquisition", acquisitionSettled.reason);
30789
+ }
30790
+ try {
30791
+ persistAcquisitionMeasurement(app.db, {
30792
+ projectId: project.id,
30793
+ runId,
30794
+ syncedAt: measurementNow,
30795
+ report: acquisitionSettled.value
30796
+ });
30797
+ return {
30798
+ status: "ready",
30799
+ days: acquisitionDays,
30800
+ rowCount: acquisitionSettled.value.rows.length
30801
+ };
30802
+ } catch (error) {
30803
+ return recordMeasurementFailure("acquisition", error);
30804
+ }
30805
+ })();
30806
+ const leadResult = (() => {
30807
+ if (leadsSettled.status === "rejected") {
30808
+ return recordMeasurementFailure("leads", leadsSettled.reason);
30809
+ }
30810
+ if (leadsSettled.value === null) {
30811
+ resetLeadMeasurement(app.db, project.id, measurementNow);
30812
+ return { status: "not-configured", days: 0, rowCount: 0 };
30813
+ }
30814
+ try {
30815
+ persistLeadMeasurement(app.db, {
30816
+ projectId: project.id,
30817
+ runId,
30818
+ syncedAt: measurementNow,
30819
+ report: leadsSettled.value
30820
+ });
30821
+ return {
30822
+ status: "ready",
30823
+ days: leadDays,
30824
+ rowCount: leadsSettled.value.rows.length,
30825
+ attributionScope: leadsSettled.value.attributionScope
30826
+ };
30827
+ } catch (error) {
30828
+ return recordMeasurementFailure("leads", error);
30829
+ }
30830
+ })();
30831
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: measurementNow }).where(eq29(runs.id, runId)).run();
30487
30832
  const syncedComponents = only ? [
30488
30833
  ...syncTraffic ? ["traffic"] : [],
30489
30834
  ...syncSummary ? ["summary"] : [],
@@ -30507,6 +30852,10 @@ async function ga4Routes(app, opts) {
30507
30852
  socialReferralCount: socialReferrals.length,
30508
30853
  days,
30509
30854
  syncedAt: now,
30855
+ measurement: {
30856
+ acquisition: acquisitionResult,
30857
+ leads: leadResult
30858
+ },
30510
30859
  ...syncedComponents ? { syncedComponents } : {}
30511
30860
  };
30512
30861
  } catch (e) {
@@ -30926,6 +31275,402 @@ async function ga4Routes(app, opts) {
30926
31275
  });
30927
31276
  }
30928
31277
 
31278
+ // ../api-routes/src/ga-measurement-analysis.ts
31279
+ import { and as and24, eq as eq30, gte as gte7, lte as lte5, or as or7, sql as sql14 } from "drizzle-orm";
31280
+ function addDays(date, offset) {
31281
+ const value = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
31282
+ value.setUTCDate(value.getUTCDate() + offset);
31283
+ return value.toISOString().slice(0, 10);
31284
+ }
31285
+ function windowDays(window) {
31286
+ if (window === "30d") return 30;
31287
+ if (window === "60d") return 60;
31288
+ return 90;
31289
+ }
31290
+ function buildPeriods(anchor, days) {
31291
+ const labels = days === 30 ? ["latest"] : days === 60 ? ["previous", "latest"] : ["earliest", "middle", "latest"];
31292
+ return labels.map((label, index2) => ({
31293
+ label,
31294
+ startDate: addDays(anchor, -days + index2 * 30 + 1),
31295
+ endDate: addDays(anchor, -days + (index2 + 1) * 30)
31296
+ }));
31297
+ }
31298
+ function aggregateByKey(rows, periods, keyOf, valueOf) {
31299
+ const result = /* @__PURE__ */ new Map();
31300
+ for (const row of rows) {
31301
+ const periodIndex = periods.findIndex(
31302
+ (period2) => row.date >= period2.startDate && row.date <= period2.endDate
31303
+ );
31304
+ if (periodIndex < 0) continue;
31305
+ const values = result.get(keyOf(row)) ?? Array(periods.length).fill(0);
31306
+ values[periodIndex] = (values[periodIndex] ?? 0) + valueOf(row);
31307
+ result.set(keyOf(row), values);
31308
+ }
31309
+ return result;
31310
+ }
31311
+ function sessionPeriods(periods, values) {
31312
+ return periods.map((period2, index2) => ({
31313
+ ...period2,
31314
+ sessions: values[index2] ?? 0
31315
+ }));
31316
+ }
31317
+ function eventPeriods(periods, values) {
31318
+ return periods.map((period2, index2) => ({
31319
+ ...period2,
31320
+ eventCount: values[index2] ?? 0
31321
+ }));
31322
+ }
31323
+ function clickPeriods(periods, clicks, impressions) {
31324
+ return periods.map((period2, index2) => ({
31325
+ ...period2,
31326
+ clicks: clicks[index2] ?? 0,
31327
+ impressions: impressions[index2] ?? 0
31328
+ }));
31329
+ }
31330
+ function rankEntries(entries) {
31331
+ const score = (values) => ({
31332
+ latest: values.at(-1) ?? 0,
31333
+ total: values.reduce((sum, value) => sum + value, 0)
31334
+ });
31335
+ return [...entries].sort(([leftKey, leftValues], [rightKey, rightValues]) => {
31336
+ const left = score(leftValues);
31337
+ const right = score(rightValues);
31338
+ return right.latest - left.latest || right.total - left.total || leftKey.localeCompare(rightKey);
31339
+ });
31340
+ }
31341
+ function normalizeHost(value) {
31342
+ return hostOf(value) ?? value.trim().toLowerCase().replace(/^www\./, "");
31343
+ }
31344
+ function stableUnique(values, normalize) {
31345
+ const result = [];
31346
+ const seen = /* @__PURE__ */ new Set();
31347
+ for (const value of values) {
31348
+ const normalized = normalize(value);
31349
+ if (!normalized || seen.has(normalized)) continue;
31350
+ seen.add(normalized);
31351
+ result.push(normalized);
31352
+ }
31353
+ return result;
31354
+ }
31355
+ function matchesHost(value, marketingHosts) {
31356
+ const normalized = normalizeHost(value);
31357
+ return marketingHosts.some(
31358
+ (candidate) => normalized === candidate || normalized.endsWith(`.${candidate}`)
31359
+ );
31360
+ }
31361
+ function normalizeLandingPage(value) {
31362
+ return normalizeUrlPath(value) ?? "/";
31363
+ }
31364
+ function normalizePathPrefix(value) {
31365
+ if (!value) return null;
31366
+ const normalized = normalizeLandingPage(value);
31367
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
31368
+ }
31369
+ function matchesPathPrefix(value, prefix) {
31370
+ if (!prefix) return true;
31371
+ const pathname = value.split("?")[0] ?? value;
31372
+ if (prefix === "/") return true;
31373
+ return pathname === prefix || pathname.startsWith(`${prefix}/`);
31374
+ }
31375
+ function splitKey(key) {
31376
+ const separator = key.indexOf("\0");
31377
+ if (separator < 0) return [key, "/"];
31378
+ return [key.slice(0, separator), key.slice(separator + 1)];
31379
+ }
31380
+ function parseGscPage(row) {
31381
+ try {
31382
+ const url = new URL(row.page);
31383
+ return {
31384
+ ...row,
31385
+ hostName: url.hostname,
31386
+ landingPage: normalizeLandingPage(url.pathname)
31387
+ };
31388
+ } catch {
31389
+ return null;
31390
+ }
31391
+ }
31392
+ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
31393
+ const parsedWindow = gaMeasurementAnalysisWindowSchema.safeParse(options.window ?? "90d");
31394
+ if (!parsedWindow.success) {
31395
+ throw validationError('"window" must be one of: 30d, 60d, 90d');
31396
+ }
31397
+ const parsedHostScope = gaMeasurementHostScopeSchema.safeParse(
31398
+ options.hostScope ?? "marketing"
31399
+ );
31400
+ if (!parsedHostScope.success) {
31401
+ throw validationError('"hostScope" must be one of: marketing, all');
31402
+ }
31403
+ const limit = Number(options.limit ?? 100);
31404
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
31405
+ throw validationError('"limit" must be an integer between 1 and 100');
31406
+ }
31407
+ const project = resolveProject(db, projectName);
31408
+ const days = windowDays(parsedWindow.data);
31409
+ const pathPrefix = normalizePathPrefix(options.pathPrefix);
31410
+ const marketingHosts = stableUnique(
31411
+ [
31412
+ project.canonicalDomain,
31413
+ ...project.ownedDomains,
31414
+ ...project.measurement.marketingHosts
31415
+ ],
31416
+ normalizeHost
31417
+ );
31418
+ const brandTerms = stableUnique(
31419
+ [project.displayName, ...project.aliases, ...project.measurement.brandTerms],
31420
+ (value) => value.trim()
31421
+ );
31422
+ const hostIsIncluded = (hostName) => parsedHostScope.data === "all" || matchesHost(hostName, marketingHosts);
31423
+ const pageIsIncluded = (landingPage) => matchesPathPrefix(landingPage, pathPrefix);
31424
+ const scopedConditions = (hostColumn, normalizedPathColumn, rawPathColumn) => {
31425
+ const conditions = [];
31426
+ if (parsedHostScope.data === "marketing") {
31427
+ const lowerHost = sql14`lower(${hostColumn})`;
31428
+ const normalizedHost = sql14`case when ${lowerHost} like 'www.%' then substr(${lowerHost}, 5) else ${lowerHost} end`;
31429
+ conditions.push(or7(...marketingHosts.flatMap((host) => [
31430
+ sql14`${normalizedHost} = ${host}`,
31431
+ sql14`${normalizedHost} like ${`%.${host}`}`
31432
+ ])));
31433
+ }
31434
+ if (pathPrefix && pathPrefix !== "/") {
31435
+ const rawPath = sql14`case when instr(${rawPathColumn}, '?') = 0 then ${rawPathColumn} else substr(${rawPathColumn}, 1, instr(${rawPathColumn}, '?') - 1) end`;
31436
+ const landingPath = sql14`case when trim(coalesce(${normalizedPathColumn}, '')) = '' then ${rawPath} else ${normalizedPathColumn} end`;
31437
+ conditions.push(or7(sql14`${landingPath} = ${pathPrefix}`, sql14`${landingPath} like ${`${pathPrefix}/%`}`));
31438
+ }
31439
+ return conditions;
31440
+ };
31441
+ const acquisitionScope = scopedConditions(gaAcquisitionDaily.hostName, gaAcquisitionDaily.landingPageNormalized, gaAcquisitionDaily.landingPage);
31442
+ const leadLandingScope = scopedConditions(gaLeadEventsDaily.hostName, gaLeadEventsDaily.landingPageNormalized, gaLeadEventsDaily.landingPage);
31443
+ const acquisitionAnchor = db.select({ date: sql14`max(${gaAcquisitionDaily.date})` }).from(gaAcquisitionDaily).where(and24(eq30(gaAcquisitionDaily.projectId, project.id), ...acquisitionScope)).get()?.date ?? null;
31444
+ const leadAnchor = db.select({ date: sql14`max(${gaLeadEventsDaily.date})` }).from(gaLeadEventsDaily).where(and24(eq30(gaLeadEventsDaily.projectId, project.id), or7(
31445
+ eq30(gaLeadEventsDaily.attributionScope, "channel"),
31446
+ and24(eq30(gaLeadEventsDaily.attributionScope, "landing-page"), ...leadLandingScope)
31447
+ ))).get()?.date ?? null;
31448
+ const gaAnchor = [acquisitionAnchor, leadAnchor].filter((date) => date !== null).sort().at(-1) ?? null;
31449
+ const gaPeriods = gaAnchor ? buildPeriods(gaAnchor, days) : [];
31450
+ const gaStartDate = gaPeriods[0]?.startDate;
31451
+ const acquisitionRows = gaStartDate ? db.select().from(gaAcquisitionDaily).where(and24(eq30(gaAcquisitionDaily.projectId, project.id), gte7(gaAcquisitionDaily.date, gaStartDate), lte5(gaAcquisitionDaily.date, gaAnchor))).all() : [];
31452
+ const leadRows = gaStartDate ? db.select().from(gaLeadEventsDaily).where(and24(eq30(gaLeadEventsDaily.projectId, project.id), gte7(gaLeadEventsDaily.date, gaStartDate), lte5(gaLeadEventsDaily.date, gaAnchor))).all() : [];
31453
+ const gscAnchor = db.select({ date: sql14`max(${gscDailyTotals.date})` }).from(gscDailyTotals).where(eq30(gscDailyTotals.projectId, project.id)).get()?.date ?? null;
31454
+ const gscPeriods = gscAnchor ? buildPeriods(gscAnchor, days) : [];
31455
+ const gscStartDate = gscPeriods[0]?.startDate;
31456
+ const propertyRows = gscStartDate ? db.select().from(gscDailyTotals).where(and24(eq30(gscDailyTotals.projectId, project.id), gte7(gscDailyTotals.date, gscStartDate), lte5(gscDailyTotals.date, gscAnchor))).all() : [];
31457
+ const queryRows = gscStartDate ? db.select().from(gscQueryDailyTotals).where(and24(eq30(gscQueryDailyTotals.projectId, project.id), gte7(gscQueryDailyTotals.date, gscStartDate), lte5(gscQueryDailyTotals.date, gscAnchor))).all() : [];
31458
+ const rawPageRows = gscStartDate ? db.select().from(gscSearchData).where(and24(eq30(gscSearchData.projectId, project.id), gte7(gscSearchData.date, gscStartDate), lte5(gscSearchData.date, gscAnchor))).all() : [];
31459
+ const state = db.select().from(gaMeasurementSyncStates).where(eq30(gaMeasurementSyncStates.projectId, project.id)).get();
31460
+ const acquisition = acquisitionRows.filter((row) => {
31461
+ const landingPage = row.landingPageNormalized ?? normalizeLandingPage(row.landingPage);
31462
+ return hostIsIncluded(row.hostName) && pageIsIncluded(landingPage);
31463
+ });
31464
+ const acquisitionTotals = aggregateByKey(
31465
+ acquisition,
31466
+ gaPeriods,
31467
+ () => "all",
31468
+ (row) => row.sessions
31469
+ ).get("all") ?? [];
31470
+ const acquisitionChannels = aggregateByKey(
31471
+ acquisition,
31472
+ gaPeriods,
31473
+ (row) => row.channelGroup,
31474
+ (row) => row.sessions
31475
+ );
31476
+ const acquisitionPages = aggregateByKey(
31477
+ acquisition,
31478
+ gaPeriods,
31479
+ (row) => `${row.hostName}\0${row.landingPageNormalized ?? normalizeLandingPage(row.landingPage)}`,
31480
+ (row) => row.sessions
31481
+ );
31482
+ const selectedLeadRows = leadRows.filter((row) => gaPeriods.some((period2) => row.date >= period2.startDate && row.date <= period2.endDate));
31483
+ const hasLeadTimeline = leadRows.length > 0 || state?.leadSyncedAt != null;
31484
+ const hasChannelOnlyLeads = selectedLeadRows.some((row) => row.attributionScope === "channel");
31485
+ const stateLeadScope = state === void 0 ? null : state.leadAttributionScope;
31486
+ const firstObservedLeadScope = selectedLeadRows.length > 0 ? selectedLeadRows[0].attributionScope : null;
31487
+ const attributionScope = hasChannelOnlyLeads ? "channel" : stateLeadScope ?? firstObservedLeadScope;
31488
+ const hostAndPathFiltersApplied = attributionScope === "landing-page";
31489
+ const leads = selectedLeadRows.filter((row) => {
31490
+ if (row.attributionScope === "channel") return true;
31491
+ const landingPage = row.landingPageNormalized ?? normalizeLandingPage(row.landingPage);
31492
+ return hostIsIncluded(row.hostName) && pageIsIncluded(landingPage);
31493
+ });
31494
+ const leadTotals = aggregateByKey(
31495
+ leads,
31496
+ gaPeriods,
31497
+ () => "all",
31498
+ (row) => row.eventCount
31499
+ ).get("all") ?? [];
31500
+ const leadChannels = aggregateByKey(
31501
+ leads,
31502
+ gaPeriods,
31503
+ (row) => row.channelGroup,
31504
+ (row) => row.eventCount
31505
+ );
31506
+ const propertyClicks = aggregateByKey(
31507
+ propertyRows,
31508
+ gscPeriods,
31509
+ () => "all",
31510
+ (row) => row.clicks
31511
+ ).get("all") ?? [];
31512
+ const propertyImpressions = aggregateByKey(
31513
+ propertyRows,
31514
+ gscPeriods,
31515
+ () => "all",
31516
+ (row) => row.impressions
31517
+ ).get("all") ?? [];
31518
+ const queryClicks = aggregateByKey(
31519
+ queryRows,
31520
+ gscPeriods,
31521
+ () => "all",
31522
+ (row) => row.clicks
31523
+ ).get("all") ?? [];
31524
+ const queryImpressions = aggregateByKey(
31525
+ queryRows,
31526
+ gscPeriods,
31527
+ () => "all",
31528
+ (row) => row.impressions
31529
+ ).get("all") ?? [];
31530
+ const brandedQueries = new Set(filterBrandedSeedCandidates({
31531
+ candidates: queryRows.map((row) => row.query),
31532
+ brandNames: brandTerms,
31533
+ canonicalDomains: [project.canonicalDomain, ...project.ownedDomains]
31534
+ }).droppedBranded);
31535
+ const brandedClicks = aggregateByKey(
31536
+ queryRows.filter((row) => brandedQueries.has(row.query)),
31537
+ gscPeriods,
31538
+ () => "all",
31539
+ (row) => row.clicks
31540
+ ).get("all") ?? [];
31541
+ const brandedImpressions = aggregateByKey(
31542
+ queryRows.filter((row) => brandedQueries.has(row.query)),
31543
+ gscPeriods,
31544
+ () => "all",
31545
+ (row) => row.impressions
31546
+ ).get("all") ?? [];
31547
+ const perQueryClicks = aggregateByKey(
31548
+ queryRows,
31549
+ gscPeriods,
31550
+ (row) => row.query,
31551
+ (row) => row.clicks
31552
+ );
31553
+ const perQueryImpressions = aggregateByKey(
31554
+ queryRows,
31555
+ gscPeriods,
31556
+ (row) => row.query,
31557
+ (row) => row.impressions
31558
+ );
31559
+ const gscPageRows = rawPageRows.map(parseGscPage).filter((row) => row !== null).filter((row) => hostIsIncluded(row.hostName) && pageIsIncluded(row.landingPage));
31560
+ const perPageClicks = aggregateByKey(
31561
+ gscPageRows,
31562
+ gscPeriods,
31563
+ (row) => `${row.hostName}\0${row.landingPage}`,
31564
+ (row) => row.clicks
31565
+ );
31566
+ const perPageImpressions = aggregateByKey(
31567
+ gscPageRows,
31568
+ gscPeriods,
31569
+ (row) => `${row.hostName}\0${row.landingPage}`,
31570
+ (row) => row.impressions
31571
+ );
31572
+ return gaMeasurementAnalysisDtoSchema.parse({
31573
+ window: parsedWindow.data,
31574
+ bucketDays: 30,
31575
+ filters: {
31576
+ hostScope: parsedHostScope.data,
31577
+ marketingHosts,
31578
+ pathPrefix,
31579
+ brandTerms,
31580
+ queryMixScope: "property"
31581
+ },
31582
+ acquisition: {
31583
+ status: state?.acquisitionStatus ?? "never-synced",
31584
+ error: state?.acquisitionError ?? null,
31585
+ syncedAt: state?.acquisitionSyncedAt ?? null,
31586
+ periods: gaAnchor ? sessionPeriods(gaPeriods, acquisitionTotals) : [],
31587
+ channels: rankEntries(acquisitionChannels).map(([channelGroup, values]) => ({
31588
+ channelGroup,
31589
+ periods: sessionPeriods(gaPeriods, values)
31590
+ })),
31591
+ pages: rankEntries(acquisitionPages).slice(0, limit).map(([key, values]) => {
31592
+ const [hostName, landingPage] = splitKey(key);
31593
+ return {
31594
+ hostName,
31595
+ landingPage,
31596
+ periods: sessionPeriods(gaPeriods, values)
31597
+ };
31598
+ })
31599
+ },
31600
+ leads: {
31601
+ status: state?.leadStatus ?? "never-synced",
31602
+ error: state?.leadError ?? null,
31603
+ syncedAt: state?.leadSyncedAt ?? null,
31604
+ attributionScope,
31605
+ hostAndPathFiltersApplied,
31606
+ periods: gaAnchor && hasLeadTimeline ? eventPeriods(gaPeriods, leadTotals) : [],
31607
+ channels: rankEntries(leadChannels).map(([channelGroup, values]) => ({
31608
+ channelGroup,
31609
+ periods: eventPeriods(gaPeriods, values)
31610
+ }))
31611
+ },
31612
+ searchDemand: gscAnchor === null ? {
31613
+ status: "unavailable",
31614
+ periods: [],
31615
+ queries: [],
31616
+ pages: [],
31617
+ latestDate: null
31618
+ } : {
31619
+ status: "ready",
31620
+ latestDate: gscAnchor,
31621
+ periods: gscPeriods.map((period2, index2) => ({
31622
+ ...period2,
31623
+ propertyClicks: propertyClicks[index2] ?? 0,
31624
+ propertyImpressions: propertyImpressions[index2] ?? 0,
31625
+ reportedQueryClicks: queryClicks[index2] ?? 0,
31626
+ reportedQueryImpressions: queryImpressions[index2] ?? 0,
31627
+ brandedClicks: brandedClicks[index2] ?? 0,
31628
+ brandedImpressions: brandedImpressions[index2] ?? 0,
31629
+ nonBrandedClicks: Math.max(
31630
+ 0,
31631
+ (queryClicks[index2] ?? 0) - (brandedClicks[index2] ?? 0)
31632
+ ),
31633
+ nonBrandedImpressions: Math.max(
31634
+ 0,
31635
+ (queryImpressions[index2] ?? 0) - (brandedImpressions[index2] ?? 0)
31636
+ ),
31637
+ unreportedClicks: Math.max(
31638
+ 0,
31639
+ (propertyClicks[index2] ?? 0) - (queryClicks[index2] ?? 0)
31640
+ ),
31641
+ unreportedImpressions: Math.max(
31642
+ 0,
31643
+ (propertyImpressions[index2] ?? 0) - (queryImpressions[index2] ?? 0)
31644
+ )
31645
+ })),
31646
+ queries: rankEntries(perQueryClicks).slice(0, limit).map(([query, values]) => ({
31647
+ query,
31648
+ classification: brandedQueries.has(query) ? "branded" : "non-branded",
31649
+ periods: clickPeriods(
31650
+ gscPeriods,
31651
+ values,
31652
+ perQueryImpressions.get(query) ?? []
31653
+ )
31654
+ })),
31655
+ pages: rankEntries(perPageImpressions).slice(0, limit).map(([key, impressions]) => {
31656
+ const [hostName, landingPage] = splitKey(key);
31657
+ return {
31658
+ hostName,
31659
+ landingPage,
31660
+ periods: clickPeriods(
31661
+ gscPeriods,
31662
+ perPageClicks.get(key) ?? [],
31663
+ impressions
31664
+ )
31665
+ };
31666
+ })
31667
+ }
31668
+ });
31669
+ }
31670
+ async function gaMeasurementAnalysisRoutes(app) {
31671
+ app.get("/projects/:name/ga/measurement-analysis", (request) => buildGaMeasurementAnalysis(app.db, request.params.name, request.query));
31672
+ }
31673
+
30929
31674
  // ../integration-wordpress/src/types.ts
30930
31675
  var WordpressApiError = class extends Error {
30931
31676
  statusCode;
@@ -32562,7 +33307,7 @@ async function wordpressRoutes(app, opts) {
32562
33307
 
32563
33308
  // ../api-routes/src/backlinks.ts
32564
33309
  import crypto26 from "crypto";
32565
- import { and as and25, asc as asc7, desc as desc14, eq as eq30, sql as sql14 } from "drizzle-orm";
33310
+ import { and as and26, asc as asc7, desc as desc14, eq as eq31, sql as sql15 } from "drizzle-orm";
32566
33311
 
32567
33312
  // ../integration-commoncrawl/src/constants.ts
32568
33313
  import os2 from "os";
@@ -32852,7 +33597,7 @@ async function queryBacklinks(opts) {
32852
33597
  const reversed = opts.targets.map(reverseDomain);
32853
33598
  const targetList = reversed.map(quote).join(", ");
32854
33599
  const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
32855
- const sql17 = `
33600
+ const sql18 = `
32856
33601
  WITH vertices AS (
32857
33602
  SELECT * FROM read_csv(
32858
33603
  ${quote(opts.vertexPath)},
@@ -32888,7 +33633,7 @@ async function queryBacklinks(opts) {
32888
33633
  const conn = await instance.connect();
32889
33634
  let rows;
32890
33635
  try {
32891
- const reader = await conn.runAndReadAll(sql17);
33636
+ const reader = await conn.runAndReadAll(sql18);
32892
33637
  rows = reader.getRowObjects();
32893
33638
  } finally {
32894
33639
  conn.disconnectSync?.();
@@ -32965,7 +33710,7 @@ function pruneCachedRelease(release, opts = {}) {
32965
33710
  }
32966
33711
 
32967
33712
  // ../api-routes/src/backlinks-filter.ts
32968
- import { and as and24, ne as ne4, notLike } from "drizzle-orm";
33713
+ import { and as and25, ne as ne4, notLike } from "drizzle-orm";
32969
33714
  var BACKLINK_FILTER_PATTERNS = [
32970
33715
  "*.google.com",
32971
33716
  "*.googleusercontent.com",
@@ -32988,7 +33733,7 @@ function backlinkCrawlerExclusionClause() {
32988
33733
  conditions.push(ne4(backlinkDomains.linkingDomain, pattern));
32989
33734
  }
32990
33735
  }
32991
- const combined = and24(...conditions);
33736
+ const combined = and25(...conditions);
32992
33737
  if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
32993
33738
  return combined;
32994
33739
  }
@@ -33059,11 +33804,11 @@ function mapRunRow(row) {
33059
33804
  }
33060
33805
  function latestSummaryForProject(db, projectId, source, release) {
33061
33806
  const conditions = [
33062
- eq30(backlinkSummaries.projectId, projectId),
33063
- eq30(backlinkSummaries.source, source)
33807
+ eq31(backlinkSummaries.projectId, projectId),
33808
+ eq31(backlinkSummaries.source, source)
33064
33809
  ];
33065
- if (release) conditions.push(eq30(backlinkSummaries.release, release));
33066
- return db.select().from(backlinkSummaries).where(and25(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
33810
+ if (release) conditions.push(eq31(backlinkSummaries.release, release));
33811
+ return db.select().from(backlinkSummaries).where(and26(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
33067
33812
  }
33068
33813
  function parseExcludeCrawlers(value) {
33069
33814
  if (!value) return false;
@@ -33071,19 +33816,19 @@ function parseExcludeCrawlers(value) {
33071
33816
  return lower === "1" || lower === "true" || lower === "yes";
33072
33817
  }
33073
33818
  function computeFilteredSummary(db, base) {
33074
- const baseDomainCondition = and25(
33075
- eq30(backlinkDomains.projectId, base.projectId),
33076
- eq30(backlinkDomains.source, base.source),
33077
- eq30(backlinkDomains.release, base.release)
33819
+ const baseDomainCondition = and26(
33820
+ eq31(backlinkDomains.projectId, base.projectId),
33821
+ eq31(backlinkDomains.source, base.source),
33822
+ eq31(backlinkDomains.release, base.release)
33078
33823
  );
33079
- const filteredCondition = and25(baseDomainCondition, backlinkCrawlerExclusionClause());
33824
+ const filteredCondition = and26(baseDomainCondition, backlinkCrawlerExclusionClause());
33080
33825
  const unfilteredAgg = db.select({
33081
- count: sql14`count(*)`,
33082
- total: sql14`coalesce(sum(${backlinkDomains.numHosts}), 0)`
33826
+ count: sql15`count(*)`,
33827
+ total: sql15`coalesce(sum(${backlinkDomains.numHosts}), 0)`
33083
33828
  }).from(backlinkDomains).where(baseDomainCondition).get();
33084
33829
  const filteredAgg = db.select({
33085
- count: sql14`count(*)`,
33086
- total: sql14`coalesce(sum(${backlinkDomains.numHosts}), 0)`
33830
+ count: sql15`count(*)`,
33831
+ total: sql15`coalesce(sum(${backlinkDomains.numHosts}), 0)`
33087
33832
  }).from(backlinkDomains).where(filteredCondition).get();
33088
33833
  const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc14(backlinkDomains.numHosts)).limit(10).all();
33089
33834
  const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
@@ -33106,13 +33851,13 @@ function computeFilteredSummary(db, base) {
33106
33851
  };
33107
33852
  }
33108
33853
  function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
33109
- const summary = db.select().from(backlinkSummaries).where(and25(eq30(backlinkSummaries.projectId, projectId), eq30(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
33854
+ const summary = db.select().from(backlinkSummaries).where(and26(eq31(backlinkSummaries.projectId, projectId), eq31(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
33110
33855
  let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
33111
33856
  if (summary && excludeCrawlers) {
33112
- const filtered = db.select({ count: sql14`count(*)` }).from(backlinkDomains).where(and25(
33113
- eq30(backlinkDomains.projectId, projectId),
33114
- eq30(backlinkDomains.source, source),
33115
- eq30(backlinkDomains.release, summary.release),
33857
+ const filtered = db.select({ count: sql15`count(*)` }).from(backlinkDomains).where(and26(
33858
+ eq31(backlinkDomains.projectId, projectId),
33859
+ eq31(backlinkDomains.source, source),
33860
+ eq31(backlinkDomains.release, summary.release),
33116
33861
  backlinkCrawlerExclusionClause()
33117
33862
  )).get();
33118
33863
  totalLinkingDomains = Number(filtered?.count ?? 0);
@@ -33127,7 +33872,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
33127
33872
  };
33128
33873
  }
33129
33874
  function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
33130
- const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq30(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
33875
+ const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
33131
33876
  const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
33132
33877
  const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
33133
33878
  const sources = [
@@ -33183,7 +33928,7 @@ async function backlinksRoutes(app, opts) {
33183
33928
  "@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
33184
33929
  );
33185
33930
  }
33186
- const existing = app.db.select().from(ccReleaseSyncs).where(eq30(ccReleaseSyncs.release, release)).get();
33931
+ const existing = app.db.select().from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.release, release)).get();
33187
33932
  const now = (/* @__PURE__ */ new Date()).toISOString();
33188
33933
  if (existing) {
33189
33934
  if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
@@ -33194,9 +33939,9 @@ async function backlinksRoutes(app, opts) {
33194
33939
  phaseDetail: null,
33195
33940
  error: null,
33196
33941
  updatedAt: now
33197
- }).where(eq30(ccReleaseSyncs.id, existing.id)).run();
33942
+ }).where(eq31(ccReleaseSyncs.id, existing.id)).run();
33198
33943
  opts.onReleaseSyncRequested(existing.id, release);
33199
- const refreshed = app.db.select().from(ccReleaseSyncs).where(eq30(ccReleaseSyncs.id, existing.id)).get();
33944
+ const refreshed = app.db.select().from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.id, existing.id)).get();
33200
33945
  return reply.status(200).send(mapSyncRow(refreshed));
33201
33946
  }
33202
33947
  const id = crypto26.randomUUID();
@@ -33208,7 +33953,7 @@ async function backlinksRoutes(app, opts) {
33208
33953
  updatedAt: now
33209
33954
  }).run();
33210
33955
  opts.onReleaseSyncRequested(id, release);
33211
- const inserted = app.db.select().from(ccReleaseSyncs).where(eq30(ccReleaseSyncs.id, id)).get();
33956
+ const inserted = app.db.select().from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.id, id)).get();
33212
33957
  return reply.status(201).send(mapSyncRow(inserted));
33213
33958
  });
33214
33959
  app.get("/backlinks/syncs/latest", async (_request, reply) => {
@@ -33266,7 +34011,7 @@ async function backlinksRoutes(app, opts) {
33266
34011
  createdAt: now
33267
34012
  }).run();
33268
34013
  opts.onBacklinkExtractRequested(runId, project.id, release);
33269
- const run = app.db.select().from(runs).where(eq30(runs.id, runId)).get();
34014
+ const run = app.db.select().from(runs).where(eq31(runs.id, runId)).get();
33270
34015
  return reply.status(201).send(mapRunRow(run));
33271
34016
  });
33272
34017
  app.get(
@@ -33292,13 +34037,13 @@ async function backlinksRoutes(app, opts) {
33292
34037
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
33293
34038
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
33294
34039
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
33295
- const baseDomainCondition = and25(
33296
- eq30(backlinkDomains.projectId, project.id),
33297
- eq30(backlinkDomains.source, source),
33298
- eq30(backlinkDomains.release, targetRelease)
34040
+ const baseDomainCondition = and26(
34041
+ eq31(backlinkDomains.projectId, project.id),
34042
+ eq31(backlinkDomains.source, source),
34043
+ eq31(backlinkDomains.release, targetRelease)
33299
34044
  );
33300
- const domainCondition = excludeCrawlers ? and25(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
33301
- const totalRow = app.db.select({ count: sql14`count(*)` }).from(backlinkDomains).where(domainCondition).get();
34045
+ const domainCondition = excludeCrawlers ? and26(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
34046
+ const totalRow = app.db.select({ count: sql15`count(*)` }).from(backlinkDomains).where(domainCondition).get();
33302
34047
  const rows = app.db.select({
33303
34048
  linkingDomain: backlinkDomains.linkingDomain,
33304
34049
  numHosts: backlinkDomains.numHosts,
@@ -33321,7 +34066,7 @@ async function backlinksRoutes(app, opts) {
33321
34066
  async (request, reply) => {
33322
34067
  const project = resolveProject(app.db, request.params.name);
33323
34068
  const source = parseSourceParam(request.query.source);
33324
- const rows = app.db.select().from(backlinkSummaries).where(and25(eq30(backlinkSummaries.projectId, project.id), eq30(backlinkSummaries.source, source))).orderBy(asc7(backlinkSummaries.queriedAt)).all();
34069
+ const rows = app.db.select().from(backlinkSummaries).where(and26(eq31(backlinkSummaries.projectId, project.id), eq31(backlinkSummaries.source, source))).orderBy(asc7(backlinkSummaries.queriedAt)).all();
33325
34070
  const response = rows.map((r) => ({
33326
34071
  release: r.release,
33327
34072
  totalLinkingDomains: r.totalLinkingDomains,
@@ -33368,7 +34113,7 @@ async function backlinksRoutes(app, opts) {
33368
34113
  createdAt: now
33369
34114
  }).run();
33370
34115
  opts.onBingBacklinkSyncRequested(runId, project.id);
33371
- const run = app.db.select().from(runs).where(eq30(runs.id, runId)).get();
34116
+ const run = app.db.select().from(runs).where(eq31(runs.id, runId)).get();
33372
34117
  return reply.status(201).send(mapRunRow(run));
33373
34118
  }
33374
34119
  );
@@ -33377,7 +34122,7 @@ async function backlinksRoutes(app, opts) {
33377
34122
  // ../api-routes/src/traffic.ts
33378
34123
  import crypto28 from "crypto";
33379
34124
  import { Agent as UndiciAgent } from "undici";
33380
- import { and as and26, desc as desc15, eq as eq31, gte as gte7, lte as lte5, sql as sql15 } from "drizzle-orm";
34125
+ import { and as and27, desc as desc15, eq as eq32, gte as gte8, lte as lte6, sql as sql16 } from "drizzle-orm";
33381
34126
 
33382
34127
  // ../integration-cloud-run/src/auth.ts
33383
34128
  import crypto27 from "crypto";
@@ -36160,24 +36905,24 @@ var DEFAULT_AI_REFERRER_RULES = [
36160
36905
 
36161
36906
  // ../integration-traffic/src/classifier.ts
36162
36907
  var USER_FETCH_PURPOSE = "user-agent";
36163
- function normalizeHost(host) {
36908
+ function normalizeHost2(host) {
36164
36909
  return host.trim().toLowerCase().replace(/^www\./, "");
36165
36910
  }
36166
36911
  function hostMatches(host, domain) {
36167
- const normalizedHost = normalizeHost(host);
36168
- const normalizedDomain = normalizeHost(domain);
36912
+ const normalizedHost = normalizeHost2(host);
36913
+ const normalizedDomain = normalizeHost2(domain);
36169
36914
  return normalizedHost === normalizedDomain || normalizedHost.endsWith(`.${normalizedDomain}`);
36170
36915
  }
36171
36916
  function utmTokenMatchesDomain(utmSource, domain) {
36172
36917
  if (hostMatches(utmSource, domain)) return true;
36173
- const normalizedUtm = normalizeHost(utmSource);
36174
- const firstLabel = normalizeHost(domain).split(".")[0];
36918
+ const normalizedUtm = normalizeHost2(utmSource);
36919
+ const firstLabel = normalizeHost2(domain).split(".")[0];
36175
36920
  return Boolean(firstLabel) && normalizedUtm === firstLabel;
36176
36921
  }
36177
36922
  function hostFromUrl(value) {
36178
36923
  if (!value) return null;
36179
36924
  try {
36180
- return normalizeHost(new URL(value).hostname);
36925
+ return normalizeHost2(new URL(value).hostname);
36181
36926
  } catch {
36182
36927
  return null;
36183
36928
  }
@@ -36186,7 +36931,7 @@ function utmSourceFromQuery(queryString) {
36186
36931
  if (!queryString) return null;
36187
36932
  const params = new URLSearchParams(queryString);
36188
36933
  const source = params.get("utm_source");
36189
- return source ? normalizeHost(source) : null;
36934
+ return source ? normalizeHost2(source) : null;
36190
36935
  }
36191
36936
  function utmSourceFromUrl(value) {
36192
36937
  if (!value) return null;
@@ -36350,13 +37095,13 @@ function sessionWindowBucket(value, windowMs) {
36350
37095
  if (Number.isNaN(date.getTime())) return value;
36351
37096
  return new Date(Math.floor(date.getTime() / windowMs) * windowMs).toISOString();
36352
37097
  }
36353
- function normalizeHost2(host) {
37098
+ function normalizeHost3(host) {
36354
37099
  if (!host) return null;
36355
37100
  return host.trim().toLowerCase().replace(/^www\./, "") || null;
36356
37101
  }
36357
37102
  function sameHost(a, b) {
36358
- const normalizedA = normalizeHost2(a);
36359
- const normalizedB = normalizeHost2(b);
37103
+ const normalizedA = normalizeHost3(a);
37104
+ const normalizedB = normalizeHost3(b);
36360
37105
  return !!normalizedA && !!normalizedB && normalizedA === normalizedB;
36361
37106
  }
36362
37107
  function pathFromSameOriginReferer(event) {
@@ -37273,8 +38018,8 @@ async function runBackfillTask(options) {
37273
38018
  const failedAt = (/* @__PURE__ */ new Date()).toISOString();
37274
38019
  try {
37275
38020
  app.db.transaction((tx) => {
37276
- tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq31(runs.id, runId)).run();
37277
- tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq31(trafficSources.id, sourceRow.id)).run();
38021
+ tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq32(runs.id, runId)).run();
38022
+ tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq32(trafficSources.id, sourceRow.id)).run();
37278
38023
  });
37279
38024
  } catch {
37280
38025
  }
@@ -37289,7 +38034,7 @@ async function runBackfillTask(options) {
37289
38034
  if (allEvents.length === 0) {
37290
38035
  const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
37291
38036
  try {
37292
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq31(runs.id, runId)).run();
38037
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq32(runs.id, runId)).run();
37293
38038
  } catch {
37294
38039
  }
37295
38040
  return;
@@ -37311,31 +38056,31 @@ async function runBackfillTask(options) {
37311
38056
  try {
37312
38057
  app.db.transaction((tx) => {
37313
38058
  tx.delete(crawlerEventsHourly).where(
37314
- and26(
37315
- eq31(crawlerEventsHourly.sourceId, sourceRow.id),
37316
- gte7(crawlerEventsHourly.tsHour, windowStartIso),
37317
- lte5(crawlerEventsHourly.tsHour, windowEndIso)
38059
+ and27(
38060
+ eq32(crawlerEventsHourly.sourceId, sourceRow.id),
38061
+ gte8(crawlerEventsHourly.tsHour, windowStartIso),
38062
+ lte6(crawlerEventsHourly.tsHour, windowEndIso)
37318
38063
  )
37319
38064
  ).run();
37320
38065
  tx.delete(aiUserFetchEventsHourly).where(
37321
- and26(
37322
- eq31(aiUserFetchEventsHourly.sourceId, sourceRow.id),
37323
- gte7(aiUserFetchEventsHourly.tsHour, windowStartIso),
37324
- lte5(aiUserFetchEventsHourly.tsHour, windowEndIso)
38066
+ and27(
38067
+ eq32(aiUserFetchEventsHourly.sourceId, sourceRow.id),
38068
+ gte8(aiUserFetchEventsHourly.tsHour, windowStartIso),
38069
+ lte6(aiUserFetchEventsHourly.tsHour, windowEndIso)
37325
38070
  )
37326
38071
  ).run();
37327
38072
  tx.delete(aiReferralEventsHourly).where(
37328
- and26(
37329
- eq31(aiReferralEventsHourly.sourceId, sourceRow.id),
37330
- gte7(aiReferralEventsHourly.tsHour, windowStartIso),
37331
- lte5(aiReferralEventsHourly.tsHour, windowEndIso)
38073
+ and27(
38074
+ eq32(aiReferralEventsHourly.sourceId, sourceRow.id),
38075
+ gte8(aiReferralEventsHourly.tsHour, windowStartIso),
38076
+ lte6(aiReferralEventsHourly.tsHour, windowEndIso)
37332
38077
  )
37333
38078
  ).run();
37334
38079
  tx.delete(rawEventSamples).where(
37335
- and26(
37336
- eq31(rawEventSamples.sourceId, sourceRow.id),
37337
- gte7(rawEventSamples.ts, windowStartIso),
37338
- lte5(rawEventSamples.ts, windowEndIso)
38080
+ and27(
38081
+ eq32(rawEventSamples.sourceId, sourceRow.id),
38082
+ gte8(rawEventSamples.ts, windowStartIso),
38083
+ lte6(rawEventSamples.ts, windowEndIso)
37339
38084
  )
37340
38085
  ).run();
37341
38086
  for (const bucket of report.crawlerEventsHourly) {
@@ -37424,8 +38169,8 @@ async function runBackfillTask(options) {
37424
38169
  lastError: null,
37425
38170
  lastEventIds: newRingBuffer,
37426
38171
  updatedAt: finishedAt
37427
- }).where(eq31(trafficSources.id, sourceRow.id)).run();
37428
- tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq31(runs.id, runId)).run();
38172
+ }).where(eq32(trafficSources.id, sourceRow.id)).run();
38173
+ tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq32(runs.id, runId)).run();
37429
38174
  });
37430
38175
  } catch (e) {
37431
38176
  markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -37507,7 +38252,7 @@ async function trafficRoutes(app, opts) {
37507
38252
  createdAt: existing?.createdAt ?? now,
37508
38253
  updatedAt: now
37509
38254
  });
37510
- const activeSource = app.db.select().from(trafficSources).where(eq31(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
38255
+ const activeSource = app.db.select().from(trafficSources).where(eq32(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
37511
38256
  const config = {
37512
38257
  gcpProjectId,
37513
38258
  serviceName: serviceName ?? null,
@@ -37523,8 +38268,8 @@ async function trafficRoutes(app, opts) {
37523
38268
  lastError: null,
37524
38269
  configJson: config,
37525
38270
  updatedAt: now
37526
- }).where(eq31(trafficSources.id, activeSource.id)).run();
37527
- sourceRow = app.db.select().from(trafficSources).where(eq31(trafficSources.id, activeSource.id)).get();
38271
+ }).where(eq32(trafficSources.id, activeSource.id)).run();
38272
+ sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, activeSource.id)).get();
37528
38273
  } else {
37529
38274
  const newId = crypto28.randomUUID();
37530
38275
  app.db.insert(trafficSources).values({
@@ -37541,7 +38286,7 @@ async function trafficRoutes(app, opts) {
37541
38286
  createdAt: now,
37542
38287
  updatedAt: now
37543
38288
  }).run();
37544
- sourceRow = app.db.select().from(trafficSources).where(eq31(trafficSources.id, newId)).get();
38289
+ sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, newId)).get();
37545
38290
  }
37546
38291
  writeAuditLog(app.db, {
37547
38292
  projectId: project.id,
@@ -37593,7 +38338,7 @@ async function trafficRoutes(app, opts) {
37593
38338
  createdAt: existing?.createdAt ?? now,
37594
38339
  updatedAt: now
37595
38340
  });
37596
- const activeSource = app.db.select().from(trafficSources).where(eq31(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
38341
+ const activeSource = app.db.select().from(trafficSources).where(eq32(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
37597
38342
  const config = { baseUrl, username };
37598
38343
  const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
37599
38344
  let sourceRow;
@@ -37604,8 +38349,8 @@ async function trafficRoutes(app, opts) {
37604
38349
  lastError: null,
37605
38350
  configJson: config,
37606
38351
  updatedAt: now
37607
- }).where(eq31(trafficSources.id, activeSource.id)).run();
37608
- sourceRow = app.db.select().from(trafficSources).where(eq31(trafficSources.id, activeSource.id)).get();
38352
+ }).where(eq32(trafficSources.id, activeSource.id)).run();
38353
+ sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, activeSource.id)).get();
37609
38354
  } else {
37610
38355
  const newId = crypto28.randomUUID();
37611
38356
  app.db.insert(trafficSources).values({
@@ -37622,7 +38367,7 @@ async function trafficRoutes(app, opts) {
37622
38367
  createdAt: now,
37623
38368
  updatedAt: now
37624
38369
  }).run();
37625
- sourceRow = app.db.select().from(trafficSources).where(eq31(trafficSources.id, newId)).get();
38370
+ sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, newId)).get();
37626
38371
  }
37627
38372
  writeAuditLog(app.db, {
37628
38373
  projectId: project.id,
@@ -37676,7 +38421,7 @@ async function trafficRoutes(app, opts) {
37676
38421
  createdAt: existing?.createdAt ?? now,
37677
38422
  updatedAt: now
37678
38423
  });
37679
- const activeSource = app.db.select().from(trafficSources).where(eq31(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
38424
+ const activeSource = app.db.select().from(trafficSources).where(eq32(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
37680
38425
  const config = { projectId, teamId, environment };
37681
38426
  const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
37682
38427
  const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
@@ -37688,8 +38433,8 @@ async function trafficRoutes(app, opts) {
37688
38433
  lastError: null,
37689
38434
  configJson: config,
37690
38435
  updatedAt: now
37691
- }).where(eq31(trafficSources.id, activeSource.id)).run();
37692
- row = tx.select().from(trafficSources).where(eq31(trafficSources.id, activeSource.id)).get();
38436
+ }).where(eq32(trafficSources.id, activeSource.id)).run();
38437
+ row = tx.select().from(trafficSources).where(eq32(trafficSources.id, activeSource.id)).get();
37693
38438
  } else {
37694
38439
  const newId = crypto28.randomUUID();
37695
38440
  tx.insert(trafficSources).values({
@@ -37714,12 +38459,12 @@ async function trafficRoutes(app, opts) {
37714
38459
  createdAt: now,
37715
38460
  updatedAt: now
37716
38461
  }).run();
37717
- row = tx.select().from(trafficSources).where(eq31(trafficSources.id, newId)).get();
38462
+ row = tx.select().from(trafficSources).where(eq32(trafficSources.id, newId)).get();
37718
38463
  }
37719
38464
  const existingSchedule = tx.select().from(schedules).where(
37720
- and26(
37721
- eq31(schedules.projectId, project.id),
37722
- eq31(schedules.kind, SchedulableRunKinds["traffic-sync"])
38465
+ and27(
38466
+ eq32(schedules.projectId, project.id),
38467
+ eq32(schedules.kind, SchedulableRunKinds["traffic-sync"])
37723
38468
  )
37724
38469
  ).get();
37725
38470
  let created = false;
@@ -37768,7 +38513,7 @@ async function trafficRoutes(app, opts) {
37768
38513
  });
37769
38514
  app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
37770
38515
  const project = resolveProject(app.db, request.params.name);
37771
- const sourceRow = app.db.select().from(trafficSources).where(eq31(trafficSources.id, request.params.id)).get();
38516
+ const sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, request.params.id)).get();
37772
38517
  if (!sourceRow || sourceRow.projectId !== project.id) {
37773
38518
  throw notFound("Traffic source", request.params.id);
37774
38519
  }
@@ -37794,8 +38539,8 @@ async function trafficRoutes(app, opts) {
37794
38539
  const markFailed = (msg, errorCode) => {
37795
38540
  const failedAt = (/* @__PURE__ */ new Date()).toISOString();
37796
38541
  app.db.transaction((tx) => {
37797
- tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq31(runs.id, runId)).run();
37798
- tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq31(trafficSources.id, sourceRow.id)).run();
38542
+ tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq32(runs.id, runId)).run();
38543
+ tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq32(trafficSources.id, sourceRow.id)).run();
37799
38544
  });
37800
38545
  try {
37801
38546
  opts.onTrafficSynced?.({
@@ -37876,7 +38621,7 @@ async function trafficRoutes(app, opts) {
37876
38621
  }
37877
38622
  const credential = credentialStore.getConnection(project.name);
37878
38623
  if (!credential) {
37879
- app.db.delete(runs).where(eq31(runs.id, runId)).run();
38624
+ app.db.delete(runs).where(eq32(runs.id, runId)).run();
37880
38625
  throw validationError(
37881
38626
  `No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
37882
38627
  );
@@ -37925,12 +38670,12 @@ async function trafficRoutes(app, opts) {
37925
38670
  auditAction = "traffic.vercel.synced";
37926
38671
  const credentialStore = opts.vercelTrafficCredentialStore;
37927
38672
  if (!credentialStore) {
37928
- app.db.delete(runs).where(eq31(runs.id, runId)).run();
38673
+ app.db.delete(runs).where(eq32(runs.id, runId)).run();
37929
38674
  throw validationError("Vercel traffic credential storage is not configured for this deployment");
37930
38675
  }
37931
38676
  const credential = credentialStore.getConnection(project.name);
37932
38677
  if (!credential) {
37933
- app.db.delete(runs).where(eq31(runs.id, runId)).run();
38678
+ app.db.delete(runs).where(eq32(runs.id, runId)).run();
37934
38679
  throw validationError(
37935
38680
  `No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
37936
38681
  );
@@ -38030,7 +38775,7 @@ async function trafficRoutes(app, opts) {
38030
38775
  let aiReferralHitsCount = 0;
38031
38776
  let unknownHitsCount = 0;
38032
38777
  app.db.transaction((tx) => {
38033
- const latestRow = tx.select().from(trafficSources).where(eq31(trafficSources.id, sourceRow.id)).get();
38778
+ const latestRow = tx.select().from(trafficSources).where(eq32(trafficSources.id, sourceRow.id)).get();
38034
38779
  const previousIds = latestRow.lastEventIds ?? [];
38035
38780
  const seenEventIds = new Set(previousIds);
38036
38781
  const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
@@ -38078,7 +38823,7 @@ async function trafficRoutes(app, opts) {
38078
38823
  crawlerEventsHourly.status
38079
38824
  ],
38080
38825
  set: {
38081
- hits: sql15`${crawlerEventsHourly.hits} + ${bucket.hits}`,
38826
+ hits: sql16`${crawlerEventsHourly.hits} + ${bucket.hits}`,
38082
38827
  sampledUserAgent: bucket.sampledUserAgent,
38083
38828
  updatedAt: finishedAt
38084
38829
  }
@@ -38111,7 +38856,7 @@ async function trafficRoutes(app, opts) {
38111
38856
  aiUserFetchEventsHourly.status
38112
38857
  ],
38113
38858
  set: {
38114
- hits: sql15`${aiUserFetchEventsHourly.hits} + ${bucket.hits}`,
38859
+ hits: sql16`${aiUserFetchEventsHourly.hits} + ${bucket.hits}`,
38115
38860
  sampledUserAgent: bucket.sampledUserAgent,
38116
38861
  updatedAt: finishedAt
38117
38862
  }
@@ -38151,9 +38896,9 @@ async function trafficRoutes(app, opts) {
38151
38896
  // colliding bucket adds its paid and organic arrivals to the ones
38152
38897
  // already there rather than overwriting a single label.
38153
38898
  set: {
38154
- sessionsOrHits: sql15`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
38155
- paidSessionsOrHits: sql15`${aiReferralEventsHourly.paidSessionsOrHits} + ${bucket.paidHits}`,
38156
- organicSessionsOrHits: sql15`${aiReferralEventsHourly.organicSessionsOrHits} + ${bucket.organicHits}`,
38899
+ sessionsOrHits: sql16`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
38900
+ paidSessionsOrHits: sql16`${aiReferralEventsHourly.paidSessionsOrHits} + ${bucket.paidHits}`,
38901
+ organicSessionsOrHits: sql16`${aiReferralEventsHourly.organicSessionsOrHits} + ${bucket.organicHits}`,
38157
38902
  updatedAt: finishedAt
38158
38903
  }
38159
38904
  }).run();
@@ -38205,8 +38950,8 @@ async function trafficRoutes(app, opts) {
38205
38950
  if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
38206
38951
  sourceUpdate.lastCursor = nextCursor ?? null;
38207
38952
  }
38208
- tx.update(trafficSources).set(sourceUpdate).where(eq31(trafficSources.id, sourceRow.id)).run();
38209
- tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq31(runs.id, runId)).run();
38953
+ tx.update(trafficSources).set(sourceUpdate).where(eq32(trafficSources.id, sourceRow.id)).run();
38954
+ tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq32(runs.id, runId)).run();
38210
38955
  });
38211
38956
  writeAuditLog(app.db, {
38212
38957
  projectId: project.id,
@@ -38258,7 +39003,7 @@ async function trafficRoutes(app, opts) {
38258
39003
  });
38259
39004
  app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
38260
39005
  const project = resolveProject(app.db, request.params.name);
38261
- const sourceRow = app.db.select().from(trafficSources).where(eq31(trafficSources.id, request.params.id)).get();
39006
+ const sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, request.params.id)).get();
38262
39007
  if (!sourceRow || sourceRow.projectId !== project.id) {
38263
39008
  throw notFound("Traffic source", request.params.id);
38264
39009
  }
@@ -38444,40 +39189,40 @@ async function trafficRoutes(app, opts) {
38444
39189
  function buildSourceDetail(projectId, row, since) {
38445
39190
  const crawlerPathRows = app.db.select({
38446
39191
  pathNormalized: crawlerEventsHourly.pathNormalized,
38447
- hits: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
39192
+ hits: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
38448
39193
  }).from(crawlerEventsHourly).where(
38449
- and26(
38450
- eq31(crawlerEventsHourly.sourceId, row.id),
38451
- gte7(crawlerEventsHourly.tsHour, since)
39194
+ and27(
39195
+ eq32(crawlerEventsHourly.sourceId, row.id),
39196
+ gte8(crawlerEventsHourly.tsHour, since)
38452
39197
  )
38453
39198
  ).groupBy(crawlerEventsHourly.pathNormalized).all();
38454
39199
  const crawlerSegments = segmentCrawlerHits(
38455
39200
  crawlerPathRows.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
38456
39201
  );
38457
39202
  const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
38458
- const aiUserFetchTotals = app.db.select({ total: sql15`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
38459
- and26(
38460
- eq31(aiUserFetchEventsHourly.sourceId, row.id),
38461
- gte7(aiUserFetchEventsHourly.tsHour, since)
39203
+ const aiUserFetchTotals = app.db.select({ total: sql16`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
39204
+ and27(
39205
+ eq32(aiUserFetchEventsHourly.sourceId, row.id),
39206
+ gte8(aiUserFetchEventsHourly.tsHour, since)
38462
39207
  )
38463
39208
  ).get();
38464
- const aiTotals = app.db.select({ total: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
38465
- and26(
38466
- eq31(aiReferralEventsHourly.sourceId, row.id),
38467
- gte7(aiReferralEventsHourly.tsHour, since)
39209
+ const aiTotals = app.db.select({ total: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
39210
+ and27(
39211
+ eq32(aiReferralEventsHourly.sourceId, row.id),
39212
+ gte8(aiReferralEventsHourly.tsHour, since)
38468
39213
  )
38469
39214
  ).get();
38470
- const sampleTotals = app.db.select({ total: sql15`COUNT(*)` }).from(rawEventSamples).where(
38471
- and26(
38472
- eq31(rawEventSamples.sourceId, row.id),
38473
- gte7(rawEventSamples.ts, since)
39215
+ const sampleTotals = app.db.select({ total: sql16`COUNT(*)` }).from(rawEventSamples).where(
39216
+ and27(
39217
+ eq32(rawEventSamples.sourceId, row.id),
39218
+ gte8(rawEventSamples.ts, since)
38474
39219
  )
38475
39220
  ).get();
38476
39221
  const latestRun = app.db.select().from(runs).where(
38477
- and26(
38478
- eq31(runs.projectId, projectId),
38479
- eq31(runs.kind, RunKinds["traffic-sync"]),
38480
- eq31(runs.sourceId, row.id)
39222
+ and27(
39223
+ eq32(runs.projectId, projectId),
39224
+ eq32(runs.kind, RunKinds["traffic-sync"]),
39225
+ eq32(runs.sourceId, row.id)
38481
39226
  )
38482
39227
  ).orderBy(desc15(runs.startedAt)).limit(1).get();
38483
39228
  return {
@@ -38508,7 +39253,7 @@ async function trafficRoutes(app, opts) {
38508
39253
  "`advanceToNow` must be `true`. There is no implicit reset."
38509
39254
  );
38510
39255
  }
38511
- const sourceRow = app.db.select().from(trafficSources).where(and26(eq31(trafficSources.projectId, project.id), eq31(trafficSources.id, request.params.id))).get();
39256
+ const sourceRow = app.db.select().from(trafficSources).where(and27(eq32(trafficSources.projectId, project.id), eq32(trafficSources.id, request.params.id))).get();
38512
39257
  if (!sourceRow) {
38513
39258
  throw notFound("traffic source", request.params.id);
38514
39259
  }
@@ -38525,7 +39270,7 @@ async function trafficRoutes(app, opts) {
38525
39270
  status: TrafficSourceStatuses.connected,
38526
39271
  lastError: null,
38527
39272
  updatedAt: now
38528
- }).where(eq31(trafficSources.id, sourceRow.id)).run();
39273
+ }).where(eq32(trafficSources.id, sourceRow.id)).run();
38529
39274
  writeAuditLog(tx, auditFromRequest(request, {
38530
39275
  projectId: project.id,
38531
39276
  actor: "api",
@@ -38533,20 +39278,20 @@ async function trafficRoutes(app, opts) {
38533
39278
  entityType: "traffic_source",
38534
39279
  entityId: sourceRow.id
38535
39280
  }));
38536
- updatedRow = tx.select().from(trafficSources).where(eq31(trafficSources.id, sourceRow.id)).get();
39281
+ updatedRow = tx.select().from(trafficSources).where(eq32(trafficSources.id, sourceRow.id)).get();
38537
39282
  });
38538
39283
  return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
38539
39284
  });
38540
39285
  app.get("/projects/:name/traffic/sources", async (request) => {
38541
39286
  const project = resolveProject(app.db, request.params.name);
38542
- const rows = app.db.select().from(trafficSources).where(eq31(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
39287
+ const rows = app.db.select().from(trafficSources).where(eq32(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
38543
39288
  const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
38544
39289
  const response = { sources };
38545
39290
  return response;
38546
39291
  });
38547
39292
  app.get("/projects/:name/traffic/status", async (request) => {
38548
39293
  const project = resolveProject(app.db, request.params.name);
38549
- const rows = app.db.select().from(trafficSources).where(eq31(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
39294
+ const rows = app.db.select().from(trafficSources).where(eq32(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
38550
39295
  const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
38551
39296
  const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
38552
39297
  const response = { sources };
@@ -38556,7 +39301,7 @@ async function trafficRoutes(app, opts) {
38556
39301
  "/projects/:name/traffic/sources/:id",
38557
39302
  async (request) => {
38558
39303
  const project = resolveProject(app.db, request.params.name);
38559
- const row = app.db.select().from(trafficSources).where(eq31(trafficSources.id, request.params.id)).get();
39304
+ const row = app.db.select().from(trafficSources).where(eq32(trafficSources.id, request.params.id)).get();
38560
39305
  if (!row || row.projectId !== project.id) {
38561
39306
  throw notFound("Traffic source", request.params.id);
38562
39307
  }
@@ -38621,26 +39366,26 @@ async function trafficRoutes(app, opts) {
38621
39366
  const seriesByBucket = /* @__PURE__ */ new Map();
38622
39367
  if (kind === "all" || kind === TrafficEventKinds.crawler) {
38623
39368
  const crawlerFilters = [
38624
- eq31(crawlerEventsHourly.projectId, project.id),
38625
- gte7(crawlerEventsHourly.tsHour, sinceIso),
38626
- lte5(crawlerEventsHourly.tsHour, untilIso)
39369
+ eq32(crawlerEventsHourly.projectId, project.id),
39370
+ gte8(crawlerEventsHourly.tsHour, sinceIso),
39371
+ lte6(crawlerEventsHourly.tsHour, untilIso)
38627
39372
  ];
38628
- if (sourceIdParam) crawlerFilters.push(eq31(crawlerEventsHourly.sourceId, sourceIdParam));
38629
- const crawlerWhere = and26(...crawlerFilters);
39373
+ if (sourceIdParam) crawlerFilters.push(eq32(crawlerEventsHourly.sourceId, sourceIdParam));
39374
+ const crawlerWhere = and27(...crawlerFilters);
38630
39375
  const pathTotals = app.db.select({
38631
39376
  pathNormalized: crawlerEventsHourly.pathNormalized,
38632
- hits: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
38633
- rows: sql15`COUNT(*)`
39377
+ hits: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
39378
+ rows: sql16`COUNT(*)`
38634
39379
  }).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerEventsHourly.pathNormalized).all();
38635
39380
  crawlerSegments = segmentCrawlerHits(
38636
39381
  pathTotals.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
38637
39382
  );
38638
39383
  crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
38639
39384
  totalEventRows += pathTotals.reduce((sum, row) => sum + Number(row.rows), 0);
38640
- const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql15`substr(${crawlerEventsHourly.tsHour}, 1, 10)` : crawlerEventsHourly.tsHour;
39385
+ const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql16`substr(${crawlerEventsHourly.tsHour}, 1, 10)` : crawlerEventsHourly.tsHour;
38641
39386
  const crawlerSeries = app.db.select({
38642
39387
  bucket: crawlerSeriesBucket,
38643
- hits: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
39388
+ hits: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
38644
39389
  }).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerSeriesBucket).all();
38645
39390
  for (const row of crawlerSeries) {
38646
39391
  trafficSeriesPoint(seriesByBucket, row.bucket).crawlerHits = Number(row.hits);
@@ -38663,22 +39408,22 @@ async function trafficRoutes(app, opts) {
38663
39408
  }
38664
39409
  if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
38665
39410
  const userFetchFilters = [
38666
- eq31(aiUserFetchEventsHourly.projectId, project.id),
38667
- gte7(aiUserFetchEventsHourly.tsHour, sinceIso),
38668
- lte5(aiUserFetchEventsHourly.tsHour, untilIso)
39411
+ eq32(aiUserFetchEventsHourly.projectId, project.id),
39412
+ gte8(aiUserFetchEventsHourly.tsHour, sinceIso),
39413
+ lte6(aiUserFetchEventsHourly.tsHour, untilIso)
38669
39414
  ];
38670
- if (sourceIdParam) userFetchFilters.push(eq31(aiUserFetchEventsHourly.sourceId, sourceIdParam));
38671
- const userFetchWhere = and26(...userFetchFilters);
39415
+ if (sourceIdParam) userFetchFilters.push(eq32(aiUserFetchEventsHourly.sourceId, sourceIdParam));
39416
+ const userFetchWhere = and27(...userFetchFilters);
38672
39417
  const total = app.db.select({
38673
- total: sql15`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`,
38674
- rows: sql15`COUNT(*)`
39418
+ total: sql16`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`,
39419
+ rows: sql16`COUNT(*)`
38675
39420
  }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
38676
39421
  aiUserFetchTotal = Number(total?.total ?? 0);
38677
39422
  totalEventRows += Number(total?.rows ?? 0);
38678
- const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql15`substr(${aiUserFetchEventsHourly.tsHour}, 1, 10)` : aiUserFetchEventsHourly.tsHour;
39423
+ const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql16`substr(${aiUserFetchEventsHourly.tsHour}, 1, 10)` : aiUserFetchEventsHourly.tsHour;
38679
39424
  const userFetchSeries = app.db.select({
38680
39425
  bucket: userFetchSeriesBucket,
38681
- hits: sql15`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
39426
+ hits: sql16`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
38682
39427
  }).from(aiUserFetchEventsHourly).where(userFetchWhere).groupBy(userFetchSeriesBucket).all();
38683
39428
  for (const row of userFetchSeries) {
38684
39429
  trafficSeriesPoint(seriesByBucket, row.bucket).aiUserFetchHits = Number(row.hits);
@@ -38700,17 +39445,17 @@ async function trafficRoutes(app, opts) {
38700
39445
  }
38701
39446
  if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
38702
39447
  const aiFilters = [
38703
- eq31(aiReferralEventsHourly.projectId, project.id),
38704
- gte7(aiReferralEventsHourly.tsHour, sinceIso),
38705
- lte5(aiReferralEventsHourly.tsHour, untilIso)
39448
+ eq32(aiReferralEventsHourly.projectId, project.id),
39449
+ gte8(aiReferralEventsHourly.tsHour, sinceIso),
39450
+ lte6(aiReferralEventsHourly.tsHour, untilIso)
38706
39451
  ];
38707
- if (sourceIdParam) aiFilters.push(eq31(aiReferralEventsHourly.sourceId, sourceIdParam));
38708
- const aiWhere = and26(...aiFilters);
39452
+ if (sourceIdParam) aiFilters.push(eq32(aiReferralEventsHourly.sourceId, sourceIdParam));
39453
+ const aiWhere = and27(...aiFilters);
38709
39454
  const total = app.db.select({
38710
- total: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
38711
- paid: sql15`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
38712
- organic: sql15`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`,
38713
- rows: sql15`COUNT(*)`
39455
+ total: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
39456
+ paid: sql16`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
39457
+ organic: sql16`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`,
39458
+ rows: sql16`COUNT(*)`
38714
39459
  }).from(aiReferralEventsHourly).where(aiWhere).get();
38715
39460
  aiReferralCounts = aiReferralClassCounts(
38716
39461
  Number(total?.total ?? 0),
@@ -38718,10 +39463,10 @@ async function trafficRoutes(app, opts) {
38718
39463
  Number(total?.organic ?? 0)
38719
39464
  );
38720
39465
  totalEventRows += Number(total?.rows ?? 0);
38721
- const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql15`substr(${aiReferralEventsHourly.tsHour}, 1, 10)` : aiReferralEventsHourly.tsHour;
39466
+ const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql16`substr(${aiReferralEventsHourly.tsHour}, 1, 10)` : aiReferralEventsHourly.tsHour;
38722
39467
  const referralSeries = app.db.select({
38723
39468
  bucket: referralSeriesBucket,
38724
- hits: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
39469
+ hits: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
38725
39470
  }).from(aiReferralEventsHourly).where(aiWhere).groupBy(referralSeriesBucket).all();
38726
39471
  for (const row of referralSeries) {
38727
39472
  trafficSeriesPoint(seriesByBucket, row.bucket).aiReferralHits = Number(row.hits);
@@ -39024,7 +39769,7 @@ function readInstalledManifest(skillDir) {
39024
39769
  var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
39025
39770
 
39026
39771
  // ../api-routes/src/doctor/checks/backlinks.ts
39027
- import { and as and27, eq as eq32 } from "drizzle-orm";
39772
+ import { and as and28, eq as eq33 } from "drizzle-orm";
39028
39773
  function skippedNoProject() {
39029
39774
  return {
39030
39775
  status: CheckStatuses.skipped,
@@ -39040,8 +39785,8 @@ var BACKLINKS_CHECKS = [
39040
39785
  title: "Backlinks source connected",
39041
39786
  run: (ctx) => {
39042
39787
  if (!ctx.project) return skippedNoProject();
39043
- const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq32(projects.id, ctx.project.id)).get();
39044
- const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq32(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
39788
+ const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq33(projects.id, ctx.project.id)).get();
39789
+ const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq33(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
39045
39790
  const ccConnected = projectRow?.autoExtract === true && !!readySync;
39046
39791
  const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
39047
39792
  const connected = [];
@@ -39056,9 +39801,9 @@ var BACKLINKS_CHECKS = [
39056
39801
  details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
39057
39802
  };
39058
39803
  }
39059
- const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and27(
39060
- eq32(backlinkSummaries.projectId, ctx.project.id),
39061
- eq32(backlinkSummaries.source, BacklinkSources.commoncrawl)
39804
+ const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and28(
39805
+ eq33(backlinkSummaries.projectId, ctx.project.id),
39806
+ eq33(backlinkSummaries.source, BacklinkSources.commoncrawl)
39062
39807
  )).limit(1).get() : false;
39063
39808
  return {
39064
39809
  status: CheckStatuses.ok,
@@ -39218,7 +39963,7 @@ var BING_AUTH_CHECKS = [
39218
39963
  ];
39219
39964
 
39220
39965
  // ../api-routes/src/doctor/checks/content.ts
39221
- import { eq as eq33 } from "drizzle-orm";
39966
+ import { eq as eq34 } from "drizzle-orm";
39222
39967
  var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
39223
39968
  var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
39224
39969
  function skippedNoProject2() {
@@ -39231,7 +39976,7 @@ function skippedNoProject2() {
39231
39976
  }
39232
39977
  function loadProject(ctx) {
39233
39978
  if (!ctx.project) return null;
39234
- return ctx.db.select().from(projects).where(eq33(projects.id, ctx.project.id)).get() ?? null;
39979
+ return ctx.db.select().from(projects).where(eq34(projects.id, ctx.project.id)).get() ?? null;
39235
39980
  }
39236
39981
  function percent(value) {
39237
39982
  return Math.round(value * 100);
@@ -39323,7 +40068,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
39323
40068
  );
39324
40069
 
39325
40070
  // ../api-routes/src/doctor/checks/ads.ts
39326
- import { eq as eq34 } from "drizzle-orm";
40071
+ import { eq as eq35 } from "drizzle-orm";
39327
40072
  var RECENT_SYNC_WARN_DAYS = 7;
39328
40073
  var RECENT_SYNC_FAIL_DAYS = 30;
39329
40074
  var adsConnectionCheck = {
@@ -39340,7 +40085,7 @@ var adsConnectionCheck = {
39340
40085
  remediation: null
39341
40086
  };
39342
40087
  }
39343
- const row = ctx.db.select().from(adsConnections).where(eq34(adsConnections.projectId, ctx.project.id)).get();
40088
+ const row = ctx.db.select().from(adsConnections).where(eq35(adsConnections.projectId, ctx.project.id)).get();
39344
40089
  if (!row) {
39345
40090
  return {
39346
40091
  status: CheckStatuses.skipped,
@@ -39390,7 +40135,7 @@ var adsRecentSyncCheck = {
39390
40135
  remediation: null
39391
40136
  };
39392
40137
  }
39393
- const row = ctx.db.select().from(adsConnections).where(eq34(adsConnections.projectId, ctx.project.id)).get();
40138
+ const row = ctx.db.select().from(adsConnections).where(eq35(adsConnections.projectId, ctx.project.id)).get();
39394
40139
  if (!row) {
39395
40140
  return {
39396
40141
  status: CheckStatuses.skipped,
@@ -39581,7 +40326,7 @@ var ga4ConnectionCheck = {
39581
40326
  var GA_AUTH_CHECKS = [ga4ConnectionCheck];
39582
40327
 
39583
40328
  // ../api-routes/src/doctor/checks/gbp-auth.ts
39584
- import { and as and28, eq as eq35 } from "drizzle-orm";
40329
+ import { and as and29, eq as eq36 } from "drizzle-orm";
39585
40330
  var RECENT_SYNC_WARN_DAYS2 = 7;
39586
40331
  var RECENT_SYNC_FAIL_DAYS2 = 30;
39587
40332
  function skippedNoProject3() {
@@ -39814,7 +40559,7 @@ var recentSyncCheck = {
39814
40559
  title: "GBP recent sync",
39815
40560
  run: (ctx) => {
39816
40561
  if (!ctx.project) return skippedNoProject3();
39817
- const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and28(eq35(gbpLocations.projectId, ctx.project.id), eq35(gbpLocations.selected, true))).all();
40562
+ const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and29(eq36(gbpLocations.projectId, ctx.project.id), eq36(gbpLocations.selected, true))).all();
39818
40563
  if (selected.length === 0) {
39819
40564
  return {
39820
40565
  status: CheckStatuses.skipped,
@@ -39874,7 +40619,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
39874
40619
  );
39875
40620
 
39876
40621
  // ../api-routes/src/doctor/checks/places.ts
39877
- import { eq as eq36 } from "drizzle-orm";
40622
+ import { eq as eq37 } from "drizzle-orm";
39878
40623
  var apiKeyCheck = {
39879
40624
  id: "gbp.places.api-key",
39880
40625
  category: CheckCategories.auth,
@@ -39919,7 +40664,7 @@ var apiKeyCheck = {
39919
40664
  details: { tier: cfg.tier }
39920
40665
  };
39921
40666
  }
39922
- const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq36(gbpLocations.projectId, ctx.project.id)).all();
40667
+ const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq37(gbpLocations.projectId, ctx.project.id)).all();
39923
40668
  const selected = rows.filter((r) => r.selected);
39924
40669
  const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
39925
40670
  const details = {
@@ -40416,7 +41161,7 @@ var RUNTIME_STATE_CHECKS = [
40416
41161
  ];
40417
41162
 
40418
41163
  // ../api-routes/src/doctor/checks/traffic-source.ts
40419
- import { and as and29, eq as eq37, gte as gte8, ne as ne5, sql as sql16 } from "drizzle-orm";
41164
+ import { and as and30, eq as eq38, gte as gte9, ne as ne5, sql as sql17 } from "drizzle-orm";
40420
41165
  var RECENT_DATA_WARN_DAYS = 7;
40421
41166
  var RECENT_DATA_FAIL_DAYS = 30;
40422
41167
  function skippedNoProject5() {
@@ -40430,8 +41175,8 @@ function skippedNoProject5() {
40430
41175
  function loadProbes(ctx) {
40431
41176
  if (!ctx.project) return [];
40432
41177
  const rows = ctx.db.select().from(trafficSources).where(
40433
- and29(
40434
- eq37(trafficSources.projectId, ctx.project.id),
41178
+ and30(
41179
+ eq38(trafficSources.projectId, ctx.project.id),
40435
41180
  ne5(trafficSources.status, TrafficSourceStatuses.archived)
40436
41181
  )
40437
41182
  ).all();
@@ -40510,18 +41255,18 @@ var recentDataCheck = {
40510
41255
  const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
40511
41256
  const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
40512
41257
  const recentCrawlers = Number(
40513
- ctx.db.select({ total: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
40514
- and29(
40515
- eq37(crawlerEventsHourly.projectId, ctx.project.id),
40516
- gte8(crawlerEventsHourly.tsHour, warnCutoff)
41258
+ ctx.db.select({ total: sql17`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
41259
+ and30(
41260
+ eq38(crawlerEventsHourly.projectId, ctx.project.id),
41261
+ gte9(crawlerEventsHourly.tsHour, warnCutoff)
40517
41262
  )
40518
41263
  ).get()?.total ?? 0
40519
41264
  );
40520
41265
  const recentReferrals = Number(
40521
- ctx.db.select({ total: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
40522
- and29(
40523
- eq37(aiReferralEventsHourly.projectId, ctx.project.id),
40524
- gte8(aiReferralEventsHourly.tsHour, warnCutoff)
41266
+ ctx.db.select({ total: sql17`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
41267
+ and30(
41268
+ eq38(aiReferralEventsHourly.projectId, ctx.project.id),
41269
+ gte9(aiReferralEventsHourly.tsHour, warnCutoff)
40525
41270
  )
40526
41271
  ).get()?.total ?? 0
40527
41272
  );
@@ -40534,18 +41279,18 @@ var recentDataCheck = {
40534
41279
  };
40535
41280
  }
40536
41281
  const olderCrawlers = Number(
40537
- ctx.db.select({ total: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
40538
- and29(
40539
- eq37(crawlerEventsHourly.projectId, ctx.project.id),
40540
- gte8(crawlerEventsHourly.tsHour, failCutoff)
41282
+ ctx.db.select({ total: sql17`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
41283
+ and30(
41284
+ eq38(crawlerEventsHourly.projectId, ctx.project.id),
41285
+ gte9(crawlerEventsHourly.tsHour, failCutoff)
40541
41286
  )
40542
41287
  ).get()?.total ?? 0
40543
41288
  );
40544
41289
  const olderReferrals = Number(
40545
- ctx.db.select({ total: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
40546
- and29(
40547
- eq37(aiReferralEventsHourly.projectId, ctx.project.id),
40548
- gte8(aiReferralEventsHourly.tsHour, failCutoff)
41290
+ ctx.db.select({ total: sql17`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
41291
+ and30(
41292
+ eq38(aiReferralEventsHourly.projectId, ctx.project.id),
41293
+ gte9(aiReferralEventsHourly.tsHour, failCutoff)
40549
41294
  )
40550
41295
  ).get()?.total ?? 0
40551
41296
  );
@@ -40962,7 +41707,7 @@ async function doctorRoutes(app, opts) {
40962
41707
 
40963
41708
  // ../api-routes/src/discovery/routes.ts
40964
41709
  import crypto30 from "crypto";
40965
- import { and as and30, desc as desc16, eq as eq38, gte as gte9, inArray as inArray13, isNull as isNull3, or as or7 } from "drizzle-orm";
41710
+ import { and as and31, desc as desc16, eq as eq39, gte as gte10, inArray as inArray13, isNull as isNull3, or as or8 } from "drizzle-orm";
40966
41711
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
40967
41712
  async function discoveryRoutes(app, opts) {
40968
41713
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -40996,13 +41741,13 @@ async function discoveryRoutes(app, opts) {
40996
41741
  const now = (/* @__PURE__ */ new Date()).toISOString();
40997
41742
  const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
40998
41743
  const decision = app.db.transaction((tx) => {
40999
- const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and30(
41000
- eq38(discoverySessions.projectId, project.id),
41001
- eq38(discoverySessions.icpDescription, icpDescription),
41744
+ const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and31(
41745
+ eq39(discoverySessions.projectId, project.id),
41746
+ eq39(discoverySessions.icpDescription, icpDescription),
41002
41747
  // Buyer is part of session identity: it changes the seed prompt's
41003
41748
  // semantics, so a request with a different (or no) buyer must start
41004
41749
  // its own session, never adopt another buyer's probes.
41005
- parsed.data.buyerDescription == null ? isNull3(discoverySessions.buyerDescription) : eq38(discoverySessions.buyerDescription, parsed.data.buyerDescription),
41750
+ parsed.data.buyerDescription == null ? isNull3(discoverySessions.buyerDescription) : eq39(discoverySessions.buyerDescription, parsed.data.buyerDescription),
41006
41751
  // Locations are identity too: a different service-area subset seeds
41007
41752
  // and probes a different geo, so it must never reuse another geo's
41008
41753
  // session. resolveLocations is deterministic (project-config order),
@@ -41012,17 +41757,17 @@ async function discoveryRoutes(app, opts) {
41012
41757
  // locations a NULL row's subset is unknowable, so it conservatively
41013
41758
  // never reuses — a one-time, bounded (2h window) non-reuse after
41014
41759
  // upgrade, never a wrong reuse.
41015
- locations.length === 0 ? or7(isNull3(discoverySessions.locations), eq38(discoverySessions.locations, locations)) : eq38(discoverySessions.locations, locations),
41760
+ locations.length === 0 ? or8(isNull3(discoverySessions.locations), eq39(discoverySessions.locations, locations)) : eq39(discoverySessions.locations, locations),
41016
41761
  // Seed provider set is identity: a different phrasing distribution
41017
41762
  // must never reuse another set's session. Null (= Gemini-only
41018
41763
  // default, including explicit ['gemini']) matches legacy rows.
41019
- seedProviders == null ? isNull3(discoverySessions.seedProviders) : eq38(discoverySessions.seedProviders, seedProviders),
41764
+ seedProviders == null ? isNull3(discoverySessions.seedProviders) : eq39(discoverySessions.seedProviders, seedProviders),
41020
41765
  inArray13(discoverySessions.status, [
41021
41766
  DiscoverySessionStatuses.queued,
41022
41767
  DiscoverySessionStatuses.seeding,
41023
41768
  DiscoverySessionStatuses.probing
41024
41769
  ]),
41025
- gte9(discoverySessions.createdAt, ageFloorIso)
41770
+ gte10(discoverySessions.createdAt, ageFloorIso)
41026
41771
  )).orderBy(desc16(discoverySessions.createdAt)).get();
41027
41772
  if (existing && existing.runId) {
41028
41773
  return { reused: true, sessionId: existing.id, runId: existing.runId };
@@ -41092,7 +41837,7 @@ async function discoveryRoutes(app, opts) {
41092
41837
  const project = resolveProject(app.db, request.params.name);
41093
41838
  const parsedLimit = parseInt(request.query.limit ?? "", 10);
41094
41839
  const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
41095
- const rows = app.db.select().from(discoverySessions).where(eq38(discoverySessions.projectId, project.id)).orderBy(desc16(discoverySessions.createdAt)).limit(limit).all();
41840
+ const rows = app.db.select().from(discoverySessions).where(eq39(discoverySessions.projectId, project.id)).orderBy(desc16(discoverySessions.createdAt)).limit(limit).all();
41096
41841
  return reply.send(rows.map(serializeSession));
41097
41842
  }
41098
41843
  );
@@ -41100,11 +41845,11 @@ async function discoveryRoutes(app, opts) {
41100
41845
  "/projects/:name/discover/sessions/:id",
41101
41846
  async (request, reply) => {
41102
41847
  const project = resolveProject(app.db, request.params.name);
41103
- const session = app.db.select().from(discoverySessions).where(eq38(discoverySessions.id, request.params.id)).get();
41848
+ const session = app.db.select().from(discoverySessions).where(eq39(discoverySessions.id, request.params.id)).get();
41104
41849
  if (!session || session.projectId !== project.id) {
41105
41850
  throw notFound("Discovery session", request.params.id);
41106
41851
  }
41107
- const probeRows = app.db.select().from(discoveryProbes).where(eq38(discoveryProbes.sessionId, session.id)).all();
41852
+ const probeRows = app.db.select().from(discoveryProbes).where(eq39(discoveryProbes.sessionId, session.id)).all();
41108
41853
  const detail = {
41109
41854
  ...serializeSession(session),
41110
41855
  probes: probeRows.map(serializeProbe)
@@ -41116,7 +41861,7 @@ async function discoveryRoutes(app, opts) {
41116
41861
  "/projects/:name/discover/sessions/:id/harvest",
41117
41862
  async (request, reply) => {
41118
41863
  const project = resolveProject(app.db, request.params.name);
41119
- const session = app.db.select().from(discoverySessions).where(eq38(discoverySessions.id, request.params.id)).get();
41864
+ const session = app.db.select().from(discoverySessions).where(eq39(discoverySessions.id, request.params.id)).get();
41120
41865
  if (!session || session.projectId !== project.id) {
41121
41866
  throw notFound("Discovery session", request.params.id);
41122
41867
  }
@@ -41124,7 +41869,7 @@ async function discoveryRoutes(app, opts) {
41124
41869
  const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
41125
41870
  const applyAnchor = request.query.anchor !== "false";
41126
41871
  const provider = session.seedProvider ?? "gemini";
41127
- const probeRows = app.db.select().from(discoveryProbes).where(eq38(discoveryProbes.sessionId, session.id)).all();
41872
+ const probeRows = app.db.select().from(discoveryProbes).where(eq39(discoveryProbes.sessionId, session.id)).all();
41128
41873
  const extract = opts.harvestSearchQueries;
41129
41874
  const probesWithQueries = probeRows.map((row) => {
41130
41875
  if (!extract || !row.rawResponse) return { searchQueries: [] };
@@ -41135,7 +41880,7 @@ async function discoveryRoutes(app, opts) {
41135
41880
  return { searchQueries: [] };
41136
41881
  }
41137
41882
  });
41138
- const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq38(queries.projectId, project.id)).all().map((r) => r.query);
41883
+ const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq39(queries.projectId, project.id)).all().map((r) => r.query);
41139
41884
  const anchorTerms = buildHarvestAnchorTerms(
41140
41885
  [session.icpDescription ?? "", ...trackedQueries],
41141
41886
  effectiveDomains(project)
@@ -41182,12 +41927,12 @@ async function discoveryRoutes(app, opts) {
41182
41927
  "/projects/:name/discover/sessions/:id/promote",
41183
41928
  async (request, reply) => {
41184
41929
  const project = resolveProject(app.db, request.params.name);
41185
- const session = app.db.select().from(discoverySessions).where(eq38(discoverySessions.id, request.params.id)).get();
41930
+ const session = app.db.select().from(discoverySessions).where(eq39(discoverySessions.id, request.params.id)).get();
41186
41931
  if (!session || session.projectId !== project.id) {
41187
41932
  throw notFound("Discovery session", request.params.id);
41188
41933
  }
41189
- const probeRows = app.db.select().from(discoveryProbes).where(eq38(discoveryProbes.sessionId, session.id)).all();
41190
- const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq38(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
41934
+ const probeRows = app.db.select().from(discoveryProbes).where(eq39(discoveryProbes.sessionId, session.id)).all();
41935
+ const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq39(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
41191
41936
  const seenCompetitors = new Set(existingCompetitors);
41192
41937
  const cited = /* @__PURE__ */ new Set();
41193
41938
  const aspirational = /* @__PURE__ */ new Set();
@@ -41216,7 +41961,7 @@ async function discoveryRoutes(app, opts) {
41216
41961
  );
41217
41962
  app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
41218
41963
  const project = resolveProject(app.db, request.params.name);
41219
- const session = app.db.select().from(discoverySessions).where(eq38(discoverySessions.id, request.params.id)).get();
41964
+ const session = app.db.select().from(discoverySessions).where(eq39(discoverySessions.id, request.params.id)).get();
41220
41965
  if (!session || session.projectId !== project.id) {
41221
41966
  throw notFound("Discovery session", request.params.id);
41222
41967
  }
@@ -41239,7 +41984,7 @@ async function discoveryRoutes(app, opts) {
41239
41984
  const bucketSet = new Set(buckets);
41240
41985
  const includeCompetitors = parsed.data.includeCompetitors ?? true;
41241
41986
  const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
41242
- const probeRows = app.db.select().from(discoveryProbes).where(eq38(discoveryProbes.sessionId, session.id)).all();
41987
+ const probeRows = app.db.select().from(discoveryProbes).where(eq39(discoveryProbes.sessionId, session.id)).all();
41243
41988
  const candidateQueries = /* @__PURE__ */ new Set();
41244
41989
  for (const probe of probeRows) {
41245
41990
  if (!probe.bucket) continue;
@@ -41247,7 +41992,7 @@ async function discoveryRoutes(app, opts) {
41247
41992
  if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
41248
41993
  }
41249
41994
  const existingQueries = new Set(
41250
- app.db.select({ query: queries.query }).from(queries).where(eq38(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
41995
+ app.db.select({ query: queries.query }).from(queries).where(eq39(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
41251
41996
  );
41252
41997
  const promotedQueries = [];
41253
41998
  const skippedQueries = [];
@@ -41263,7 +42008,7 @@ async function discoveryRoutes(app, opts) {
41263
42008
  const skippedCompetitors = [];
41264
42009
  if (includeCompetitors) {
41265
42010
  const existingCompetitors = new Set(
41266
- app.db.select({ domain: competitors.domain }).from(competitors).where(eq38(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
42011
+ app.db.select({ domain: competitors.domain }).from(competitors).where(eq39(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
41267
42012
  );
41268
42013
  const competitorMap = parseCompetitorMap(session.competitorMap);
41269
42014
  for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
@@ -41382,7 +42127,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
41382
42127
 
41383
42128
  // ../api-routes/src/discovery/orchestrate.ts
41384
42129
  import crypto31 from "crypto";
41385
- import { eq as eq39 } from "drizzle-orm";
42130
+ import { eq as eq40 } from "drizzle-orm";
41386
42131
  var DEFAULT_MAX_PROBES = 100;
41387
42132
  var ABSOLUTE_MAX_PROBES = 500;
41388
42133
  function classifyProbeBucket(input) {
@@ -41469,7 +42214,7 @@ async function executeDiscovery(opts) {
41469
42214
  status: DiscoverySessionStatuses.seeding,
41470
42215
  dedupThreshold,
41471
42216
  startedAt
41472
- }).where(eq39(discoverySessions.id, opts.sessionId)).run();
42217
+ }).where(eq40(discoverySessions.id, opts.sessionId)).run();
41473
42218
  const seedResult = await opts.deps.seed({
41474
42219
  project: opts.project,
41475
42220
  icpDescription: opts.icpDescription,
@@ -41530,7 +42275,7 @@ async function executeDiscovery(opts) {
41530
42275
  dedupBandPairFraction: dedupStats.bandPairFraction,
41531
42276
  dedupPairsTotal: dedupStats.pairsTotal,
41532
42277
  warning
41533
- }).where(eq39(discoverySessions.id, opts.sessionId)).run();
42278
+ }).where(eq40(discoverySessions.id, opts.sessionId)).run();
41534
42279
  const probeLocation = opts.locations?.[0];
41535
42280
  const probeResults = await mapWithConcurrency(
41536
42281
  probedCanonicals,
@@ -41584,7 +42329,7 @@ async function executeDiscovery(opts) {
41584
42329
  wastedCount: buckets["wasted-surface"],
41585
42330
  competitorMap,
41586
42331
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
41587
- }).where(eq39(discoverySessions.id, opts.sessionId)).run();
42332
+ }).where(eq40(discoverySessions.id, opts.sessionId)).run();
41588
42333
  upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
41589
42334
  return {
41590
42335
  buckets,
@@ -41624,7 +42369,7 @@ function markSessionFailed(db, sessionId, error) {
41624
42369
  status: DiscoverySessionStatuses.failed,
41625
42370
  error,
41626
42371
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
41627
- }).where(eq39(discoverySessions.id, sessionId)).run();
42372
+ }).where(eq40(discoverySessions.id, sessionId)).run();
41628
42373
  }
41629
42374
  function dedupeStrings(input) {
41630
42375
  const seen = /* @__PURE__ */ new Set();
@@ -41642,7 +42387,7 @@ function dedupeStrings(input) {
41642
42387
 
41643
42388
  // ../api-routes/src/technical-aeo.ts
41644
42389
  import crypto32 from "crypto";
41645
- import { and as and31, asc as asc8, count, desc as desc17, eq as eq40, inArray as inArray14, lt as lt4 } from "drizzle-orm";
42390
+ import { and as and32, asc as asc8, count, desc as desc17, eq as eq41, inArray as inArray14, lt as lt4 } from "drizzle-orm";
41646
42391
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
41647
42392
  function emptyScore(projectName) {
41648
42393
  return {
@@ -41675,19 +42420,19 @@ async function technicalAeoRoutes(app, opts) {
41675
42420
  app.get("/projects/:name/technical-aeo", async (request) => {
41676
42421
  const project = resolveProject(app.db, request.params.name);
41677
42422
  const baseFilters = [
41678
- eq40(siteAuditSnapshots.projectId, project.id),
41679
- eq40(runs.kind, RunKinds["site-audit"]),
42423
+ eq41(siteAuditSnapshots.projectId, project.id),
42424
+ eq41(runs.kind, RunKinds["site-audit"]),
41680
42425
  inArray14(runs.status, SURFACEABLE_STATUSES),
41681
42426
  notProbeRun()
41682
42427
  ];
41683
- const targetFilters = request.query.runId ? [...baseFilters, eq40(siteAuditSnapshots.runId, request.query.runId)] : baseFilters;
41684
- const latest = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq40(siteAuditSnapshots.runId, runs.id)).where(and31(...targetFilters)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
42428
+ const targetFilters = request.query.runId ? [...baseFilters, eq41(siteAuditSnapshots.runId, request.query.runId)] : baseFilters;
42429
+ const latest = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq41(siteAuditSnapshots.runId, runs.id)).where(and32(...targetFilters)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
41685
42430
  if (!latest) {
41686
42431
  if (request.query.runId) throw notFound("Site audit run", request.query.runId);
41687
42432
  return emptyScore(project.name);
41688
42433
  }
41689
42434
  const snap = latest.snap;
41690
- const previous = app.db.select({ snap: siteAuditSnapshots }).from(siteAuditSnapshots).innerJoin(runs, eq40(siteAuditSnapshots.runId, runs.id)).where(and31(...baseFilters, lt4(siteAuditSnapshots.createdAt, snap.createdAt))).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get()?.snap ?? null;
42435
+ const previous = app.db.select({ snap: siteAuditSnapshots }).from(siteAuditSnapshots).innerJoin(runs, eq41(siteAuditSnapshots.runId, runs.id)).where(and32(...baseFilters, lt4(siteAuditSnapshots.createdAt, snap.createdAt))).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get()?.snap ?? null;
41691
42436
  const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
41692
42437
  const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
41693
42438
  return {
@@ -41714,13 +42459,13 @@ async function technicalAeoRoutes(app, opts) {
41714
42459
  app.get("/projects/:name/technical-aeo/pages", async (request) => {
41715
42460
  const project = resolveProject(app.db, request.params.name);
41716
42461
  const targetFilters = [
41717
- eq40(siteAuditSnapshots.projectId, project.id),
41718
- eq40(runs.kind, RunKinds["site-audit"]),
42462
+ eq41(siteAuditSnapshots.projectId, project.id),
42463
+ eq41(runs.kind, RunKinds["site-audit"]),
41719
42464
  inArray14(runs.status, SURFACEABLE_STATUSES),
41720
42465
  notProbeRun()
41721
42466
  ];
41722
- if (request.query.runId) targetFilters.push(eq40(siteAuditSnapshots.runId, request.query.runId));
41723
- const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq40(siteAuditSnapshots.runId, runs.id)).where(and31(...targetFilters)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
42467
+ if (request.query.runId) targetFilters.push(eq41(siteAuditSnapshots.runId, request.query.runId));
42468
+ const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq41(siteAuditSnapshots.runId, runs.id)).where(and32(...targetFilters)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
41724
42469
  if (!latest && request.query.runId) {
41725
42470
  throw notFound("Site audit run", request.query.runId);
41726
42471
  }
@@ -41728,9 +42473,9 @@ async function technicalAeoRoutes(app, opts) {
41728
42473
  return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
41729
42474
  }
41730
42475
  const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
41731
- const conds = [eq40(siteAuditPages.runId, latest.runId)];
41732
- if (statusFilter) conds.push(eq40(siteAuditPages.status, statusFilter));
41733
- const where = and31(...conds);
42476
+ const conds = [eq41(siteAuditPages.runId, latest.runId)];
42477
+ if (statusFilter) conds.push(eq41(siteAuditPages.status, statusFilter));
42478
+ const where = and32(...conds);
41734
42479
  const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
41735
42480
  const total = totalRow?.value ?? 0;
41736
42481
  const limit = parsePositiveInt(request.query.limit, 100, 500);
@@ -41754,9 +42499,9 @@ async function technicalAeoRoutes(app, opts) {
41754
42499
  auditedAt: siteAuditSnapshots.auditedAt,
41755
42500
  aggregateScore: siteAuditSnapshots.aggregateScore,
41756
42501
  pagesAudited: siteAuditSnapshots.pagesAudited
41757
- }).from(siteAuditSnapshots).innerJoin(runs, eq40(siteAuditSnapshots.runId, runs.id)).where(and31(
41758
- eq40(siteAuditSnapshots.projectId, project.id),
41759
- eq40(runs.kind, RunKinds["site-audit"]),
42502
+ }).from(siteAuditSnapshots).innerJoin(runs, eq41(siteAuditSnapshots.runId, runs.id)).where(and32(
42503
+ eq41(siteAuditSnapshots.projectId, project.id),
42504
+ eq41(runs.kind, RunKinds["site-audit"]),
41760
42505
  inArray14(runs.status, SURFACEABLE_STATUSES),
41761
42506
  notProbeRun()
41762
42507
  )).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(limit).all();
@@ -41768,9 +42513,9 @@ async function technicalAeoRoutes(app, opts) {
41768
42513
  if (!parsed.success) {
41769
42514
  throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
41770
42515
  }
41771
- const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and31(
41772
- eq40(runs.projectId, project.id),
41773
- eq40(runs.kind, RunKinds["site-audit"]),
42516
+ const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and32(
42517
+ eq41(runs.projectId, project.id),
42518
+ eq41(runs.kind, RunKinds["site-audit"]),
41774
42519
  inArray14(runs.status, [RunStatuses.queued, RunStatuses.running])
41775
42520
  )).get();
41776
42521
  if (existing) {
@@ -41796,7 +42541,7 @@ async function technicalAeoRoutes(app, opts) {
41796
42541
 
41797
42542
  // ../api-routes/src/research.ts
41798
42543
  import crypto33 from "crypto";
41799
- import { and as and32, desc as desc18, eq as eq41 } from "drizzle-orm";
42544
+ import { and as and33, desc as desc18, eq as eq42 } from "drizzle-orm";
41800
42545
  var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
41801
42546
  async function researchRoutes(app, opts) {
41802
42547
  app.post("/projects/:name/research/runs", async (request, reply) => {
@@ -41826,7 +42571,7 @@ async function researchRoutes(app, opts) {
41826
42571
  const now = (/* @__PURE__ */ new Date()).toISOString();
41827
42572
  const decision = app.db.transaction((tx) => {
41828
42573
  if (input.idempotencyKey) {
41829
- const existing = tx.select().from(researchRuns).where(and32(eq41(researchRuns.projectId, project.id), eq41(researchRuns.idempotencyKey, input.idempotencyKey))).get();
42574
+ const existing = tx.select().from(researchRuns).where(and33(eq42(researchRuns.projectId, project.id), eq42(researchRuns.idempotencyKey, input.idempotencyKey))).get();
41830
42575
  if (existing) {
41831
42576
  if (existing.requestHash !== requestHash2) throw alreadyExists("Research idempotency key", input.idempotencyKey);
41832
42577
  return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
@@ -41847,7 +42592,7 @@ async function researchRoutes(app, opts) {
41847
42592
  const project = resolveProject(app.db, request.params.name);
41848
42593
  const requested = Number.parseInt(request.query.limit ?? "", 10);
41849
42594
  const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
41850
- const runs2 = app.db.select().from(researchRuns).where(eq41(researchRuns.projectId, project.id)).orderBy(desc18(researchRuns.createdAt)).limit(limit).all().map(serializeRun);
42595
+ const runs2 = app.db.select().from(researchRuns).where(eq42(researchRuns.projectId, project.id)).orderBy(desc18(researchRuns.createdAt)).limit(limit).all().map(serializeRun);
41851
42596
  return { runs: runs2 };
41852
42597
  });
41853
42598
  app.get("/projects/:name/research/runs/:runId", async (request) => {
@@ -41856,9 +42601,9 @@ async function researchRoutes(app, opts) {
41856
42601
  });
41857
42602
  }
41858
42603
  function getDetail(app, projectId, id) {
41859
- const row = app.db.select().from(researchRuns).where(and32(eq41(researchRuns.id, id), eq41(researchRuns.projectId, projectId))).get();
42604
+ const row = app.db.select().from(researchRuns).where(and33(eq42(researchRuns.id, id), eq42(researchRuns.projectId, projectId))).get();
41860
42605
  if (!row) throw notFound("Research run", id);
41861
- const queries2 = app.db.select().from(researchRunQueries).where(eq41(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
42606
+ const queries2 = app.db.select().from(researchRunQueries).where(eq42(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
41862
42607
  return { ...serializeRun(row), queries: queries2 };
41863
42608
  }
41864
42609
  function serializeRun(row) {
@@ -42025,6 +42770,7 @@ async function apiRoutes(app, opts) {
42025
42770
  googleConnectionStore: opts.googleConnectionStore,
42026
42771
  getGoogleAuthConfig: opts.getGoogleAuthConfig
42027
42772
  });
42773
+ await api.register(gaMeasurementAnalysisRoutes);
42028
42774
  await api.register(trafficRoutes, {
42029
42775
  cloudRunCredentialStore: opts.cloudRunCredentialStore,
42030
42776
  pullCloudRunEvents: opts.pullCloudRunEvents,
@@ -42452,9 +43198,9 @@ var IntelligenceService = class {
42452
43198
  */
42453
43199
  analyzeAndPersist(runId, projectId) {
42454
43200
  const recentRuns = this.db.select().from(runs).where(
42455
- and33(
42456
- eq42(runs.projectId, projectId),
42457
- or8(eq42(runs.status, "completed"), eq42(runs.status, "partial")),
43201
+ and34(
43202
+ eq43(runs.projectId, projectId),
43203
+ or9(eq43(runs.status, "completed"), eq43(runs.status, "partial")),
42458
43204
  // Defensive: RunCoordinator already skips probes before this is
42459
43205
  // called, but if a future call site invokes analyzeAndPersist
42460
43206
  // directly for a probe, probes still must not pollute the
@@ -42536,7 +43282,7 @@ var IntelligenceService = class {
42536
43282
  * Returns the persisted insights so the coordinator can count critical/high.
42537
43283
  */
42538
43284
  analyzeAndPersistGbp(runId, projectId) {
42539
- const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq42(runs.id, runId)).get();
43285
+ const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq43(runs.id, runId)).get();
42540
43286
  if (!runRow) {
42541
43287
  log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
42542
43288
  this.persistGbpInsights(runId, projectId, [], []);
@@ -42544,11 +43290,11 @@ var IntelligenceService = class {
42544
43290
  }
42545
43291
  const windowStart = runRow.startedAt ?? runRow.createdAt;
42546
43292
  const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
42547
- const selected = this.db.select().from(gbpLocations).where(and33(
42548
- eq42(gbpLocations.projectId, projectId),
42549
- eq42(gbpLocations.selected, true),
42550
- gte10(gbpLocations.syncedAt, windowStart),
42551
- lte6(gbpLocations.syncedAt, windowEnd)
43293
+ const selected = this.db.select().from(gbpLocations).where(and34(
43294
+ eq43(gbpLocations.projectId, projectId),
43295
+ eq43(gbpLocations.selected, true),
43296
+ gte11(gbpLocations.syncedAt, windowStart),
43297
+ lte7(gbpLocations.syncedAt, windowEnd)
42552
43298
  )).all();
42553
43299
  if (selected.length === 0) {
42554
43300
  log.info("gbp-intelligence.skip", { runId, reason: "no locations synced during run" });
@@ -42581,12 +43327,12 @@ var IntelligenceService = class {
42581
43327
  }
42582
43328
  /** Build the per-location signal bundle the GBP analyzer consumes. */
42583
43329
  buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
42584
- const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and33(eq42(gbpDailyMetrics.projectId, projectId), eq42(gbpDailyMetrics.locationName, locationName))).all();
42585
- const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and33(eq42(gbpPlaceActions.projectId, projectId), eq42(gbpPlaceActions.locationName, locationName))).all();
42586
- const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and33(eq42(gbpLodgingSnapshots.projectId, projectId), eq42(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc19(gbpLodgingSnapshots.syncedAt)).limit(1).get();
42587
- const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and33(eq42(gbpLocations.projectId, projectId), eq42(gbpLocations.locationName, locationName))).get();
43330
+ const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and34(eq43(gbpDailyMetrics.projectId, projectId), eq43(gbpDailyMetrics.locationName, locationName))).all();
43331
+ const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and34(eq43(gbpPlaceActions.projectId, projectId), eq43(gbpPlaceActions.locationName, locationName))).all();
43332
+ const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and34(eq43(gbpLodgingSnapshots.projectId, projectId), eq43(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc19(gbpLodgingSnapshots.syncedAt)).limit(1).get();
43333
+ const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and34(eq43(gbpLocations.projectId, projectId), eq43(gbpLocations.locationName, locationName))).get();
42588
43334
  const descriptionMissing = !(ownerRow?.description ?? "").trim();
42589
- const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and33(eq42(gbpPlaceDetails.projectId, projectId), eq42(gbpPlaceDetails.locationName, locationName))).orderBy(desc19(gbpPlaceDetails.syncedAt)).limit(1).get();
43335
+ const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and34(eq43(gbpPlaceDetails.projectId, projectId), eq43(gbpPlaceDetails.locationName, locationName))).orderBy(desc19(gbpPlaceDetails.syncedAt)).limit(1).get();
42590
43336
  const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
42591
43337
  const summary = buildGbpSummary({
42592
43338
  locationName,
@@ -42622,7 +43368,7 @@ var IntelligenceService = class {
42622
43368
  /** Build the month-over-month keyword series for a location from the
42623
43369
  * accumulating gbp_keyword_monthly table (latest complete month vs prior). */
42624
43370
  buildGbpKeywordTrend(projectId, locationName) {
42625
- const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and33(eq42(gbpKeywordMonthly.projectId, projectId), eq42(gbpKeywordMonthly.locationName, locationName))).all();
43371
+ const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and34(eq43(gbpKeywordMonthly.projectId, projectId), eq43(gbpKeywordMonthly.locationName, locationName))).all();
42626
43372
  if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
42627
43373
  const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
42628
43374
  const recentMonth = months[0] ?? null;
@@ -42653,7 +43399,7 @@ var IntelligenceService = class {
42653
43399
  */
42654
43400
  persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
42655
43401
  const covered = new Set(coveredLocationNames);
42656
- const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and33(eq42(insights.projectId, projectId), eq42(insights.provider, GBP_INSIGHT_PROVIDER))).all();
43402
+ const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and34(eq43(insights.projectId, projectId), eq43(insights.provider, GBP_INSIGHT_PROVIDER))).all();
42657
43403
  const staleIds = [];
42658
43404
  const dismissedSlots = /* @__PURE__ */ new Set();
42659
43405
  for (const row of existing) {
@@ -42664,7 +43410,7 @@ var IntelligenceService = class {
42664
43410
  }
42665
43411
  this.db.transaction((tx) => {
42666
43412
  for (const id of staleIds) {
42667
- tx.delete(insights).where(eq42(insights.id, id)).run();
43413
+ tx.delete(insights).where(eq43(insights.id, id)).run();
42668
43414
  }
42669
43415
  for (const insight of gbpInsights) {
42670
43416
  const parsed = parseGbpInsightId(insight.id);
@@ -42742,7 +43488,7 @@ var IntelligenceService = class {
42742
43488
  * create per run + aggregate). DB is left untouched.
42743
43489
  */
42744
43490
  backfill(projectName, opts, onProgress) {
42745
- const project = this.db.select().from(projects).where(eq42(projects.name, projectName)).get();
43491
+ const project = this.db.select().from(projects).where(eq43(projects.name, projectName)).get();
42746
43492
  if (!project) {
42747
43493
  throw new Error(`Project "${projectName}" not found`);
42748
43494
  }
@@ -42755,9 +43501,9 @@ var IntelligenceService = class {
42755
43501
  sinceTimestamp = parsed;
42756
43502
  }
42757
43503
  const allRuns = this.db.select().from(runs).where(
42758
- and33(
42759
- eq42(runs.projectId, project.id),
42760
- or8(eq42(runs.status, "completed"), eq42(runs.status, "partial")),
43504
+ and34(
43505
+ eq43(runs.projectId, project.id),
43506
+ or9(eq43(runs.status, "completed"), eq43(runs.status, "partial")),
42761
43507
  // Backfill must not replay probe runs as if they were real sweeps.
42762
43508
  ne6(runs.trigger, RunTriggers.probe)
42763
43509
  )
@@ -42836,7 +43582,7 @@ var IntelligenceService = class {
42836
43582
  return { processed, skipped, totalInsights };
42837
43583
  }
42838
43584
  loadTrackedCompetitors(projectId) {
42839
- return this.db.select({ domain: competitors.domain }).from(competitors).where(eq42(competitors.projectId, projectId)).all().map((r) => r.domain);
43585
+ return this.db.select({ domain: competitors.domain }).from(competitors).where(eq43(competitors.projectId, projectId)).all().map((r) => r.domain);
42840
43586
  }
42841
43587
  /**
42842
43588
  * Wipe transition signals from an analysis result while keeping health.
@@ -42857,15 +43603,15 @@ var IntelligenceService = class {
42857
43603
  }
42858
43604
  persistResult(result, runId, projectId) {
42859
43605
  const previouslyDismissed = /* @__PURE__ */ new Set();
42860
- const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq42(insights.runId, runId)).all();
43606
+ const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq43(insights.runId, runId)).all();
42861
43607
  for (const row of existingInsights) {
42862
43608
  if (row.dismissed) {
42863
43609
  previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
42864
43610
  }
42865
43611
  }
42866
43612
  this.db.transaction((tx) => {
42867
- tx.delete(insights).where(eq42(insights.runId, runId)).run();
42868
- tx.delete(healthSnapshots).where(eq42(healthSnapshots.runId, runId)).run();
43613
+ tx.delete(insights).where(eq43(insights.runId, runId)).run();
43614
+ tx.delete(healthSnapshots).where(eq43(healthSnapshots.runId, runId)).run();
42869
43615
  const now = (/* @__PURE__ */ new Date()).toISOString();
42870
43616
  for (const insight of result.insights) {
42871
43617
  const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
@@ -42918,24 +43664,24 @@ var IntelligenceService = class {
42918
43664
  applySeverityTiering(rawInsights, excludeRunId, projectId) {
42919
43665
  const regressions = rawInsights.filter((i) => i.type === "regression");
42920
43666
  if (regressions.length === 0) return rawInsights;
42921
- const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq42(gscSearchData.projectId, projectId)).all();
43667
+ const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq43(gscSearchData.projectId, projectId)).all();
42922
43668
  const gscConnected = gscRows.length > 0;
42923
43669
  const gscImpressionsByQuery = /* @__PURE__ */ new Map();
42924
43670
  for (const row of gscRows) {
42925
43671
  const key = row.query.toLowerCase();
42926
43672
  gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
42927
43673
  }
42928
- const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq42(projects.id, projectId)).get();
43674
+ const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq43(projects.id, projectId)).get();
42929
43675
  const locationCount = Math.max(
42930
43676
  1,
42931
43677
  (projectRow?.locations ?? []).length
42932
43678
  );
42933
43679
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
42934
43680
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
42935
- and33(
42936
- eq42(runs.projectId, projectId),
42937
- eq42(runs.kind, RunKinds["answer-visibility"]),
42938
- or8(eq42(runs.status, "completed"), eq42(runs.status, "partial")),
43681
+ and34(
43682
+ eq43(runs.projectId, projectId),
43683
+ eq43(runs.kind, RunKinds["answer-visibility"]),
43684
+ or9(eq43(runs.status, "completed"), eq43(runs.status, "partial")),
42939
43685
  // Defensive — see top of file.
42940
43686
  ne6(runs.trigger, RunTriggers.probe)
42941
43687
  )
@@ -42955,7 +43701,7 @@ var IntelligenceService = class {
42955
43701
  const haveHistory = recentRunIds.length > 0;
42956
43702
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
42957
43703
  if (haveHistory) {
42958
- const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and33(eq42(insights.type, "regression"), inArray15(insights.runId, recentRunIds))).all();
43704
+ const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and34(eq43(insights.type, "regression"), inArray15(insights.runId, recentRunIds))).all();
42959
43705
  const regressionGroups = /* @__PURE__ */ new Map();
42960
43706
  for (const row of priorRows) {
42961
43707
  if (!row.runId) continue;
@@ -42984,7 +43730,7 @@ var IntelligenceService = class {
42984
43730
  });
42985
43731
  }
42986
43732
  buildRunData(runId, projectId, completedAt, location = null) {
42987
- const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq42(projects.id, projectId)).get();
43733
+ const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq43(projects.id, projectId)).get();
42988
43734
  const projectDomains = projectDomainRow ? effectiveDomains({
42989
43735
  canonicalDomain: projectDomainRow.canonicalDomain,
42990
43736
  ownedDomains: projectDomainRow.ownedDomains
@@ -43001,7 +43747,7 @@ var IntelligenceService = class {
43001
43747
  citedDomains: querySnapshots.citedDomains,
43002
43748
  competitorOverlap: querySnapshots.competitorOverlap,
43003
43749
  snapshotLocation: querySnapshots.location
43004
- }).from(querySnapshots).leftJoin(queries, eq42(querySnapshots.queryId, queries.id)).where(eq42(querySnapshots.runId, runId)).all();
43750
+ }).from(querySnapshots).leftJoin(queries, eq43(querySnapshots.queryId, queries.id)).where(eq43(querySnapshots.runId, runId)).all();
43005
43751
  const snapshots = [];
43006
43752
  let orphanCount = 0;
43007
43753
  for (const r of rows) {