@canonry/canonry 4.129.1 → 4.130.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-Ydzb2Y_Y.js} +1 -1
- package/assets/assets/BacklinksPage-DYodEusK.js +1 -0
- package/assets/assets/{ChartPrimitives-DSr5ldMf.js → ChartPrimitives-D67hsnF7.js} +1 -1
- package/assets/assets/{HistoryPage-Ca-GGnTI.js → HistoryPage-_h9ITL1w.js} +1 -1
- package/assets/assets/ProjectPage-fCJKCcHE.js +8 -0
- package/assets/assets/{RunRow-O9TbOdQY.js → RunRow-DzUNyTE-.js} +1 -1
- package/assets/assets/{RunsPage-C1Sqd7AN.js → RunsPage-FTvAjptk.js} +1 -1
- package/assets/assets/{SettingsPage-DPFh62BV.js → SettingsPage-CvgpYEAt.js} +1 -1
- package/assets/assets/{TrafficPage-5vKoQyoP.js → TrafficPage-qaMW9k6o.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-D17oADs8.js → TrafficSourceDetailPage-CRCso7_A.js} +1 -1
- package/assets/assets/{arrow-left-De5P5AIE.js → arrow-left-C--lHdP2.js} +1 -1
- package/assets/assets/{extract-error-message-BqMd-fQ6.js → extract-error-message-DVqYbKpB.js} +1 -1
- package/assets/assets/index-CdXn0JEf.js +210 -0
- package/assets/assets/{index-jymsMBpZ.css → index-JhX-wfQO.css} +1 -1
- package/assets/assets/trash-2-CyKRU3tR.js +1 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-LPEE5AYY.js → chunk-5XU6WEMK.js} +649 -298
- package/dist/{chunk-K4FDDCYG.js → chunk-NVRJZU6V.js} +2742 -2626
- package/dist/{chunk-GYAKRVE2.js → chunk-QR5U2K3O.js} +508 -355
- package/dist/{chunk-WWZE7VDQ.js → chunk-WIALMCXM.js} +118 -1
- package/dist/cli.js +432 -182
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-2QBAGT5X.js → intelligence-service-53GUE6EI.js} +2 -2
- package/dist/mcp.js +5 -5
- package/package.json +8 -8
- 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-NVRJZU6V.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) {
|
|
@@ -23726,11 +23996,11 @@ async function googleRoutes(app, opts) {
|
|
|
23726
23996
|
const { startDate, endDate, query, page, limit, offset } = request.query;
|
|
23727
23997
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
23728
23998
|
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(
|
|
23999
|
+
if (startDate) conditions.push(sql10`${gscSearchData.date} >= ${startDate}`);
|
|
24000
|
+
else if (cutoffDate) conditions.push(sql10`${gscSearchData.date} >= ${cutoffDate}`);
|
|
24001
|
+
if (endDate) conditions.push(sql10`${gscSearchData.date} <= ${endDate}`);
|
|
24002
|
+
if (query) conditions.push(sql10`${gscSearchData.query} LIKE ${"%" + escapeLikePattern(query) + "%"} ESCAPE '\\'`);
|
|
24003
|
+
if (page) conditions.push(sql10`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
|
|
23734
24004
|
const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
|
|
23735
24005
|
const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
|
|
23736
24006
|
const rows = app.db.select().from(gscSearchData).where(and18(...conditions)).orderBy(desc11(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
|
|
@@ -23754,13 +24024,13 @@ async function googleRoutes(app, opts) {
|
|
|
23754
24024
|
const windowEnd = endDate ?? "9999-12-31";
|
|
23755
24025
|
const dailyTotals = readGscDailyTotals(app.db, project.id, windowStart, windowEnd);
|
|
23756
24026
|
const conditions = [eq24(gscSearchData.projectId, project.id)];
|
|
23757
|
-
if (startDate) conditions.push(
|
|
23758
|
-
else if (cutoffDate) conditions.push(
|
|
23759
|
-
if (endDate) conditions.push(
|
|
24027
|
+
if (startDate) conditions.push(sql10`${gscSearchData.date} >= ${startDate}`);
|
|
24028
|
+
else if (cutoffDate) conditions.push(sql10`${gscSearchData.date} >= ${cutoffDate}`);
|
|
24029
|
+
if (endDate) conditions.push(sql10`${gscSearchData.date} <= ${endDate}`);
|
|
23760
24030
|
const dimensionedRows = app.db.select({
|
|
23761
24031
|
date: gscSearchData.date,
|
|
23762
|
-
clicks:
|
|
23763
|
-
impressions:
|
|
24032
|
+
clicks: sql10`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
|
|
24033
|
+
impressions: sql10`COALESCE(SUM(${gscSearchData.impressions}), 0)`
|
|
23764
24034
|
}).from(gscSearchData).where(and18(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
|
|
23765
24035
|
const daily = mergeGscDailyTotalsWithFallback(
|
|
23766
24036
|
dailyTotals,
|
|
@@ -24656,11 +24926,11 @@ async function googleRoutes(app, opts) {
|
|
|
24656
24926
|
|
|
24657
24927
|
// ../api-routes/src/ads.ts
|
|
24658
24928
|
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
|
|
24929
|
+
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
24930
|
|
|
24661
24931
|
// ../api-routes/src/ads-activation-routes.ts
|
|
24662
24932
|
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
|
|
24933
|
+
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
24934
|
|
|
24665
24935
|
// ../api-routes/src/ads-activation.ts
|
|
24666
24936
|
import crypto20 from "crypto";
|
|
@@ -26533,7 +26803,7 @@ function selectWatchdogOperations(app, nowIso, batchSize, allowLiveRevocationLea
|
|
|
26533
26803
|
),
|
|
26534
26804
|
leaseAvailable
|
|
26535
26805
|
);
|
|
26536
|
-
const freshRevocationPriority =
|
|
26806
|
+
const freshRevocationPriority = sql11`CASE
|
|
26537
26807
|
WHEN ${adsActivationGrants.revocationRequestedAt} IS NOT NULL
|
|
26538
26808
|
AND (
|
|
26539
26809
|
${adsOperations.lastReconciledAt} IS NULL
|
|
@@ -27629,7 +27899,7 @@ function claimOperationForReconciliation(app, row, leaseOwner, now, leaseMs, pol
|
|
|
27629
27899
|
state: AdsOperationStates.reconciling,
|
|
27630
27900
|
leaseOwner,
|
|
27631
27901
|
leaseExpiresAt,
|
|
27632
|
-
reconcileAttempts:
|
|
27902
|
+
reconcileAttempts: sql12`${adsOperations.reconcileAttempts} + 1`,
|
|
27633
27903
|
updatedAt: nowIso
|
|
27634
27904
|
}).where(and20(
|
|
27635
27905
|
eq26(adsOperations.id, row.id),
|
|
@@ -29701,7 +29971,7 @@ async function cdpRoutes(app, opts) {
|
|
|
29701
29971
|
|
|
29702
29972
|
// ../api-routes/src/ga.ts
|
|
29703
29973
|
import crypto24 from "crypto";
|
|
29704
|
-
import { eq as eq29, desc as desc13, and as and23, sql as
|
|
29974
|
+
import { eq as eq29, desc as desc13, and as and23, sql as sql13 } from "drizzle-orm";
|
|
29705
29975
|
|
|
29706
29976
|
// ../api-routes/src/ga-session-history.ts
|
|
29707
29977
|
function buildSessionHistory(landingPageDays, dailyTotals) {
|
|
@@ -30088,8 +30358,8 @@ async function ga4Routes(app, opts) {
|
|
|
30088
30358
|
tx.delete(gaTrafficSnapshots).where(
|
|
30089
30359
|
and23(
|
|
30090
30360
|
eq29(gaTrafficSnapshots.projectId, project.id),
|
|
30091
|
-
|
|
30092
|
-
|
|
30361
|
+
sql13`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
30362
|
+
sql13`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
30093
30363
|
)
|
|
30094
30364
|
).run();
|
|
30095
30365
|
for (const row of rows) {
|
|
@@ -30111,8 +30381,8 @@ async function ga4Routes(app, opts) {
|
|
|
30111
30381
|
tx.delete(gaDailyTotals).where(
|
|
30112
30382
|
and23(
|
|
30113
30383
|
eq29(gaDailyTotals.projectId, project.id),
|
|
30114
|
-
|
|
30115
|
-
|
|
30384
|
+
sql13`${gaDailyTotals.date} >= ${summary.periodStart}`,
|
|
30385
|
+
sql13`${gaDailyTotals.date} <= ${summary.periodEnd}`
|
|
30116
30386
|
)
|
|
30117
30387
|
).run();
|
|
30118
30388
|
for (const row of dailyTotals) {
|
|
@@ -30131,8 +30401,8 @@ async function ga4Routes(app, opts) {
|
|
|
30131
30401
|
tx.delete(gaAiReferrals).where(
|
|
30132
30402
|
and23(
|
|
30133
30403
|
eq29(gaAiReferrals.projectId, project.id),
|
|
30134
|
-
|
|
30135
|
-
|
|
30404
|
+
sql13`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
30405
|
+
sql13`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
30136
30406
|
)
|
|
30137
30407
|
).run();
|
|
30138
30408
|
for (const row of aiReferrals) {
|
|
@@ -30163,8 +30433,8 @@ async function ga4Routes(app, opts) {
|
|
|
30163
30433
|
tx.delete(gaSocialReferrals).where(
|
|
30164
30434
|
and23(
|
|
30165
30435
|
eq29(gaSocialReferrals.projectId, project.id),
|
|
30166
|
-
|
|
30167
|
-
|
|
30436
|
+
sql13`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
30437
|
+
sql13`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
30168
30438
|
)
|
|
30169
30439
|
).run();
|
|
30170
30440
|
for (const row of socialReferrals) {
|
|
@@ -30254,11 +30524,11 @@ async function ga4Routes(app, opts) {
|
|
|
30254
30524
|
const cutoff = windowCutoff(window);
|
|
30255
30525
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
30256
30526
|
const snapshotConditions = [eq29(gaTrafficSnapshots.projectId, project.id)];
|
|
30257
|
-
if (cutoffDate) snapshotConditions.push(
|
|
30527
|
+
if (cutoffDate) snapshotConditions.push(sql13`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
30258
30528
|
const aiConditions = [eq29(gaAiReferrals.projectId, project.id)];
|
|
30259
|
-
if (cutoffDate) aiConditions.push(
|
|
30529
|
+
if (cutoffDate) aiConditions.push(sql13`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
30260
30530
|
const socialConditions = [eq29(gaSocialReferrals.projectId, project.id)];
|
|
30261
|
-
if (cutoffDate) socialConditions.push(
|
|
30531
|
+
if (cutoffDate) socialConditions.push(sql13`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
30262
30532
|
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
30263
30533
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
30264
30534
|
totalOrganicSessions: gaTrafficWindowSummaries.totalOrganicSessions,
|
|
@@ -30271,9 +30541,9 @@ async function ga4Routes(app, opts) {
|
|
|
30271
30541
|
)
|
|
30272
30542
|
).get() : null;
|
|
30273
30543
|
const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
|
|
30274
|
-
totalSessions:
|
|
30275
|
-
totalOrganicSessions:
|
|
30276
|
-
totalUsers:
|
|
30544
|
+
totalSessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
30545
|
+
totalOrganicSessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
30546
|
+
totalUsers: sql13`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
30277
30547
|
}).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).get() : null;
|
|
30278
30548
|
const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
30279
30549
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
@@ -30281,41 +30551,41 @@ async function ga4Routes(app, opts) {
|
|
|
30281
30551
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
30282
30552
|
}).from(gaTrafficSummaries).where(eq29(gaTrafficSummaries.projectId, project.id)).get();
|
|
30283
30553
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
30284
|
-
totalDirectSessions:
|
|
30554
|
+
totalDirectSessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
30285
30555
|
}).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).get();
|
|
30286
30556
|
const summaryMeta = app.db.select({
|
|
30287
30557
|
periodStart: gaTrafficSummaries.periodStart,
|
|
30288
30558
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
30289
30559
|
}).from(gaTrafficSummaries).where(eq29(gaTrafficSummaries.projectId, project.id)).get();
|
|
30290
30560
|
const rows = app.db.select({
|
|
30291
|
-
landingPage:
|
|
30292
|
-
sessions:
|
|
30293
|
-
organicSessions:
|
|
30294
|
-
directSessions:
|
|
30295
|
-
users:
|
|
30296
|
-
}).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).groupBy(
|
|
30561
|
+
landingPage: sql13`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
30562
|
+
sessions: sql13`SUM(${gaTrafficSnapshots.sessions})`,
|
|
30563
|
+
organicSessions: sql13`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
30564
|
+
directSessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
|
|
30565
|
+
users: sql13`SUM(${gaTrafficSnapshots.users})`
|
|
30566
|
+
}).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).groupBy(sql13`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql13`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
30297
30567
|
const aiReferralRows = app.db.select({
|
|
30298
30568
|
source: gaAiReferrals.source,
|
|
30299
30569
|
medium: gaAiReferrals.medium,
|
|
30300
30570
|
trafficClass: gaAiReferrals.trafficClass,
|
|
30301
30571
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
30302
|
-
sessions:
|
|
30303
|
-
users:
|
|
30572
|
+
sessions: sql13`SUM(${gaAiReferrals.sessions})`,
|
|
30573
|
+
users: sql13`SUM(${gaAiReferrals.users})`
|
|
30304
30574
|
}).from(gaAiReferrals).where(and23(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.trafficClass, gaAiReferrals.sourceDimension).all();
|
|
30305
30575
|
const aiReferralLandingPageRows = app.db.select({
|
|
30306
30576
|
source: gaAiReferrals.source,
|
|
30307
30577
|
medium: gaAiReferrals.medium,
|
|
30308
30578
|
trafficClass: gaAiReferrals.trafficClass,
|
|
30309
30579
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
30310
|
-
landingPage:
|
|
30311
|
-
sessions:
|
|
30312
|
-
users:
|
|
30580
|
+
landingPage: sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
30581
|
+
sessions: sql13`SUM(${gaAiReferrals.sessions})`,
|
|
30582
|
+
users: sql13`SUM(${gaAiReferrals.users})`
|
|
30313
30583
|
}).from(gaAiReferrals).where(and23(...aiConditions)).groupBy(
|
|
30314
30584
|
gaAiReferrals.source,
|
|
30315
30585
|
gaAiReferrals.medium,
|
|
30316
30586
|
gaAiReferrals.trafficClass,
|
|
30317
30587
|
gaAiReferrals.sourceDimension,
|
|
30318
|
-
|
|
30588
|
+
sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
|
|
30319
30589
|
).all();
|
|
30320
30590
|
const aiRowsForTotals = app.db.select({
|
|
30321
30591
|
date: gaAiReferrals.date,
|
|
@@ -30340,12 +30610,12 @@ async function ga4Routes(app, opts) {
|
|
|
30340
30610
|
source: gaSocialReferrals.source,
|
|
30341
30611
|
medium: gaSocialReferrals.medium,
|
|
30342
30612
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
30343
|
-
sessions:
|
|
30344
|
-
users:
|
|
30345
|
-
}).from(gaSocialReferrals).where(and23(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(
|
|
30613
|
+
sessions: sql13`SUM(${gaSocialReferrals.sessions})`,
|
|
30614
|
+
users: sql13`SUM(${gaSocialReferrals.users})`
|
|
30615
|
+
}).from(gaSocialReferrals).where(and23(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql13`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
30346
30616
|
const socialTotals = app.db.select({
|
|
30347
|
-
sessions:
|
|
30348
|
-
users:
|
|
30617
|
+
sessions: sql13`SUM(${gaSocialReferrals.sessions})`,
|
|
30618
|
+
users: sql13`SUM(${gaSocialReferrals.users})`
|
|
30349
30619
|
}).from(gaSocialReferrals).where(and23(...socialConditions)).get();
|
|
30350
30620
|
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq29(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
30351
30621
|
const total = summaryRow?.totalSessions ?? 0;
|
|
@@ -30446,23 +30716,23 @@ async function ga4Routes(app, opts) {
|
|
|
30446
30716
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
30447
30717
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
30448
30718
|
const conditions = [eq29(gaAiReferrals.projectId, project.id)];
|
|
30449
|
-
if (cutoffDate) conditions.push(
|
|
30719
|
+
if (cutoffDate) conditions.push(sql13`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
30450
30720
|
const rows = app.db.select({
|
|
30451
30721
|
date: gaAiReferrals.date,
|
|
30452
30722
|
source: gaAiReferrals.source,
|
|
30453
30723
|
medium: gaAiReferrals.medium,
|
|
30454
30724
|
trafficClass: gaAiReferrals.trafficClass,
|
|
30455
|
-
landingPage:
|
|
30725
|
+
landingPage: sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
30456
30726
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
30457
|
-
sessions:
|
|
30458
|
-
users:
|
|
30727
|
+
sessions: sql13`SUM(${gaAiReferrals.sessions})`,
|
|
30728
|
+
users: sql13`SUM(${gaAiReferrals.users})`
|
|
30459
30729
|
}).from(gaAiReferrals).where(and23(...conditions)).groupBy(
|
|
30460
30730
|
gaAiReferrals.date,
|
|
30461
30731
|
gaAiReferrals.source,
|
|
30462
30732
|
gaAiReferrals.medium,
|
|
30463
30733
|
gaAiReferrals.trafficClass,
|
|
30464
30734
|
gaAiReferrals.sourceDimension,
|
|
30465
|
-
|
|
30735
|
+
sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
|
|
30466
30736
|
).orderBy(gaAiReferrals.date).all();
|
|
30467
30737
|
return rows;
|
|
30468
30738
|
});
|
|
@@ -30471,7 +30741,7 @@ async function ga4Routes(app, opts) {
|
|
|
30471
30741
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
30472
30742
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
30473
30743
|
const conditions = [eq29(gaSocialReferrals.projectId, project.id)];
|
|
30474
|
-
if (cutoffDate) conditions.push(
|
|
30744
|
+
if (cutoffDate) conditions.push(sql13`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
30475
30745
|
const rows = app.db.select({
|
|
30476
30746
|
date: gaSocialReferrals.date,
|
|
30477
30747
|
source: gaSocialReferrals.source,
|
|
@@ -30492,10 +30762,10 @@ async function ga4Routes(app, opts) {
|
|
|
30492
30762
|
d.setDate(d.getDate() - n);
|
|
30493
30763
|
return fmt(d);
|
|
30494
30764
|
};
|
|
30495
|
-
const sumSocial = (from, to) => app.db.select({ sessions:
|
|
30765
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and23(
|
|
30496
30766
|
eq29(gaSocialReferrals.projectId, project.id),
|
|
30497
|
-
|
|
30498
|
-
|
|
30767
|
+
sql13`${gaSocialReferrals.date} >= ${from}`,
|
|
30768
|
+
sql13`${gaSocialReferrals.date} < ${to}`
|
|
30499
30769
|
)).get();
|
|
30500
30770
|
const current7d = sumSocial(daysAgo(7), fmt(today));
|
|
30501
30771
|
const prev7d = sumSocial(daysAgo(14), daysAgo(7));
|
|
@@ -30504,19 +30774,19 @@ async function ga4Routes(app, opts) {
|
|
|
30504
30774
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
30505
30775
|
const sourceCurrent = app.db.select({
|
|
30506
30776
|
source: gaSocialReferrals.source,
|
|
30507
|
-
sessions:
|
|
30777
|
+
sessions: sql13`SUM(${gaSocialReferrals.sessions})`
|
|
30508
30778
|
}).from(gaSocialReferrals).where(and23(
|
|
30509
30779
|
eq29(gaSocialReferrals.projectId, project.id),
|
|
30510
|
-
|
|
30511
|
-
|
|
30780
|
+
sql13`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
|
|
30781
|
+
sql13`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
30512
30782
|
)).groupBy(gaSocialReferrals.source).all();
|
|
30513
30783
|
const sourcePrev = app.db.select({
|
|
30514
30784
|
source: gaSocialReferrals.source,
|
|
30515
|
-
sessions:
|
|
30785
|
+
sessions: sql13`SUM(${gaSocialReferrals.sessions})`
|
|
30516
30786
|
}).from(gaSocialReferrals).where(and23(
|
|
30517
30787
|
eq29(gaSocialReferrals.projectId, project.id),
|
|
30518
|
-
|
|
30519
|
-
|
|
30788
|
+
sql13`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
|
|
30789
|
+
sql13`${gaSocialReferrals.date} < ${daysAgo(7)}`
|
|
30520
30790
|
)).groupBy(gaSocialReferrals.source).all();
|
|
30521
30791
|
const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
|
|
30522
30792
|
let biggestMover = null;
|
|
@@ -30555,16 +30825,16 @@ async function ga4Routes(app, opts) {
|
|
|
30555
30825
|
return fmt(d);
|
|
30556
30826
|
};
|
|
30557
30827
|
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:
|
|
30828
|
+
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();
|
|
30829
|
+
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();
|
|
30830
|
+
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();
|
|
30831
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
|
|
30562
30832
|
eq29(gaAiReferrals.projectId, project.id),
|
|
30563
|
-
|
|
30564
|
-
|
|
30833
|
+
sql13`${gaAiReferrals.date} >= ${from}`,
|
|
30834
|
+
sql13`${gaAiReferrals.date} < ${to}`,
|
|
30565
30835
|
eq29(gaAiReferrals.sourceDimension, "session")
|
|
30566
30836
|
)).get();
|
|
30567
|
-
const sumSocial = (from, to) => app.db.select({ sessions:
|
|
30837
|
+
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
30838
|
const todayStr = fmt(today);
|
|
30569
30839
|
const buildTrend = (sum) => {
|
|
30570
30840
|
const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
|
|
@@ -30573,16 +30843,16 @@ async function ga4Routes(app, opts) {
|
|
|
30573
30843
|
const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
|
|
30574
30844
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
30575
30845
|
};
|
|
30576
|
-
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions:
|
|
30846
|
+
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql13`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
|
|
30577
30847
|
eq29(gaAiReferrals.projectId, project.id),
|
|
30578
|
-
|
|
30579
|
-
|
|
30848
|
+
sql13`${gaAiReferrals.date} >= ${daysAgo(7)}`,
|
|
30849
|
+
sql13`${gaAiReferrals.date} < ${todayStr}`,
|
|
30580
30850
|
eq29(gaAiReferrals.sourceDimension, "session")
|
|
30581
30851
|
)).groupBy(gaAiReferrals.source).all();
|
|
30582
|
-
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions:
|
|
30852
|
+
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql13`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
|
|
30583
30853
|
eq29(gaAiReferrals.projectId, project.id),
|
|
30584
|
-
|
|
30585
|
-
|
|
30854
|
+
sql13`${gaAiReferrals.date} >= ${daysAgo(14)}`,
|
|
30855
|
+
sql13`${gaAiReferrals.date} < ${daysAgo(7)}`,
|
|
30586
30856
|
eq29(gaAiReferrals.sourceDimension, "session")
|
|
30587
30857
|
)).groupBy(gaAiReferrals.source).all();
|
|
30588
30858
|
const findBiggestMover = (current, prev) => {
|
|
@@ -30599,8 +30869,8 @@ async function ga4Routes(app, opts) {
|
|
|
30599
30869
|
}
|
|
30600
30870
|
return mover;
|
|
30601
30871
|
};
|
|
30602
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions:
|
|
30603
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions:
|
|
30872
|
+
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();
|
|
30873
|
+
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
30874
|
return {
|
|
30605
30875
|
total: buildTrend(sumTotal),
|
|
30606
30876
|
organic: buildTrend(sumOrganic),
|
|
@@ -30616,15 +30886,15 @@ async function ga4Routes(app, opts) {
|
|
|
30616
30886
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
30617
30887
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
30618
30888
|
const conditions = [eq29(gaTrafficSnapshots.projectId, project.id)];
|
|
30619
|
-
if (cutoffDate) conditions.push(
|
|
30889
|
+
if (cutoffDate) conditions.push(sql13`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
30620
30890
|
const rows = app.db.select({
|
|
30621
30891
|
date: gaTrafficSnapshots.date,
|
|
30622
|
-
sessions:
|
|
30623
|
-
organicSessions:
|
|
30624
|
-
users:
|
|
30892
|
+
sessions: sql13`SUM(${gaTrafficSnapshots.sessions})`,
|
|
30893
|
+
organicSessions: sql13`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
30894
|
+
users: sql13`SUM(${gaTrafficSnapshots.users})`
|
|
30625
30895
|
}).from(gaTrafficSnapshots).where(and23(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
30626
30896
|
const totalConditions = [eq29(gaDailyTotals.projectId, project.id)];
|
|
30627
|
-
if (cutoffDate) totalConditions.push(
|
|
30897
|
+
if (cutoffDate) totalConditions.push(sql13`${gaDailyTotals.date} >= ${cutoffDate}`);
|
|
30628
30898
|
const totals = app.db.select({ date: gaDailyTotals.date, users: gaDailyTotals.users }).from(gaDailyTotals).where(and23(...totalConditions)).all();
|
|
30629
30899
|
return buildSessionHistory(
|
|
30630
30900
|
rows.map((r) => ({
|
|
@@ -30640,11 +30910,11 @@ async function ga4Routes(app, opts) {
|
|
|
30640
30910
|
const project = resolveProject(app.db, request.params.name);
|
|
30641
30911
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
30642
30912
|
const trafficPages = app.db.select({
|
|
30643
|
-
landingPage:
|
|
30644
|
-
sessions:
|
|
30645
|
-
organicSessions:
|
|
30646
|
-
users:
|
|
30647
|
-
}).from(gaTrafficSnapshots).where(eq29(gaTrafficSnapshots.projectId, project.id)).groupBy(
|
|
30913
|
+
landingPage: sql13`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
30914
|
+
sessions: sql13`SUM(${gaTrafficSnapshots.sessions})`,
|
|
30915
|
+
organicSessions: sql13`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
30916
|
+
users: sql13`SUM(${gaTrafficSnapshots.users})`
|
|
30917
|
+
}).from(gaTrafficSnapshots).where(eq29(gaTrafficSnapshots.projectId, project.id)).groupBy(sql13`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql13`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
30648
30918
|
return {
|
|
30649
30919
|
pages: trafficPages.map((r) => ({
|
|
30650
30920
|
landingPage: r.landingPage,
|
|
@@ -31752,9 +32022,9 @@ async function wordpressRoutes(app, opts) {
|
|
|
31752
32022
|
throw validationError("WordPress connection storage is not configured for this deployment");
|
|
31753
32023
|
}
|
|
31754
32024
|
async function assertWordpressUrlAllowed(rawUrl, field) {
|
|
31755
|
-
const
|
|
31756
|
-
if (!
|
|
31757
|
-
throw validationError(`${field} ${
|
|
32025
|
+
const check2 = await resolveWebhookTarget(rawUrl, { allowLoopback });
|
|
32026
|
+
if (!check2.ok) {
|
|
32027
|
+
throw validationError(`${field} ${check2.message.replace(/^"url" /, "")}`);
|
|
31758
32028
|
}
|
|
31759
32029
|
}
|
|
31760
32030
|
function requireConnection(store, projectName) {
|
|
@@ -32292,7 +32562,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
32292
32562
|
|
|
32293
32563
|
// ../api-routes/src/backlinks.ts
|
|
32294
32564
|
import crypto26 from "crypto";
|
|
32295
|
-
import { and as and25, asc as asc7, desc as desc14, eq as eq30, sql as
|
|
32565
|
+
import { and as and25, asc as asc7, desc as desc14, eq as eq30, sql as sql14 } from "drizzle-orm";
|
|
32296
32566
|
|
|
32297
32567
|
// ../integration-commoncrawl/src/constants.ts
|
|
32298
32568
|
import os2 from "os";
|
|
@@ -32582,7 +32852,7 @@ async function queryBacklinks(opts) {
|
|
|
32582
32852
|
const reversed = opts.targets.map(reverseDomain);
|
|
32583
32853
|
const targetList = reversed.map(quote).join(", ");
|
|
32584
32854
|
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
|
|
32855
|
+
const sql17 = `
|
|
32586
32856
|
WITH vertices AS (
|
|
32587
32857
|
SELECT * FROM read_csv(
|
|
32588
32858
|
${quote(opts.vertexPath)},
|
|
@@ -32618,7 +32888,7 @@ async function queryBacklinks(opts) {
|
|
|
32618
32888
|
const conn = await instance.connect();
|
|
32619
32889
|
let rows;
|
|
32620
32890
|
try {
|
|
32621
|
-
const reader = await conn.runAndReadAll(
|
|
32891
|
+
const reader = await conn.runAndReadAll(sql17);
|
|
32622
32892
|
rows = reader.getRowObjects();
|
|
32623
32893
|
} finally {
|
|
32624
32894
|
conn.disconnectSync?.();
|
|
@@ -32808,12 +33078,12 @@ function computeFilteredSummary(db, base) {
|
|
|
32808
33078
|
);
|
|
32809
33079
|
const filteredCondition = and25(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
32810
33080
|
const unfilteredAgg = db.select({
|
|
32811
|
-
count:
|
|
32812
|
-
total:
|
|
33081
|
+
count: sql14`count(*)`,
|
|
33082
|
+
total: sql14`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
32813
33083
|
}).from(backlinkDomains).where(baseDomainCondition).get();
|
|
32814
33084
|
const filteredAgg = db.select({
|
|
32815
|
-
count:
|
|
32816
|
-
total:
|
|
33085
|
+
count: sql14`count(*)`,
|
|
33086
|
+
total: sql14`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
32817
33087
|
}).from(backlinkDomains).where(filteredCondition).get();
|
|
32818
33088
|
const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc14(backlinkDomains.numHosts)).limit(10).all();
|
|
32819
33089
|
const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
|
|
@@ -32839,7 +33109,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
|
|
|
32839
33109
|
const summary = db.select().from(backlinkSummaries).where(and25(eq30(backlinkSummaries.projectId, projectId), eq30(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
32840
33110
|
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
32841
33111
|
if (summary && excludeCrawlers) {
|
|
32842
|
-
const filtered = db.select({ count:
|
|
33112
|
+
const filtered = db.select({ count: sql14`count(*)` }).from(backlinkDomains).where(and25(
|
|
32843
33113
|
eq30(backlinkDomains.projectId, projectId),
|
|
32844
33114
|
eq30(backlinkDomains.source, source),
|
|
32845
33115
|
eq30(backlinkDomains.release, summary.release),
|
|
@@ -33028,7 +33298,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33028
33298
|
eq30(backlinkDomains.release, targetRelease)
|
|
33029
33299
|
);
|
|
33030
33300
|
const domainCondition = excludeCrawlers ? and25(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
33031
|
-
const totalRow = app.db.select({ count:
|
|
33301
|
+
const totalRow = app.db.select({ count: sql14`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
33032
33302
|
const rows = app.db.select({
|
|
33033
33303
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
33034
33304
|
numHosts: backlinkDomains.numHosts,
|
|
@@ -33107,7 +33377,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33107
33377
|
// ../api-routes/src/traffic.ts
|
|
33108
33378
|
import crypto28 from "crypto";
|
|
33109
33379
|
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
|
|
33380
|
+
import { and as and26, desc as desc15, eq as eq31, gte as gte7, lte as lte5, sql as sql15 } from "drizzle-orm";
|
|
33111
33381
|
|
|
33112
33382
|
// ../integration-cloud-run/src/auth.ts
|
|
33113
33383
|
import crypto27 from "crypto";
|
|
@@ -37168,11 +37438,11 @@ async function trafficRoutes(app, opts) {
|
|
|
37168
37438
|
const pullVercelEvents = opts.pullVercelTrafficEvents ?? listVercelTrafficEvents;
|
|
37169
37439
|
const allowLoopback = opts.allowLoopbackWebhooks === true;
|
|
37170
37440
|
async function assertWordpressTargetAllowed(baseUrl) {
|
|
37171
|
-
const
|
|
37172
|
-
if (!
|
|
37173
|
-
throw validationError(`WordPress baseUrl rejected: ${
|
|
37441
|
+
const check2 = await resolveWebhookTarget(baseUrl, { allowLoopback });
|
|
37442
|
+
if (!check2.ok) {
|
|
37443
|
+
throw validationError(`WordPress baseUrl rejected: ${check2.message}`);
|
|
37174
37444
|
}
|
|
37175
|
-
const { address, family } =
|
|
37445
|
+
const { address, family } = check2.target;
|
|
37176
37446
|
return new UndiciAgent({
|
|
37177
37447
|
connect: {
|
|
37178
37448
|
lookup: (_hostname, options, cb) => {
|
|
@@ -37808,7 +38078,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37808
38078
|
crawlerEventsHourly.status
|
|
37809
38079
|
],
|
|
37810
38080
|
set: {
|
|
37811
|
-
hits:
|
|
38081
|
+
hits: sql15`${crawlerEventsHourly.hits} + ${bucket.hits}`,
|
|
37812
38082
|
sampledUserAgent: bucket.sampledUserAgent,
|
|
37813
38083
|
updatedAt: finishedAt
|
|
37814
38084
|
}
|
|
@@ -37841,7 +38111,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37841
38111
|
aiUserFetchEventsHourly.status
|
|
37842
38112
|
],
|
|
37843
38113
|
set: {
|
|
37844
|
-
hits:
|
|
38114
|
+
hits: sql15`${aiUserFetchEventsHourly.hits} + ${bucket.hits}`,
|
|
37845
38115
|
sampledUserAgent: bucket.sampledUserAgent,
|
|
37846
38116
|
updatedAt: finishedAt
|
|
37847
38117
|
}
|
|
@@ -37881,9 +38151,9 @@ async function trafficRoutes(app, opts) {
|
|
|
37881
38151
|
// colliding bucket adds its paid and organic arrivals to the ones
|
|
37882
38152
|
// already there rather than overwriting a single label.
|
|
37883
38153
|
set: {
|
|
37884
|
-
sessionsOrHits:
|
|
37885
|
-
paidSessionsOrHits:
|
|
37886
|
-
organicSessionsOrHits:
|
|
38154
|
+
sessionsOrHits: sql15`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
|
|
38155
|
+
paidSessionsOrHits: sql15`${aiReferralEventsHourly.paidSessionsOrHits} + ${bucket.paidHits}`,
|
|
38156
|
+
organicSessionsOrHits: sql15`${aiReferralEventsHourly.organicSessionsOrHits} + ${bucket.organicHits}`,
|
|
37887
38157
|
updatedAt: finishedAt
|
|
37888
38158
|
}
|
|
37889
38159
|
}).run();
|
|
@@ -38174,7 +38444,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38174
38444
|
function buildSourceDetail(projectId, row, since) {
|
|
38175
38445
|
const crawlerPathRows = app.db.select({
|
|
38176
38446
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
38177
|
-
hits:
|
|
38447
|
+
hits: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
38178
38448
|
}).from(crawlerEventsHourly).where(
|
|
38179
38449
|
and26(
|
|
38180
38450
|
eq31(crawlerEventsHourly.sourceId, row.id),
|
|
@@ -38185,19 +38455,19 @@ async function trafficRoutes(app, opts) {
|
|
|
38185
38455
|
crawlerPathRows.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
|
|
38186
38456
|
);
|
|
38187
38457
|
const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
|
|
38188
|
-
const aiUserFetchTotals = app.db.select({ total:
|
|
38458
|
+
const aiUserFetchTotals = app.db.select({ total: sql15`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
38189
38459
|
and26(
|
|
38190
38460
|
eq31(aiUserFetchEventsHourly.sourceId, row.id),
|
|
38191
38461
|
gte7(aiUserFetchEventsHourly.tsHour, since)
|
|
38192
38462
|
)
|
|
38193
38463
|
).get();
|
|
38194
|
-
const aiTotals = app.db.select({ total:
|
|
38464
|
+
const aiTotals = app.db.select({ total: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
38195
38465
|
and26(
|
|
38196
38466
|
eq31(aiReferralEventsHourly.sourceId, row.id),
|
|
38197
38467
|
gte7(aiReferralEventsHourly.tsHour, since)
|
|
38198
38468
|
)
|
|
38199
38469
|
).get();
|
|
38200
|
-
const sampleTotals = app.db.select({ total:
|
|
38470
|
+
const sampleTotals = app.db.select({ total: sql15`COUNT(*)` }).from(rawEventSamples).where(
|
|
38201
38471
|
and26(
|
|
38202
38472
|
eq31(rawEventSamples.sourceId, row.id),
|
|
38203
38473
|
gte7(rawEventSamples.ts, since)
|
|
@@ -38359,18 +38629,18 @@ async function trafficRoutes(app, opts) {
|
|
|
38359
38629
|
const crawlerWhere = and26(...crawlerFilters);
|
|
38360
38630
|
const pathTotals = app.db.select({
|
|
38361
38631
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
38362
|
-
hits:
|
|
38363
|
-
rows:
|
|
38632
|
+
hits: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
|
|
38633
|
+
rows: sql15`COUNT(*)`
|
|
38364
38634
|
}).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
38365
38635
|
crawlerSegments = segmentCrawlerHits(
|
|
38366
38636
|
pathTotals.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
|
|
38367
38637
|
);
|
|
38368
38638
|
crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
|
|
38369
38639
|
totalEventRows += pathTotals.reduce((sum, row) => sum + Number(row.rows), 0);
|
|
38370
|
-
const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ?
|
|
38640
|
+
const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql15`substr(${crawlerEventsHourly.tsHour}, 1, 10)` : crawlerEventsHourly.tsHour;
|
|
38371
38641
|
const crawlerSeries = app.db.select({
|
|
38372
38642
|
bucket: crawlerSeriesBucket,
|
|
38373
|
-
hits:
|
|
38643
|
+
hits: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
38374
38644
|
}).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerSeriesBucket).all();
|
|
38375
38645
|
for (const row of crawlerSeries) {
|
|
38376
38646
|
trafficSeriesPoint(seriesByBucket, row.bucket).crawlerHits = Number(row.hits);
|
|
@@ -38400,15 +38670,15 @@ async function trafficRoutes(app, opts) {
|
|
|
38400
38670
|
if (sourceIdParam) userFetchFilters.push(eq31(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
38401
38671
|
const userFetchWhere = and26(...userFetchFilters);
|
|
38402
38672
|
const total = app.db.select({
|
|
38403
|
-
total:
|
|
38404
|
-
rows:
|
|
38673
|
+
total: sql15`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`,
|
|
38674
|
+
rows: sql15`COUNT(*)`
|
|
38405
38675
|
}).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
38406
38676
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
38407
38677
|
totalEventRows += Number(total?.rows ?? 0);
|
|
38408
|
-
const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ?
|
|
38678
|
+
const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql15`substr(${aiUserFetchEventsHourly.tsHour}, 1, 10)` : aiUserFetchEventsHourly.tsHour;
|
|
38409
38679
|
const userFetchSeries = app.db.select({
|
|
38410
38680
|
bucket: userFetchSeriesBucket,
|
|
38411
|
-
hits:
|
|
38681
|
+
hits: sql15`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
|
|
38412
38682
|
}).from(aiUserFetchEventsHourly).where(userFetchWhere).groupBy(userFetchSeriesBucket).all();
|
|
38413
38683
|
for (const row of userFetchSeries) {
|
|
38414
38684
|
trafficSeriesPoint(seriesByBucket, row.bucket).aiUserFetchHits = Number(row.hits);
|
|
@@ -38437,10 +38707,10 @@ async function trafficRoutes(app, opts) {
|
|
|
38437
38707
|
if (sourceIdParam) aiFilters.push(eq31(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
38438
38708
|
const aiWhere = and26(...aiFilters);
|
|
38439
38709
|
const total = app.db.select({
|
|
38440
|
-
total:
|
|
38441
|
-
paid:
|
|
38442
|
-
organic:
|
|
38443
|
-
rows:
|
|
38710
|
+
total: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
38711
|
+
paid: sql15`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
|
|
38712
|
+
organic: sql15`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`,
|
|
38713
|
+
rows: sql15`COUNT(*)`
|
|
38444
38714
|
}).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
38445
38715
|
aiReferralCounts = aiReferralClassCounts(
|
|
38446
38716
|
Number(total?.total ?? 0),
|
|
@@ -38448,10 +38718,10 @@ async function trafficRoutes(app, opts) {
|
|
|
38448
38718
|
Number(total?.organic ?? 0)
|
|
38449
38719
|
);
|
|
38450
38720
|
totalEventRows += Number(total?.rows ?? 0);
|
|
38451
|
-
const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ?
|
|
38721
|
+
const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql15`substr(${aiReferralEventsHourly.tsHour}, 1, 10)` : aiReferralEventsHourly.tsHour;
|
|
38452
38722
|
const referralSeries = app.db.select({
|
|
38453
38723
|
bucket: referralSeriesBucket,
|
|
38454
|
-
hits:
|
|
38724
|
+
hits: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
38455
38725
|
}).from(aiReferralEventsHourly).where(aiWhere).groupBy(referralSeriesBucket).all();
|
|
38456
38726
|
for (const row of referralSeries) {
|
|
38457
38727
|
trafficSeriesPoint(seriesByBucket, row.bucket).aiReferralHits = Number(row.hits);
|
|
@@ -39049,7 +39319,7 @@ var winnabilityCoverageCheck = {
|
|
|
39049
39319
|
};
|
|
39050
39320
|
var CONTENT_CHECKS = [winnabilityCoverageCheck];
|
|
39051
39321
|
var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
39052
|
-
CONTENT_CHECKS.map((
|
|
39322
|
+
CONTENT_CHECKS.map((check2) => [check2.id, check2])
|
|
39053
39323
|
);
|
|
39054
39324
|
|
|
39055
39325
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
@@ -39600,7 +39870,7 @@ var GBP_AUTH_CHECKS = [
|
|
|
39600
39870
|
recentSyncCheck
|
|
39601
39871
|
];
|
|
39602
39872
|
var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
39603
|
-
GBP_AUTH_CHECKS.map((
|
|
39873
|
+
GBP_AUTH_CHECKS.map((check2) => [check2.id, check2])
|
|
39604
39874
|
);
|
|
39605
39875
|
|
|
39606
39876
|
// ../api-routes/src/doctor/checks/places.ts
|
|
@@ -39678,7 +39948,7 @@ var apiKeyCheck = {
|
|
|
39678
39948
|
};
|
|
39679
39949
|
var PLACES_CHECKS = [apiKeyCheck];
|
|
39680
39950
|
var PLACES_CHECK_BY_ID = Object.fromEntries(
|
|
39681
|
-
PLACES_CHECKS.map((
|
|
39951
|
+
PLACES_CHECKS.map((check2) => [check2.id, check2])
|
|
39682
39952
|
);
|
|
39683
39953
|
|
|
39684
39954
|
// ../api-routes/src/doctor/checks/google-auth.ts
|
|
@@ -39985,7 +40255,7 @@ var GOOGLE_AUTH_CHECKS = [
|
|
|
39985
40255
|
scopesCheck2
|
|
39986
40256
|
];
|
|
39987
40257
|
var GOOGLE_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
39988
|
-
GOOGLE_AUTH_CHECKS.map((
|
|
40258
|
+
GOOGLE_AUTH_CHECKS.map((check2) => [check2.id, check2])
|
|
39989
40259
|
);
|
|
39990
40260
|
|
|
39991
40261
|
// ../api-routes/src/doctor/checks/providers.ts
|
|
@@ -40146,7 +40416,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
40146
40416
|
];
|
|
40147
40417
|
|
|
40148
40418
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
40149
|
-
import { and as and29, eq as eq37, gte as gte8, ne as ne5, sql as
|
|
40419
|
+
import { and as and29, eq as eq37, gte as gte8, ne as ne5, sql as sql16 } from "drizzle-orm";
|
|
40150
40420
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
40151
40421
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
40152
40422
|
function skippedNoProject5() {
|
|
@@ -40240,7 +40510,7 @@ var recentDataCheck = {
|
|
|
40240
40510
|
const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
|
|
40241
40511
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
40242
40512
|
const recentCrawlers = Number(
|
|
40243
|
-
ctx.db.select({ total:
|
|
40513
|
+
ctx.db.select({ total: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
40244
40514
|
and29(
|
|
40245
40515
|
eq37(crawlerEventsHourly.projectId, ctx.project.id),
|
|
40246
40516
|
gte8(crawlerEventsHourly.tsHour, warnCutoff)
|
|
@@ -40248,7 +40518,7 @@ var recentDataCheck = {
|
|
|
40248
40518
|
).get()?.total ?? 0
|
|
40249
40519
|
);
|
|
40250
40520
|
const recentReferrals = Number(
|
|
40251
|
-
ctx.db.select({ total:
|
|
40521
|
+
ctx.db.select({ total: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
40252
40522
|
and29(
|
|
40253
40523
|
eq37(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
40254
40524
|
gte8(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
@@ -40264,7 +40534,7 @@ var recentDataCheck = {
|
|
|
40264
40534
|
};
|
|
40265
40535
|
}
|
|
40266
40536
|
const olderCrawlers = Number(
|
|
40267
|
-
ctx.db.select({ total:
|
|
40537
|
+
ctx.db.select({ total: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
40268
40538
|
and29(
|
|
40269
40539
|
eq37(crawlerEventsHourly.projectId, ctx.project.id),
|
|
40270
40540
|
gte8(crawlerEventsHourly.tsHour, failCutoff)
|
|
@@ -40272,7 +40542,7 @@ var recentDataCheck = {
|
|
|
40272
40542
|
).get()?.total ?? 0
|
|
40273
40543
|
);
|
|
40274
40544
|
const olderReferrals = Number(
|
|
40275
|
-
ctx.db.select({ total:
|
|
40545
|
+
ctx.db.select({ total: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
40276
40546
|
and29(
|
|
40277
40547
|
eq37(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
40278
40548
|
gte8(aiReferralEventsHourly.tsHour, failCutoff)
|
|
@@ -40584,9 +40854,9 @@ async function runChecks(ctx, checks, options = {}) {
|
|
|
40584
40854
|
const filters = options.checkIds ?? [];
|
|
40585
40855
|
const targetScope = ctx.project ? CheckScopes.project : CheckScopes.global;
|
|
40586
40856
|
const projectName = ctx.project?.name ?? null;
|
|
40587
|
-
const selected = checks.filter((
|
|
40588
|
-
if (
|
|
40589
|
-
return matchesCheckId(
|
|
40857
|
+
const selected = checks.filter((check2) => {
|
|
40858
|
+
if (check2.scope !== targetScope) return false;
|
|
40859
|
+
return matchesCheckId(check2.id, filters);
|
|
40590
40860
|
});
|
|
40591
40861
|
const results = [];
|
|
40592
40862
|
for (const definition of selected) {
|
|
@@ -41524,6 +41794,80 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
41524
41794
|
});
|
|
41525
41795
|
}
|
|
41526
41796
|
|
|
41797
|
+
// ../api-routes/src/research.ts
|
|
41798
|
+
import crypto33 from "crypto";
|
|
41799
|
+
import { and as and32, desc as desc18, eq as eq41 } from "drizzle-orm";
|
|
41800
|
+
var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
|
|
41801
|
+
async function researchRoutes(app, opts) {
|
|
41802
|
+
app.post("/projects/:name/research/runs", async (request, reply) => {
|
|
41803
|
+
const project = resolveProject(app.db, request.params.name);
|
|
41804
|
+
if (!opts.onResearchRunRequested) throw missingDependency("Research execution is not available on this deployment.", { reason: "no-research-handler" });
|
|
41805
|
+
const parsed = researchRunCreateSchema.safeParse(request.body ?? {});
|
|
41806
|
+
if (!parsed.success) throw validationError("Invalid research run request", { issues: parsed.error.issues });
|
|
41807
|
+
const input = parsed.data;
|
|
41808
|
+
const adapters = opts.providerAdapters ?? [];
|
|
41809
|
+
const configured = new Set(opts.configuredProviderNames ?? []);
|
|
41810
|
+
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;
|
|
41811
|
+
const adapter = adapters.find((candidate) => candidate.name === providerName);
|
|
41812
|
+
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) });
|
|
41813
|
+
if (input.model) {
|
|
41814
|
+
adapter.modelValidationPattern.lastIndex = 0;
|
|
41815
|
+
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 });
|
|
41816
|
+
}
|
|
41817
|
+
const location = input.location === void 0 ? project.defaultLocation ? project.locations.find((item) => item.label === project.defaultLocation) ?? null : null : input.location;
|
|
41818
|
+
if (location && !project.locations.some((item) => sameLocation(item, location))) throw validationError("Research location must exactly match a configured project location.", { location });
|
|
41819
|
+
const requestedModel = input.model ?? null;
|
|
41820
|
+
const resolvedModel = requestedModel ?? (project.providerModels[providerName] || adapter.defaultModel);
|
|
41821
|
+
adapter.modelValidationPattern.lastIndex = 0;
|
|
41822
|
+
if (!adapter.modelValidationPattern.test(resolvedModel)) throw validationError(`Invalid resolved model "${resolvedModel}" for provider "${providerName}".`, { provider: providerName, model: resolvedModel, hint: adapter.modelValidationHint });
|
|
41823
|
+
if (new Set(input.queries.map((query) => query.toLocaleLowerCase())).size !== input.queries.length) throw validationError("Research queries must be unique within a batch.");
|
|
41824
|
+
const normalized = { queries: input.queries, provider: providerName, model: requestedModel, location: location ?? null };
|
|
41825
|
+
const requestHash2 = crypto33.createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
41826
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41827
|
+
const decision = app.db.transaction((tx) => {
|
|
41828
|
+
if (input.idempotencyKey) {
|
|
41829
|
+
const existing = tx.select().from(researchRuns).where(and32(eq41(researchRuns.projectId, project.id), eq41(researchRuns.idempotencyKey, input.idempotencyKey))).get();
|
|
41830
|
+
if (existing) {
|
|
41831
|
+
if (existing.requestHash !== requestHash2) throw alreadyExists("Research idempotency key", input.idempotencyKey);
|
|
41832
|
+
return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
|
|
41833
|
+
}
|
|
41834
|
+
}
|
|
41835
|
+
const id = crypto33.randomUUID();
|
|
41836
|
+
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();
|
|
41837
|
+
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();
|
|
41838
|
+
writeAuditLog(tx, { projectId: project.id, actor: "api", action: "research.created", entityType: "research_run", entityId: id });
|
|
41839
|
+
return { reused: false, id, shouldDispatch: true };
|
|
41840
|
+
});
|
|
41841
|
+
const result = getDetail(app, project.id, decision.id);
|
|
41842
|
+
if (decision.shouldDispatch) opts.onResearchRunRequested(decision.id, project.id);
|
|
41843
|
+
if (decision.reused) return reply.status(200).send(result);
|
|
41844
|
+
return reply.status(202).send(result);
|
|
41845
|
+
});
|
|
41846
|
+
app.get("/projects/:name/research/runs", async (request) => {
|
|
41847
|
+
const project = resolveProject(app.db, request.params.name);
|
|
41848
|
+
const requested = Number.parseInt(request.query.limit ?? "", 10);
|
|
41849
|
+
const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
|
|
41850
|
+
const runs2 = app.db.select().from(researchRuns).where(eq41(researchRuns.projectId, project.id)).orderBy(desc18(researchRuns.createdAt)).limit(limit).all().map(serializeRun);
|
|
41851
|
+
return { runs: runs2 };
|
|
41852
|
+
});
|
|
41853
|
+
app.get("/projects/:name/research/runs/:runId", async (request) => {
|
|
41854
|
+
const project = resolveProject(app.db, request.params.name);
|
|
41855
|
+
return getDetail(app, project.id, request.params.runId);
|
|
41856
|
+
});
|
|
41857
|
+
}
|
|
41858
|
+
function getDetail(app, projectId, id) {
|
|
41859
|
+
const row = app.db.select().from(researchRuns).where(and32(eq41(researchRuns.id, id), eq41(researchRuns.projectId, projectId))).get();
|
|
41860
|
+
if (!row) throw notFound("Research run", id);
|
|
41861
|
+
const queries2 = app.db.select().from(researchRunQueries).where(eq41(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
|
|
41862
|
+
return { ...serializeRun(row), queries: queries2 };
|
|
41863
|
+
}
|
|
41864
|
+
function serializeRun(row) {
|
|
41865
|
+
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 };
|
|
41866
|
+
}
|
|
41867
|
+
function serializeQuery(row) {
|
|
41868
|
+
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 };
|
|
41869
|
+
}
|
|
41870
|
+
|
|
41527
41871
|
// ../api-routes/src/index.ts
|
|
41528
41872
|
async function apiRoutes(app, opts) {
|
|
41529
41873
|
app.decorate("db", opts.db);
|
|
@@ -41710,6 +42054,11 @@ async function apiRoutes(app, opts) {
|
|
|
41710
42054
|
harvestSearchQueries: opts.harvestSearchQueries,
|
|
41711
42055
|
embedQueries: opts.embedQueries
|
|
41712
42056
|
});
|
|
42057
|
+
await api.register(researchRoutes, {
|
|
42058
|
+
providerAdapters: opts.providerAdapters,
|
|
42059
|
+
configuredProviderNames: opts.providerSummary?.filter((provider) => provider.configured).map((provider) => provider.name),
|
|
42060
|
+
onResearchRunRequested: opts.onResearchRunRequested
|
|
42061
|
+
});
|
|
41713
42062
|
await api.register(technicalAeoRoutes, {
|
|
41714
42063
|
onSiteAuditRequested: opts.onSiteAuditRequested
|
|
41715
42064
|
});
|
|
@@ -41871,7 +42220,7 @@ function buildTrafficSourceValidators(opts) {
|
|
|
41871
42220
|
}
|
|
41872
42221
|
|
|
41873
42222
|
// src/intelligence-service.ts
|
|
41874
|
-
import
|
|
42223
|
+
import crypto34 from "crypto";
|
|
41875
42224
|
|
|
41876
42225
|
// src/logger.ts
|
|
41877
42226
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -42103,16 +42452,16 @@ var IntelligenceService = class {
|
|
|
42103
42452
|
*/
|
|
42104
42453
|
analyzeAndPersist(runId, projectId) {
|
|
42105
42454
|
const recentRuns = this.db.select().from(runs).where(
|
|
42106
|
-
|
|
42107
|
-
|
|
42108
|
-
or8(
|
|
42455
|
+
and33(
|
|
42456
|
+
eq42(runs.projectId, projectId),
|
|
42457
|
+
or8(eq42(runs.status, "completed"), eq42(runs.status, "partial")),
|
|
42109
42458
|
// Defensive: RunCoordinator already skips probes before this is
|
|
42110
42459
|
// called, but if a future call site invokes analyzeAndPersist
|
|
42111
42460
|
// directly for a probe, probes still must not pollute the
|
|
42112
42461
|
// intelligence window.
|
|
42113
42462
|
ne6(runs.trigger, RunTriggers.probe)
|
|
42114
42463
|
)
|
|
42115
|
-
).orderBy(
|
|
42464
|
+
).orderBy(desc19(runs.finishedAt), desc19(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
|
|
42116
42465
|
if (recentRuns.length === 0) {
|
|
42117
42466
|
log.info("intelligence.skip", { runId, reason: "no completed runs" });
|
|
42118
42467
|
return null;
|
|
@@ -42187,7 +42536,7 @@ var IntelligenceService = class {
|
|
|
42187
42536
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
42188
42537
|
*/
|
|
42189
42538
|
analyzeAndPersistGbp(runId, projectId) {
|
|
42190
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
42539
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq42(runs.id, runId)).get();
|
|
42191
42540
|
if (!runRow) {
|
|
42192
42541
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
42193
42542
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -42195,9 +42544,9 @@ var IntelligenceService = class {
|
|
|
42195
42544
|
}
|
|
42196
42545
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
42197
42546
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
42198
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
42199
|
-
|
|
42200
|
-
|
|
42547
|
+
const selected = this.db.select().from(gbpLocations).where(and33(
|
|
42548
|
+
eq42(gbpLocations.projectId, projectId),
|
|
42549
|
+
eq42(gbpLocations.selected, true),
|
|
42201
42550
|
gte10(gbpLocations.syncedAt, windowStart),
|
|
42202
42551
|
lte6(gbpLocations.syncedAt, windowEnd)
|
|
42203
42552
|
)).all();
|
|
@@ -42232,12 +42581,12 @@ var IntelligenceService = class {
|
|
|
42232
42581
|
}
|
|
42233
42582
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
42234
42583
|
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(
|
|
42584
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and33(eq42(gbpDailyMetrics.projectId, projectId), eq42(gbpDailyMetrics.locationName, locationName))).all();
|
|
42585
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and33(eq42(gbpPlaceActions.projectId, projectId), eq42(gbpPlaceActions.locationName, locationName))).all();
|
|
42586
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and33(eq42(gbpLodgingSnapshots.projectId, projectId), eq42(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc19(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
42587
|
+
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and33(eq42(gbpLocations.projectId, projectId), eq42(gbpLocations.locationName, locationName))).get();
|
|
42239
42588
|
const descriptionMissing = !(ownerRow?.description ?? "").trim();
|
|
42240
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(
|
|
42589
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and33(eq42(gbpPlaceDetails.projectId, projectId), eq42(gbpPlaceDetails.locationName, locationName))).orderBy(desc19(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
42241
42590
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
42242
42591
|
const summary = buildGbpSummary({
|
|
42243
42592
|
locationName,
|
|
@@ -42273,7 +42622,7 @@ var IntelligenceService = class {
|
|
|
42273
42622
|
/** Build the month-over-month keyword series for a location from the
|
|
42274
42623
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
42275
42624
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
42276
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
42625
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and33(eq42(gbpKeywordMonthly.projectId, projectId), eq42(gbpKeywordMonthly.locationName, locationName))).all();
|
|
42277
42626
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
42278
42627
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
42279
42628
|
const recentMonth = months[0] ?? null;
|
|
@@ -42304,7 +42653,7 @@ var IntelligenceService = class {
|
|
|
42304
42653
|
*/
|
|
42305
42654
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
42306
42655
|
const covered = new Set(coveredLocationNames);
|
|
42307
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
42656
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and33(eq42(insights.projectId, projectId), eq42(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
42308
42657
|
const staleIds = [];
|
|
42309
42658
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
42310
42659
|
for (const row of existing) {
|
|
@@ -42315,7 +42664,7 @@ var IntelligenceService = class {
|
|
|
42315
42664
|
}
|
|
42316
42665
|
this.db.transaction((tx) => {
|
|
42317
42666
|
for (const id of staleIds) {
|
|
42318
|
-
tx.delete(insights).where(
|
|
42667
|
+
tx.delete(insights).where(eq42(insights.id, id)).run();
|
|
42319
42668
|
}
|
|
42320
42669
|
for (const insight of gbpInsights) {
|
|
42321
42670
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -42393,7 +42742,7 @@ var IntelligenceService = class {
|
|
|
42393
42742
|
* create per run + aggregate). DB is left untouched.
|
|
42394
42743
|
*/
|
|
42395
42744
|
backfill(projectName, opts, onProgress) {
|
|
42396
|
-
const project = this.db.select().from(projects).where(
|
|
42745
|
+
const project = this.db.select().from(projects).where(eq42(projects.name, projectName)).get();
|
|
42397
42746
|
if (!project) {
|
|
42398
42747
|
throw new Error(`Project "${projectName}" not found`);
|
|
42399
42748
|
}
|
|
@@ -42406,9 +42755,9 @@ var IntelligenceService = class {
|
|
|
42406
42755
|
sinceTimestamp = parsed;
|
|
42407
42756
|
}
|
|
42408
42757
|
const allRuns = this.db.select().from(runs).where(
|
|
42409
|
-
|
|
42410
|
-
|
|
42411
|
-
or8(
|
|
42758
|
+
and33(
|
|
42759
|
+
eq42(runs.projectId, project.id),
|
|
42760
|
+
or8(eq42(runs.status, "completed"), eq42(runs.status, "partial")),
|
|
42412
42761
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
42413
42762
|
ne6(runs.trigger, RunTriggers.probe)
|
|
42414
42763
|
)
|
|
@@ -42487,7 +42836,7 @@ var IntelligenceService = class {
|
|
|
42487
42836
|
return { processed, skipped, totalInsights };
|
|
42488
42837
|
}
|
|
42489
42838
|
loadTrackedCompetitors(projectId) {
|
|
42490
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
42839
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq42(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
42491
42840
|
}
|
|
42492
42841
|
/**
|
|
42493
42842
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -42508,15 +42857,15 @@ var IntelligenceService = class {
|
|
|
42508
42857
|
}
|
|
42509
42858
|
persistResult(result, runId, projectId) {
|
|
42510
42859
|
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(
|
|
42860
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq42(insights.runId, runId)).all();
|
|
42512
42861
|
for (const row of existingInsights) {
|
|
42513
42862
|
if (row.dismissed) {
|
|
42514
42863
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
42515
42864
|
}
|
|
42516
42865
|
}
|
|
42517
42866
|
this.db.transaction((tx) => {
|
|
42518
|
-
tx.delete(insights).where(
|
|
42519
|
-
tx.delete(healthSnapshots).where(
|
|
42867
|
+
tx.delete(insights).where(eq42(insights.runId, runId)).run();
|
|
42868
|
+
tx.delete(healthSnapshots).where(eq42(healthSnapshots.runId, runId)).run();
|
|
42520
42869
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
42521
42870
|
for (const insight of result.insights) {
|
|
42522
42871
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -42536,7 +42885,7 @@ var IntelligenceService = class {
|
|
|
42536
42885
|
}).run();
|
|
42537
42886
|
}
|
|
42538
42887
|
tx.insert(healthSnapshots).values({
|
|
42539
|
-
id:
|
|
42888
|
+
id: crypto34.randomUUID(),
|
|
42540
42889
|
projectId,
|
|
42541
42890
|
runId,
|
|
42542
42891
|
overallCitedRate: String(result.health.overallCitedRate),
|
|
@@ -42569,28 +42918,28 @@ var IntelligenceService = class {
|
|
|
42569
42918
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
42570
42919
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
42571
42920
|
if (regressions.length === 0) return rawInsights;
|
|
42572
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
42921
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq42(gscSearchData.projectId, projectId)).all();
|
|
42573
42922
|
const gscConnected = gscRows.length > 0;
|
|
42574
42923
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
42575
42924
|
for (const row of gscRows) {
|
|
42576
42925
|
const key = row.query.toLowerCase();
|
|
42577
42926
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
42578
42927
|
}
|
|
42579
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
42928
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq42(projects.id, projectId)).get();
|
|
42580
42929
|
const locationCount = Math.max(
|
|
42581
42930
|
1,
|
|
42582
42931
|
(projectRow?.locations ?? []).length
|
|
42583
42932
|
);
|
|
42584
42933
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
42585
42934
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
42586
|
-
|
|
42587
|
-
|
|
42588
|
-
|
|
42589
|
-
or8(
|
|
42935
|
+
and33(
|
|
42936
|
+
eq42(runs.projectId, projectId),
|
|
42937
|
+
eq42(runs.kind, RunKinds["answer-visibility"]),
|
|
42938
|
+
or8(eq42(runs.status, "completed"), eq42(runs.status, "partial")),
|
|
42590
42939
|
// Defensive — see top of file.
|
|
42591
42940
|
ne6(runs.trigger, RunTriggers.probe)
|
|
42592
42941
|
)
|
|
42593
|
-
).orderBy(
|
|
42942
|
+
).orderBy(desc19(runs.createdAt), desc19(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
|
|
42594
42943
|
const recentGroups = groupRunsByCreatedAt(recentRunRows);
|
|
42595
42944
|
const recentRunIds = [];
|
|
42596
42945
|
const recentRunIdToCreatedAt = /* @__PURE__ */ new Map();
|
|
@@ -42606,7 +42955,7 @@ var IntelligenceService = class {
|
|
|
42606
42955
|
const haveHistory = recentRunIds.length > 0;
|
|
42607
42956
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
42608
42957
|
if (haveHistory) {
|
|
42609
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
42958
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and33(eq42(insights.type, "regression"), inArray15(insights.runId, recentRunIds))).all();
|
|
42610
42959
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
42611
42960
|
for (const row of priorRows) {
|
|
42612
42961
|
if (!row.runId) continue;
|
|
@@ -42635,7 +42984,7 @@ var IntelligenceService = class {
|
|
|
42635
42984
|
});
|
|
42636
42985
|
}
|
|
42637
42986
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
42638
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
42987
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq42(projects.id, projectId)).get();
|
|
42639
42988
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
42640
42989
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
42641
42990
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -42652,7 +43001,7 @@ var IntelligenceService = class {
|
|
|
42652
43001
|
citedDomains: querySnapshots.citedDomains,
|
|
42653
43002
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
42654
43003
|
snapshotLocation: querySnapshots.location
|
|
42655
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
43004
|
+
}).from(querySnapshots).leftJoin(queries, eq42(querySnapshots.queryId, queries.id)).where(eq42(querySnapshots.runId, runId)).all();
|
|
42656
43005
|
const snapshots = [];
|
|
42657
43006
|
let orphanCount = 0;
|
|
42658
43007
|
for (const r of rows) {
|
|
@@ -42728,6 +43077,8 @@ export {
|
|
|
42728
43077
|
aiUserFetchEventsHourly,
|
|
42729
43078
|
rawEventSamples,
|
|
42730
43079
|
discoverySessions,
|
|
43080
|
+
researchRuns,
|
|
43081
|
+
researchRunQueries,
|
|
42731
43082
|
gbpLocations,
|
|
42732
43083
|
gbpDailyMetrics,
|
|
42733
43084
|
gbpKeywordImpressions,
|