@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.
- package/README.md +1 -0
- package/assets/assets/{AuditHistoryPanel-wJqs99Ak.js → AuditHistoryPanel-yJXZMO5t.js} +1 -1
- package/assets/assets/BacklinksPage-6VTLbAYx.js +1 -0
- package/assets/assets/{ChartPrimitives-DSr5ldMf.js → ChartPrimitives-D5YGiDLi.js} +1 -1
- package/assets/assets/{HistoryPage-Ca-GGnTI.js → HistoryPage-BxdTM6-R.js} +1 -1
- package/assets/assets/ProjectPage-BJKxZPRO.js +8 -0
- package/assets/assets/{RunRow-O9TbOdQY.js → RunRow-DTrAO7tE.js} +1 -1
- package/assets/assets/{RunsPage-C1Sqd7AN.js → RunsPage-CHU0DQGl.js} +1 -1
- package/assets/assets/{SettingsPage-DPFh62BV.js → SettingsPage-bOIwju6t.js} +1 -1
- package/assets/assets/{TrafficPage-5vKoQyoP.js → TrafficPage-BKvuR482.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-D17oADs8.js → TrafficSourceDetailPage-3-iUTgQg.js} +1 -1
- package/assets/assets/{arrow-left-De5P5AIE.js → arrow-left-BXSfGhr6.js} +1 -1
- package/assets/assets/{extract-error-message-BqMd-fQ6.js → extract-error-message-DEAY62LS.js} +1 -1
- package/assets/assets/index-CfjIYitV.js +210 -0
- package/assets/assets/{index-jymsMBpZ.css → index-JhX-wfQO.css} +1 -1
- package/assets/assets/trash-2-O1wo9IX1.js +1 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-K4FDDCYG.js → chunk-3WBAJKRR.js} +2746 -2626
- package/dist/{chunk-GYAKRVE2.js → chunk-5GQ6FF5R.js} +508 -355
- package/dist/{chunk-WWZE7VDQ.js → chunk-73HPY6XQ.js} +118 -1
- package/dist/{chunk-LPEE5AYY.js → chunk-ZXVWL5HX.js} +956 -301
- package/dist/cli.js +432 -182
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-2QBAGT5X.js → intelligence-service-YJKQQYJQ.js} +2 -2
- package/dist/mcp.js +5 -5
- package/package.json +10 -10
- package/assets/assets/BacklinksPage-BBDExYo_.js +0 -1
- package/assets/assets/ProjectPage-BuaSDSwf.js +0 -7
- package/assets/assets/index-C1ABEgkr.js +0 -210
- 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-
|
|
347
|
+
} from "./chunk-3WBAJKRR.js";
|
|
341
348
|
|
|
342
349
|
// src/intelligence-service.ts
|
|
343
|
-
import { eq as
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
2676
|
-
tx.run(
|
|
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(
|
|
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(
|
|
3133
|
-
db.run(
|
|
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(
|
|
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(
|
|
3149
|
-
db.run(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3887
|
+
tx.run(sql2.raw(`ALTER TABLE ga_ai_referrals ADD COLUMN traffic_class TEXT NOT NULL DEFAULT 'organic'`));
|
|
3740
3888
|
}
|
|
3741
|
-
tx.run(
|
|
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(
|
|
4032
|
-
tx.run(
|
|
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(
|
|
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(
|
|
4051
|
-
tx.run(
|
|
4052
|
-
tx.run(
|
|
4053
|
-
tx.run(
|
|
4054
|
-
tx.run(
|
|
4055
|
-
tx.run(
|
|
4056
|
-
tx.run(
|
|
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(
|
|
4060
|
-
tx.run(
|
|
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(
|
|
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(
|
|
4081
|
-
tx.run(
|
|
4082
|
-
tx.run(
|
|
4083
|
-
tx.run(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
4372
|
+
db.run(sql2.raw(`DROP TABLE queries`));
|
|
4141
4373
|
}
|
|
4142
|
-
db.run(
|
|
4143
|
-
db.run(
|
|
4144
|
-
db.run(
|
|
4145
|
-
db.run(
|
|
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(
|
|
4379
|
+
db.run(sql2.raw(`ALTER TABLE keywords RENAME TO queries`));
|
|
4148
4380
|
}
|
|
4149
4381
|
if (columnExists(db, "queries", "keyword")) {
|
|
4150
|
-
db.run(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
6924
|
-
const competitorCount = app.db.select({ n:
|
|
6925
|
-
const runCount = app.db.select({ n:
|
|
6926
|
-
const snapshotCount = app.db.select({ n:
|
|
6927
|
-
const insightCount = app.db.select({ n:
|
|
6928
|
-
const auditLogCount = app.db.select({ n:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
10686
|
-
|
|
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
|
-
|
|
10716
|
-
|
|
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:
|
|
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
|
-
|
|
13323
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
13460
|
-
paid:
|
|
13461
|
-
organic:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
13568
|
-
operators:
|
|
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(
|
|
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:
|
|
13585
|
-
landingPaths:
|
|
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(
|
|
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:
|
|
13602
|
-
products:
|
|
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(
|
|
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:
|
|
13618
|
-
hits:
|
|
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(
|
|
13866
|
+
).groupBy(sql8`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
|
|
13627
13867
|
const referralTrendRows = db.select({
|
|
13628
|
-
date:
|
|
13629
|
-
hits:
|
|
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(
|
|
13877
|
+
).groupBy(sql8`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
|
|
13638
13878
|
const userFetchTrendRows = db.select({
|
|
13639
|
-
date:
|
|
13640
|
-
hits:
|
|
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(
|
|
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
|
|
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
|
-
|
|
15677
|
-
|
|
15678
|
-
|
|
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
|
-
|
|
15690
|
-
|
|
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:
|
|
15897
|
-
clicks:
|
|
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:
|
|
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
|
-
|
|
15905
|
-
|
|
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
|
|
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(
|
|
23730
|
-
else if (cutoffDate) conditions.push(
|
|
23731
|
-
if (endDate) conditions.push(
|
|
23732
|
-
if (query) conditions.push(
|
|
23733
|
-
if (page) conditions.push(
|
|
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(
|
|
23758
|
-
else if (cutoffDate) conditions.push(
|
|
23759
|
-
if (endDate) conditions.push(
|
|
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:
|
|
23763
|
-
impressions:
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
-
|
|
30092
|
-
|
|
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
|
-
|
|
30115
|
-
|
|
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
|
-
|
|
30135
|
-
|
|
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
|
-
|
|
30167
|
-
|
|
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
|
-
|
|
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(
|
|
30831
|
+
if (cutoffDate) snapshotConditions.push(sql13`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
30258
30832
|
const aiConditions = [eq29(gaAiReferrals.projectId, project.id)];
|
|
30259
|
-
if (cutoffDate) aiConditions.push(
|
|
30833
|
+
if (cutoffDate) aiConditions.push(sql13`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
30260
30834
|
const socialConditions = [eq29(gaSocialReferrals.projectId, project.id)];
|
|
30261
|
-
if (cutoffDate) socialConditions.push(
|
|
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:
|
|
30275
|
-
totalOrganicSessions:
|
|
30276
|
-
totalUsers:
|
|
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:
|
|
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:
|
|
30292
|
-
sessions:
|
|
30293
|
-
organicSessions:
|
|
30294
|
-
directSessions:
|
|
30295
|
-
users:
|
|
30296
|
-
}).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).groupBy(
|
|
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:
|
|
30303
|
-
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:
|
|
30311
|
-
sessions:
|
|
30312
|
-
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
|
-
|
|
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:
|
|
30344
|
-
users:
|
|
30345
|
-
}).from(gaSocialReferrals).where(and23(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(
|
|
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:
|
|
30348
|
-
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(
|
|
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:
|
|
31029
|
+
landingPage: sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
30456
31030
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
30457
|
-
sessions:
|
|
30458
|
-
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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
30498
|
-
|
|
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:
|
|
31081
|
+
sessions: sql13`SUM(${gaSocialReferrals.sessions})`
|
|
30508
31082
|
}).from(gaSocialReferrals).where(and23(
|
|
30509
31083
|
eq29(gaSocialReferrals.projectId, project.id),
|
|
30510
|
-
|
|
30511
|
-
|
|
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:
|
|
31089
|
+
sessions: sql13`SUM(${gaSocialReferrals.sessions})`
|
|
30516
31090
|
}).from(gaSocialReferrals).where(and23(
|
|
30517
31091
|
eq29(gaSocialReferrals.projectId, project.id),
|
|
30518
|
-
|
|
30519
|
-
|
|
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:
|
|
30559
|
-
const sumOrganic = (from, to) => app.db.select({ sessions:
|
|
30560
|
-
const sumDirect = (from, to) => app.db.select({ sessions:
|
|
30561
|
-
const sumAi = (from, to) => app.db.select({ sessions:
|
|
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
|
-
|
|
30564
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
30579
|
-
|
|
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:
|
|
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
|
-
|
|
30585
|
-
|
|
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:
|
|
30603
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions:
|
|
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(
|
|
31193
|
+
if (cutoffDate) conditions.push(sql13`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
30620
31194
|
const rows = app.db.select({
|
|
30621
31195
|
date: gaTrafficSnapshots.date,
|
|
30622
|
-
sessions:
|
|
30623
|
-
organicSessions:
|
|
30624
|
-
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(
|
|
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:
|
|
30644
|
-
sessions:
|
|
30645
|
-
organicSessions:
|
|
30646
|
-
users:
|
|
30647
|
-
}).from(gaTrafficSnapshots).where(eq29(gaTrafficSnapshots.projectId, project.id)).groupBy(
|
|
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
|
|
31756
|
-
if (!
|
|
31757
|
-
throw validationError(`${field} ${
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
32812
|
-
total:
|
|
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:
|
|
32816
|
-
total:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
37172
|
-
if (!
|
|
37173
|
-
throw validationError(`WordPress baseUrl rejected: ${
|
|
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 } =
|
|
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:
|
|
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:
|
|
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:
|
|
37885
|
-
paidSessionsOrHits:
|
|
37886
|
-
organicSessionsOrHits:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
38363
|
-
rows:
|
|
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 ?
|
|
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:
|
|
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:
|
|
38404
|
-
rows:
|
|
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 ?
|
|
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:
|
|
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:
|
|
38441
|
-
paid:
|
|
38442
|
-
organic:
|
|
38443
|
-
rows:
|
|
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 ?
|
|
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:
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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((
|
|
40588
|
-
if (
|
|
40589
|
-
return matchesCheckId(
|
|
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
|
|
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
|
-
|
|
42107
|
-
|
|
42108
|
-
or8(
|
|
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(
|
|
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(
|
|
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(
|
|
42199
|
-
|
|
42200
|
-
|
|
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(
|
|
42236
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
42237
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
42238
|
-
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
42410
|
-
|
|
42411
|
-
or8(
|
|
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(
|
|
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(
|
|
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(
|
|
42519
|
-
tx.delete(healthSnapshots).where(
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
42587
|
-
|
|
42588
|
-
|
|
42589
|
-
or8(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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,
|