@canonry/canonry 4.129.1 → 4.131.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 (30) hide show
  1. package/README.md +1 -0
  2. package/assets/assets/{AuditHistoryPanel-wJqs99Ak.js → AuditHistoryPanel-yJXZMO5t.js} +1 -1
  3. package/assets/assets/BacklinksPage-6VTLbAYx.js +1 -0
  4. package/assets/assets/{ChartPrimitives-DSr5ldMf.js → ChartPrimitives-D5YGiDLi.js} +1 -1
  5. package/assets/assets/{HistoryPage-Ca-GGnTI.js → HistoryPage-BxdTM6-R.js} +1 -1
  6. package/assets/assets/ProjectPage-BJKxZPRO.js +8 -0
  7. package/assets/assets/{RunRow-O9TbOdQY.js → RunRow-DTrAO7tE.js} +1 -1
  8. package/assets/assets/{RunsPage-C1Sqd7AN.js → RunsPage-CHU0DQGl.js} +1 -1
  9. package/assets/assets/{SettingsPage-DPFh62BV.js → SettingsPage-bOIwju6t.js} +1 -1
  10. package/assets/assets/{TrafficPage-5vKoQyoP.js → TrafficPage-BKvuR482.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-D17oADs8.js → TrafficSourceDetailPage-3-iUTgQg.js} +1 -1
  12. package/assets/assets/{arrow-left-De5P5AIE.js → arrow-left-BXSfGhr6.js} +1 -1
  13. package/assets/assets/{extract-error-message-BqMd-fQ6.js → extract-error-message-DEAY62LS.js} +1 -1
  14. package/assets/assets/index-CfjIYitV.js +210 -0
  15. package/assets/assets/{index-jymsMBpZ.css → index-JhX-wfQO.css} +1 -1
  16. package/assets/assets/trash-2-O1wo9IX1.js +1 -0
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-K4FDDCYG.js → chunk-3WBAJKRR.js} +2746 -2626
  19. package/dist/{chunk-GYAKRVE2.js → chunk-5GQ6FF5R.js} +508 -355
  20. package/dist/{chunk-WWZE7VDQ.js → chunk-73HPY6XQ.js} +118 -1
  21. package/dist/{chunk-LPEE5AYY.js → chunk-ZXVWL5HX.js} +956 -301
  22. package/dist/cli.js +432 -182
  23. package/dist/index.js +4 -4
  24. package/dist/{intelligence-service-2QBAGT5X.js → intelligence-service-YJKQQYJQ.js} +2 -2
  25. package/dist/mcp.js +5 -5
  26. package/package.json +10 -10
  27. package/assets/assets/BacklinksPage-BBDExYo_.js +0 -1
  28. package/assets/assets/ProjectPage-BuaSDSwf.js +0 -7
  29. package/assets/assets/index-C1ABEgkr.js +0 -210
  30. package/assets/assets/trash-2-6ZpABj9G.js +0 -1
@@ -29,6 +29,7 @@ import {
29
29
  ContentActions,
30
30
  DEFAULT_DISCOVERY_PROMOTE_BUCKETS,
31
31
  DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES,
32
+ DEFAULT_MEASUREMENT_CONFIG,
32
33
  DISCOVERY_DEFAULT_DEDUP_THRESHOLD,
33
34
  DISCOVERY_DEFAULT_PROBE_CONCURRENCY,
34
35
  DISCOVERY_PROBE_CONCURRENCY_CAP,
@@ -38,6 +39,8 @@ import {
38
39
  DiscoveryCompetitorTypes,
39
40
  DiscoverySessionStatuses,
40
41
  MODEL_ATTRIBUTION_EVENT_LIMIT,
42
+ ResearchQueryStatuses,
43
+ ResearchRunStatuses,
41
44
  RunKinds,
42
45
  RunStatuses,
43
46
  RunTriggers,
@@ -227,6 +230,7 @@ import {
227
230
  hostOf,
228
231
  indexingRequestResponseDtoSchema,
229
232
  internalError,
233
+ isBrowserProvider,
230
234
  isReadOnlyKey,
231
235
  keywordDtoSchema,
232
236
  keywordGenerateRequestSchema,
@@ -271,6 +275,9 @@ import {
271
275
  reportConfidenceLabel,
272
276
  reportHorizonLabel,
273
277
  reportSeverityLabel,
278
+ researchRunCreateSchema,
279
+ researchRunDetailSchema,
280
+ researchRunListSchema,
274
281
  resolveConfigSpecQueries,
275
282
  resolveLocations,
276
283
  resolveSnapshotRequestQueries,
@@ -337,10 +344,10 @@ import {
337
344
  wordpressSchemaDeployResultDtoSchema,
338
345
  wordpressSchemaStatusResultDtoSchema,
339
346
  wordpressStatusDtoSchema
340
- } from "./chunk-K4FDDCYG.js";
347
+ } from "./chunk-3WBAJKRR.js";
341
348
 
342
349
  // src/intelligence-service.ts
343
- import { eq as eq41, desc as desc18, asc as asc9, and as and32, ne as ne6, or as or8, inArray as inArray15, gte as gte10, lte as lte6 } from "drizzle-orm";
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";
344
351
 
345
352
  // ../db/src/client.ts
346
353
  import { mkdirSync } from "fs";
@@ -379,9 +386,12 @@ __export(schema_exports, {
379
386
  discoveryProbes: () => discoveryProbes,
380
387
  discoverySessions: () => discoverySessions,
381
388
  domainClassifications: () => domainClassifications,
389
+ gaAcquisitionDaily: () => gaAcquisitionDaily,
382
390
  gaAiReferrals: () => gaAiReferrals,
383
391
  gaConnections: () => gaConnections,
384
392
  gaDailyTotals: () => gaDailyTotals,
393
+ gaLeadEventsDaily: () => gaLeadEventsDaily,
394
+ gaMeasurementSyncStates: () => gaMeasurementSyncStates,
385
395
  gaSocialReferrals: () => gaSocialReferrals,
386
396
  gaTrafficSnapshots: () => gaTrafficSnapshots,
387
397
  gaTrafficSummaries: () => gaTrafficSummaries,
@@ -411,6 +421,8 @@ __export(schema_exports, {
411
421
  rawEventSamples: () => rawEventSamples,
412
422
  recommendationBriefs: () => recommendationBriefs,
413
423
  recommendationExplanations: () => recommendationExplanations,
424
+ researchRunQueries: () => researchRunQueries,
425
+ researchRuns: () => researchRuns,
414
426
  runs: () => runs,
415
427
  schedules: () => schedules,
416
428
  siteAuditPages: () => siteAuditPages,
@@ -418,7 +430,8 @@ __export(schema_exports, {
418
430
  trafficSources: () => trafficSources,
419
431
  usageCounters: () => usageCounters
420
432
  });
421
- import { index, integer, primaryKey, real, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
433
+ import { sql } from "drizzle-orm";
434
+ import { check, index, integer, primaryKey, real, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
422
435
  var projects = sqliteTable("projects", {
423
436
  id: text("id").primaryKey(),
424
437
  name: text("name").notNull().unique(),
@@ -432,6 +445,11 @@ var projects = sqliteTable("projects", {
432
445
  labels: text("labels", { mode: "json" }).$type().notNull().default({}),
433
446
  providers: text("providers", { mode: "json" }).$type().notNull().default([]),
434
447
  providerModels: text("provider_models", { mode: "json" }).$type().notNull().default({}),
448
+ measurement: text("measurement_config", { mode: "json" }).$type().notNull().default({
449
+ marketingHosts: [],
450
+ brandTerms: [],
451
+ leadEventNames: ["generate_lead"]
452
+ }),
435
453
  locations: text("locations", { mode: "json" }).$type().notNull().default([]),
436
454
  defaultLocation: text("default_location"),
437
455
  autoExtractBacklinks: integer("auto_extract_backlinks", { mode: "boolean" }).notNull().default(false),
@@ -863,6 +881,97 @@ var gaDailyTotals = sqliteTable("ga_daily_totals", {
863
881
  index("idx_ga_daily_totals_project").on(table.projectId),
864
882
  index("idx_ga_daily_totals_run").on(table.syncRunId)
865
883
  ]);
884
+ var gaAcquisitionDaily = sqliteTable("ga_acquisition_daily", {
885
+ id: text("id").primaryKey(),
886
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
887
+ date: text("date").notNull(),
888
+ channelGroup: text("channel_group").notNull(),
889
+ source: text("source").notNull(),
890
+ medium: text("medium").notNull(),
891
+ hostName: text("host_name").notNull(),
892
+ landingPage: text("landing_page").notNull(),
893
+ landingPageNormalized: text("landing_page_normalized"),
894
+ sessions: integer("sessions").notNull().default(0),
895
+ syncedAt: text("synced_at").notNull(),
896
+ syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" }),
897
+ createdAt: text("created_at").notNull()
898
+ }, (table) => [
899
+ uniqueIndex("idx_ga_acquisition_daily_grain").on(
900
+ table.projectId,
901
+ table.date,
902
+ table.channelGroup,
903
+ table.source,
904
+ table.medium,
905
+ table.hostName,
906
+ table.landingPage
907
+ ),
908
+ index("idx_ga_acquisition_daily_project_date").on(table.projectId, table.date),
909
+ index("idx_ga_acquisition_daily_project_channel").on(table.projectId, table.date, table.channelGroup),
910
+ index("idx_ga_acquisition_daily_project_page").on(table.projectId, table.date, table.landingPageNormalized),
911
+ check("chk_ga_acquisition_daily_sessions", sql`${table.sessions} >= 0`)
912
+ ]);
913
+ var gaLeadEventsDaily = sqliteTable("ga_lead_events_daily", {
914
+ id: text("id").primaryKey(),
915
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
916
+ date: text("date").notNull(),
917
+ eventName: text("event_name").notNull(),
918
+ channelGroup: text("channel_group").notNull(),
919
+ source: text("source").notNull(),
920
+ medium: text("medium").notNull(),
921
+ hostName: text("host_name").notNull(),
922
+ landingPage: text("landing_page").notNull(),
923
+ landingPageNormalized: text("landing_page_normalized"),
924
+ attributionScope: text("attribution_scope").$type().notNull(),
925
+ eventCount: integer("event_count").notNull().default(0),
926
+ syncedAt: text("synced_at").notNull(),
927
+ syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" }),
928
+ createdAt: text("created_at").notNull()
929
+ }, (table) => [
930
+ uniqueIndex("idx_ga_lead_events_daily_grain").on(
931
+ table.projectId,
932
+ table.date,
933
+ table.eventName,
934
+ table.channelGroup,
935
+ table.source,
936
+ table.medium,
937
+ table.hostName,
938
+ table.landingPage,
939
+ table.attributionScope
940
+ ),
941
+ index("idx_ga_lead_events_daily_project_date").on(table.projectId, table.date),
942
+ index("idx_ga_lead_events_daily_project_channel").on(table.projectId, table.date, table.channelGroup),
943
+ index("idx_ga_lead_events_daily_project_event").on(table.projectId, table.date, table.eventName),
944
+ index("idx_ga_lead_events_daily_project_page").on(table.projectId, table.date, table.landingPageNormalized),
945
+ check("chk_ga_lead_events_daily_count", sql`${table.eventCount} >= 0`),
946
+ check(
947
+ "chk_ga_lead_events_daily_scope",
948
+ sql`${table.attributionScope} IN ('landing-page', 'channel')`
949
+ )
950
+ ]);
951
+ var gaMeasurementSyncStates = sqliteTable("ga_measurement_sync_state", {
952
+ projectId: text("project_id").primaryKey().references(() => projects.id, { onDelete: "cascade" }),
953
+ acquisitionStatus: text("acquisition_status").$type().notNull().default("never-synced"),
954
+ acquisitionError: text("acquisition_error"),
955
+ acquisitionSyncedAt: text("acquisition_synced_at"),
956
+ leadStatus: text("lead_status").$type().notNull().default("never-synced"),
957
+ leadError: text("lead_error"),
958
+ leadSyncedAt: text("lead_synced_at"),
959
+ leadAttributionScope: text("lead_attribution_scope").$type(),
960
+ updatedAt: text("updated_at").notNull()
961
+ }, (table) => [
962
+ check(
963
+ "chk_ga_measurement_sync_acquisition_status",
964
+ sql`${table.acquisitionStatus} IN ('never-synced', 'ready', 'error')`
965
+ ),
966
+ check(
967
+ "chk_ga_measurement_sync_lead_status",
968
+ sql`${table.leadStatus} IN ('never-synced', 'ready', 'error')`
969
+ ),
970
+ check(
971
+ "chk_ga_measurement_sync_lead_scope",
972
+ sql`${table.leadAttributionScope} IS NULL OR ${table.leadAttributionScope} IN ('landing-page', 'channel')`
973
+ )
974
+ ]);
866
975
  var gaAiReferrals = sqliteTable("ga_ai_referrals", {
867
976
  id: text("id").primaryKey(),
868
977
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
@@ -1310,6 +1419,45 @@ var discoverySessions = sqliteTable("discovery_sessions", {
1310
1419
  index("idx_discovery_sessions_project_created").on(table.projectId, table.createdAt),
1311
1420
  index("idx_discovery_sessions_run").on(table.runId)
1312
1421
  ]);
1422
+ var researchRuns = sqliteTable("research_runs", {
1423
+ id: text("id").primaryKey(),
1424
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1425
+ status: text("status").notNull().default("queued"),
1426
+ provider: text("provider").notNull(),
1427
+ requestedModel: text("requested_model"),
1428
+ resolvedModel: text("resolved_model").notNull(),
1429
+ location: text("location", { mode: "json" }).$type(),
1430
+ totalQueries: integer("total_queries").notNull(),
1431
+ completedQueries: integer("completed_queries").notNull().default(0),
1432
+ failedQueries: integer("failed_queries").notNull().default(0),
1433
+ idempotencyKey: text("idempotency_key"),
1434
+ requestHash: text("request_hash"),
1435
+ error: text("error"),
1436
+ startedAt: text("started_at"),
1437
+ finishedAt: text("finished_at"),
1438
+ createdAt: text("created_at").notNull()
1439
+ }, (table) => [index("idx_research_runs_project_created").on(table.projectId, table.createdAt), index("idx_research_runs_status").on(table.status), uniqueIndex("idx_research_runs_project_idempotency").on(table.projectId, table.idempotencyKey)]);
1440
+ var researchRunQueries = sqliteTable("research_run_queries", {
1441
+ id: text("id").primaryKey(),
1442
+ researchRunId: text("research_run_id").notNull().references(() => researchRuns.id, { onDelete: "cascade" }),
1443
+ position: integer("position").notNull(),
1444
+ queryText: text("query_text").notNull(),
1445
+ status: text("status").notNull().default("queued"),
1446
+ requestedModel: text("requested_model"),
1447
+ resolvedModel: text("resolved_model").notNull(),
1448
+ servedModel: text("served_model"),
1449
+ answerText: text("answer_text"),
1450
+ groundingSources: text("grounding_sources", { mode: "json" }).$type().notNull().default([]),
1451
+ citedDomains: text("cited_domains", { mode: "json" }).$type().notNull().default([]),
1452
+ searchQueries: text("search_queries", { mode: "json" }).$type().notNull().default([]),
1453
+ answerMentioned: integer("answer_mentioned", { mode: "boolean" }),
1454
+ citationState: text("citation_state"),
1455
+ rawResponse: text("raw_response", { mode: "json" }).$type(),
1456
+ error: text("error"),
1457
+ startedAt: text("started_at"),
1458
+ finishedAt: text("finished_at"),
1459
+ createdAt: text("created_at").notNull()
1460
+ }, (table) => [index("idx_research_run_queries_run").on(table.researchRunId), uniqueIndex("idx_research_run_queries_run_position").on(table.researchRunId, table.position)]);
1313
1461
  var discoveryProbes = sqliteTable("discovery_probes", {
1314
1462
  id: text("id").primaryKey(),
1315
1463
  sessionId: text("session_id").notNull().references(() => discoverySessions.id, { onDelete: "cascade" }),
@@ -1716,7 +1864,7 @@ function parseJsonColumn(value, fallback) {
1716
1864
  }
1717
1865
 
1718
1866
  // ../db/src/migrate.ts
1719
- import { sql } from "drizzle-orm";
1867
+ import { sql as sql2 } from "drizzle-orm";
1720
1868
  var MIGRATION_SQL = `
1721
1869
  CREATE TABLE IF NOT EXISTS projects (
1722
1870
  id TEXT PRIMARY KEY,
@@ -1859,14 +2007,14 @@ CREATE TABLE IF NOT EXISTS _migrations (
1859
2007
  );
1860
2008
  `;
1861
2009
  function relinkOrphanedSnapshotQueryIds(tx) {
1862
- const orphans = tx.all(sql`
2010
+ const orphans = tx.all(sql2`
1863
2011
  SELECT qs.id AS snapId, qs.query_text AS text, r.project_id AS projectId
1864
2012
  FROM query_snapshots qs
1865
2013
  JOIN runs r ON r.id = qs.run_id
1866
2014
  WHERE qs.query_id IS NULL AND qs.query_text IS NOT NULL
1867
2015
  `);
1868
2016
  if (orphans.length === 0) return;
1869
- const queryRows = tx.all(sql`SELECT id, project_id AS projectId, query FROM queries`);
2017
+ const queryRows = tx.all(sql2`SELECT id, project_id AS projectId, query FROM queries`);
1870
2018
  const byProject = /* @__PURE__ */ new Map();
1871
2019
  for (const row of queryRows) {
1872
2020
  let perProject = byProject.get(row.projectId);
@@ -1880,12 +2028,12 @@ function relinkOrphanedSnapshotQueryIds(tx) {
1880
2028
  for (const orphan of orphans) {
1881
2029
  const queryId = byProject.get(orphan.projectId)?.get(normalizeQueryText(orphan.text));
1882
2030
  if (queryId) {
1883
- tx.run(sql`UPDATE query_snapshots SET query_id = ${queryId} WHERE id = ${orphan.snapId}`);
2031
+ tx.run(sql2`UPDATE query_snapshots SET query_id = ${queryId} WHERE id = ${orphan.snapId}`);
1884
2032
  }
1885
2033
  }
1886
2034
  }
1887
2035
  function backfillQuerySnapshotServedModel(tx) {
1888
- tx.run(sql.raw(`
2036
+ tx.run(sql2.raw(`
1889
2037
  UPDATE query_snapshots
1890
2038
  SET served_model = json_extract(raw_response, '$.apiResponse.model')
1891
2039
  WHERE served_model IS NULL
@@ -2670,10 +2818,10 @@ var MIGRATION_VERSIONS = [
2670
2818
  run: (tx) => {
2671
2819
  if (!tableExists(tx, "ga_ai_referrals")) return;
2672
2820
  if (!columnExists(tx, "ga_ai_referrals", "channel_group")) {
2673
- tx.run(sql.raw(`ALTER TABLE ga_ai_referrals ADD COLUMN channel_group TEXT NOT NULL DEFAULT '(not set)'`));
2821
+ tx.run(sql2.raw(`ALTER TABLE ga_ai_referrals ADD COLUMN channel_group TEXT NOT NULL DEFAULT '(not set)'`));
2674
2822
  }
2675
- tx.run(sql.raw(`DROP INDEX IF EXISTS idx_ga_ai_ref_unique_v3`));
2676
- tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v4
2823
+ tx.run(sql2.raw(`DROP INDEX IF EXISTS idx_ga_ai_ref_unique_v3`));
2824
+ tx.run(sql2.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v4
2677
2825
  ON ga_ai_referrals(project_id, date, source, medium, source_dimension, channel_group, landing_page)`));
2678
2826
  }
2679
2827
  },
@@ -3126,11 +3274,11 @@ var MIGRATION_VERSIONS = [
3126
3274
  statements: [],
3127
3275
  run: (db) => {
3128
3276
  if (tableExists(db, "google_connections") && !columnExists(db, "google_connections", "created_by_project_id")) {
3129
- db.run(sql.raw(
3277
+ db.run(sql2.raw(
3130
3278
  `ALTER TABLE google_connections ADD COLUMN created_by_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`
3131
3279
  ));
3132
- db.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_google_conn_project ON google_connections(created_by_project_id)`));
3133
- db.run(sql.raw(
3280
+ db.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_google_conn_project ON google_connections(created_by_project_id)`));
3281
+ db.run(sql2.raw(
3134
3282
  `UPDATE google_connections
3135
3283
  SET created_by_project_id = (
3136
3284
  SELECT p.id FROM projects p
@@ -3142,11 +3290,11 @@ var MIGRATION_VERSIONS = [
3142
3290
  ));
3143
3291
  }
3144
3292
  if (tableExists(db, "bing_connections") && !columnExists(db, "bing_connections", "created_by_project_id")) {
3145
- db.run(sql.raw(
3293
+ db.run(sql2.raw(
3146
3294
  `ALTER TABLE bing_connections ADD COLUMN created_by_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`
3147
3295
  ));
3148
- db.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_bing_conn_project ON bing_connections(created_by_project_id)`));
3149
- db.run(sql.raw(
3296
+ db.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_bing_conn_project ON bing_connections(created_by_project_id)`));
3297
+ db.run(sql2.raw(
3150
3298
  `UPDATE bing_connections
3151
3299
  SET created_by_project_id = (
3152
3300
  SELECT p.id FROM projects p
@@ -3516,10 +3664,10 @@ var MIGRATION_VERSIONS = [
3516
3664
  run: (db) => {
3517
3665
  if (!tableExists(db, "health_snapshots")) return;
3518
3666
  if (!columnExists(db, "health_snapshots", "overall_mention_rate")) {
3519
- db.run(sql.raw(`ALTER TABLE health_snapshots ADD COLUMN overall_mention_rate TEXT`));
3667
+ db.run(sql2.raw(`ALTER TABLE health_snapshots ADD COLUMN overall_mention_rate TEXT`));
3520
3668
  }
3521
3669
  if (!columnExists(db, "health_snapshots", "mentioned_pairs")) {
3522
- db.run(sql.raw(`ALTER TABLE health_snapshots ADD COLUMN mentioned_pairs INTEGER`));
3670
+ db.run(sql2.raw(`ALTER TABLE health_snapshots ADD COLUMN mentioned_pairs INTEGER`));
3523
3671
  }
3524
3672
  }
3525
3673
  },
@@ -3540,7 +3688,7 @@ var MIGRATION_VERSIONS = [
3540
3688
  ];
3541
3689
  for (const col of cols) {
3542
3690
  if (!columnExists(db, "gbp_locations", col)) {
3543
- db.run(sql.raw(`ALTER TABLE gbp_locations ADD COLUMN ${col} TEXT`));
3691
+ db.run(sql2.raw(`ALTER TABLE gbp_locations ADD COLUMN ${col} TEXT`));
3544
3692
  }
3545
3693
  }
3546
3694
  }
@@ -3736,9 +3884,9 @@ var MIGRATION_VERSIONS = [
3736
3884
  run: (tx) => {
3737
3885
  if (!tableExists(tx, "ga_ai_referrals")) return;
3738
3886
  if (!columnExists(tx, "ga_ai_referrals", "traffic_class")) {
3739
- tx.run(sql.raw(`ALTER TABLE ga_ai_referrals ADD COLUMN traffic_class TEXT NOT NULL DEFAULT 'organic'`));
3887
+ tx.run(sql2.raw(`ALTER TABLE ga_ai_referrals ADD COLUMN traffic_class TEXT NOT NULL DEFAULT 'organic'`));
3740
3888
  }
3741
- tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_ga_ai_ref_traffic_class ON ga_ai_referrals(project_id, date, traffic_class)`));
3889
+ tx.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_ga_ai_ref_traffic_class ON ga_ai_referrals(project_id, date, traffic_class)`));
3742
3890
  }
3743
3891
  },
3744
3892
  {
@@ -4022,14 +4170,98 @@ var MIGRATION_VERSIONS = [
4022
4170
  `CREATE INDEX IF NOT EXISTS idx_gsc_query_daily_totals_query ON gsc_query_daily_totals(query)`,
4023
4171
  `CREATE INDEX IF NOT EXISTS idx_gsc_query_daily_totals_run ON gsc_query_daily_totals(sync_run_id)`
4024
4172
  ]
4173
+ },
4174
+ {
4175
+ version: 108,
4176
+ name: "research-query-runs",
4177
+ statements: [
4178
+ `CREATE TABLE IF NOT EXISTS research_runs (id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'queued', provider TEXT NOT NULL, requested_model TEXT, resolved_model TEXT NOT NULL, location TEXT, total_queries INTEGER NOT NULL, completed_queries INTEGER NOT NULL DEFAULT 0, failed_queries INTEGER NOT NULL DEFAULT 0, idempotency_key TEXT, request_hash TEXT, error TEXT, started_at TEXT, finished_at TEXT, created_at TEXT NOT NULL)`,
4179
+ `CREATE INDEX IF NOT EXISTS idx_research_runs_project_created ON research_runs(project_id, created_at)`,
4180
+ `CREATE INDEX IF NOT EXISTS idx_research_runs_status ON research_runs(status)`,
4181
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_research_runs_project_idempotency ON research_runs(project_id, idempotency_key)`,
4182
+ `CREATE TABLE IF NOT EXISTS research_run_queries (id TEXT PRIMARY KEY, research_run_id TEXT NOT NULL REFERENCES research_runs(id) ON DELETE CASCADE, position INTEGER NOT NULL, query_text TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued', requested_model TEXT, resolved_model TEXT NOT NULL, served_model TEXT, answer_text TEXT, grounding_sources TEXT NOT NULL DEFAULT '[]', cited_domains TEXT NOT NULL DEFAULT '[]', search_queries TEXT NOT NULL DEFAULT '[]', answer_mentioned INTEGER, citation_state TEXT, raw_response TEXT, error TEXT, started_at TEXT, finished_at TEXT, created_at TEXT NOT NULL)`,
4183
+ `CREATE INDEX IF NOT EXISTS idx_research_run_queries_run ON research_run_queries(research_run_id)`,
4184
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_research_run_queries_run_position ON research_run_queries(research_run_id, position)`
4185
+ ]
4186
+ },
4187
+ {
4188
+ version: 109,
4189
+ name: "ga-measurement-foundation",
4190
+ statements: [
4191
+ `ALTER TABLE projects ADD COLUMN measurement_config TEXT NOT NULL DEFAULT '{"marketingHosts":[],"brandTerms":[],"leadEventNames":["generate_lead"]}'`,
4192
+ `CREATE TABLE IF NOT EXISTS ga_acquisition_daily (
4193
+ id TEXT PRIMARY KEY,
4194
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
4195
+ date TEXT NOT NULL,
4196
+ channel_group TEXT NOT NULL,
4197
+ source TEXT NOT NULL,
4198
+ medium TEXT NOT NULL,
4199
+ host_name TEXT NOT NULL,
4200
+ landing_page TEXT NOT NULL,
4201
+ landing_page_normalized TEXT,
4202
+ sessions INTEGER NOT NULL DEFAULT 0 CHECK (sessions >= 0),
4203
+ synced_at TEXT NOT NULL,
4204
+ sync_run_id TEXT REFERENCES runs(id) ON DELETE CASCADE,
4205
+ created_at TEXT NOT NULL
4206
+ )`,
4207
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_acquisition_daily_grain
4208
+ ON ga_acquisition_daily(project_id, date, channel_group, source, medium, host_name, landing_page)`,
4209
+ `CREATE INDEX IF NOT EXISTS idx_ga_acquisition_daily_project_date
4210
+ ON ga_acquisition_daily(project_id, date)`,
4211
+ `CREATE INDEX IF NOT EXISTS idx_ga_acquisition_daily_project_channel
4212
+ ON ga_acquisition_daily(project_id, date, channel_group)`,
4213
+ `CREATE INDEX IF NOT EXISTS idx_ga_acquisition_daily_project_page
4214
+ ON ga_acquisition_daily(project_id, date, landing_page_normalized)`,
4215
+ `CREATE TABLE IF NOT EXISTS ga_lead_events_daily (
4216
+ id TEXT PRIMARY KEY,
4217
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
4218
+ date TEXT NOT NULL,
4219
+ event_name TEXT NOT NULL,
4220
+ channel_group TEXT NOT NULL,
4221
+ source TEXT NOT NULL,
4222
+ medium TEXT NOT NULL,
4223
+ host_name TEXT NOT NULL,
4224
+ landing_page TEXT NOT NULL,
4225
+ landing_page_normalized TEXT,
4226
+ attribution_scope TEXT NOT NULL CHECK (attribution_scope IN ('landing-page', 'channel')),
4227
+ event_count INTEGER NOT NULL DEFAULT 0 CHECK (event_count >= 0),
4228
+ synced_at TEXT NOT NULL,
4229
+ sync_run_id TEXT REFERENCES runs(id) ON DELETE CASCADE,
4230
+ created_at TEXT NOT NULL
4231
+ )`,
4232
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_lead_events_daily_grain
4233
+ ON ga_lead_events_daily(project_id, date, event_name, channel_group, source, medium, host_name, landing_page, attribution_scope)`,
4234
+ `CREATE INDEX IF NOT EXISTS idx_ga_lead_events_daily_project_date
4235
+ ON ga_lead_events_daily(project_id, date)`,
4236
+ `CREATE INDEX IF NOT EXISTS idx_ga_lead_events_daily_project_channel
4237
+ ON ga_lead_events_daily(project_id, date, channel_group)`,
4238
+ `CREATE INDEX IF NOT EXISTS idx_ga_lead_events_daily_project_event
4239
+ ON ga_lead_events_daily(project_id, date, event_name)`,
4240
+ `CREATE INDEX IF NOT EXISTS idx_ga_lead_events_daily_project_page
4241
+ ON ga_lead_events_daily(project_id, date, landing_page_normalized)`,
4242
+ `CREATE TABLE IF NOT EXISTS ga_measurement_sync_state (
4243
+ project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
4244
+ acquisition_status TEXT NOT NULL DEFAULT 'never-synced'
4245
+ CHECK (acquisition_status IN ('never-synced', 'ready', 'error')),
4246
+ acquisition_error TEXT,
4247
+ acquisition_synced_at TEXT,
4248
+ lead_status TEXT NOT NULL DEFAULT 'never-synced'
4249
+ CHECK (lead_status IN ('never-synced', 'ready', 'error')),
4250
+ lead_error TEXT,
4251
+ lead_synced_at TEXT,
4252
+ lead_attribution_scope TEXT
4253
+ CHECK (lead_attribution_scope IS NULL OR lead_attribution_scope IN ('landing-page', 'channel')),
4254
+ updated_at TEXT NOT NULL
4255
+ )`
4256
+ ]
4025
4257
  }
4026
4258
  ];
4027
4259
  function rebuildBacklinkTableWithSource(tx, table) {
4028
4260
  if (!tableExists(tx, table)) return;
4029
4261
  if (columnExists(tx, table, "source")) return;
4030
4262
  if (table === "backlink_domains") {
4031
- tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_domains_v78`));
4032
- tx.run(sql.raw(`CREATE TABLE backlink_domains_v78 (
4263
+ tx.run(sql2.raw(`DROP TABLE IF EXISTS backlink_domains_v78`));
4264
+ tx.run(sql2.raw(`CREATE TABLE backlink_domains_v78 (
4033
4265
  id TEXT PRIMARY KEY,
4034
4266
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
4035
4267
  release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
@@ -4040,24 +4272,24 @@ function rebuildBacklinkTableWithSource(tx, table) {
4040
4272
  num_hosts INTEGER NOT NULL,
4041
4273
  created_at TEXT NOT NULL
4042
4274
  )`));
4043
- tx.run(sql.raw(`INSERT INTO backlink_domains_v78
4275
+ tx.run(sql2.raw(`INSERT INTO backlink_domains_v78
4044
4276
  (id, project_id, release_sync_id, source, release, target_domain, linking_domain, num_hosts, created_at)
4045
4277
  SELECT bd.id, bd.project_id,
4046
4278
  CASE WHEN bd.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bd.release_sync_id ELSE NULL END,
4047
4279
  'commoncrawl', bd.release, bd.target_domain, bd.linking_domain, bd.num_hosts, bd.created_at
4048
4280
  FROM backlink_domains bd
4049
4281
  WHERE bd.project_id IN (SELECT id FROM projects)`));
4050
- tx.run(sql.raw(`DROP TABLE backlink_domains`));
4051
- tx.run(sql.raw(`ALTER TABLE backlink_domains_v78 RENAME TO backlink_domains`));
4052
- tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project ON backlink_domains(project_id)`));
4053
- tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_release_sync ON backlink_domains(release_sync_id)`));
4054
- tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project_release ON backlink_domains(project_id, release)`));
4055
- tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_hosts ON backlink_domains(num_hosts)`));
4056
- tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_domains_unique ON backlink_domains(project_id, source, release, linking_domain)`));
4282
+ tx.run(sql2.raw(`DROP TABLE backlink_domains`));
4283
+ tx.run(sql2.raw(`ALTER TABLE backlink_domains_v78 RENAME TO backlink_domains`));
4284
+ tx.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project ON backlink_domains(project_id)`));
4285
+ tx.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_release_sync ON backlink_domains(release_sync_id)`));
4286
+ tx.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project_release ON backlink_domains(project_id, release)`));
4287
+ tx.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_hosts ON backlink_domains(num_hosts)`));
4288
+ tx.run(sql2.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_domains_unique ON backlink_domains(project_id, source, release, linking_domain)`));
4057
4289
  return;
4058
4290
  }
4059
- tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_summaries_v78`));
4060
- tx.run(sql.raw(`CREATE TABLE backlink_summaries_v78 (
4291
+ tx.run(sql2.raw(`DROP TABLE IF EXISTS backlink_summaries_v78`));
4292
+ tx.run(sql2.raw(`CREATE TABLE backlink_summaries_v78 (
4061
4293
  id TEXT PRIMARY KEY,
4062
4294
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
4063
4295
  release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
@@ -4070,17 +4302,17 @@ function rebuildBacklinkTableWithSource(tx, table) {
4070
4302
  queried_at TEXT NOT NULL,
4071
4303
  created_at TEXT NOT NULL
4072
4304
  )`));
4073
- tx.run(sql.raw(`INSERT INTO backlink_summaries_v78
4305
+ tx.run(sql2.raw(`INSERT INTO backlink_summaries_v78
4074
4306
  (id, project_id, release_sync_id, source, release, target_domain, total_linking_domains, total_hosts, top_10_hosts_share, queried_at, created_at)
4075
4307
  SELECT bs.id, bs.project_id,
4076
4308
  CASE WHEN bs.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bs.release_sync_id ELSE NULL END,
4077
4309
  'commoncrawl', bs.release, bs.target_domain, bs.total_linking_domains, bs.total_hosts, bs.top_10_hosts_share, bs.queried_at, bs.created_at
4078
4310
  FROM backlink_summaries bs
4079
4311
  WHERE bs.project_id IN (SELECT id FROM projects)`));
4080
- tx.run(sql.raw(`DROP TABLE backlink_summaries`));
4081
- tx.run(sql.raw(`ALTER TABLE backlink_summaries_v78 RENAME TO backlink_summaries`));
4082
- tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_summaries_project_release ON backlink_summaries(project_id, source, release)`));
4083
- tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_summaries_project ON backlink_summaries(project_id)`));
4312
+ tx.run(sql2.raw(`DROP TABLE backlink_summaries`));
4313
+ tx.run(sql2.raw(`ALTER TABLE backlink_summaries_v78 RENAME TO backlink_summaries`));
4314
+ tx.run(sql2.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_summaries_project_release ON backlink_summaries(project_id, source, release)`));
4315
+ tx.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_summaries_project ON backlink_summaries(project_id)`));
4084
4316
  }
4085
4317
  function addBacklinkSourceDiscriminator(tx) {
4086
4318
  rebuildBacklinkTableWithSource(tx, "backlink_domains");
@@ -4093,25 +4325,25 @@ function isDuplicateColumnError(err) {
4093
4325
  return false;
4094
4326
  }
4095
4327
  function columnExists(db, table, column) {
4096
- const rows = db.all(sql.raw(
4328
+ const rows = db.all(sql2.raw(
4097
4329
  `SELECT COUNT(*) as c FROM pragma_table_info('${table}') WHERE name = '${column}'`
4098
4330
  ));
4099
4331
  return (rows[0]?.c ?? 0) > 0;
4100
4332
  }
4101
4333
  function tableExists(db, table) {
4102
- const rows = db.all(sql.raw(
4334
+ const rows = db.all(sql2.raw(
4103
4335
  `SELECT COUNT(*) as c FROM sqlite_master WHERE type = 'table' AND name = '${table}'`
4104
4336
  ));
4105
4337
  return (rows[0]?.c ?? 0) > 0;
4106
4338
  }
4107
4339
  function tableIsEmpty(db, table) {
4108
- const rows = db.all(sql.raw(`SELECT COUNT(*) as c FROM ${table}`));
4340
+ const rows = db.all(sql2.raw(`SELECT COUNT(*) as c FROM ${table}`));
4109
4341
  return (rows[0]?.c ?? 0) === 0;
4110
4342
  }
4111
4343
  function backfillGaAiReferralTrafficClass(tx) {
4112
4344
  if (!tableExists(tx, "ga_ai_referrals")) return 0;
4113
4345
  if (!columnExists(tx, "ga_ai_referrals", "traffic_class")) return 0;
4114
- const rows = tx.all(sql.raw(
4346
+ const rows = tx.all(sql2.raw(
4115
4347
  `SELECT id, source, medium, channel_group, landing_page, traffic_class FROM ga_ai_referrals`
4116
4348
  ));
4117
4349
  let updated = 0;
@@ -4123,7 +4355,7 @@ function backfillGaAiReferralTrafficClass(tx) {
4123
4355
  landingPage: row.landing_page
4124
4356
  });
4125
4357
  if (next === row.traffic_class) continue;
4126
- tx.run(sql`UPDATE ga_ai_referrals SET traffic_class = ${next} WHERE id = ${row.id}`);
4358
+ tx.run(sql2`UPDATE ga_ai_referrals SET traffic_class = ${next} WHERE id = ${row.id}`);
4127
4359
  updated += 1;
4128
4360
  }
4129
4361
  return updated;
@@ -4137,28 +4369,28 @@ function normalizeLegacyQuerySchema(db) {
4137
4369
  if (!tableIsEmpty(db, "queries")) {
4138
4370
  throw new Error("Cannot migrate keywords to queries because both tables contain data");
4139
4371
  }
4140
- db.run(sql.raw(`DROP TABLE queries`));
4372
+ db.run(sql2.raw(`DROP TABLE queries`));
4141
4373
  }
4142
- db.run(sql.raw(`DROP INDEX IF EXISTS idx_keywords_project`));
4143
- db.run(sql.raw(`DROP INDEX IF EXISTS idx_keywords_project_keyword`));
4144
- db.run(sql.raw(`DROP INDEX IF EXISTS idx_snapshots_keyword`));
4145
- db.run(sql.raw(`DROP INDEX IF EXISTS idx_insights_keyword_provider`));
4374
+ db.run(sql2.raw(`DROP INDEX IF EXISTS idx_keywords_project`));
4375
+ db.run(sql2.raw(`DROP INDEX IF EXISTS idx_keywords_project_keyword`));
4376
+ db.run(sql2.raw(`DROP INDEX IF EXISTS idx_snapshots_keyword`));
4377
+ db.run(sql2.raw(`DROP INDEX IF EXISTS idx_insights_keyword_provider`));
4146
4378
  if (tableExists(db, "keywords")) {
4147
- db.run(sql.raw(`ALTER TABLE keywords RENAME TO queries`));
4379
+ db.run(sql2.raw(`ALTER TABLE keywords RENAME TO queries`));
4148
4380
  }
4149
4381
  if (columnExists(db, "queries", "keyword")) {
4150
- db.run(sql.raw(`ALTER TABLE queries RENAME COLUMN keyword TO query`));
4382
+ db.run(sql2.raw(`ALTER TABLE queries RENAME COLUMN keyword TO query`));
4151
4383
  }
4152
4384
  if (columnExists(db, "query_snapshots", "keyword_id")) {
4153
- db.run(sql.raw(`ALTER TABLE query_snapshots RENAME COLUMN keyword_id TO query_id`));
4385
+ db.run(sql2.raw(`ALTER TABLE query_snapshots RENAME COLUMN keyword_id TO query_id`));
4154
4386
  }
4155
4387
  if (columnExists(db, "insights", "keyword")) {
4156
- db.run(sql.raw(`ALTER TABLE insights RENAME COLUMN keyword TO query`));
4388
+ db.run(sql2.raw(`ALTER TABLE insights RENAME COLUMN keyword TO query`));
4157
4389
  }
4158
4390
  }
4159
4391
  function dropColumnIfExists(db, table, column) {
4160
4392
  try {
4161
- db.run(sql.raw(`ALTER TABLE ${table} DROP COLUMN ${column}`));
4393
+ db.run(sql2.raw(`ALTER TABLE ${table} DROP COLUMN ${column}`));
4162
4394
  } catch (err) {
4163
4395
  if (!(err instanceof Error)) throw err;
4164
4396
  const msg = err.message;
@@ -4173,7 +4405,7 @@ function dropColumnIfExists(db, table, column) {
4173
4405
  function extractLegacyCredentials(db) {
4174
4406
  const out = { google: [], ga4: [] };
4175
4407
  if (columnExists(db, "google_connections", "access_token")) {
4176
- const rows = db.all(sql.raw(
4408
+ const rows = db.all(sql2.raw(
4177
4409
  `SELECT domain, connection_type, property_id, sitemap_url, access_token, refresh_token, token_expires_at, scopes, created_at, updated_at
4178
4410
  FROM google_connections
4179
4411
  WHERE refresh_token IS NOT NULL AND refresh_token != ''`
@@ -4194,7 +4426,7 @@ function extractLegacyCredentials(db) {
4194
4426
  }
4195
4427
  }
4196
4428
  if (columnExists(db, "ga_connections", "private_key")) {
4197
- const rows = db.all(sql.raw(
4429
+ const rows = db.all(sql2.raw(
4198
4430
  `SELECT p.name AS project_name, ga.property_id, ga.client_email, ga.private_key, ga.created_at, ga.updated_at
4199
4431
  FROM ga_connections ga
4200
4432
  INNER JOIN projects p ON p.id = ga.project_id
@@ -4228,11 +4460,11 @@ function dropLegacyCredentialColumns(db) {
4228
4460
  }
4229
4461
  }
4230
4462
  function getAppliedVersion(db) {
4231
- const rows = db.all(sql`SELECT MAX(version) as max_version FROM _migrations`);
4463
+ const rows = db.all(sql2`SELECT MAX(version) as max_version FROM _migrations`);
4232
4464
  return rows[0]?.max_version ?? 0;
4233
4465
  }
4234
4466
  function recordMigration(db, version, name) {
4235
- db.run(sql`INSERT OR IGNORE INTO _migrations (version, name) VALUES (${version}, ${name})`);
4467
+ db.run(sql2`INSERT OR IGNORE INTO _migrations (version, name) VALUES (${version}, ${name})`);
4236
4468
  }
4237
4469
  function migrate(db, versions = MIGRATION_VERSIONS) {
4238
4470
  db.transaction((tx) => {
@@ -4240,7 +4472,7 @@ function migrate(db, versions = MIGRATION_VERSIONS) {
4240
4472
  });
4241
4473
  const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
4242
4474
  for (const statement of statements) {
4243
- db.run(sql.raw(statement));
4475
+ db.run(sql2.raw(statement));
4244
4476
  }
4245
4477
  const appliedVersion = getAppliedVersion(db);
4246
4478
  for (const mv of versions) {
@@ -4248,7 +4480,7 @@ function migrate(db, versions = MIGRATION_VERSIONS) {
4248
4480
  db.transaction((tx) => {
4249
4481
  for (const statement of mv.statements) {
4250
4482
  try {
4251
- tx.run(sql.raw(statement));
4483
+ tx.run(sql2.raw(statement));
4252
4484
  } catch (err) {
4253
4485
  if (isDuplicateColumnError(err)) continue;
4254
4486
  throw err;
@@ -6554,11 +6786,11 @@ async function authPlugin(app, opts = {}) {
6554
6786
 
6555
6787
  // ../api-routes/src/projects.ts
6556
6788
  import crypto4 from "crypto";
6557
- import { eq as eq3, sql as sql3 } from "drizzle-orm";
6789
+ import { eq as eq3, sql as sql4 } from "drizzle-orm";
6558
6790
 
6559
6791
  // ../api-routes/src/helpers.ts
6560
6792
  import crypto3 from "crypto";
6561
- import { eq as eq2, ne, sql as sql2 } from "drizzle-orm";
6793
+ import { eq as eq2, ne, sql as sql3 } from "drizzle-orm";
6562
6794
  function notProbeRun() {
6563
6795
  return ne(runs.trigger, RunTriggers.probe);
6564
6796
  }
@@ -6836,6 +7068,7 @@ async function projectRoutes(app, opts) {
6836
7068
  });
6837
7069
  }
6838
7070
  const nextAutoExtractBacklinks = body.autoExtractBacklinks !== void 0 ? body.autoExtractBacklinks : existing?.autoExtractBacklinks ?? false;
7071
+ const nextMeasurement = body.measurement ?? existing?.measurement ?? DEFAULT_MEASUREMENT_CONFIG;
6839
7072
  const nextAliases = normalizeProjectAliases(body.displayName, body.aliases ?? []);
6840
7073
  if (existing) {
6841
7074
  const prevAliases = existing.aliases;
@@ -6852,6 +7085,7 @@ async function projectRoutes(app, opts) {
6852
7085
  labels: body.labels ?? {},
6853
7086
  providers: body.providers ?? [],
6854
7087
  providerModels,
7088
+ measurement: nextMeasurement,
6855
7089
  locations: nextLocations,
6856
7090
  defaultLocation: nextDefaultLocation,
6857
7091
  autoExtractBacklinks: nextAutoExtractBacklinks,
@@ -6887,6 +7121,7 @@ async function projectRoutes(app, opts) {
6887
7121
  labels: body.labels ?? {},
6888
7122
  providers: body.providers ?? [],
6889
7123
  providerModels,
7124
+ measurement: nextMeasurement,
6890
7125
  locations: nextLocations,
6891
7126
  defaultLocation: nextDefaultLocation,
6892
7127
  autoExtractBacklinks: nextAutoExtractBacklinks,
@@ -6920,12 +7155,12 @@ async function projectRoutes(app, opts) {
6920
7155
  const project = resolveProject(app.db, request.params.name);
6921
7156
  const pid = project.id;
6922
7157
  const count2 = (n) => n ?? 0;
6923
- const queryCount = app.db.select({ n: sql3`count(*)` }).from(queries).where(eq3(queries.projectId, pid)).get();
6924
- const competitorCount = app.db.select({ n: sql3`count(*)` }).from(competitors).where(eq3(competitors.projectId, pid)).get();
6925
- const runCount = app.db.select({ n: sql3`count(*)` }).from(runs).where(eq3(runs.projectId, pid)).get();
6926
- const snapshotCount = app.db.select({ n: sql3`count(*)` }).from(querySnapshots).innerJoin(runs, eq3(querySnapshots.runId, runs.id)).where(eq3(runs.projectId, pid)).get();
6927
- const insightCount = app.db.select({ n: sql3`count(*)` }).from(insights).where(eq3(insights.projectId, pid)).get();
6928
- const auditLogCount = app.db.select({ n: sql3`count(*)` }).from(auditLog).where(eq3(auditLog.projectId, pid)).get();
7158
+ const queryCount = app.db.select({ n: sql4`count(*)` }).from(queries).where(eq3(queries.projectId, pid)).get();
7159
+ const competitorCount = app.db.select({ n: sql4`count(*)` }).from(competitors).where(eq3(competitors.projectId, pid)).get();
7160
+ const runCount = app.db.select({ n: sql4`count(*)` }).from(runs).where(eq3(runs.projectId, pid)).get();
7161
+ const snapshotCount = app.db.select({ n: sql4`count(*)` }).from(querySnapshots).innerJoin(runs, eq3(querySnapshots.runId, runs.id)).where(eq3(runs.projectId, pid)).get();
7162
+ const insightCount = app.db.select({ n: sql4`count(*)` }).from(insights).where(eq3(insights.projectId, pid)).get();
7163
+ const auditLogCount = app.db.select({ n: sql4`count(*)` }).from(auditLog).where(eq3(auditLog.projectId, pid)).get();
6929
7164
  return reply.send({
6930
7165
  project: { id: project.id, name: project.name },
6931
7166
  cascadeRows: {
@@ -7059,6 +7294,7 @@ async function projectRoutes(app, opts) {
7059
7294
  competitors: comps.map((c) => c.domain),
7060
7295
  providers: project.providers,
7061
7296
  ...Object.keys(project.providerModels).length > 0 ? { providerModels: project.providerModels } : {},
7297
+ measurement: project.measurement,
7062
7298
  locations: project.locations,
7063
7299
  ...project.defaultLocation ? { defaultLocation: project.defaultLocation } : {},
7064
7300
  ...project.autoExtractBacklinks ? { autoExtractBacklinks: true } : {},
@@ -7107,6 +7343,7 @@ function formatProject(row) {
7107
7343
  labels: row.labels,
7108
7344
  providers: row.providers,
7109
7345
  providerModels: row.providerModels,
7346
+ measurement: row.measurement,
7110
7347
  locations: row.locations,
7111
7348
  defaultLocation: row.defaultLocation,
7112
7349
  autoExtractBacklinks: row.autoExtractBacklinks,
@@ -7126,7 +7363,7 @@ function aliasArraysEqual(a, b) {
7126
7363
 
7127
7364
  // ../api-routes/src/queries.ts
7128
7365
  import crypto6 from "crypto";
7129
- import { and, eq as eq5, inArray as inArray2, sql as sql4 } from "drizzle-orm";
7366
+ import { and, eq as eq5, inArray as inArray2, sql as sql5 } from "drizzle-orm";
7130
7367
 
7131
7368
  // ../api-routes/src/query-replace.ts
7132
7369
  import crypto5 from "crypto";
@@ -7248,9 +7485,9 @@ async function queryRoutes(app, opts) {
7248
7485
  let snapshotsDetached = 0;
7249
7486
  let affectedQueries = 0;
7250
7487
  if (removedIds.length > 0) {
7251
- const snapshotCount = app.db.select({ n: sql4`count(*)` }).from(querySnapshots).where(inArray2(querySnapshots.queryId, removedIds)).get();
7488
+ const snapshotCount = app.db.select({ n: sql5`count(*)` }).from(querySnapshots).where(inArray2(querySnapshots.queryId, removedIds)).get();
7252
7489
  snapshotsDetached = snapshotCount?.n ?? 0;
7253
- const distinctAffected = app.db.select({ n: sql4`count(distinct ${querySnapshots.queryId})` }).from(querySnapshots).where(inArray2(querySnapshots.queryId, removedIds)).get();
7490
+ const distinctAffected = app.db.select({ n: sql5`count(distinct ${querySnapshots.queryId})` }).from(querySnapshots).where(inArray2(querySnapshots.queryId, removedIds)).get();
7254
7491
  affectedQueries = distinctAffected?.n ?? 0;
7255
7492
  }
7256
7493
  return reply.send({
@@ -7659,7 +7896,7 @@ function parseCompetitorBatch(value) {
7659
7896
 
7660
7897
  // ../api-routes/src/runs.ts
7661
7898
  import crypto9 from "crypto";
7662
- import { and as and4, eq as eq8, asc, desc, or as or2, sql as sql5 } from "drizzle-orm";
7899
+ import { and as and4, eq as eq8, asc, desc, or as or2, sql as sql6 } from "drizzle-orm";
7663
7900
  import { gte } from "drizzle-orm";
7664
7901
 
7665
7902
  // ../api-routes/src/run-queue.ts
@@ -7834,7 +8071,7 @@ async function runRoutes(app, opts) {
7834
8071
  });
7835
8072
  app.get("/projects/:name/runs/latest", async (request, reply) => {
7836
8073
  const project = resolveProject(app.db, request.params.name);
7837
- const countRow = app.db.select({ count: sql5`count(*)` }).from(runs).where(and4(eq8(runs.projectId, project.id), notProbeRun())).get();
8074
+ const countRow = app.db.select({ count: sql6`count(*)` }).from(runs).where(and4(eq8(runs.projectId, project.id), notProbeRun())).get();
7838
8075
  const totalRuns = countRow?.count ?? 0;
7839
8076
  const latestRun = app.db.select().from(runs).where(and4(eq8(runs.projectId, project.id), notProbeRun())).orderBy(desc(runs.createdAt), desc(runs.id)).limit(1).get();
7840
8077
  if (!latestRun) {
@@ -8440,6 +8677,7 @@ async function applyRoutes(app, opts) {
8440
8677
  labels: config.metadata.labels,
8441
8678
  providers: config.spec.providers ?? [],
8442
8679
  providerModels,
8680
+ measurement: config.spec.measurement,
8443
8681
  locations: config.spec.locations ?? [],
8444
8682
  defaultLocation: config.spec.defaultLocation ?? null,
8445
8683
  autoExtractBacklinks: config.spec.autoExtractBacklinks ?? false,
@@ -8469,6 +8707,7 @@ async function applyRoutes(app, opts) {
8469
8707
  labels: config.metadata.labels,
8470
8708
  providers: config.spec.providers ?? [],
8471
8709
  providerModels,
8710
+ measurement: config.spec.measurement,
8472
8711
  locations: config.spec.locations ?? [],
8473
8712
  defaultLocation: config.spec.defaultLocation ?? null,
8474
8713
  autoExtractBacklinks: config.spec.autoExtractBacklinks ?? false,
@@ -8594,6 +8833,7 @@ async function applyRoutes(app, opts) {
8594
8833
  labels: project.labels,
8595
8834
  providers: project.providers,
8596
8835
  providerModels: project.providerModels,
8836
+ measurement: project.measurement,
8597
8837
  locations: project.locations,
8598
8838
  defaultLocation: project.defaultLocation,
8599
8839
  autoExtractBacklinks: project.autoExtractBacklinks,
@@ -9931,7 +10171,7 @@ async function intelligenceRoutes(app) {
9931
10171
  }
9932
10172
 
9933
10173
  // ../api-routes/src/report.ts
9934
- import { and as and12, desc as desc7, eq as eq16, gte as gte4, inArray as inArray7, lt as lt2, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
10174
+ import { and as and12, desc as desc7, eq as eq16, gte as gte4, inArray as inArray7, lt as lt2, lte, ne as ne2, or as or3, sql as sql8 } from "drizzle-orm";
9935
10175
 
9936
10176
  // ../api-routes/src/content.ts
9937
10177
  import crypto12 from "crypto";
@@ -10672,7 +10912,7 @@ function winnabilityClassRank(winnabilityClass) {
10672
10912
  }
10673
10913
 
10674
10914
  // ../api-routes/src/gsc-totals.ts
10675
- import { and as and11, asc as asc3, eq as eq15, sql as sql6 } from "drizzle-orm";
10915
+ import { and as and11, asc as asc3, eq as eq15, sql as sql7 } from "drizzle-orm";
10676
10916
  function readGscDailyTotals(db, projectId, startDate, endDate) {
10677
10917
  const rows = db.select({
10678
10918
  date: gscDailyTotals.date,
@@ -10682,8 +10922,8 @@ function readGscDailyTotals(db, projectId, startDate, endDate) {
10682
10922
  }).from(gscDailyTotals).where(
10683
10923
  and11(
10684
10924
  eq15(gscDailyTotals.projectId, projectId),
10685
- sql6`${gscDailyTotals.date} >= ${startDate}`,
10686
- sql6`${gscDailyTotals.date} <= ${endDate}`
10925
+ sql7`${gscDailyTotals.date} >= ${startDate}`,
10926
+ sql7`${gscDailyTotals.date} <= ${endDate}`
10687
10927
  )
10688
10928
  ).orderBy(asc3(gscDailyTotals.date)).all();
10689
10929
  return rows.map((r) => {
@@ -10712,8 +10952,8 @@ function readGscQueryDailyRows(db, projectId, startDate, endDate) {
10712
10952
  }).from(gscQueryDailyTotals).where(
10713
10953
  and11(
10714
10954
  eq15(gscQueryDailyTotals.projectId, projectId),
10715
- sql6`${gscQueryDailyTotals.date} >= ${startDate}`,
10716
- sql6`${gscQueryDailyTotals.date} <= ${endDate}`
10955
+ sql7`${gscQueryDailyTotals.date} >= ${startDate}`,
10956
+ sql7`${gscQueryDailyTotals.date} <= ${endDate}`
10717
10957
  )
10718
10958
  ).all();
10719
10959
  return rows.map((r) => {
@@ -13225,7 +13465,7 @@ function buildGaSection(db, projectId, windowDays) {
13225
13465
  const aiSessionRows = db.select({
13226
13466
  trafficClass: gaAiReferrals.trafficClass,
13227
13467
  channelGroup: gaAiReferrals.channelGroup,
13228
- sessions: sql7`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`
13468
+ sessions: sql8`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`
13229
13469
  }).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(gaAiReferrals.trafficClass, gaAiReferrals.channelGroup).all();
13230
13470
  let paidAiSessions = 0;
13231
13471
  let organicAiSessions = 0;
@@ -13319,8 +13559,8 @@ function buildAiReferrals(db, projectId, windowDays) {
13319
13559
  const window = resolveAiReferralWindow(db, projectId, windowDays);
13320
13560
  const rows = db.select().from(gaAiReferrals).where(and12(
13321
13561
  eq16(gaAiReferrals.projectId, projectId),
13322
- sql7`${gaAiReferrals.date} >= ${window.start}`,
13323
- sql7`${gaAiReferrals.date} <= ${window.end}`
13562
+ sql8`${gaAiReferrals.date} >= ${window.start}`,
13563
+ sql8`${gaAiReferrals.date} <= ${window.end}`
13324
13564
  )).all();
13325
13565
  if (rows.length === 0) return null;
13326
13566
  const dimSessionsByTuple = /* @__PURE__ */ new Map();
@@ -13396,7 +13636,7 @@ function buildAiReferrals(db, projectId, windowDays) {
13396
13636
  };
13397
13637
  }
13398
13638
  function nonSubresourceReferralPathCondition() {
13399
- return sql7`
13639
+ return sql8`
13400
13640
  LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/_next/static/%'
13401
13641
  AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/assets/%'
13402
13642
  AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/static/%'
@@ -13435,7 +13675,7 @@ function buildServerActivity(db, projectId, windowDays) {
13435
13675
  const priorStart = new Date(priorStartMs).toISOString();
13436
13676
  const trendStart = new Date(trendStartMs).toISOString();
13437
13677
  const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
13438
- db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
13678
+ db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
13439
13679
  and12(
13440
13680
  eq16(crawlerEventsHourly.projectId, projectId),
13441
13681
  eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
@@ -13445,7 +13685,7 @@ function buildServerActivity(db, projectId, windowDays) {
13445
13685
  ).get()?.total ?? 0
13446
13686
  );
13447
13687
  const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
13448
- db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
13688
+ db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
13449
13689
  and12(
13450
13690
  eq16(crawlerEventsHourly.projectId, projectId),
13451
13691
  ne2(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
@@ -13456,9 +13696,9 @@ function buildServerActivity(db, projectId, windowDays) {
13456
13696
  );
13457
13697
  const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => {
13458
13698
  const row = db.select({
13459
- total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
13460
- paid: sql7`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
13461
- organic: sql7`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`
13699
+ total: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
13700
+ paid: sql8`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
13701
+ organic: sql8`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`
13462
13702
  }).from(aiReferralEventsHourly).where(
13463
13703
  and12(
13464
13704
  eq16(aiReferralEventsHourly.projectId, projectId),
@@ -13470,7 +13710,7 @@ function buildServerActivity(db, projectId, windowDays) {
13470
13710
  return aiReferralClassCounts(Number(row?.total ?? 0), Number(row?.paid ?? 0), Number(row?.organic ?? 0));
13471
13711
  };
13472
13712
  const sumUserFetches = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
13473
- db.select({ total: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
13713
+ db.select({ total: sql8`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
13474
13714
  and12(
13475
13715
  eq16(aiUserFetchEventsHourly.projectId, projectId),
13476
13716
  gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
@@ -13489,7 +13729,7 @@ function buildServerActivity(db, projectId, windowDays) {
13489
13729
  const crawlerByOperatorRows = db.select({
13490
13730
  operator: crawlerEventsHourly.operator,
13491
13731
  verificationStatus: crawlerEventsHourly.verificationStatus,
13492
- hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
13732
+ hits: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
13493
13733
  }).from(crawlerEventsHourly).where(
13494
13734
  and12(
13495
13735
  eq16(crawlerEventsHourly.projectId, projectId),
@@ -13499,7 +13739,7 @@ function buildServerActivity(db, projectId, windowDays) {
13499
13739
  ).groupBy(crawlerEventsHourly.operator, crawlerEventsHourly.verificationStatus).all();
13500
13740
  const crawlerByOperatorPriorRows = db.select({
13501
13741
  operator: crawlerEventsHourly.operator,
13502
- hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
13742
+ hits: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
13503
13743
  }).from(crawlerEventsHourly).where(
13504
13744
  and12(
13505
13745
  eq16(crawlerEventsHourly.projectId, projectId),
@@ -13510,7 +13750,7 @@ function buildServerActivity(db, projectId, windowDays) {
13510
13750
  ).groupBy(crawlerEventsHourly.operator).all();
13511
13751
  const referralByOperatorRows = db.select({
13512
13752
  operator: aiReferralEventsHourly.operator,
13513
- hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
13753
+ hits: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
13514
13754
  }).from(aiReferralEventsHourly).where(
13515
13755
  and12(
13516
13756
  eq16(aiReferralEventsHourly.projectId, projectId),
@@ -13521,7 +13761,7 @@ function buildServerActivity(db, projectId, windowDays) {
13521
13761
  ).groupBy(aiReferralEventsHourly.operator).all();
13522
13762
  const userFetchByOperatorRows = db.select({
13523
13763
  operator: aiUserFetchEventsHourly.operator,
13524
- hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
13764
+ hits: sql8`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
13525
13765
  }).from(aiUserFetchEventsHourly).where(
13526
13766
  and12(
13527
13767
  eq16(aiUserFetchEventsHourly.projectId, projectId),
@@ -13564,8 +13804,8 @@ function buildServerActivity(db, projectId, windowDays) {
13564
13804
  );
13565
13805
  const topPathsRows = db.select({
13566
13806
  path: crawlerEventsHourly.pathNormalized,
13567
- hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
13568
- operators: sql7`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
13807
+ hits: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
13808
+ operators: sql8`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
13569
13809
  }).from(crawlerEventsHourly).where(
13570
13810
  and12(
13571
13811
  eq16(crawlerEventsHourly.projectId, projectId),
@@ -13573,7 +13813,7 @@ function buildServerActivity(db, projectId, windowDays) {
13573
13813
  gte4(crawlerEventsHourly.tsHour, headlineStart),
13574
13814
  lte(crawlerEventsHourly.tsHour, headlineEnd)
13575
13815
  )
13576
- ).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc7(sql7`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
13816
+ ).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc7(sql8`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
13577
13817
  const topCrawledPaths = topPathsRows.map((r) => ({
13578
13818
  path: r.path,
13579
13819
  verifiedHits: Number(r.hits),
@@ -13581,8 +13821,8 @@ function buildServerActivity(db, projectId, windowDays) {
13581
13821
  }));
13582
13822
  const referralProductsRows = db.select({
13583
13823
  product: aiReferralEventsHourly.product,
13584
- arrivals: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
13585
- landingPaths: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
13824
+ arrivals: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
13825
+ landingPaths: sql8`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
13586
13826
  }).from(aiReferralEventsHourly).where(
13587
13827
  and12(
13588
13828
  eq16(aiReferralEventsHourly.projectId, projectId),
@@ -13590,7 +13830,7 @@ function buildServerActivity(db, projectId, windowDays) {
13590
13830
  gte4(aiReferralEventsHourly.tsHour, headlineStart),
13591
13831
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
13592
13832
  )
13593
- ).groupBy(aiReferralEventsHourly.product).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
13833
+ ).groupBy(aiReferralEventsHourly.product).orderBy(desc7(sql8`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
13594
13834
  const referralProducts = referralProductsRows.map((r) => ({
13595
13835
  product: r.product,
13596
13836
  arrivals: Number(r.arrivals),
@@ -13598,8 +13838,8 @@ function buildServerActivity(db, projectId, windowDays) {
13598
13838
  }));
13599
13839
  const topReferralRows = db.select({
13600
13840
  path: aiReferralEventsHourly.landingPathNormalized,
13601
- arrivals: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
13602
- products: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
13841
+ arrivals: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
13842
+ products: sql8`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
13603
13843
  }).from(aiReferralEventsHourly).where(
13604
13844
  and12(
13605
13845
  eq16(aiReferralEventsHourly.projectId, projectId),
@@ -13607,15 +13847,15 @@ function buildServerActivity(db, projectId, windowDays) {
13607
13847
  gte4(aiReferralEventsHourly.tsHour, headlineStart),
13608
13848
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
13609
13849
  )
13610
- ).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
13850
+ ).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc7(sql8`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
13611
13851
  const topReferralLandingPaths = topReferralRows.map((r) => ({
13612
13852
  path: r.path,
13613
13853
  arrivals: Number(r.arrivals),
13614
13854
  distinctProducts: Number(r.products)
13615
13855
  }));
13616
13856
  const crawlerTrendRows = db.select({
13617
- date: sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
13618
- hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
13857
+ date: sql8`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
13858
+ hits: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
13619
13859
  }).from(crawlerEventsHourly).where(
13620
13860
  and12(
13621
13861
  eq16(crawlerEventsHourly.projectId, projectId),
@@ -13623,10 +13863,10 @@ function buildServerActivity(db, projectId, windowDays) {
13623
13863
  gte4(crawlerEventsHourly.tsHour, trendStart),
13624
13864
  lte(crawlerEventsHourly.tsHour, headlineEnd)
13625
13865
  )
13626
- ).groupBy(sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
13866
+ ).groupBy(sql8`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
13627
13867
  const referralTrendRows = db.select({
13628
- date: sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
13629
- hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
13868
+ date: sql8`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
13869
+ hits: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
13630
13870
  }).from(aiReferralEventsHourly).where(
13631
13871
  and12(
13632
13872
  eq16(aiReferralEventsHourly.projectId, projectId),
@@ -13634,17 +13874,17 @@ function buildServerActivity(db, projectId, windowDays) {
13634
13874
  gte4(aiReferralEventsHourly.tsHour, trendStart),
13635
13875
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
13636
13876
  )
13637
- ).groupBy(sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
13877
+ ).groupBy(sql8`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
13638
13878
  const userFetchTrendRows = db.select({
13639
- date: sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`,
13640
- hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
13879
+ date: sql8`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`,
13880
+ hits: sql8`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
13641
13881
  }).from(aiUserFetchEventsHourly).where(
13642
13882
  and12(
13643
13883
  eq16(aiUserFetchEventsHourly.projectId, projectId),
13644
13884
  gte4(aiUserFetchEventsHourly.tsHour, trendStart),
13645
13885
  lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
13646
13886
  )
13647
- ).groupBy(sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`).all();
13887
+ ).groupBy(sql8`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`).all();
13648
13888
  const emptyTrendEntry = () => ({ verifiedCrawlerHits: 0, userFetchHits: 0, referralArrivals: 0 });
13649
13889
  const dailyTrendMap = /* @__PURE__ */ new Map();
13650
13890
  for (const r of crawlerTrendRows) {
@@ -15496,7 +15736,7 @@ async function resultsExportRoutes(app) {
15496
15736
  }
15497
15737
 
15498
15738
  // ../api-routes/src/composites.ts
15499
- import { eq as eq20, and as and16, desc as desc9, sql as sql8, like as like2, or as or4, inArray as inArray10 } from "drizzle-orm";
15739
+ import { eq as eq20, and as and16, desc as desc9, sql as sql9, like as like2, or as or4, inArray as inArray10 } from "drizzle-orm";
15500
15740
  var TOP_INSIGHT_LIMIT = 5;
15501
15741
  var SEARCH_HIT_HARD_LIMIT = 50;
15502
15742
  var SEARCH_SNIPPET_RADIUS = 80;
@@ -15673,9 +15913,9 @@ async function compositeRoutes(app) {
15673
15913
  and16(
15674
15914
  eq20(queries.projectId, project.id),
15675
15915
  or4(
15676
- sql8`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
15677
- sql8`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
15678
- sql8`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
15916
+ sql9`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
15917
+ sql9`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
15918
+ sql9`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
15679
15919
  like2(queries.query, pattern)
15680
15920
  )
15681
15921
  )
@@ -15686,8 +15926,8 @@ async function compositeRoutes(app) {
15686
15926
  or4(
15687
15927
  like2(insights.title, pattern),
15688
15928
  like2(insights.query, pattern),
15689
- sql8`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
15690
- sql8`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
15929
+ sql9`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
15930
+ sql9`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
15691
15931
  )
15692
15932
  )
15693
15933
  ).orderBy(desc9(insights.createdAt)).limit(limit + 1).all();
@@ -15893,16 +16133,16 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
15893
16133
  const dimensionedRows = app.db.select({
15894
16134
  date: gscSearchData.date,
15895
16135
  query: gscSearchData.query,
15896
- impressions: sql8`COALESCE(SUM(${gscSearchData.impressions}), 0)`,
15897
- clicks: sql8`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
16136
+ impressions: sql9`COALESCE(SUM(${gscSearchData.impressions}), 0)`,
16137
+ clicks: sql9`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
15898
16138
  // Weighted average: SUM(position * impressions) / SUM(impressions).
15899
16139
  // NULLIF guards the degenerate impressions=0 case (SQLite returns NULL,
15900
16140
  // which the JS coerces to 0 — caught by the impression floor anyway).
15901
- avgPosition: sql8`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
16141
+ avgPosition: sql9`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
15902
16142
  }).from(gscSearchData).where(and16(
15903
16143
  eq20(gscSearchData.projectId, projectId),
15904
- sql8`${gscSearchData.date} >= ${cutoff}`,
15905
- sql8`${gscSearchData.impressions} > 0`
16144
+ sql9`${gscSearchData.date} >= ${cutoff}`,
16145
+ sql9`${gscSearchData.impressions} > 0`
15906
16146
  )).groupBy(gscSearchData.date, gscSearchData.query).all();
15907
16147
  const todayIso = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15908
16148
  const merged = mergeGscQueryTotalsWithFallback(
@@ -16114,6 +16354,7 @@ function formatProject2(row) {
16114
16354
  labels: row.labels,
16115
16355
  providers: row.providers,
16116
16356
  providerModels: row.providerModels,
16357
+ measurement: row.measurement,
16117
16358
  locations: row.locations,
16118
16359
  defaultLocation: row.defaultLocation,
16119
16360
  autoExtractBacklinks: row.autoExtractBacklinks,
@@ -16265,6 +16506,9 @@ var SCHEMA_TABLE = {
16265
16506
  DiscoveryPromoteResult: discoveryPromoteResultSchema,
16266
16507
  DiscoverySessionDetailDto: discoverySessionDetailDtoSchema,
16267
16508
  DiscoverySessionDto: discoverySessionDtoSchema,
16509
+ ResearchRunCreate: researchRunCreateSchema,
16510
+ ResearchRunDetailDto: researchRunDetailSchema,
16511
+ ResearchRunListDto: researchRunListSchema,
16268
16512
  DoctorReportDto: doctorReportSchema,
16269
16513
  GA4AiReferralHistoryEntry: ga4AiReferralHistoryEntrySchema,
16270
16514
  GA4SessionHistoryEntry: ga4SessionHistoryEntrySchema,
@@ -16767,6 +17011,32 @@ var routeCatalog = [
16767
17011
  404: errorResponse("Project not found.")
16768
17012
  }
16769
17013
  },
17014
+ {
17015
+ method: "post",
17016
+ path: "/api/v1/projects/{name}/research/runs",
17017
+ summary: "Start an isolated research query batch",
17018
+ description: "Runs one to fifty ad-hoc queries through one API provider and saves the answer evidence. Research never creates tracked queries, shared runs, snapshots, insights, or notifications.",
17019
+ tags: ["research"],
17020
+ parameters: [nameParameter],
17021
+ requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ResearchRunCreate" } } } },
17022
+ responses: { 200: jsonResponse("Idempotent request returned its existing research run.", "ResearchRunDetailDto"), 202: jsonResponse("Research batch queued.", "ResearchRunDetailDto"), 400: errorResponse("Invalid provider, model, location, or request."), 404: errorResponse("Project not found."), 409: errorResponse("Idempotency key was reused with a different payload."), 422: errorResponse("Research executor is unavailable on this deployment.") }
17023
+ },
17024
+ {
17025
+ method: "get",
17026
+ path: "/api/v1/projects/{name}/research/runs",
17027
+ summary: "List saved research query batches",
17028
+ tags: ["research"],
17029
+ parameters: [nameParameter, { name: "limit", in: "query", description: "Max runs, default 20 and maximum 100.", schema: integerSchema }],
17030
+ responses: { 200: jsonResponse("Research runs returned newest first.", "ResearchRunListDto"), 404: errorResponse("Project not found.") }
17031
+ },
17032
+ {
17033
+ method: "get",
17034
+ path: "/api/v1/projects/{name}/research/runs/{runId}",
17035
+ summary: "Get a research query batch and saved answers",
17036
+ tags: ["research"],
17037
+ parameters: [nameParameter, { name: "runId", in: "path", required: true, description: "Research run ID.", schema: stringSchema }],
17038
+ responses: { 200: jsonResponse("Research run detail returned.", "ResearchRunDetailDto"), 404: errorResponse("Project or research run not found.") }
17039
+ },
16770
17040
  {
16771
17041
  method: "post",
16772
17042
  path: "/api/v1/projects/{name}/locations",
@@ -21723,7 +21993,7 @@ function formatNotification(row) {
21723
21993
 
21724
21994
  // ../api-routes/src/google.ts
21725
21995
  import crypto19 from "crypto";
21726
- import { eq as eq24, and as and18, desc as desc11, sql as sql9, inArray as inArray11 } from "drizzle-orm";
21996
+ import { eq as eq24, and as and18, desc as desc11, sql as sql10, inArray as inArray11 } from "drizzle-orm";
21727
21997
 
21728
21998
  // ../api-routes/src/gbp-summary.ts
21729
21999
  function computeMetricTotals(rows) {
@@ -22281,11 +22551,14 @@ var GA4_DIMENSIONS = {
22281
22551
  firstUserSource: "firstUserSource",
22282
22552
  firstUserMedium: "firstUserMedium",
22283
22553
  sessionDefaultChannelGrouping: "sessionDefaultChannelGrouping",
22284
- sessionDefaultChannelGroup: "sessionDefaultChannelGroup"
22554
+ sessionDefaultChannelGroup: "sessionDefaultChannelGroup",
22555
+ hostName: "hostName",
22556
+ eventName: "eventName"
22285
22557
  };
22286
22558
  var GA4_METRICS = {
22287
22559
  sessions: "sessions",
22288
- totalUsers: "totalUsers"
22560
+ totalUsers: "totalUsers",
22561
+ eventCount: "eventCount"
22289
22562
  };
22290
22563
 
22291
22564
  // ../integration-google-analytics/src/types.ts
@@ -22554,6 +22827,100 @@ async function batchRunReports(accessToken, propertyId, requests) {
22554
22827
  function formatDate2(d) {
22555
22828
  return d.toISOString().slice(0, 10);
22556
22829
  }
22830
+ function measurementDateRange(days) {
22831
+ const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
22832
+ const endDate = /* @__PURE__ */ new Date();
22833
+ const startDate = /* @__PURE__ */ new Date();
22834
+ startDate.setUTCDate(startDate.getUTCDate() - syncDays);
22835
+ return { syncDays, dateRange: { startDate: formatDate2(startDate), endDate: formatDate2(endDate) } };
22836
+ }
22837
+ async function fetchPagedReport(accessToken, propertyId, baseRequest, pageSize = 1e4) {
22838
+ const rows = [];
22839
+ let offset = 0;
22840
+ for (let page = 0; page < GA4_MAX_PAGES; page++) {
22841
+ const response = await runReport(accessToken, propertyId, { ...baseRequest, limit: pageSize, offset });
22842
+ const pageRows = response.rows ?? [];
22843
+ rows.push(...pageRows);
22844
+ offset += pageRows.length;
22845
+ if (pageRows.length < pageSize || response.rowCount !== void 0 && offset >= response.rowCount) break;
22846
+ if (page === GA4_MAX_PAGES - 1) {
22847
+ throw new Error(`GA4 report truncated: page limit (${GA4_MAX_PAGES}) reached before all rows were fetched`);
22848
+ }
22849
+ }
22850
+ return rows;
22851
+ }
22852
+ async function fetchAcquisitionByChannel(accessToken, propertyId, days, options) {
22853
+ validateAccessToken2(accessToken);
22854
+ validatePropertyId(propertyId);
22855
+ const { syncDays, dateRange } = measurementDateRange(days);
22856
+ const rows = await fetchPagedReport(accessToken, propertyId, {
22857
+ dateRanges: [dateRange],
22858
+ dimensions: [
22859
+ { name: GA4_DIMENSIONS.date },
22860
+ { name: GA4_DIMENSIONS.sessionDefaultChannelGroup },
22861
+ { name: GA4_DIMENSIONS.sessionSource },
22862
+ { name: GA4_DIMENSIONS.sessionMedium },
22863
+ { name: GA4_DIMENSIONS.hostName },
22864
+ { name: GA4_DIMENSIONS.landingPagePlusQueryString }
22865
+ ],
22866
+ metrics: [{ name: GA4_METRICS.sessions }]
22867
+ }, options?.pageSize);
22868
+ ga4Log("info", "fetch-acquisition.done", { propertyId, days: syncDays, rowCount: rows.length });
22869
+ return { ...dateRange, rows: rows.map((row) => ({
22870
+ date: compactDateToIso(row.dimensionValues[0]?.value ?? ""),
22871
+ channelGroup: row.dimensionValues[1]?.value ?? "(not set)",
22872
+ source: row.dimensionValues[2]?.value ?? "(not set)",
22873
+ medium: row.dimensionValues[3]?.value ?? "(not set)",
22874
+ hostName: row.dimensionValues[4]?.value ?? "(not set)",
22875
+ landingPage: row.dimensionValues[5]?.value ?? "(not set)",
22876
+ sessions: parseInt(row.metricValues[0]?.value ?? "0", 10) || 0
22877
+ })) };
22878
+ }
22879
+ function isLandingPageDimensionIncompatibility(error) {
22880
+ return error instanceof GA4ApiError && error.status === 400 && /landingPagePlusQueryString/i.test(error.message) && /incompatib/i.test(error.message);
22881
+ }
22882
+ async function fetchLeadEvents(accessToken, propertyId, eventNames, days) {
22883
+ validateAccessToken2(accessToken);
22884
+ validatePropertyId(propertyId);
22885
+ const { dateRange } = measurementDateRange(days);
22886
+ const filter = { orGroup: { expressions: eventNames.map((value) => ({ filter: {
22887
+ fieldName: GA4_DIMENSIONS.eventName,
22888
+ stringFilter: { matchType: "EXACT", value }
22889
+ } })) } };
22890
+ const dimensions = [
22891
+ { name: GA4_DIMENSIONS.date },
22892
+ { name: GA4_DIMENSIONS.eventName },
22893
+ { name: GA4_DIMENSIONS.sessionDefaultChannelGroup },
22894
+ { name: GA4_DIMENSIONS.sessionSource },
22895
+ { name: GA4_DIMENSIONS.sessionMedium }
22896
+ ];
22897
+ const request = (landingPage) => ({
22898
+ dateRanges: [dateRange],
22899
+ dimensions: landingPage ? [...dimensions, { name: GA4_DIMENSIONS.hostName }, { name: GA4_DIMENSIONS.landingPagePlusQueryString }] : dimensions,
22900
+ metrics: [{ name: GA4_METRICS.eventCount }],
22901
+ dimensionFilter: filter
22902
+ });
22903
+ let attributionScope = "landing-page";
22904
+ let rawRows;
22905
+ try {
22906
+ rawRows = await fetchPagedReport(accessToken, propertyId, request(true));
22907
+ } catch (error) {
22908
+ if (!isLandingPageDimensionIncompatibility(error)) throw error;
22909
+ attributionScope = "channel";
22910
+ rawRows = await fetchPagedReport(accessToken, propertyId, request(false));
22911
+ }
22912
+ const rows = rawRows.map((row) => ({
22913
+ date: compactDateToIso(row.dimensionValues[0]?.value ?? ""),
22914
+ eventName: row.dimensionValues[1]?.value ?? "(not set)",
22915
+ channelGroup: row.dimensionValues[2]?.value ?? "(not set)",
22916
+ source: row.dimensionValues[3]?.value ?? "(not set)",
22917
+ medium: row.dimensionValues[4]?.value ?? "(not set)",
22918
+ hostName: attributionScope === "landing-page" ? row.dimensionValues[5]?.value ?? "(not available)" : "(not available)",
22919
+ landingPage: attributionScope === "landing-page" ? row.dimensionValues[6]?.value ?? "(not available)" : "(not available)",
22920
+ eventCount: parseInt(row.metricValues[0]?.value ?? "0", 10) || 0
22921
+ }));
22922
+ return { ...dateRange, attributionScope, rows };
22923
+ }
22557
22924
  var AI_REFERRAL_SOURCE_FILTERS = [
22558
22925
  { matchType: "CONTAINS", value: "perplexity" },
22559
22926
  { matchType: "CONTAINS", value: "gemini" },
@@ -23726,11 +24093,11 @@ async function googleRoutes(app, opts) {
23726
24093
  const { startDate, endDate, query, page, limit, offset } = request.query;
23727
24094
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
23728
24095
  const conditions = [eq24(gscSearchData.projectId, project.id)];
23729
- if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
23730
- else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
23731
- if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
23732
- if (query) conditions.push(sql9`${gscSearchData.query} LIKE ${"%" + escapeLikePattern(query) + "%"} ESCAPE '\\'`);
23733
- if (page) conditions.push(sql9`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
24096
+ if (startDate) conditions.push(sql10`${gscSearchData.date} >= ${startDate}`);
24097
+ else if (cutoffDate) conditions.push(sql10`${gscSearchData.date} >= ${cutoffDate}`);
24098
+ if (endDate) conditions.push(sql10`${gscSearchData.date} <= ${endDate}`);
24099
+ if (query) conditions.push(sql10`${gscSearchData.query} LIKE ${"%" + escapeLikePattern(query) + "%"} ESCAPE '\\'`);
24100
+ if (page) conditions.push(sql10`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
23734
24101
  const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
23735
24102
  const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
23736
24103
  const rows = app.db.select().from(gscSearchData).where(and18(...conditions)).orderBy(desc11(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
@@ -23754,13 +24121,13 @@ async function googleRoutes(app, opts) {
23754
24121
  const windowEnd = endDate ?? "9999-12-31";
23755
24122
  const dailyTotals = readGscDailyTotals(app.db, project.id, windowStart, windowEnd);
23756
24123
  const conditions = [eq24(gscSearchData.projectId, project.id)];
23757
- if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
23758
- else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
23759
- if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
24124
+ if (startDate) conditions.push(sql10`${gscSearchData.date} >= ${startDate}`);
24125
+ else if (cutoffDate) conditions.push(sql10`${gscSearchData.date} >= ${cutoffDate}`);
24126
+ if (endDate) conditions.push(sql10`${gscSearchData.date} <= ${endDate}`);
23760
24127
  const dimensionedRows = app.db.select({
23761
24128
  date: gscSearchData.date,
23762
- clicks: sql9`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
23763
- impressions: sql9`COALESCE(SUM(${gscSearchData.impressions}), 0)`
24129
+ clicks: sql10`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
24130
+ impressions: sql10`COALESCE(SUM(${gscSearchData.impressions}), 0)`
23764
24131
  }).from(gscSearchData).where(and18(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
23765
24132
  const daily = mergeGscDailyTotalsWithFallback(
23766
24133
  dailyTotals,
@@ -24656,11 +25023,11 @@ async function googleRoutes(app, opts) {
24656
25023
 
24657
25024
  // ../api-routes/src/ads.ts
24658
25025
  import crypto22 from "crypto";
24659
- import { eq as eq26, and as and20, asc as asc6, gt as gt2, gte as gte6, lt as lt3, lte as lte4, ne as ne3, inArray as inArray12, or as or6, isNull as isNull2, isNotNull as isNotNull2, sql as sql11 } from "drizzle-orm";
25026
+ import { eq as eq26, and as and20, asc as asc6, gt as gt2, gte as gte6, lt as lt3, lte as lte4, ne as ne3, inArray as inArray12, or as or6, isNull as isNull2, isNotNull as isNotNull2, sql as sql12 } from "drizzle-orm";
24660
25027
 
24661
25028
  // ../api-routes/src/ads-activation-routes.ts
24662
25029
  import crypto21 from "crypto";
24663
- import { and as and19, asc as asc5, eq as eq25, gt, isNotNull, isNull, lte as lte3, or as or5, sql as sql10 } from "drizzle-orm";
25030
+ import { and as and19, asc as asc5, eq as eq25, gt, isNotNull, isNull, lte as lte3, or as or5, sql as sql11 } from "drizzle-orm";
24664
25031
 
24665
25032
  // ../api-routes/src/ads-activation.ts
24666
25033
  import crypto20 from "crypto";
@@ -26533,7 +26900,7 @@ function selectWatchdogOperations(app, nowIso, batchSize, allowLiveRevocationLea
26533
26900
  ),
26534
26901
  leaseAvailable
26535
26902
  );
26536
- const freshRevocationPriority = sql10`CASE
26903
+ const freshRevocationPriority = sql11`CASE
26537
26904
  WHEN ${adsActivationGrants.revocationRequestedAt} IS NOT NULL
26538
26905
  AND (
26539
26906
  ${adsOperations.lastReconciledAt} IS NULL
@@ -27629,7 +27996,7 @@ function claimOperationForReconciliation(app, row, leaseOwner, now, leaseMs, pol
27629
27996
  state: AdsOperationStates.reconciling,
27630
27997
  leaseOwner,
27631
27998
  leaseExpiresAt,
27632
- reconcileAttempts: sql11`${adsOperations.reconcileAttempts} + 1`,
27999
+ reconcileAttempts: sql12`${adsOperations.reconcileAttempts} + 1`,
27633
28000
  updatedAt: nowIso
27634
28001
  }).where(and20(
27635
28002
  eq26(adsOperations.id, row.id),
@@ -29701,7 +30068,7 @@ async function cdpRoutes(app, opts) {
29701
30068
 
29702
30069
  // ../api-routes/src/ga.ts
29703
30070
  import crypto24 from "crypto";
29704
- import { eq as eq29, desc as desc13, and as and23, sql as sql12 } from "drizzle-orm";
30071
+ import { eq as eq29, desc as desc13, and as and23, sql as sql13 } from "drizzle-orm";
29705
30072
 
29706
30073
  // ../api-routes/src/ga-session-history.ts
29707
30074
  function buildSessionHistory(landingPageDays, dailyTotals) {
@@ -29780,6 +30147,134 @@ function normalizeAiTrafficClass(value) {
29780
30147
  function emptyAiCounts() {
29781
30148
  return { sessions: 0, users: 0 };
29782
30149
  }
30150
+ function persistAcquisitionMeasurement(db, input) {
30151
+ const { startDate, endDate } = input.report;
30152
+ db.transaction((tx) => {
30153
+ tx.delete(gaAcquisitionDaily).where(and23(
30154
+ eq29(gaAcquisitionDaily.projectId, input.projectId),
30155
+ sql13`${gaAcquisitionDaily.date} >= ${startDate}`,
30156
+ sql13`${gaAcquisitionDaily.date} <= ${endDate}`
30157
+ )).run();
30158
+ for (const row of input.report.rows) {
30159
+ tx.insert(gaAcquisitionDaily).values({
30160
+ id: crypto24.randomUUID(),
30161
+ projectId: input.projectId,
30162
+ date: row.date,
30163
+ channelGroup: row.channelGroup,
30164
+ source: row.source,
30165
+ medium: row.medium,
30166
+ hostName: row.hostName,
30167
+ landingPage: row.landingPage,
30168
+ landingPageNormalized: normalizeUrlPath(row.landingPage),
30169
+ sessions: row.sessions,
30170
+ syncedAt: input.syncedAt,
30171
+ syncRunId: input.runId,
30172
+ createdAt: input.syncedAt
30173
+ }).run();
30174
+ }
30175
+ tx.insert(gaMeasurementSyncStates).values({
30176
+ projectId: input.projectId,
30177
+ acquisitionStatus: "ready",
30178
+ acquisitionError: null,
30179
+ acquisitionSyncedAt: input.syncedAt,
30180
+ updatedAt: input.syncedAt
30181
+ }).onConflictDoUpdate({
30182
+ target: gaMeasurementSyncStates.projectId,
30183
+ set: {
30184
+ acquisitionStatus: "ready",
30185
+ acquisitionError: null,
30186
+ acquisitionSyncedAt: input.syncedAt,
30187
+ updatedAt: input.syncedAt
30188
+ }
30189
+ }).run();
30190
+ });
30191
+ }
30192
+ function persistLeadMeasurement(db, input) {
30193
+ const { startDate, endDate } = input.report;
30194
+ db.transaction((tx) => {
30195
+ tx.delete(gaLeadEventsDaily).where(and23(
30196
+ eq29(gaLeadEventsDaily.projectId, input.projectId),
30197
+ sql13`${gaLeadEventsDaily.date} >= ${startDate}`,
30198
+ sql13`${gaLeadEventsDaily.date} <= ${endDate}`
30199
+ )).run();
30200
+ for (const row of input.report.rows) {
30201
+ tx.insert(gaLeadEventsDaily).values({
30202
+ id: crypto24.randomUUID(),
30203
+ projectId: input.projectId,
30204
+ date: row.date,
30205
+ eventName: row.eventName,
30206
+ channelGroup: row.channelGroup,
30207
+ source: row.source,
30208
+ medium: row.medium,
30209
+ hostName: row.hostName,
30210
+ landingPage: row.landingPage,
30211
+ landingPageNormalized: input.report.attributionScope === "landing-page" ? normalizeUrlPath(row.landingPage) : null,
30212
+ attributionScope: input.report.attributionScope,
30213
+ eventCount: row.eventCount,
30214
+ syncedAt: input.syncedAt,
30215
+ syncRunId: input.runId,
30216
+ createdAt: input.syncedAt
30217
+ }).run();
30218
+ }
30219
+ tx.insert(gaMeasurementSyncStates).values({
30220
+ projectId: input.projectId,
30221
+ leadStatus: "ready",
30222
+ leadError: null,
30223
+ leadSyncedAt: input.syncedAt,
30224
+ leadAttributionScope: input.report.attributionScope,
30225
+ updatedAt: input.syncedAt
30226
+ }).onConflictDoUpdate({
30227
+ target: gaMeasurementSyncStates.projectId,
30228
+ set: {
30229
+ leadStatus: "ready",
30230
+ leadError: null,
30231
+ leadSyncedAt: input.syncedAt,
30232
+ leadAttributionScope: input.report.attributionScope,
30233
+ updatedAt: input.syncedAt
30234
+ }
30235
+ }).run();
30236
+ });
30237
+ }
30238
+ function resetLeadMeasurement(db, projectId, updatedAt) {
30239
+ db.transaction((tx) => {
30240
+ tx.delete(gaLeadEventsDaily).where(eq29(gaLeadEventsDaily.projectId, projectId)).run();
30241
+ tx.insert(gaMeasurementSyncStates).values({ projectId, leadStatus: "never-synced", leadError: null, leadSyncedAt: null, leadAttributionScope: null, updatedAt }).onConflictDoUpdate({
30242
+ target: gaMeasurementSyncStates.projectId,
30243
+ set: { leadStatus: "never-synced", leadError: null, leadSyncedAt: null, leadAttributionScope: null, updatedAt }
30244
+ }).run();
30245
+ });
30246
+ }
30247
+ function persistMeasurementError(db, projectId, component, message, updatedAt) {
30248
+ if (component === "acquisition") {
30249
+ db.insert(gaMeasurementSyncStates).values({
30250
+ projectId,
30251
+ acquisitionStatus: "error",
30252
+ acquisitionError: message,
30253
+ updatedAt
30254
+ }).onConflictDoUpdate({
30255
+ target: gaMeasurementSyncStates.projectId,
30256
+ set: {
30257
+ acquisitionStatus: "error",
30258
+ acquisitionError: message,
30259
+ updatedAt
30260
+ }
30261
+ }).run();
30262
+ return;
30263
+ }
30264
+ db.insert(gaMeasurementSyncStates).values({
30265
+ projectId,
30266
+ leadStatus: "error",
30267
+ leadError: message,
30268
+ updatedAt
30269
+ }).onConflictDoUpdate({
30270
+ target: gaMeasurementSyncStates.projectId,
30271
+ set: {
30272
+ leadStatus: "error",
30273
+ leadError: message,
30274
+ updatedAt
30275
+ }
30276
+ }).run();
30277
+ }
29783
30278
  function summarizeAiReferralCounts(rows) {
29784
30279
  const dedupeGroups = /* @__PURE__ */ new Map();
29785
30280
  const bySessionChannelGroup = /* @__PURE__ */ new Map();
@@ -30043,6 +30538,10 @@ async function ga4Routes(app, opts) {
30043
30538
  const syncSummary = true;
30044
30539
  const syncAi = !only || only === "ai";
30045
30540
  const syncSocial = !only || only === "social";
30541
+ const measurementState = app.db.select().from(gaMeasurementSyncStates).where(eq29(gaMeasurementSyncStates.projectId, project.id)).get();
30542
+ const acquisitionDays = measurementState?.acquisitionSyncedAt != null ? Math.min(Math.max(1, days), 90) : 90;
30543
+ const leadDays = measurementState?.leadSyncedAt != null ? Math.min(Math.max(1, days), 90) : 90;
30544
+ const leadEventNames = project.measurement.leadEventNames;
30046
30545
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
30047
30546
  const runId = crypto24.randomUUID();
30048
30547
  app.db.insert(runs).values({
@@ -30068,6 +30567,10 @@ async function ga4Routes(app, opts) {
30068
30567
  if (syncTraffic) fetches.push(fetchTrafficByLandingPage(accessToken, propertyId, days));
30069
30568
  if (syncAi) fetches.push(fetchAiReferrals(accessToken, propertyId, days));
30070
30569
  if (syncSocial) fetches.push(fetchSocialReferrals(accessToken, propertyId, days));
30570
+ const measurementFetch = Promise.allSettled([
30571
+ fetchAcquisitionByChannel(accessToken, propertyId, acquisitionDays),
30572
+ leadEventNames.length > 0 ? fetchLeadEvents(accessToken, propertyId, leadEventNames, leadDays) : Promise.resolve(null)
30573
+ ]);
30071
30574
  const results = await Promise.all(fetches);
30072
30575
  const summary = results[0];
30073
30576
  const windowSummaries = results.slice(1, 1 + WINDOW_KEYS.length);
@@ -30088,8 +30591,8 @@ async function ga4Routes(app, opts) {
30088
30591
  tx.delete(gaTrafficSnapshots).where(
30089
30592
  and23(
30090
30593
  eq29(gaTrafficSnapshots.projectId, project.id),
30091
- sql12`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
30092
- sql12`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
30594
+ sql13`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
30595
+ sql13`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
30093
30596
  )
30094
30597
  ).run();
30095
30598
  for (const row of rows) {
@@ -30111,8 +30614,8 @@ async function ga4Routes(app, opts) {
30111
30614
  tx.delete(gaDailyTotals).where(
30112
30615
  and23(
30113
30616
  eq29(gaDailyTotals.projectId, project.id),
30114
- sql12`${gaDailyTotals.date} >= ${summary.periodStart}`,
30115
- sql12`${gaDailyTotals.date} <= ${summary.periodEnd}`
30617
+ sql13`${gaDailyTotals.date} >= ${summary.periodStart}`,
30618
+ sql13`${gaDailyTotals.date} <= ${summary.periodEnd}`
30116
30619
  )
30117
30620
  ).run();
30118
30621
  for (const row of dailyTotals) {
@@ -30131,8 +30634,8 @@ async function ga4Routes(app, opts) {
30131
30634
  tx.delete(gaAiReferrals).where(
30132
30635
  and23(
30133
30636
  eq29(gaAiReferrals.projectId, project.id),
30134
- sql12`${gaAiReferrals.date} >= ${summary.periodStart}`,
30135
- sql12`${gaAiReferrals.date} <= ${summary.periodEnd}`
30637
+ sql13`${gaAiReferrals.date} >= ${summary.periodStart}`,
30638
+ sql13`${gaAiReferrals.date} <= ${summary.periodEnd}`
30136
30639
  )
30137
30640
  ).run();
30138
30641
  for (const row of aiReferrals) {
@@ -30163,8 +30666,8 @@ async function ga4Routes(app, opts) {
30163
30666
  tx.delete(gaSocialReferrals).where(
30164
30667
  and23(
30165
30668
  eq29(gaSocialReferrals.projectId, project.id),
30166
- sql12`${gaSocialReferrals.date} >= ${summary.periodStart}`,
30167
- sql12`${gaSocialReferrals.date} <= ${summary.periodEnd}`
30669
+ sql13`${gaSocialReferrals.date} >= ${summary.periodStart}`,
30670
+ sql13`${gaSocialReferrals.date} <= ${summary.periodEnd}`
30168
30671
  )
30169
30672
  ).run();
30170
30673
  for (const row of socialReferrals) {
@@ -30213,7 +30716,74 @@ async function ga4Routes(app, opts) {
30213
30716
  }
30214
30717
  }
30215
30718
  });
30216
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq29(runs.id, runId)).run();
30719
+ const [acquisitionSettled, leadsSettled] = await measurementFetch;
30720
+ const measurementNow = (/* @__PURE__ */ new Date()).toISOString();
30721
+ const recordMeasurementFailure = (component, reason) => {
30722
+ const message = reason instanceof Error ? reason.message : String(reason);
30723
+ gaLog("warn", "measurement.component-failed", {
30724
+ projectId: project.id,
30725
+ runId,
30726
+ component,
30727
+ error: message
30728
+ });
30729
+ try {
30730
+ persistMeasurementError(app.db, project.id, component, message, measurementNow);
30731
+ } catch (stateError) {
30732
+ gaLog("error", "measurement.state-write-failed", {
30733
+ projectId: project.id,
30734
+ runId,
30735
+ component,
30736
+ error: stateError instanceof Error ? stateError.message : String(stateError)
30737
+ });
30738
+ }
30739
+ return { status: "error", days: component === "acquisition" ? acquisitionDays : leadDays, rowCount: 0, error: message };
30740
+ };
30741
+ const acquisitionResult = (() => {
30742
+ if (acquisitionSettled.status === "rejected") {
30743
+ return recordMeasurementFailure("acquisition", acquisitionSettled.reason);
30744
+ }
30745
+ try {
30746
+ persistAcquisitionMeasurement(app.db, {
30747
+ projectId: project.id,
30748
+ runId,
30749
+ syncedAt: measurementNow,
30750
+ report: acquisitionSettled.value
30751
+ });
30752
+ return {
30753
+ status: "ready",
30754
+ days: acquisitionDays,
30755
+ rowCount: acquisitionSettled.value.rows.length
30756
+ };
30757
+ } catch (error) {
30758
+ return recordMeasurementFailure("acquisition", error);
30759
+ }
30760
+ })();
30761
+ const leadResult = (() => {
30762
+ if (leadsSettled.status === "rejected") {
30763
+ return recordMeasurementFailure("leads", leadsSettled.reason);
30764
+ }
30765
+ if (leadsSettled.value === null) {
30766
+ resetLeadMeasurement(app.db, project.id, measurementNow);
30767
+ return { status: "not-configured", days: 0, rowCount: 0 };
30768
+ }
30769
+ try {
30770
+ persistLeadMeasurement(app.db, {
30771
+ projectId: project.id,
30772
+ runId,
30773
+ syncedAt: measurementNow,
30774
+ report: leadsSettled.value
30775
+ });
30776
+ return {
30777
+ status: "ready",
30778
+ days: leadDays,
30779
+ rowCount: leadsSettled.value.rows.length,
30780
+ attributionScope: leadsSettled.value.attributionScope
30781
+ };
30782
+ } catch (error) {
30783
+ return recordMeasurementFailure("leads", error);
30784
+ }
30785
+ })();
30786
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: measurementNow }).where(eq29(runs.id, runId)).run();
30217
30787
  const syncedComponents = only ? [
30218
30788
  ...syncTraffic ? ["traffic"] : [],
30219
30789
  ...syncSummary ? ["summary"] : [],
@@ -30237,6 +30807,10 @@ async function ga4Routes(app, opts) {
30237
30807
  socialReferralCount: socialReferrals.length,
30238
30808
  days,
30239
30809
  syncedAt: now,
30810
+ measurement: {
30811
+ acquisition: acquisitionResult,
30812
+ leads: leadResult
30813
+ },
30240
30814
  ...syncedComponents ? { syncedComponents } : {}
30241
30815
  };
30242
30816
  } catch (e) {
@@ -30254,11 +30828,11 @@ async function ga4Routes(app, opts) {
30254
30828
  const cutoff = windowCutoff(window);
30255
30829
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
30256
30830
  const snapshotConditions = [eq29(gaTrafficSnapshots.projectId, project.id)];
30257
- if (cutoffDate) snapshotConditions.push(sql12`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
30831
+ if (cutoffDate) snapshotConditions.push(sql13`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
30258
30832
  const aiConditions = [eq29(gaAiReferrals.projectId, project.id)];
30259
- if (cutoffDate) aiConditions.push(sql12`${gaAiReferrals.date} >= ${cutoffDate}`);
30833
+ if (cutoffDate) aiConditions.push(sql13`${gaAiReferrals.date} >= ${cutoffDate}`);
30260
30834
  const socialConditions = [eq29(gaSocialReferrals.projectId, project.id)];
30261
- if (cutoffDate) socialConditions.push(sql12`${gaSocialReferrals.date} >= ${cutoffDate}`);
30835
+ if (cutoffDate) socialConditions.push(sql13`${gaSocialReferrals.date} >= ${cutoffDate}`);
30262
30836
  const windowSummaryRow = cutoffDate ? app.db.select({
30263
30837
  totalSessions: gaTrafficWindowSummaries.totalSessions,
30264
30838
  totalOrganicSessions: gaTrafficWindowSummaries.totalOrganicSessions,
@@ -30271,9 +30845,9 @@ async function ga4Routes(app, opts) {
30271
30845
  )
30272
30846
  ).get() : null;
30273
30847
  const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
30274
- totalSessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
30275
- totalOrganicSessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
30276
- totalUsers: sql12`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
30848
+ totalSessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
30849
+ totalOrganicSessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
30850
+ totalUsers: sql13`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
30277
30851
  }).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).get() : null;
30278
30852
  const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
30279
30853
  totalSessions: gaTrafficSummaries.totalSessions,
@@ -30281,41 +30855,41 @@ async function ga4Routes(app, opts) {
30281
30855
  totalUsers: gaTrafficSummaries.totalUsers
30282
30856
  }).from(gaTrafficSummaries).where(eq29(gaTrafficSummaries.projectId, project.id)).get();
30283
30857
  const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
30284
- totalDirectSessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
30858
+ totalDirectSessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
30285
30859
  }).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).get();
30286
30860
  const summaryMeta = app.db.select({
30287
30861
  periodStart: gaTrafficSummaries.periodStart,
30288
30862
  periodEnd: gaTrafficSummaries.periodEnd
30289
30863
  }).from(gaTrafficSummaries).where(eq29(gaTrafficSummaries.projectId, project.id)).get();
30290
30864
  const rows = app.db.select({
30291
- landingPage: sql12`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
30292
- sessions: sql12`SUM(${gaTrafficSnapshots.sessions})`,
30293
- organicSessions: sql12`SUM(${gaTrafficSnapshots.organicSessions})`,
30294
- directSessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
30295
- users: sql12`SUM(${gaTrafficSnapshots.users})`
30296
- }).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).groupBy(sql12`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql12`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
30865
+ landingPage: sql13`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
30866
+ sessions: sql13`SUM(${gaTrafficSnapshots.sessions})`,
30867
+ organicSessions: sql13`SUM(${gaTrafficSnapshots.organicSessions})`,
30868
+ directSessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
30869
+ users: sql13`SUM(${gaTrafficSnapshots.users})`
30870
+ }).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).groupBy(sql13`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql13`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
30297
30871
  const aiReferralRows = app.db.select({
30298
30872
  source: gaAiReferrals.source,
30299
30873
  medium: gaAiReferrals.medium,
30300
30874
  trafficClass: gaAiReferrals.trafficClass,
30301
30875
  sourceDimension: gaAiReferrals.sourceDimension,
30302
- sessions: sql12`SUM(${gaAiReferrals.sessions})`,
30303
- users: sql12`SUM(${gaAiReferrals.users})`
30876
+ sessions: sql13`SUM(${gaAiReferrals.sessions})`,
30877
+ users: sql13`SUM(${gaAiReferrals.users})`
30304
30878
  }).from(gaAiReferrals).where(and23(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.trafficClass, gaAiReferrals.sourceDimension).all();
30305
30879
  const aiReferralLandingPageRows = app.db.select({
30306
30880
  source: gaAiReferrals.source,
30307
30881
  medium: gaAiReferrals.medium,
30308
30882
  trafficClass: gaAiReferrals.trafficClass,
30309
30883
  sourceDimension: gaAiReferrals.sourceDimension,
30310
- landingPage: sql12`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
30311
- sessions: sql12`SUM(${gaAiReferrals.sessions})`,
30312
- users: sql12`SUM(${gaAiReferrals.users})`
30884
+ landingPage: sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
30885
+ sessions: sql13`SUM(${gaAiReferrals.sessions})`,
30886
+ users: sql13`SUM(${gaAiReferrals.users})`
30313
30887
  }).from(gaAiReferrals).where(and23(...aiConditions)).groupBy(
30314
30888
  gaAiReferrals.source,
30315
30889
  gaAiReferrals.medium,
30316
30890
  gaAiReferrals.trafficClass,
30317
30891
  gaAiReferrals.sourceDimension,
30318
- sql12`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
30892
+ sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
30319
30893
  ).all();
30320
30894
  const aiRowsForTotals = app.db.select({
30321
30895
  date: gaAiReferrals.date,
@@ -30340,12 +30914,12 @@ async function ga4Routes(app, opts) {
30340
30914
  source: gaSocialReferrals.source,
30341
30915
  medium: gaSocialReferrals.medium,
30342
30916
  channelGroup: gaSocialReferrals.channelGroup,
30343
- sessions: sql12`SUM(${gaSocialReferrals.sessions})`,
30344
- users: sql12`SUM(${gaSocialReferrals.users})`
30345
- }).from(gaSocialReferrals).where(and23(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql12`SUM(${gaSocialReferrals.sessions}) DESC`).all();
30917
+ sessions: sql13`SUM(${gaSocialReferrals.sessions})`,
30918
+ users: sql13`SUM(${gaSocialReferrals.users})`
30919
+ }).from(gaSocialReferrals).where(and23(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql13`SUM(${gaSocialReferrals.sessions}) DESC`).all();
30346
30920
  const socialTotals = app.db.select({
30347
- sessions: sql12`SUM(${gaSocialReferrals.sessions})`,
30348
- users: sql12`SUM(${gaSocialReferrals.users})`
30921
+ sessions: sql13`SUM(${gaSocialReferrals.sessions})`,
30922
+ users: sql13`SUM(${gaSocialReferrals.users})`
30349
30923
  }).from(gaSocialReferrals).where(and23(...socialConditions)).get();
30350
30924
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq29(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
30351
30925
  const total = summaryRow?.totalSessions ?? 0;
@@ -30446,23 +31020,23 @@ async function ga4Routes(app, opts) {
30446
31020
  requireGa4Connection(opts, project.name, project.canonicalDomain);
30447
31021
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
30448
31022
  const conditions = [eq29(gaAiReferrals.projectId, project.id)];
30449
- if (cutoffDate) conditions.push(sql12`${gaAiReferrals.date} >= ${cutoffDate}`);
31023
+ if (cutoffDate) conditions.push(sql13`${gaAiReferrals.date} >= ${cutoffDate}`);
30450
31024
  const rows = app.db.select({
30451
31025
  date: gaAiReferrals.date,
30452
31026
  source: gaAiReferrals.source,
30453
31027
  medium: gaAiReferrals.medium,
30454
31028
  trafficClass: gaAiReferrals.trafficClass,
30455
- landingPage: sql12`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
31029
+ landingPage: sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
30456
31030
  sourceDimension: gaAiReferrals.sourceDimension,
30457
- sessions: sql12`SUM(${gaAiReferrals.sessions})`,
30458
- users: sql12`SUM(${gaAiReferrals.users})`
31031
+ sessions: sql13`SUM(${gaAiReferrals.sessions})`,
31032
+ users: sql13`SUM(${gaAiReferrals.users})`
30459
31033
  }).from(gaAiReferrals).where(and23(...conditions)).groupBy(
30460
31034
  gaAiReferrals.date,
30461
31035
  gaAiReferrals.source,
30462
31036
  gaAiReferrals.medium,
30463
31037
  gaAiReferrals.trafficClass,
30464
31038
  gaAiReferrals.sourceDimension,
30465
- sql12`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
31039
+ sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
30466
31040
  ).orderBy(gaAiReferrals.date).all();
30467
31041
  return rows;
30468
31042
  });
@@ -30471,7 +31045,7 @@ async function ga4Routes(app, opts) {
30471
31045
  requireGa4Connection(opts, project.name, project.canonicalDomain);
30472
31046
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
30473
31047
  const conditions = [eq29(gaSocialReferrals.projectId, project.id)];
30474
- if (cutoffDate) conditions.push(sql12`${gaSocialReferrals.date} >= ${cutoffDate}`);
31048
+ if (cutoffDate) conditions.push(sql13`${gaSocialReferrals.date} >= ${cutoffDate}`);
30475
31049
  const rows = app.db.select({
30476
31050
  date: gaSocialReferrals.date,
30477
31051
  source: gaSocialReferrals.source,
@@ -30492,10 +31066,10 @@ async function ga4Routes(app, opts) {
30492
31066
  d.setDate(d.getDate() - n);
30493
31067
  return fmt(d);
30494
31068
  };
30495
- const sumSocial = (from, to) => app.db.select({ sessions: sql12`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and23(
31069
+ const sumSocial = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and23(
30496
31070
  eq29(gaSocialReferrals.projectId, project.id),
30497
- sql12`${gaSocialReferrals.date} >= ${from}`,
30498
- sql12`${gaSocialReferrals.date} < ${to}`
31071
+ sql13`${gaSocialReferrals.date} >= ${from}`,
31072
+ sql13`${gaSocialReferrals.date} < ${to}`
30499
31073
  )).get();
30500
31074
  const current7d = sumSocial(daysAgo(7), fmt(today));
30501
31075
  const prev7d = sumSocial(daysAgo(14), daysAgo(7));
@@ -30504,19 +31078,19 @@ async function ga4Routes(app, opts) {
30504
31078
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
30505
31079
  const sourceCurrent = app.db.select({
30506
31080
  source: gaSocialReferrals.source,
30507
- sessions: sql12`SUM(${gaSocialReferrals.sessions})`
31081
+ sessions: sql13`SUM(${gaSocialReferrals.sessions})`
30508
31082
  }).from(gaSocialReferrals).where(and23(
30509
31083
  eq29(gaSocialReferrals.projectId, project.id),
30510
- sql12`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
30511
- sql12`${gaSocialReferrals.date} < ${fmt(today)}`
31084
+ sql13`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
31085
+ sql13`${gaSocialReferrals.date} < ${fmt(today)}`
30512
31086
  )).groupBy(gaSocialReferrals.source).all();
30513
31087
  const sourcePrev = app.db.select({
30514
31088
  source: gaSocialReferrals.source,
30515
- sessions: sql12`SUM(${gaSocialReferrals.sessions})`
31089
+ sessions: sql13`SUM(${gaSocialReferrals.sessions})`
30516
31090
  }).from(gaSocialReferrals).where(and23(
30517
31091
  eq29(gaSocialReferrals.projectId, project.id),
30518
- sql12`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
30519
- sql12`${gaSocialReferrals.date} < ${daysAgo(7)}`
31092
+ sql13`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
31093
+ sql13`${gaSocialReferrals.date} < ${daysAgo(7)}`
30520
31094
  )).groupBy(gaSocialReferrals.source).all();
30521
31095
  const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
30522
31096
  let biggestMover = null;
@@ -30555,16 +31129,16 @@ async function ga4Routes(app, opts) {
30555
31129
  return fmt(d);
30556
31130
  };
30557
31131
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
30558
- const sumTotal = (from, to) => app.db.select({ sessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and23(eq29(gaTrafficSnapshots.projectId, project.id), sql12`${gaTrafficSnapshots.date} >= ${from}`, sql12`${gaTrafficSnapshots.date} < ${to}`)).get();
30559
- const sumOrganic = (from, to) => app.db.select({ sessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and23(eq29(gaTrafficSnapshots.projectId, project.id), sql12`${gaTrafficSnapshots.date} >= ${from}`, sql12`${gaTrafficSnapshots.date} < ${to}`)).get();
30560
- const sumDirect = (from, to) => app.db.select({ sessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and23(eq29(gaTrafficSnapshots.projectId, project.id), sql12`${gaTrafficSnapshots.date} >= ${from}`, sql12`${gaTrafficSnapshots.date} < ${to}`)).get();
30561
- const sumAi = (from, to) => app.db.select({ sessions: sql12`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
31132
+ const sumTotal = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and23(eq29(gaTrafficSnapshots.projectId, project.id), sql13`${gaTrafficSnapshots.date} >= ${from}`, sql13`${gaTrafficSnapshots.date} < ${to}`)).get();
31133
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and23(eq29(gaTrafficSnapshots.projectId, project.id), sql13`${gaTrafficSnapshots.date} >= ${from}`, sql13`${gaTrafficSnapshots.date} < ${to}`)).get();
31134
+ const sumDirect = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and23(eq29(gaTrafficSnapshots.projectId, project.id), sql13`${gaTrafficSnapshots.date} >= ${from}`, sql13`${gaTrafficSnapshots.date} < ${to}`)).get();
31135
+ const sumAi = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
30562
31136
  eq29(gaAiReferrals.projectId, project.id),
30563
- sql12`${gaAiReferrals.date} >= ${from}`,
30564
- sql12`${gaAiReferrals.date} < ${to}`,
31137
+ sql13`${gaAiReferrals.date} >= ${from}`,
31138
+ sql13`${gaAiReferrals.date} < ${to}`,
30565
31139
  eq29(gaAiReferrals.sourceDimension, "session")
30566
31140
  )).get();
30567
- const sumSocial = (from, to) => app.db.select({ sessions: sql12`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and23(eq29(gaSocialReferrals.projectId, project.id), sql12`${gaSocialReferrals.date} >= ${from}`, sql12`${gaSocialReferrals.date} < ${to}`)).get();
31141
+ const sumSocial = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and23(eq29(gaSocialReferrals.projectId, project.id), sql13`${gaSocialReferrals.date} >= ${from}`, sql13`${gaSocialReferrals.date} < ${to}`)).get();
30568
31142
  const todayStr = fmt(today);
30569
31143
  const buildTrend = (sum) => {
30570
31144
  const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
@@ -30573,16 +31147,16 @@ async function ga4Routes(app, opts) {
30573
31147
  const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
30574
31148
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
30575
31149
  };
30576
- const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql12`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
31150
+ const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql13`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
30577
31151
  eq29(gaAiReferrals.projectId, project.id),
30578
- sql12`${gaAiReferrals.date} >= ${daysAgo(7)}`,
30579
- sql12`${gaAiReferrals.date} < ${todayStr}`,
31152
+ sql13`${gaAiReferrals.date} >= ${daysAgo(7)}`,
31153
+ sql13`${gaAiReferrals.date} < ${todayStr}`,
30580
31154
  eq29(gaAiReferrals.sourceDimension, "session")
30581
31155
  )).groupBy(gaAiReferrals.source).all();
30582
- const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql12`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
31156
+ const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql13`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
30583
31157
  eq29(gaAiReferrals.projectId, project.id),
30584
- sql12`${gaAiReferrals.date} >= ${daysAgo(14)}`,
30585
- sql12`${gaAiReferrals.date} < ${daysAgo(7)}`,
31158
+ sql13`${gaAiReferrals.date} >= ${daysAgo(14)}`,
31159
+ sql13`${gaAiReferrals.date} < ${daysAgo(7)}`,
30586
31160
  eq29(gaAiReferrals.sourceDimension, "session")
30587
31161
  )).groupBy(gaAiReferrals.source).all();
30588
31162
  const findBiggestMover = (current, prev) => {
@@ -30599,8 +31173,8 @@ async function ga4Routes(app, opts) {
30599
31173
  }
30600
31174
  return mover;
30601
31175
  };
30602
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql12`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and23(eq29(gaSocialReferrals.projectId, project.id), sql12`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql12`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
30603
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql12`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and23(eq29(gaSocialReferrals.projectId, project.id), sql12`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql12`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
31176
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql13`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and23(eq29(gaSocialReferrals.projectId, project.id), sql13`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql13`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
31177
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql13`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and23(eq29(gaSocialReferrals.projectId, project.id), sql13`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql13`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
30604
31178
  return {
30605
31179
  total: buildTrend(sumTotal),
30606
31180
  organic: buildTrend(sumOrganic),
@@ -30616,15 +31190,15 @@ async function ga4Routes(app, opts) {
30616
31190
  requireGa4Connection(opts, project.name, project.canonicalDomain);
30617
31191
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
30618
31192
  const conditions = [eq29(gaTrafficSnapshots.projectId, project.id)];
30619
- if (cutoffDate) conditions.push(sql12`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
31193
+ if (cutoffDate) conditions.push(sql13`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
30620
31194
  const rows = app.db.select({
30621
31195
  date: gaTrafficSnapshots.date,
30622
- sessions: sql12`SUM(${gaTrafficSnapshots.sessions})`,
30623
- organicSessions: sql12`SUM(${gaTrafficSnapshots.organicSessions})`,
30624
- users: sql12`SUM(${gaTrafficSnapshots.users})`
31196
+ sessions: sql13`SUM(${gaTrafficSnapshots.sessions})`,
31197
+ organicSessions: sql13`SUM(${gaTrafficSnapshots.organicSessions})`,
31198
+ users: sql13`SUM(${gaTrafficSnapshots.users})`
30625
31199
  }).from(gaTrafficSnapshots).where(and23(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
30626
31200
  const totalConditions = [eq29(gaDailyTotals.projectId, project.id)];
30627
- if (cutoffDate) totalConditions.push(sql12`${gaDailyTotals.date} >= ${cutoffDate}`);
31201
+ if (cutoffDate) totalConditions.push(sql13`${gaDailyTotals.date} >= ${cutoffDate}`);
30628
31202
  const totals = app.db.select({ date: gaDailyTotals.date, users: gaDailyTotals.users }).from(gaDailyTotals).where(and23(...totalConditions)).all();
30629
31203
  return buildSessionHistory(
30630
31204
  rows.map((r) => ({
@@ -30640,11 +31214,11 @@ async function ga4Routes(app, opts) {
30640
31214
  const project = resolveProject(app.db, request.params.name);
30641
31215
  requireGa4Connection(opts, project.name, project.canonicalDomain);
30642
31216
  const trafficPages = app.db.select({
30643
- landingPage: sql12`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
30644
- sessions: sql12`SUM(${gaTrafficSnapshots.sessions})`,
30645
- organicSessions: sql12`SUM(${gaTrafficSnapshots.organicSessions})`,
30646
- users: sql12`SUM(${gaTrafficSnapshots.users})`
30647
- }).from(gaTrafficSnapshots).where(eq29(gaTrafficSnapshots.projectId, project.id)).groupBy(sql12`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql12`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
31217
+ landingPage: sql13`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
31218
+ sessions: sql13`SUM(${gaTrafficSnapshots.sessions})`,
31219
+ organicSessions: sql13`SUM(${gaTrafficSnapshots.organicSessions})`,
31220
+ users: sql13`SUM(${gaTrafficSnapshots.users})`
31221
+ }).from(gaTrafficSnapshots).where(eq29(gaTrafficSnapshots.projectId, project.id)).groupBy(sql13`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql13`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
30648
31222
  return {
30649
31223
  pages: trafficPages.map((r) => ({
30650
31224
  landingPage: r.landingPage,
@@ -31752,9 +32326,9 @@ async function wordpressRoutes(app, opts) {
31752
32326
  throw validationError("WordPress connection storage is not configured for this deployment");
31753
32327
  }
31754
32328
  async function assertWordpressUrlAllowed(rawUrl, field) {
31755
- const check = await resolveWebhookTarget(rawUrl, { allowLoopback });
31756
- if (!check.ok) {
31757
- throw validationError(`${field} ${check.message.replace(/^"url" /, "")}`);
32329
+ const check2 = await resolveWebhookTarget(rawUrl, { allowLoopback });
32330
+ if (!check2.ok) {
32331
+ throw validationError(`${field} ${check2.message.replace(/^"url" /, "")}`);
31758
32332
  }
31759
32333
  }
31760
32334
  function requireConnection(store, projectName) {
@@ -32292,7 +32866,7 @@ async function wordpressRoutes(app, opts) {
32292
32866
 
32293
32867
  // ../api-routes/src/backlinks.ts
32294
32868
  import crypto26 from "crypto";
32295
- import { and as and25, asc as asc7, desc as desc14, eq as eq30, sql as sql13 } from "drizzle-orm";
32869
+ import { and as and25, asc as asc7, desc as desc14, eq as eq30, sql as sql14 } from "drizzle-orm";
32296
32870
 
32297
32871
  // ../integration-commoncrawl/src/constants.ts
32298
32872
  import os2 from "os";
@@ -32582,7 +33156,7 @@ async function queryBacklinks(opts) {
32582
33156
  const reversed = opts.targets.map(reverseDomain);
32583
33157
  const targetList = reversed.map(quote).join(", ");
32584
33158
  const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
32585
- const sql16 = `
33159
+ const sql17 = `
32586
33160
  WITH vertices AS (
32587
33161
  SELECT * FROM read_csv(
32588
33162
  ${quote(opts.vertexPath)},
@@ -32618,7 +33192,7 @@ async function queryBacklinks(opts) {
32618
33192
  const conn = await instance.connect();
32619
33193
  let rows;
32620
33194
  try {
32621
- const reader = await conn.runAndReadAll(sql16);
33195
+ const reader = await conn.runAndReadAll(sql17);
32622
33196
  rows = reader.getRowObjects();
32623
33197
  } finally {
32624
33198
  conn.disconnectSync?.();
@@ -32808,12 +33382,12 @@ function computeFilteredSummary(db, base) {
32808
33382
  );
32809
33383
  const filteredCondition = and25(baseDomainCondition, backlinkCrawlerExclusionClause());
32810
33384
  const unfilteredAgg = db.select({
32811
- count: sql13`count(*)`,
32812
- total: sql13`coalesce(sum(${backlinkDomains.numHosts}), 0)`
33385
+ count: sql14`count(*)`,
33386
+ total: sql14`coalesce(sum(${backlinkDomains.numHosts}), 0)`
32813
33387
  }).from(backlinkDomains).where(baseDomainCondition).get();
32814
33388
  const filteredAgg = db.select({
32815
- count: sql13`count(*)`,
32816
- total: sql13`coalesce(sum(${backlinkDomains.numHosts}), 0)`
33389
+ count: sql14`count(*)`,
33390
+ total: sql14`coalesce(sum(${backlinkDomains.numHosts}), 0)`
32817
33391
  }).from(backlinkDomains).where(filteredCondition).get();
32818
33392
  const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc14(backlinkDomains.numHosts)).limit(10).all();
32819
33393
  const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
@@ -32839,7 +33413,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
32839
33413
  const summary = db.select().from(backlinkSummaries).where(and25(eq30(backlinkSummaries.projectId, projectId), eq30(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
32840
33414
  let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
32841
33415
  if (summary && excludeCrawlers) {
32842
- const filtered = db.select({ count: sql13`count(*)` }).from(backlinkDomains).where(and25(
33416
+ const filtered = db.select({ count: sql14`count(*)` }).from(backlinkDomains).where(and25(
32843
33417
  eq30(backlinkDomains.projectId, projectId),
32844
33418
  eq30(backlinkDomains.source, source),
32845
33419
  eq30(backlinkDomains.release, summary.release),
@@ -33028,7 +33602,7 @@ async function backlinksRoutes(app, opts) {
33028
33602
  eq30(backlinkDomains.release, targetRelease)
33029
33603
  );
33030
33604
  const domainCondition = excludeCrawlers ? and25(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
33031
- const totalRow = app.db.select({ count: sql13`count(*)` }).from(backlinkDomains).where(domainCondition).get();
33605
+ const totalRow = app.db.select({ count: sql14`count(*)` }).from(backlinkDomains).where(domainCondition).get();
33032
33606
  const rows = app.db.select({
33033
33607
  linkingDomain: backlinkDomains.linkingDomain,
33034
33608
  numHosts: backlinkDomains.numHosts,
@@ -33107,7 +33681,7 @@ async function backlinksRoutes(app, opts) {
33107
33681
  // ../api-routes/src/traffic.ts
33108
33682
  import crypto28 from "crypto";
33109
33683
  import { Agent as UndiciAgent } from "undici";
33110
- import { and as and26, desc as desc15, eq as eq31, gte as gte7, lte as lte5, sql as sql14 } from "drizzle-orm";
33684
+ import { and as and26, desc as desc15, eq as eq31, gte as gte7, lte as lte5, sql as sql15 } from "drizzle-orm";
33111
33685
 
33112
33686
  // ../integration-cloud-run/src/auth.ts
33113
33687
  import crypto27 from "crypto";
@@ -37168,11 +37742,11 @@ async function trafficRoutes(app, opts) {
37168
37742
  const pullVercelEvents = opts.pullVercelTrafficEvents ?? listVercelTrafficEvents;
37169
37743
  const allowLoopback = opts.allowLoopbackWebhooks === true;
37170
37744
  async function assertWordpressTargetAllowed(baseUrl) {
37171
- const check = await resolveWebhookTarget(baseUrl, { allowLoopback });
37172
- if (!check.ok) {
37173
- throw validationError(`WordPress baseUrl rejected: ${check.message}`);
37745
+ const check2 = await resolveWebhookTarget(baseUrl, { allowLoopback });
37746
+ if (!check2.ok) {
37747
+ throw validationError(`WordPress baseUrl rejected: ${check2.message}`);
37174
37748
  }
37175
- const { address, family } = check.target;
37749
+ const { address, family } = check2.target;
37176
37750
  return new UndiciAgent({
37177
37751
  connect: {
37178
37752
  lookup: (_hostname, options, cb) => {
@@ -37808,7 +38382,7 @@ async function trafficRoutes(app, opts) {
37808
38382
  crawlerEventsHourly.status
37809
38383
  ],
37810
38384
  set: {
37811
- hits: sql14`${crawlerEventsHourly.hits} + ${bucket.hits}`,
38385
+ hits: sql15`${crawlerEventsHourly.hits} + ${bucket.hits}`,
37812
38386
  sampledUserAgent: bucket.sampledUserAgent,
37813
38387
  updatedAt: finishedAt
37814
38388
  }
@@ -37841,7 +38415,7 @@ async function trafficRoutes(app, opts) {
37841
38415
  aiUserFetchEventsHourly.status
37842
38416
  ],
37843
38417
  set: {
37844
- hits: sql14`${aiUserFetchEventsHourly.hits} + ${bucket.hits}`,
38418
+ hits: sql15`${aiUserFetchEventsHourly.hits} + ${bucket.hits}`,
37845
38419
  sampledUserAgent: bucket.sampledUserAgent,
37846
38420
  updatedAt: finishedAt
37847
38421
  }
@@ -37881,9 +38455,9 @@ async function trafficRoutes(app, opts) {
37881
38455
  // colliding bucket adds its paid and organic arrivals to the ones
37882
38456
  // already there rather than overwriting a single label.
37883
38457
  set: {
37884
- sessionsOrHits: sql14`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
37885
- paidSessionsOrHits: sql14`${aiReferralEventsHourly.paidSessionsOrHits} + ${bucket.paidHits}`,
37886
- organicSessionsOrHits: sql14`${aiReferralEventsHourly.organicSessionsOrHits} + ${bucket.organicHits}`,
38458
+ sessionsOrHits: sql15`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
38459
+ paidSessionsOrHits: sql15`${aiReferralEventsHourly.paidSessionsOrHits} + ${bucket.paidHits}`,
38460
+ organicSessionsOrHits: sql15`${aiReferralEventsHourly.organicSessionsOrHits} + ${bucket.organicHits}`,
37887
38461
  updatedAt: finishedAt
37888
38462
  }
37889
38463
  }).run();
@@ -38174,7 +38748,7 @@ async function trafficRoutes(app, opts) {
38174
38748
  function buildSourceDetail(projectId, row, since) {
38175
38749
  const crawlerPathRows = app.db.select({
38176
38750
  pathNormalized: crawlerEventsHourly.pathNormalized,
38177
- hits: sql14`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
38751
+ hits: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
38178
38752
  }).from(crawlerEventsHourly).where(
38179
38753
  and26(
38180
38754
  eq31(crawlerEventsHourly.sourceId, row.id),
@@ -38185,19 +38759,19 @@ async function trafficRoutes(app, opts) {
38185
38759
  crawlerPathRows.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
38186
38760
  );
38187
38761
  const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
38188
- const aiUserFetchTotals = app.db.select({ total: sql14`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
38762
+ const aiUserFetchTotals = app.db.select({ total: sql15`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
38189
38763
  and26(
38190
38764
  eq31(aiUserFetchEventsHourly.sourceId, row.id),
38191
38765
  gte7(aiUserFetchEventsHourly.tsHour, since)
38192
38766
  )
38193
38767
  ).get();
38194
- const aiTotals = app.db.select({ total: sql14`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
38768
+ const aiTotals = app.db.select({ total: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
38195
38769
  and26(
38196
38770
  eq31(aiReferralEventsHourly.sourceId, row.id),
38197
38771
  gte7(aiReferralEventsHourly.tsHour, since)
38198
38772
  )
38199
38773
  ).get();
38200
- const sampleTotals = app.db.select({ total: sql14`COUNT(*)` }).from(rawEventSamples).where(
38774
+ const sampleTotals = app.db.select({ total: sql15`COUNT(*)` }).from(rawEventSamples).where(
38201
38775
  and26(
38202
38776
  eq31(rawEventSamples.sourceId, row.id),
38203
38777
  gte7(rawEventSamples.ts, since)
@@ -38359,18 +38933,18 @@ async function trafficRoutes(app, opts) {
38359
38933
  const crawlerWhere = and26(...crawlerFilters);
38360
38934
  const pathTotals = app.db.select({
38361
38935
  pathNormalized: crawlerEventsHourly.pathNormalized,
38362
- hits: sql14`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
38363
- rows: sql14`COUNT(*)`
38936
+ hits: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
38937
+ rows: sql15`COUNT(*)`
38364
38938
  }).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerEventsHourly.pathNormalized).all();
38365
38939
  crawlerSegments = segmentCrawlerHits(
38366
38940
  pathTotals.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
38367
38941
  );
38368
38942
  crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
38369
38943
  totalEventRows += pathTotals.reduce((sum, row) => sum + Number(row.rows), 0);
38370
- const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql14`substr(${crawlerEventsHourly.tsHour}, 1, 10)` : crawlerEventsHourly.tsHour;
38944
+ const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql15`substr(${crawlerEventsHourly.tsHour}, 1, 10)` : crawlerEventsHourly.tsHour;
38371
38945
  const crawlerSeries = app.db.select({
38372
38946
  bucket: crawlerSeriesBucket,
38373
- hits: sql14`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
38947
+ hits: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
38374
38948
  }).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerSeriesBucket).all();
38375
38949
  for (const row of crawlerSeries) {
38376
38950
  trafficSeriesPoint(seriesByBucket, row.bucket).crawlerHits = Number(row.hits);
@@ -38400,15 +38974,15 @@ async function trafficRoutes(app, opts) {
38400
38974
  if (sourceIdParam) userFetchFilters.push(eq31(aiUserFetchEventsHourly.sourceId, sourceIdParam));
38401
38975
  const userFetchWhere = and26(...userFetchFilters);
38402
38976
  const total = app.db.select({
38403
- total: sql14`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`,
38404
- rows: sql14`COUNT(*)`
38977
+ total: sql15`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`,
38978
+ rows: sql15`COUNT(*)`
38405
38979
  }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
38406
38980
  aiUserFetchTotal = Number(total?.total ?? 0);
38407
38981
  totalEventRows += Number(total?.rows ?? 0);
38408
- const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql14`substr(${aiUserFetchEventsHourly.tsHour}, 1, 10)` : aiUserFetchEventsHourly.tsHour;
38982
+ const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql15`substr(${aiUserFetchEventsHourly.tsHour}, 1, 10)` : aiUserFetchEventsHourly.tsHour;
38409
38983
  const userFetchSeries = app.db.select({
38410
38984
  bucket: userFetchSeriesBucket,
38411
- hits: sql14`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
38985
+ hits: sql15`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
38412
38986
  }).from(aiUserFetchEventsHourly).where(userFetchWhere).groupBy(userFetchSeriesBucket).all();
38413
38987
  for (const row of userFetchSeries) {
38414
38988
  trafficSeriesPoint(seriesByBucket, row.bucket).aiUserFetchHits = Number(row.hits);
@@ -38437,10 +39011,10 @@ async function trafficRoutes(app, opts) {
38437
39011
  if (sourceIdParam) aiFilters.push(eq31(aiReferralEventsHourly.sourceId, sourceIdParam));
38438
39012
  const aiWhere = and26(...aiFilters);
38439
39013
  const total = app.db.select({
38440
- total: sql14`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
38441
- paid: sql14`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
38442
- organic: sql14`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`,
38443
- rows: sql14`COUNT(*)`
39014
+ total: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
39015
+ paid: sql15`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
39016
+ organic: sql15`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`,
39017
+ rows: sql15`COUNT(*)`
38444
39018
  }).from(aiReferralEventsHourly).where(aiWhere).get();
38445
39019
  aiReferralCounts = aiReferralClassCounts(
38446
39020
  Number(total?.total ?? 0),
@@ -38448,10 +39022,10 @@ async function trafficRoutes(app, opts) {
38448
39022
  Number(total?.organic ?? 0)
38449
39023
  );
38450
39024
  totalEventRows += Number(total?.rows ?? 0);
38451
- const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql14`substr(${aiReferralEventsHourly.tsHour}, 1, 10)` : aiReferralEventsHourly.tsHour;
39025
+ const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql15`substr(${aiReferralEventsHourly.tsHour}, 1, 10)` : aiReferralEventsHourly.tsHour;
38452
39026
  const referralSeries = app.db.select({
38453
39027
  bucket: referralSeriesBucket,
38454
- hits: sql14`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
39028
+ hits: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
38455
39029
  }).from(aiReferralEventsHourly).where(aiWhere).groupBy(referralSeriesBucket).all();
38456
39030
  for (const row of referralSeries) {
38457
39031
  trafficSeriesPoint(seriesByBucket, row.bucket).aiReferralHits = Number(row.hits);
@@ -39049,7 +39623,7 @@ var winnabilityCoverageCheck = {
39049
39623
  };
39050
39624
  var CONTENT_CHECKS = [winnabilityCoverageCheck];
39051
39625
  var CONTENT_CHECK_BY_ID = Object.fromEntries(
39052
- CONTENT_CHECKS.map((check) => [check.id, check])
39626
+ CONTENT_CHECKS.map((check2) => [check2.id, check2])
39053
39627
  );
39054
39628
 
39055
39629
  // ../api-routes/src/doctor/checks/ads.ts
@@ -39600,7 +40174,7 @@ var GBP_AUTH_CHECKS = [
39600
40174
  recentSyncCheck
39601
40175
  ];
39602
40176
  var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
39603
- GBP_AUTH_CHECKS.map((check) => [check.id, check])
40177
+ GBP_AUTH_CHECKS.map((check2) => [check2.id, check2])
39604
40178
  );
39605
40179
 
39606
40180
  // ../api-routes/src/doctor/checks/places.ts
@@ -39678,7 +40252,7 @@ var apiKeyCheck = {
39678
40252
  };
39679
40253
  var PLACES_CHECKS = [apiKeyCheck];
39680
40254
  var PLACES_CHECK_BY_ID = Object.fromEntries(
39681
- PLACES_CHECKS.map((check) => [check.id, check])
40255
+ PLACES_CHECKS.map((check2) => [check2.id, check2])
39682
40256
  );
39683
40257
 
39684
40258
  // ../api-routes/src/doctor/checks/google-auth.ts
@@ -39985,7 +40559,7 @@ var GOOGLE_AUTH_CHECKS = [
39985
40559
  scopesCheck2
39986
40560
  ];
39987
40561
  var GOOGLE_AUTH_CHECK_BY_ID = Object.fromEntries(
39988
- GOOGLE_AUTH_CHECKS.map((check) => [check.id, check])
40562
+ GOOGLE_AUTH_CHECKS.map((check2) => [check2.id, check2])
39989
40563
  );
39990
40564
 
39991
40565
  // ../api-routes/src/doctor/checks/providers.ts
@@ -40146,7 +40720,7 @@ var RUNTIME_STATE_CHECKS = [
40146
40720
  ];
40147
40721
 
40148
40722
  // ../api-routes/src/doctor/checks/traffic-source.ts
40149
- import { and as and29, eq as eq37, gte as gte8, ne as ne5, sql as sql15 } from "drizzle-orm";
40723
+ import { and as and29, eq as eq37, gte as gte8, ne as ne5, sql as sql16 } from "drizzle-orm";
40150
40724
  var RECENT_DATA_WARN_DAYS = 7;
40151
40725
  var RECENT_DATA_FAIL_DAYS = 30;
40152
40726
  function skippedNoProject5() {
@@ -40240,7 +40814,7 @@ var recentDataCheck = {
40240
40814
  const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
40241
40815
  const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
40242
40816
  const recentCrawlers = Number(
40243
- ctx.db.select({ total: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
40817
+ ctx.db.select({ total: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
40244
40818
  and29(
40245
40819
  eq37(crawlerEventsHourly.projectId, ctx.project.id),
40246
40820
  gte8(crawlerEventsHourly.tsHour, warnCutoff)
@@ -40248,7 +40822,7 @@ var recentDataCheck = {
40248
40822
  ).get()?.total ?? 0
40249
40823
  );
40250
40824
  const recentReferrals = Number(
40251
- ctx.db.select({ total: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
40825
+ ctx.db.select({ total: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
40252
40826
  and29(
40253
40827
  eq37(aiReferralEventsHourly.projectId, ctx.project.id),
40254
40828
  gte8(aiReferralEventsHourly.tsHour, warnCutoff)
@@ -40264,7 +40838,7 @@ var recentDataCheck = {
40264
40838
  };
40265
40839
  }
40266
40840
  const olderCrawlers = Number(
40267
- ctx.db.select({ total: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
40841
+ ctx.db.select({ total: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
40268
40842
  and29(
40269
40843
  eq37(crawlerEventsHourly.projectId, ctx.project.id),
40270
40844
  gte8(crawlerEventsHourly.tsHour, failCutoff)
@@ -40272,7 +40846,7 @@ var recentDataCheck = {
40272
40846
  ).get()?.total ?? 0
40273
40847
  );
40274
40848
  const olderReferrals = Number(
40275
- ctx.db.select({ total: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
40849
+ ctx.db.select({ total: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
40276
40850
  and29(
40277
40851
  eq37(aiReferralEventsHourly.projectId, ctx.project.id),
40278
40852
  gte8(aiReferralEventsHourly.tsHour, failCutoff)
@@ -40584,9 +41158,9 @@ async function runChecks(ctx, checks, options = {}) {
40584
41158
  const filters = options.checkIds ?? [];
40585
41159
  const targetScope = ctx.project ? CheckScopes.project : CheckScopes.global;
40586
41160
  const projectName = ctx.project?.name ?? null;
40587
- const selected = checks.filter((check) => {
40588
- if (check.scope !== targetScope) return false;
40589
- return matchesCheckId(check.id, filters);
41161
+ const selected = checks.filter((check2) => {
41162
+ if (check2.scope !== targetScope) return false;
41163
+ return matchesCheckId(check2.id, filters);
40590
41164
  });
40591
41165
  const results = [];
40592
41166
  for (const definition of selected) {
@@ -41524,6 +42098,80 @@ async function technicalAeoRoutes(app, opts) {
41524
42098
  });
41525
42099
  }
41526
42100
 
42101
+ // ../api-routes/src/research.ts
42102
+ import crypto33 from "crypto";
42103
+ import { and as and32, desc as desc18, eq as eq41 } from "drizzle-orm";
42104
+ var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
42105
+ async function researchRoutes(app, opts) {
42106
+ app.post("/projects/:name/research/runs", async (request, reply) => {
42107
+ const project = resolveProject(app.db, request.params.name);
42108
+ if (!opts.onResearchRunRequested) throw missingDependency("Research execution is not available on this deployment.", { reason: "no-research-handler" });
42109
+ const parsed = researchRunCreateSchema.safeParse(request.body ?? {});
42110
+ if (!parsed.success) throw validationError("Invalid research run request", { issues: parsed.error.issues });
42111
+ const input = parsed.data;
42112
+ const adapters = opts.providerAdapters ?? [];
42113
+ const configured = new Set(opts.configuredProviderNames ?? []);
42114
+ const providerName = input.provider ?? project.providers.find((name) => configured.has(name) && adapters.some((adapter2) => adapter2.name === name && adapter2.mode === "api")) ?? adapters.find((adapter2) => adapter2.mode === "api" && configured.has(adapter2.name))?.name;
42115
+ const adapter = adapters.find((candidate) => candidate.name === providerName);
42116
+ if (!providerName || !adapter || adapter.mode !== "api" || isBrowserProvider(providerName) || !configured.has(providerName)) throw validationError("Research requires a configured API provider.", { provider: input.provider, validProviders: adapters.filter((a) => a.mode === "api" && configured.has(a.name)).map((a) => a.name) });
42117
+ if (input.model) {
42118
+ adapter.modelValidationPattern.lastIndex = 0;
42119
+ if (!adapter.modelConfigurable || !adapter.modelValidationPattern.test(input.model)) throw validationError(`Invalid model "${input.model}" for provider "${providerName}".`, { provider: providerName, model: input.model, hint: adapter.modelValidationHint });
42120
+ }
42121
+ const location = input.location === void 0 ? project.defaultLocation ? project.locations.find((item) => item.label === project.defaultLocation) ?? null : null : input.location;
42122
+ if (location && !project.locations.some((item) => sameLocation(item, location))) throw validationError("Research location must exactly match a configured project location.", { location });
42123
+ const requestedModel = input.model ?? null;
42124
+ const resolvedModel = requestedModel ?? (project.providerModels[providerName] || adapter.defaultModel);
42125
+ adapter.modelValidationPattern.lastIndex = 0;
42126
+ if (!adapter.modelValidationPattern.test(resolvedModel)) throw validationError(`Invalid resolved model "${resolvedModel}" for provider "${providerName}".`, { provider: providerName, model: resolvedModel, hint: adapter.modelValidationHint });
42127
+ if (new Set(input.queries.map((query) => query.toLocaleLowerCase())).size !== input.queries.length) throw validationError("Research queries must be unique within a batch.");
42128
+ const normalized = { queries: input.queries, provider: providerName, model: requestedModel, location: location ?? null };
42129
+ const requestHash2 = crypto33.createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
42130
+ const now = (/* @__PURE__ */ new Date()).toISOString();
42131
+ const decision = app.db.transaction((tx) => {
42132
+ if (input.idempotencyKey) {
42133
+ const existing = tx.select().from(researchRuns).where(and32(eq41(researchRuns.projectId, project.id), eq41(researchRuns.idempotencyKey, input.idempotencyKey))).get();
42134
+ if (existing) {
42135
+ if (existing.requestHash !== requestHash2) throw alreadyExists("Research idempotency key", input.idempotencyKey);
42136
+ return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
42137
+ }
42138
+ }
42139
+ const id = crypto33.randomUUID();
42140
+ tx.insert(researchRuns).values({ id, projectId: project.id, status: ResearchRunStatuses.queued, provider: providerName, requestedModel, resolvedModel, location: location ?? null, totalQueries: input.queries.length, idempotencyKey: input.idempotencyKey ?? null, requestHash: input.idempotencyKey ? requestHash2 : null, createdAt: now }).run();
42141
+ for (const [position, query] of input.queries.entries()) tx.insert(researchRunQueries).values({ id: crypto33.randomUUID(), researchRunId: id, position, queryText: query, status: ResearchQueryStatuses.queued, requestedModel, resolvedModel, groundingSources: [], citedDomains: [], searchQueries: [], createdAt: now }).run();
42142
+ writeAuditLog(tx, { projectId: project.id, actor: "api", action: "research.created", entityType: "research_run", entityId: id });
42143
+ return { reused: false, id, shouldDispatch: true };
42144
+ });
42145
+ const result = getDetail(app, project.id, decision.id);
42146
+ if (decision.shouldDispatch) opts.onResearchRunRequested(decision.id, project.id);
42147
+ if (decision.reused) return reply.status(200).send(result);
42148
+ return reply.status(202).send(result);
42149
+ });
42150
+ app.get("/projects/:name/research/runs", async (request) => {
42151
+ const project = resolveProject(app.db, request.params.name);
42152
+ const requested = Number.parseInt(request.query.limit ?? "", 10);
42153
+ const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
42154
+ const runs2 = app.db.select().from(researchRuns).where(eq41(researchRuns.projectId, project.id)).orderBy(desc18(researchRuns.createdAt)).limit(limit).all().map(serializeRun);
42155
+ return { runs: runs2 };
42156
+ });
42157
+ app.get("/projects/:name/research/runs/:runId", async (request) => {
42158
+ const project = resolveProject(app.db, request.params.name);
42159
+ return getDetail(app, project.id, request.params.runId);
42160
+ });
42161
+ }
42162
+ function getDetail(app, projectId, id) {
42163
+ const row = app.db.select().from(researchRuns).where(and32(eq41(researchRuns.id, id), eq41(researchRuns.projectId, projectId))).get();
42164
+ if (!row) throw notFound("Research run", id);
42165
+ const queries2 = app.db.select().from(researchRunQueries).where(eq41(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
42166
+ return { ...serializeRun(row), queries: queries2 };
42167
+ }
42168
+ function serializeRun(row) {
42169
+ return { id: row.id, projectId: row.projectId, status: row.status, provider: row.provider, requestedModel: row.requestedModel, resolvedModel: row.resolvedModel, location: row.location ?? null, totalQueries: row.totalQueries, completedQueries: row.completedQueries, failedQueries: row.failedQueries, error: row.error, startedAt: row.startedAt, finishedAt: row.finishedAt, createdAt: row.createdAt };
42170
+ }
42171
+ function serializeQuery(row) {
42172
+ return { id: row.id, position: row.position, query: row.queryText, status: row.status, requestedModel: row.requestedModel, resolvedModel: row.resolvedModel, servedModel: row.servedModel, answerText: row.answerText, groundingSources: row.groundingSources, citedDomains: row.citedDomains, searchQueries: row.searchQueries, answerMentioned: row.answerMentioned, citationState: row.citationState, error: row.error, startedAt: row.startedAt, finishedAt: row.finishedAt, createdAt: row.createdAt };
42173
+ }
42174
+
41527
42175
  // ../api-routes/src/index.ts
41528
42176
  async function apiRoutes(app, opts) {
41529
42177
  app.decorate("db", opts.db);
@@ -41710,6 +42358,11 @@ async function apiRoutes(app, opts) {
41710
42358
  harvestSearchQueries: opts.harvestSearchQueries,
41711
42359
  embedQueries: opts.embedQueries
41712
42360
  });
42361
+ await api.register(researchRoutes, {
42362
+ providerAdapters: opts.providerAdapters,
42363
+ configuredProviderNames: opts.providerSummary?.filter((provider) => provider.configured).map((provider) => provider.name),
42364
+ onResearchRunRequested: opts.onResearchRunRequested
42365
+ });
41713
42366
  await api.register(technicalAeoRoutes, {
41714
42367
  onSiteAuditRequested: opts.onSiteAuditRequested
41715
42368
  });
@@ -41871,7 +42524,7 @@ function buildTrafficSourceValidators(opts) {
41871
42524
  }
41872
42525
 
41873
42526
  // src/intelligence-service.ts
41874
- import crypto33 from "crypto";
42527
+ import crypto34 from "crypto";
41875
42528
 
41876
42529
  // src/logger.ts
41877
42530
  var IS_TTY = process.stdout.isTTY === true;
@@ -42103,16 +42756,16 @@ var IntelligenceService = class {
42103
42756
  */
42104
42757
  analyzeAndPersist(runId, projectId) {
42105
42758
  const recentRuns = this.db.select().from(runs).where(
42106
- and32(
42107
- eq41(runs.projectId, projectId),
42108
- or8(eq41(runs.status, "completed"), eq41(runs.status, "partial")),
42759
+ and33(
42760
+ eq42(runs.projectId, projectId),
42761
+ or8(eq42(runs.status, "completed"), eq42(runs.status, "partial")),
42109
42762
  // Defensive: RunCoordinator already skips probes before this is
42110
42763
  // called, but if a future call site invokes analyzeAndPersist
42111
42764
  // directly for a probe, probes still must not pollute the
42112
42765
  // intelligence window.
42113
42766
  ne6(runs.trigger, RunTriggers.probe)
42114
42767
  )
42115
- ).orderBy(desc18(runs.finishedAt), desc18(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
42768
+ ).orderBy(desc19(runs.finishedAt), desc19(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
42116
42769
  if (recentRuns.length === 0) {
42117
42770
  log.info("intelligence.skip", { runId, reason: "no completed runs" });
42118
42771
  return null;
@@ -42187,7 +42840,7 @@ var IntelligenceService = class {
42187
42840
  * Returns the persisted insights so the coordinator can count critical/high.
42188
42841
  */
42189
42842
  analyzeAndPersistGbp(runId, projectId) {
42190
- const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq41(runs.id, runId)).get();
42843
+ const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq42(runs.id, runId)).get();
42191
42844
  if (!runRow) {
42192
42845
  log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
42193
42846
  this.persistGbpInsights(runId, projectId, [], []);
@@ -42195,9 +42848,9 @@ var IntelligenceService = class {
42195
42848
  }
42196
42849
  const windowStart = runRow.startedAt ?? runRow.createdAt;
42197
42850
  const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
42198
- const selected = this.db.select().from(gbpLocations).where(and32(
42199
- eq41(gbpLocations.projectId, projectId),
42200
- eq41(gbpLocations.selected, true),
42851
+ const selected = this.db.select().from(gbpLocations).where(and33(
42852
+ eq42(gbpLocations.projectId, projectId),
42853
+ eq42(gbpLocations.selected, true),
42201
42854
  gte10(gbpLocations.syncedAt, windowStart),
42202
42855
  lte6(gbpLocations.syncedAt, windowEnd)
42203
42856
  )).all();
@@ -42232,12 +42885,12 @@ var IntelligenceService = class {
42232
42885
  }
42233
42886
  /** Build the per-location signal bundle the GBP analyzer consumes. */
42234
42887
  buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
42235
- const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and32(eq41(gbpDailyMetrics.projectId, projectId), eq41(gbpDailyMetrics.locationName, locationName))).all();
42236
- const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and32(eq41(gbpPlaceActions.projectId, projectId), eq41(gbpPlaceActions.locationName, locationName))).all();
42237
- const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and32(eq41(gbpLodgingSnapshots.projectId, projectId), eq41(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
42238
- const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and32(eq41(gbpLocations.projectId, projectId), eq41(gbpLocations.locationName, locationName))).get();
42888
+ 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();
42889
+ const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and33(eq42(gbpPlaceActions.projectId, projectId), eq42(gbpPlaceActions.locationName, locationName))).all();
42890
+ 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();
42891
+ const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and33(eq42(gbpLocations.projectId, projectId), eq42(gbpLocations.locationName, locationName))).get();
42239
42892
  const descriptionMissing = !(ownerRow?.description ?? "").trim();
42240
- const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and32(eq41(gbpPlaceDetails.projectId, projectId), eq41(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
42893
+ 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();
42241
42894
  const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
42242
42895
  const summary = buildGbpSummary({
42243
42896
  locationName,
@@ -42273,7 +42926,7 @@ var IntelligenceService = class {
42273
42926
  /** Build the month-over-month keyword series for a location from the
42274
42927
  * accumulating gbp_keyword_monthly table (latest complete month vs prior). */
42275
42928
  buildGbpKeywordTrend(projectId, locationName) {
42276
- const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and32(eq41(gbpKeywordMonthly.projectId, projectId), eq41(gbpKeywordMonthly.locationName, locationName))).all();
42929
+ 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();
42277
42930
  if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
42278
42931
  const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
42279
42932
  const recentMonth = months[0] ?? null;
@@ -42304,7 +42957,7 @@ var IntelligenceService = class {
42304
42957
  */
42305
42958
  persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
42306
42959
  const covered = new Set(coveredLocationNames);
42307
- const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and32(eq41(insights.projectId, projectId), eq41(insights.provider, GBP_INSIGHT_PROVIDER))).all();
42960
+ 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();
42308
42961
  const staleIds = [];
42309
42962
  const dismissedSlots = /* @__PURE__ */ new Set();
42310
42963
  for (const row of existing) {
@@ -42315,7 +42968,7 @@ var IntelligenceService = class {
42315
42968
  }
42316
42969
  this.db.transaction((tx) => {
42317
42970
  for (const id of staleIds) {
42318
- tx.delete(insights).where(eq41(insights.id, id)).run();
42971
+ tx.delete(insights).where(eq42(insights.id, id)).run();
42319
42972
  }
42320
42973
  for (const insight of gbpInsights) {
42321
42974
  const parsed = parseGbpInsightId(insight.id);
@@ -42393,7 +43046,7 @@ var IntelligenceService = class {
42393
43046
  * create per run + aggregate). DB is left untouched.
42394
43047
  */
42395
43048
  backfill(projectName, opts, onProgress) {
42396
- const project = this.db.select().from(projects).where(eq41(projects.name, projectName)).get();
43049
+ const project = this.db.select().from(projects).where(eq42(projects.name, projectName)).get();
42397
43050
  if (!project) {
42398
43051
  throw new Error(`Project "${projectName}" not found`);
42399
43052
  }
@@ -42406,9 +43059,9 @@ var IntelligenceService = class {
42406
43059
  sinceTimestamp = parsed;
42407
43060
  }
42408
43061
  const allRuns = this.db.select().from(runs).where(
42409
- and32(
42410
- eq41(runs.projectId, project.id),
42411
- or8(eq41(runs.status, "completed"), eq41(runs.status, "partial")),
43062
+ and33(
43063
+ eq42(runs.projectId, project.id),
43064
+ or8(eq42(runs.status, "completed"), eq42(runs.status, "partial")),
42412
43065
  // Backfill must not replay probe runs as if they were real sweeps.
42413
43066
  ne6(runs.trigger, RunTriggers.probe)
42414
43067
  )
@@ -42487,7 +43140,7 @@ var IntelligenceService = class {
42487
43140
  return { processed, skipped, totalInsights };
42488
43141
  }
42489
43142
  loadTrackedCompetitors(projectId) {
42490
- return this.db.select({ domain: competitors.domain }).from(competitors).where(eq41(competitors.projectId, projectId)).all().map((r) => r.domain);
43143
+ return this.db.select({ domain: competitors.domain }).from(competitors).where(eq42(competitors.projectId, projectId)).all().map((r) => r.domain);
42491
43144
  }
42492
43145
  /**
42493
43146
  * Wipe transition signals from an analysis result while keeping health.
@@ -42508,15 +43161,15 @@ var IntelligenceService = class {
42508
43161
  }
42509
43162
  persistResult(result, runId, projectId) {
42510
43163
  const previouslyDismissed = /* @__PURE__ */ new Set();
42511
- const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq41(insights.runId, runId)).all();
43164
+ 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();
42512
43165
  for (const row of existingInsights) {
42513
43166
  if (row.dismissed) {
42514
43167
  previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
42515
43168
  }
42516
43169
  }
42517
43170
  this.db.transaction((tx) => {
42518
- tx.delete(insights).where(eq41(insights.runId, runId)).run();
42519
- tx.delete(healthSnapshots).where(eq41(healthSnapshots.runId, runId)).run();
43171
+ tx.delete(insights).where(eq42(insights.runId, runId)).run();
43172
+ tx.delete(healthSnapshots).where(eq42(healthSnapshots.runId, runId)).run();
42520
43173
  const now = (/* @__PURE__ */ new Date()).toISOString();
42521
43174
  for (const insight of result.insights) {
42522
43175
  const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
@@ -42536,7 +43189,7 @@ var IntelligenceService = class {
42536
43189
  }).run();
42537
43190
  }
42538
43191
  tx.insert(healthSnapshots).values({
42539
- id: crypto33.randomUUID(),
43192
+ id: crypto34.randomUUID(),
42540
43193
  projectId,
42541
43194
  runId,
42542
43195
  overallCitedRate: String(result.health.overallCitedRate),
@@ -42569,28 +43222,28 @@ var IntelligenceService = class {
42569
43222
  applySeverityTiering(rawInsights, excludeRunId, projectId) {
42570
43223
  const regressions = rawInsights.filter((i) => i.type === "regression");
42571
43224
  if (regressions.length === 0) return rawInsights;
42572
- const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq41(gscSearchData.projectId, projectId)).all();
43225
+ const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq42(gscSearchData.projectId, projectId)).all();
42573
43226
  const gscConnected = gscRows.length > 0;
42574
43227
  const gscImpressionsByQuery = /* @__PURE__ */ new Map();
42575
43228
  for (const row of gscRows) {
42576
43229
  const key = row.query.toLowerCase();
42577
43230
  gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
42578
43231
  }
42579
- const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq41(projects.id, projectId)).get();
43232
+ const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq42(projects.id, projectId)).get();
42580
43233
  const locationCount = Math.max(
42581
43234
  1,
42582
43235
  (projectRow?.locations ?? []).length
42583
43236
  );
42584
43237
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
42585
43238
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
42586
- and32(
42587
- eq41(runs.projectId, projectId),
42588
- eq41(runs.kind, RunKinds["answer-visibility"]),
42589
- or8(eq41(runs.status, "completed"), eq41(runs.status, "partial")),
43239
+ and33(
43240
+ eq42(runs.projectId, projectId),
43241
+ eq42(runs.kind, RunKinds["answer-visibility"]),
43242
+ or8(eq42(runs.status, "completed"), eq42(runs.status, "partial")),
42590
43243
  // Defensive — see top of file.
42591
43244
  ne6(runs.trigger, RunTriggers.probe)
42592
43245
  )
42593
- ).orderBy(desc18(runs.createdAt), desc18(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
43246
+ ).orderBy(desc19(runs.createdAt), desc19(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
42594
43247
  const recentGroups = groupRunsByCreatedAt(recentRunRows);
42595
43248
  const recentRunIds = [];
42596
43249
  const recentRunIdToCreatedAt = /* @__PURE__ */ new Map();
@@ -42606,7 +43259,7 @@ var IntelligenceService = class {
42606
43259
  const haveHistory = recentRunIds.length > 0;
42607
43260
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
42608
43261
  if (haveHistory) {
42609
- const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and32(eq41(insights.type, "regression"), inArray15(insights.runId, recentRunIds))).all();
43262
+ 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();
42610
43263
  const regressionGroups = /* @__PURE__ */ new Map();
42611
43264
  for (const row of priorRows) {
42612
43265
  if (!row.runId) continue;
@@ -42635,7 +43288,7 @@ var IntelligenceService = class {
42635
43288
  });
42636
43289
  }
42637
43290
  buildRunData(runId, projectId, completedAt, location = null) {
42638
- const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq41(projects.id, projectId)).get();
43291
+ const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq42(projects.id, projectId)).get();
42639
43292
  const projectDomains = projectDomainRow ? effectiveDomains({
42640
43293
  canonicalDomain: projectDomainRow.canonicalDomain,
42641
43294
  ownedDomains: projectDomainRow.ownedDomains
@@ -42652,7 +43305,7 @@ var IntelligenceService = class {
42652
43305
  citedDomains: querySnapshots.citedDomains,
42653
43306
  competitorOverlap: querySnapshots.competitorOverlap,
42654
43307
  snapshotLocation: querySnapshots.location
42655
- }).from(querySnapshots).leftJoin(queries, eq41(querySnapshots.queryId, queries.id)).where(eq41(querySnapshots.runId, runId)).all();
43308
+ }).from(querySnapshots).leftJoin(queries, eq42(querySnapshots.queryId, queries.id)).where(eq42(querySnapshots.runId, runId)).all();
42656
43309
  const snapshots = [];
42657
43310
  let orphanCount = 0;
42658
43311
  for (const r of rows) {
@@ -42728,6 +43381,8 @@ export {
42728
43381
  aiUserFetchEventsHourly,
42729
43382
  rawEventSamples,
42730
43383
  discoverySessions,
43384
+ researchRuns,
43385
+ researchRunQueries,
42731
43386
  gbpLocations,
42732
43387
  gbpDailyMetrics,
42733
43388
  gbpKeywordImpressions,