@ainyc/canonry 4.78.0 → 4.81.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/assets/agent-workspace/skills/canonry/references/canonry-cli.md +38 -12
- package/assets/assets/{BacklinksPage-CwXveumn.js → BacklinksPage-DHShKKpo.js} +1 -1
- package/assets/assets/{ChartPrimitives-DntKGI5J.js → ChartPrimitives-udHScxjY.js} +1 -1
- package/assets/assets/ProjectPage-BsS1anh7.js +6 -0
- package/assets/assets/{RunRow-DMtYXaxG.js → RunRow-CXyPHMVQ.js} +1 -1
- package/assets/assets/{RunsPage-Cz-YlucO.js → RunsPage-BpQ_NpFt.js} +1 -1
- package/assets/assets/{SettingsPage-BCuG3C-0.js → SettingsPage-1ep4ch7n.js} +1 -1
- package/assets/assets/{TrafficPage-DV8X47wa.js → TrafficPage-C3Hx-sE7.js} +1 -1
- package/assets/assets/TrafficSourceDetailPage-B26n2R6G.js +1 -0
- package/assets/assets/{arrow-left-CUmHyNnF.js → arrow-left-Dc_IPJxw.js} +1 -1
- package/assets/assets/{extract-error-message-DFjy9_zi.js → extract-error-message-B3PoKkHW.js} +1 -1
- package/assets/assets/{index-D9smxU6R.js → index-DhdFTQkU.js} +86 -86
- package/assets/assets/{trash-2-B_UtEEm8.js → trash-2-BQ69cGl0.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-XI6YSTGE.js → chunk-6XOZSS3Y.js} +258 -8
- package/dist/{chunk-KPN22EWK.js → chunk-GMT3YPLT.js} +214 -4
- package/dist/{chunk-3WXARKUE.js → chunk-UAQ42NVJ.js} +1346 -357
- package/dist/{chunk-QKTIP6GC.js → chunk-VX5C7DK7.js} +902 -313
- package/dist/cli.js +468 -152
- package/dist/index.d.ts +17 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-CDVUUG7O.js → intelligence-service-CAAQAKPN.js} +2 -2
- package/dist/mcp.js +9 -3
- package/package.json +9 -8
- package/assets/assets/ProjectPage-CVudiU8X.js +0 -6
- package/assets/assets/TrafficSourceDetailPage-BmYhK9jm.js +0 -1
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
AI_ENGINE_DOMAINS,
|
|
4
4
|
AI_PROVIDER_INFRA_DOMAINS,
|
|
5
5
|
AppError,
|
|
6
|
+
BacklinkSources,
|
|
6
7
|
CcReleaseSyncStatuses,
|
|
7
8
|
CheckCategories,
|
|
8
9
|
CheckScopes,
|
|
@@ -34,6 +35,16 @@ import {
|
|
|
34
35
|
__export,
|
|
35
36
|
absolutizeProjectUrl,
|
|
36
37
|
actionConfidenceLabel,
|
|
38
|
+
adsCampaignListResponseSchema,
|
|
39
|
+
adsConnectRequestSchema,
|
|
40
|
+
adsConnectionStatusDtoSchema,
|
|
41
|
+
adsCpcMicros,
|
|
42
|
+
adsCtr,
|
|
43
|
+
adsDisconnectResponseSchema,
|
|
44
|
+
adsInsightLevelSchema,
|
|
45
|
+
adsInsightsResponseSchema,
|
|
46
|
+
adsSummaryDtoSchema,
|
|
47
|
+
adsSyncResponseSchema,
|
|
37
48
|
agentProvidersResponseDtoSchema,
|
|
38
49
|
apiKeyDtoSchema,
|
|
39
50
|
apiKeyListDtoSchema,
|
|
@@ -42,6 +53,8 @@ import {
|
|
|
42
53
|
authRequired,
|
|
43
54
|
backlinkHistoryEntrySchema,
|
|
44
55
|
backlinkListResponseSchema,
|
|
56
|
+
backlinkSourceSchema,
|
|
57
|
+
backlinkSourcesResponseSchema,
|
|
45
58
|
backlinkSummaryDtoSchema,
|
|
46
59
|
backlinksInstallResultDtoSchema,
|
|
47
60
|
backlinksInstallStatusDtoSchema,
|
|
@@ -232,10 +245,10 @@ import {
|
|
|
232
245
|
wordpressSchemaDeployResultDtoSchema,
|
|
233
246
|
wordpressSchemaStatusResultDtoSchema,
|
|
234
247
|
wordpressStatusDtoSchema
|
|
235
|
-
} from "./chunk-
|
|
248
|
+
} from "./chunk-GMT3YPLT.js";
|
|
236
249
|
|
|
237
250
|
// src/intelligence-service.ts
|
|
238
|
-
import { eq as
|
|
251
|
+
import { eq as eq36, desc as desc17, asc as asc5, and as and26, ne as ne5, or as or5, inArray as inArray13, gte as gte7, lte as lte4 } from "drizzle-orm";
|
|
239
252
|
|
|
240
253
|
// ../db/src/client.ts
|
|
241
254
|
import { mkdirSync } from "fs";
|
|
@@ -246,6 +259,11 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
|
246
259
|
// ../db/src/schema.ts
|
|
247
260
|
var schema_exports = {};
|
|
248
261
|
__export(schema_exports, {
|
|
262
|
+
adsAdGroups: () => adsAdGroups,
|
|
263
|
+
adsAds: () => adsAds,
|
|
264
|
+
adsCampaigns: () => adsCampaigns,
|
|
265
|
+
adsConnections: () => adsConnections,
|
|
266
|
+
adsInsightsDaily: () => adsInsightsDaily,
|
|
249
267
|
agentMemory: () => agentMemory,
|
|
250
268
|
agentSessions: () => agentSessions,
|
|
251
269
|
aiReferralEventsHourly: () => aiReferralEventsHourly,
|
|
@@ -835,7 +853,9 @@ var ccReleaseSyncs = sqliteTable("cc_release_syncs", {
|
|
|
835
853
|
var backlinkDomains = sqliteTable("backlink_domains", {
|
|
836
854
|
id: text("id").primaryKey(),
|
|
837
855
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
838
|
-
|
|
856
|
+
// Nullable: Bing Webmaster backlink rows have no Common Crawl release sync.
|
|
857
|
+
releaseSyncId: text("release_sync_id").references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
|
|
858
|
+
source: text("source").$type().notNull().default("commoncrawl"),
|
|
839
859
|
release: text("release").notNull(),
|
|
840
860
|
targetDomain: text("target_domain").notNull(),
|
|
841
861
|
linkingDomain: text("linking_domain").notNull(),
|
|
@@ -846,12 +866,14 @@ var backlinkDomains = sqliteTable("backlink_domains", {
|
|
|
846
866
|
index("idx_backlink_domains_release_sync").on(table.releaseSyncId),
|
|
847
867
|
index("idx_backlink_domains_project_release").on(table.projectId, table.release),
|
|
848
868
|
index("idx_backlink_domains_hosts").on(table.numHosts),
|
|
849
|
-
uniqueIndex("idx_backlink_domains_unique").on(table.projectId, table.release, table.linkingDomain)
|
|
869
|
+
uniqueIndex("idx_backlink_domains_unique").on(table.projectId, table.source, table.release, table.linkingDomain)
|
|
850
870
|
]);
|
|
851
871
|
var backlinkSummaries = sqliteTable("backlink_summaries", {
|
|
852
872
|
id: text("id").primaryKey(),
|
|
853
873
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
854
|
-
|
|
874
|
+
// Nullable: Bing Webmaster summaries have no Common Crawl release sync.
|
|
875
|
+
releaseSyncId: text("release_sync_id").references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
|
|
876
|
+
source: text("source").$type().notNull().default("commoncrawl"),
|
|
855
877
|
release: text("release").notNull(),
|
|
856
878
|
targetDomain: text("target_domain").notNull(),
|
|
857
879
|
totalLinkingDomains: integer("total_linking_domains").notNull(),
|
|
@@ -860,7 +882,7 @@ var backlinkSummaries = sqliteTable("backlink_summaries", {
|
|
|
860
882
|
queriedAt: text("queried_at").notNull(),
|
|
861
883
|
createdAt: text("created_at").notNull()
|
|
862
884
|
}, (table) => [
|
|
863
|
-
uniqueIndex("idx_backlink_summaries_project_release").on(table.projectId, table.release),
|
|
885
|
+
uniqueIndex("idx_backlink_summaries_project_release").on(table.projectId, table.source, table.release),
|
|
864
886
|
index("idx_backlink_summaries_project").on(table.projectId)
|
|
865
887
|
]);
|
|
866
888
|
var agentMemory = sqliteTable("agent_memory", {
|
|
@@ -1219,6 +1241,83 @@ var gbpPlaceDetails = sqliteTable("gbp_place_details", {
|
|
|
1219
1241
|
}, (table) => [
|
|
1220
1242
|
index("idx_gbp_place_details_loc").on(table.projectId, table.locationName, table.syncedAt)
|
|
1221
1243
|
]);
|
|
1244
|
+
var adsConnections = sqliteTable("ads_connections", {
|
|
1245
|
+
id: text("id").primaryKey(),
|
|
1246
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1247
|
+
adAccountId: text("ad_account_id").notNull(),
|
|
1248
|
+
displayName: text("display_name"),
|
|
1249
|
+
currencyCode: text("currency_code"),
|
|
1250
|
+
timezone: text("timezone"),
|
|
1251
|
+
status: text("status"),
|
|
1252
|
+
lastSyncedAt: text("last_synced_at"),
|
|
1253
|
+
createdAt: text("created_at").notNull(),
|
|
1254
|
+
updatedAt: text("updated_at").notNull()
|
|
1255
|
+
}, (table) => [
|
|
1256
|
+
uniqueIndex("idx_ads_conn_project").on(table.projectId)
|
|
1257
|
+
]);
|
|
1258
|
+
var adsCampaigns = sqliteTable("ads_campaigns", {
|
|
1259
|
+
id: text("id").primaryKey(),
|
|
1260
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1261
|
+
name: text("name").notNull(),
|
|
1262
|
+
status: text("status").notNull(),
|
|
1263
|
+
biddingType: text("bidding_type"),
|
|
1264
|
+
dailySpendLimitMicros: integer("daily_spend_limit_micros"),
|
|
1265
|
+
lifetimeSpendLimitMicros: integer("lifetime_spend_limit_micros"),
|
|
1266
|
+
targeting: text("targeting", { mode: "json" }).$type(),
|
|
1267
|
+
upstreamCreatedAt: integer("upstream_created_at"),
|
|
1268
|
+
upstreamUpdatedAt: integer("upstream_updated_at"),
|
|
1269
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" }),
|
|
1270
|
+
syncedAt: text("synced_at").notNull()
|
|
1271
|
+
}, (table) => [
|
|
1272
|
+
index("idx_ads_campaigns_project").on(table.projectId)
|
|
1273
|
+
]);
|
|
1274
|
+
var adsAdGroups = sqliteTable("ads_ad_groups", {
|
|
1275
|
+
id: text("id").primaryKey(),
|
|
1276
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1277
|
+
campaignId: text("campaign_id").notNull().references(() => adsCampaigns.id, { onDelete: "cascade" }),
|
|
1278
|
+
name: text("name").notNull(),
|
|
1279
|
+
status: text("status").notNull(),
|
|
1280
|
+
billingEventType: text("billing_event_type"),
|
|
1281
|
+
maxBidMicros: integer("max_bid_micros"),
|
|
1282
|
+
contextHints: text("context_hints", { mode: "json" }).$type().notNull().default([]),
|
|
1283
|
+
upstreamCreatedAt: integer("upstream_created_at"),
|
|
1284
|
+
upstreamUpdatedAt: integer("upstream_updated_at"),
|
|
1285
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" }),
|
|
1286
|
+
syncedAt: text("synced_at").notNull()
|
|
1287
|
+
}, (table) => [
|
|
1288
|
+
index("idx_ads_ad_groups_project").on(table.projectId),
|
|
1289
|
+
index("idx_ads_ad_groups_campaign").on(table.campaignId)
|
|
1290
|
+
]);
|
|
1291
|
+
var adsAds = sqliteTable("ads_ads", {
|
|
1292
|
+
id: text("id").primaryKey(),
|
|
1293
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1294
|
+
adGroupId: text("ad_group_id").notNull().references(() => adsAdGroups.id, { onDelete: "cascade" }),
|
|
1295
|
+
name: text("name").notNull(),
|
|
1296
|
+
status: text("status").notNull(),
|
|
1297
|
+
creative: text("creative", { mode: "json" }).$type(),
|
|
1298
|
+
reviewStatus: text("review_status"),
|
|
1299
|
+
upstreamCreatedAt: integer("upstream_created_at"),
|
|
1300
|
+
upstreamUpdatedAt: integer("upstream_updated_at"),
|
|
1301
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" }),
|
|
1302
|
+
syncedAt: text("synced_at").notNull()
|
|
1303
|
+
}, (table) => [
|
|
1304
|
+
index("idx_ads_ads_project").on(table.projectId),
|
|
1305
|
+
index("idx_ads_ads_group").on(table.adGroupId)
|
|
1306
|
+
]);
|
|
1307
|
+
var adsInsightsDaily = sqliteTable("ads_insights_daily", {
|
|
1308
|
+
id: text("id").primaryKey(),
|
|
1309
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1310
|
+
level: text("level").notNull(),
|
|
1311
|
+
entityId: text("entity_id").notNull(),
|
|
1312
|
+
date: text("date").notNull(),
|
|
1313
|
+
impressions: integer("impressions").notNull().default(0),
|
|
1314
|
+
clicks: integer("clicks").notNull().default(0),
|
|
1315
|
+
spendMicros: integer("spend_micros").notNull().default(0),
|
|
1316
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" })
|
|
1317
|
+
}, (table) => [
|
|
1318
|
+
uniqueIndex("uniq_ads_insights_daily").on(table.projectId, table.level, table.entityId, table.date),
|
|
1319
|
+
index("idx_ads_insights_project_date").on(table.projectId, table.date)
|
|
1320
|
+
]);
|
|
1222
1321
|
|
|
1223
1322
|
// ../db/src/client.ts
|
|
1224
1323
|
function createClient(databasePath) {
|
|
@@ -2876,8 +2975,169 @@ var MIGRATION_VERSIONS = [
|
|
|
2876
2975
|
statements: [
|
|
2877
2976
|
`ALTER TABLE discovery_sessions ADD COLUMN warning TEXT`
|
|
2878
2977
|
]
|
|
2978
|
+
},
|
|
2979
|
+
{
|
|
2980
|
+
// OpenAI Advertiser API (ChatGPT ads) — connection metadata, entity
|
|
2981
|
+
// snapshots (campaigns / ad groups / ads), and daily paid-performance
|
|
2982
|
+
// rollups. One connection per project (ad accounts are not domain-bound).
|
|
2983
|
+
// Money columns are integer micros; ads-sync normalizes the insights
|
|
2984
|
+
// API's decimal-dollar spend at ingest. Credentials live in config.yaml.
|
|
2985
|
+
version: 77,
|
|
2986
|
+
name: "openai-ads-tables",
|
|
2987
|
+
statements: [
|
|
2988
|
+
`CREATE TABLE IF NOT EXISTS ads_connections (
|
|
2989
|
+
id TEXT PRIMARY KEY,
|
|
2990
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
2991
|
+
ad_account_id TEXT NOT NULL,
|
|
2992
|
+
display_name TEXT,
|
|
2993
|
+
currency_code TEXT,
|
|
2994
|
+
timezone TEXT,
|
|
2995
|
+
status TEXT,
|
|
2996
|
+
last_synced_at TEXT,
|
|
2997
|
+
created_at TEXT NOT NULL,
|
|
2998
|
+
updated_at TEXT NOT NULL
|
|
2999
|
+
)`,
|
|
3000
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_ads_conn_project ON ads_connections(project_id)`,
|
|
3001
|
+
`CREATE TABLE IF NOT EXISTS ads_campaigns (
|
|
3002
|
+
id TEXT PRIMARY KEY,
|
|
3003
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3004
|
+
name TEXT NOT NULL,
|
|
3005
|
+
status TEXT NOT NULL,
|
|
3006
|
+
bidding_type TEXT,
|
|
3007
|
+
daily_spend_limit_micros INTEGER,
|
|
3008
|
+
lifetime_spend_limit_micros INTEGER,
|
|
3009
|
+
targeting TEXT,
|
|
3010
|
+
upstream_created_at INTEGER,
|
|
3011
|
+
upstream_updated_at INTEGER,
|
|
3012
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
|
|
3013
|
+
synced_at TEXT NOT NULL
|
|
3014
|
+
)`,
|
|
3015
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_campaigns_project ON ads_campaigns(project_id)`,
|
|
3016
|
+
`CREATE TABLE IF NOT EXISTS ads_ad_groups (
|
|
3017
|
+
id TEXT PRIMARY KEY,
|
|
3018
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3019
|
+
campaign_id TEXT NOT NULL REFERENCES ads_campaigns(id) ON DELETE CASCADE,
|
|
3020
|
+
name TEXT NOT NULL,
|
|
3021
|
+
status TEXT NOT NULL,
|
|
3022
|
+
billing_event_type TEXT,
|
|
3023
|
+
max_bid_micros INTEGER,
|
|
3024
|
+
context_hints TEXT NOT NULL DEFAULT '[]',
|
|
3025
|
+
upstream_created_at INTEGER,
|
|
3026
|
+
upstream_updated_at INTEGER,
|
|
3027
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
|
|
3028
|
+
synced_at TEXT NOT NULL
|
|
3029
|
+
)`,
|
|
3030
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_ad_groups_project ON ads_ad_groups(project_id)`,
|
|
3031
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_ad_groups_campaign ON ads_ad_groups(campaign_id)`,
|
|
3032
|
+
`CREATE TABLE IF NOT EXISTS ads_ads (
|
|
3033
|
+
id TEXT PRIMARY KEY,
|
|
3034
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3035
|
+
ad_group_id TEXT NOT NULL REFERENCES ads_ad_groups(id) ON DELETE CASCADE,
|
|
3036
|
+
name TEXT NOT NULL,
|
|
3037
|
+
status TEXT NOT NULL,
|
|
3038
|
+
creative TEXT,
|
|
3039
|
+
review_status TEXT,
|
|
3040
|
+
upstream_created_at INTEGER,
|
|
3041
|
+
upstream_updated_at INTEGER,
|
|
3042
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
|
|
3043
|
+
synced_at TEXT NOT NULL
|
|
3044
|
+
)`,
|
|
3045
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_ads_project ON ads_ads(project_id)`,
|
|
3046
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_ads_group ON ads_ads(ad_group_id)`,
|
|
3047
|
+
`CREATE TABLE IF NOT EXISTS ads_insights_daily (
|
|
3048
|
+
id TEXT PRIMARY KEY,
|
|
3049
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3050
|
+
level TEXT NOT NULL,
|
|
3051
|
+
entity_id TEXT NOT NULL,
|
|
3052
|
+
date TEXT NOT NULL,
|
|
3053
|
+
impressions INTEGER NOT NULL DEFAULT 0,
|
|
3054
|
+
clicks INTEGER NOT NULL DEFAULT 0,
|
|
3055
|
+
spend_micros INTEGER NOT NULL DEFAULT 0,
|
|
3056
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL
|
|
3057
|
+
)`,
|
|
3058
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS uniq_ads_insights_daily ON ads_insights_daily(project_id, level, entity_id, date)`,
|
|
3059
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_insights_project_date ON ads_insights_daily(project_id, date)`
|
|
3060
|
+
]
|
|
3061
|
+
},
|
|
3062
|
+
{
|
|
3063
|
+
// Bing Webmaster inbound links land in the SAME backlink store as Common
|
|
3064
|
+
// Crawl, tagged by a `source` discriminator (commoncrawl | bing-webmaster).
|
|
3065
|
+
// Bing rows have no `cc_release_syncs` row, so `release_sync_id` becomes
|
|
3066
|
+
// nullable and the per-window UNIQUE gains `source`. SQLite can't drop a
|
|
3067
|
+
// NOT NULL or rewrite a UNIQUE in place — canonical table rebuild (the
|
|
3068
|
+
// v58/v60 pattern). Guarded on the `source` column's absence so a replay
|
|
3069
|
+
// over the already-migrated schema is a no-op (the hardcoded
|
|
3070
|
+
// `source='commoncrawl'` backfill must never clobber real bing rows).
|
|
3071
|
+
version: 78,
|
|
3072
|
+
name: "backlinks-source-discriminator",
|
|
3073
|
+
statements: [],
|
|
3074
|
+
run: (tx) => {
|
|
3075
|
+
addBacklinkSourceDiscriminator(tx);
|
|
3076
|
+
}
|
|
2879
3077
|
}
|
|
2880
3078
|
];
|
|
3079
|
+
function rebuildBacklinkTableWithSource(tx, table) {
|
|
3080
|
+
if (!tableExists(tx, table)) return;
|
|
3081
|
+
if (columnExists(tx, table, "source")) return;
|
|
3082
|
+
if (table === "backlink_domains") {
|
|
3083
|
+
tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_domains_v78`));
|
|
3084
|
+
tx.run(sql.raw(`CREATE TABLE backlink_domains_v78 (
|
|
3085
|
+
id TEXT PRIMARY KEY,
|
|
3086
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3087
|
+
release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
|
|
3088
|
+
source TEXT NOT NULL DEFAULT 'commoncrawl',
|
|
3089
|
+
release TEXT NOT NULL,
|
|
3090
|
+
target_domain TEXT NOT NULL,
|
|
3091
|
+
linking_domain TEXT NOT NULL,
|
|
3092
|
+
num_hosts INTEGER NOT NULL,
|
|
3093
|
+
created_at TEXT NOT NULL
|
|
3094
|
+
)`));
|
|
3095
|
+
tx.run(sql.raw(`INSERT INTO backlink_domains_v78
|
|
3096
|
+
(id, project_id, release_sync_id, source, release, target_domain, linking_domain, num_hosts, created_at)
|
|
3097
|
+
SELECT bd.id, bd.project_id,
|
|
3098
|
+
CASE WHEN bd.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bd.release_sync_id ELSE NULL END,
|
|
3099
|
+
'commoncrawl', bd.release, bd.target_domain, bd.linking_domain, bd.num_hosts, bd.created_at
|
|
3100
|
+
FROM backlink_domains bd
|
|
3101
|
+
WHERE bd.project_id IN (SELECT id FROM projects)`));
|
|
3102
|
+
tx.run(sql.raw(`DROP TABLE backlink_domains`));
|
|
3103
|
+
tx.run(sql.raw(`ALTER TABLE backlink_domains_v78 RENAME TO backlink_domains`));
|
|
3104
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project ON backlink_domains(project_id)`));
|
|
3105
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_release_sync ON backlink_domains(release_sync_id)`));
|
|
3106
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project_release ON backlink_domains(project_id, release)`));
|
|
3107
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_hosts ON backlink_domains(num_hosts)`));
|
|
3108
|
+
tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_domains_unique ON backlink_domains(project_id, source, release, linking_domain)`));
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_summaries_v78`));
|
|
3112
|
+
tx.run(sql.raw(`CREATE TABLE backlink_summaries_v78 (
|
|
3113
|
+
id TEXT PRIMARY KEY,
|
|
3114
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3115
|
+
release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
|
|
3116
|
+
source TEXT NOT NULL DEFAULT 'commoncrawl',
|
|
3117
|
+
release TEXT NOT NULL,
|
|
3118
|
+
target_domain TEXT NOT NULL,
|
|
3119
|
+
total_linking_domains INTEGER NOT NULL,
|
|
3120
|
+
total_hosts INTEGER NOT NULL,
|
|
3121
|
+
top_10_hosts_share TEXT NOT NULL,
|
|
3122
|
+
queried_at TEXT NOT NULL,
|
|
3123
|
+
created_at TEXT NOT NULL
|
|
3124
|
+
)`));
|
|
3125
|
+
tx.run(sql.raw(`INSERT INTO backlink_summaries_v78
|
|
3126
|
+
(id, project_id, release_sync_id, source, release, target_domain, total_linking_domains, total_hosts, top_10_hosts_share, queried_at, created_at)
|
|
3127
|
+
SELECT bs.id, bs.project_id,
|
|
3128
|
+
CASE WHEN bs.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bs.release_sync_id ELSE NULL END,
|
|
3129
|
+
'commoncrawl', bs.release, bs.target_domain, bs.total_linking_domains, bs.total_hosts, bs.top_10_hosts_share, bs.queried_at, bs.created_at
|
|
3130
|
+
FROM backlink_summaries bs
|
|
3131
|
+
WHERE bs.project_id IN (SELECT id FROM projects)`));
|
|
3132
|
+
tx.run(sql.raw(`DROP TABLE backlink_summaries`));
|
|
3133
|
+
tx.run(sql.raw(`ALTER TABLE backlink_summaries_v78 RENAME TO backlink_summaries`));
|
|
3134
|
+
tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_summaries_project_release ON backlink_summaries(project_id, source, release)`));
|
|
3135
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_summaries_project ON backlink_summaries(project_id)`));
|
|
3136
|
+
}
|
|
3137
|
+
function addBacklinkSourceDiscriminator(tx) {
|
|
3138
|
+
rebuildBacklinkTableWithSource(tx, "backlink_domains");
|
|
3139
|
+
rebuildBacklinkTableWithSource(tx, "backlink_summaries");
|
|
3140
|
+
}
|
|
2881
3141
|
function isDuplicateColumnError(err) {
|
|
2882
3142
|
if (!(err instanceof Error)) return false;
|
|
2883
3143
|
if (err.message.includes("duplicate column name")) return true;
|
|
@@ -13020,11 +13280,18 @@ function makeSnippet(text2, query) {
|
|
|
13020
13280
|
import { z } from "zod";
|
|
13021
13281
|
var SCHEMA_TABLE = {
|
|
13022
13282
|
AgentProvidersResponseDto: agentProvidersResponseDtoSchema,
|
|
13283
|
+
AdsCampaignListResponse: adsCampaignListResponseSchema,
|
|
13284
|
+
AdsConnectionStatusDto: adsConnectionStatusDtoSchema,
|
|
13285
|
+
AdsDisconnectResponse: adsDisconnectResponseSchema,
|
|
13286
|
+
AdsInsightsResponse: adsInsightsResponseSchema,
|
|
13287
|
+
AdsSummaryDto: adsSummaryDtoSchema,
|
|
13288
|
+
AdsSyncResponse: adsSyncResponseSchema,
|
|
13023
13289
|
ApiKeyDto: apiKeyDtoSchema,
|
|
13024
13290
|
ApiKeyListDto: apiKeyListDtoSchema,
|
|
13025
13291
|
AuditLogEntry: auditLogEntrySchema,
|
|
13026
13292
|
BacklinkHistoryEntry: backlinkHistoryEntrySchema,
|
|
13027
13293
|
BacklinkListResponse: backlinkListResponseSchema,
|
|
13294
|
+
BacklinkSourcesResponse: backlinkSourcesResponseSchema,
|
|
13028
13295
|
BacklinkSummaryDto: backlinkSummaryDtoSchema,
|
|
13029
13296
|
BacklinksInstallResultDto: backlinksInstallResultDtoSchema,
|
|
13030
13297
|
BacklinksInstallStatusDto: backlinksInstallStatusDtoSchema,
|
|
@@ -15061,6 +15328,106 @@ var routeCatalog = [
|
|
|
15061
15328
|
404: errorResponse("Project not found.")
|
|
15062
15329
|
}
|
|
15063
15330
|
},
|
|
15331
|
+
{
|
|
15332
|
+
method: "post",
|
|
15333
|
+
path: "/api/v1/projects/{name}/ads/connect",
|
|
15334
|
+
summary: "Connect an OpenAI ad account (ChatGPT ads) with an Ads Manager SDK key",
|
|
15335
|
+
tags: ["ads"],
|
|
15336
|
+
parameters: [nameParameter],
|
|
15337
|
+
requestBody: {
|
|
15338
|
+
required: true,
|
|
15339
|
+
content: {
|
|
15340
|
+
"application/json": {
|
|
15341
|
+
schema: {
|
|
15342
|
+
type: "object",
|
|
15343
|
+
required: ["apiKey"],
|
|
15344
|
+
properties: {
|
|
15345
|
+
apiKey: stringSchema
|
|
15346
|
+
}
|
|
15347
|
+
}
|
|
15348
|
+
}
|
|
15349
|
+
}
|
|
15350
|
+
},
|
|
15351
|
+
responses: {
|
|
15352
|
+
200: jsonResponse("Connected; key validated against the upstream ad account.", "AdsConnectionStatusDto"),
|
|
15353
|
+
400: errorResponse("Missing/invalid key or credential storage unavailable."),
|
|
15354
|
+
404: errorResponse("Project not found.")
|
|
15355
|
+
}
|
|
15356
|
+
},
|
|
15357
|
+
{
|
|
15358
|
+
method: "delete",
|
|
15359
|
+
path: "/api/v1/projects/{name}/ads/connection",
|
|
15360
|
+
summary: "Disconnect the OpenAI ad account (removes the stored credential)",
|
|
15361
|
+
tags: ["ads"],
|
|
15362
|
+
parameters: [nameParameter],
|
|
15363
|
+
responses: {
|
|
15364
|
+
200: jsonResponse("Disconnected (idempotent).", "AdsDisconnectResponse"),
|
|
15365
|
+
404: errorResponse("Project not found.")
|
|
15366
|
+
}
|
|
15367
|
+
},
|
|
15368
|
+
{
|
|
15369
|
+
method: "get",
|
|
15370
|
+
path: "/api/v1/projects/{name}/ads/status",
|
|
15371
|
+
summary: "OpenAI ads connection status and last sync time",
|
|
15372
|
+
tags: ["ads"],
|
|
15373
|
+
parameters: [nameParameter],
|
|
15374
|
+
responses: {
|
|
15375
|
+
200: jsonResponse("Connection status.", "AdsConnectionStatusDto"),
|
|
15376
|
+
404: errorResponse("Project not found.")
|
|
15377
|
+
}
|
|
15378
|
+
},
|
|
15379
|
+
{
|
|
15380
|
+
method: "post",
|
|
15381
|
+
path: "/api/v1/projects/{name}/ads/sync",
|
|
15382
|
+
summary: "Trigger an ads-sync run (entity snapshots + daily paid-performance rollups)",
|
|
15383
|
+
tags: ["ads"],
|
|
15384
|
+
parameters: [nameParameter],
|
|
15385
|
+
responses: {
|
|
15386
|
+
200: jsonResponse("Sync run queued.", "AdsSyncResponse"),
|
|
15387
|
+
400: errorResponse("No ads connection for this project."),
|
|
15388
|
+
404: errorResponse("Project not found.")
|
|
15389
|
+
}
|
|
15390
|
+
},
|
|
15391
|
+
{
|
|
15392
|
+
method: "get",
|
|
15393
|
+
path: "/api/v1/projects/{name}/ads/campaigns",
|
|
15394
|
+
summary: "Synced campaign snapshots with nested ad groups (context hints) and ads",
|
|
15395
|
+
tags: ["ads"],
|
|
15396
|
+
parameters: [nameParameter],
|
|
15397
|
+
responses: {
|
|
15398
|
+
200: jsonResponse("Campaign snapshots.", "AdsCampaignListResponse"),
|
|
15399
|
+
404: errorResponse("Project not found.")
|
|
15400
|
+
}
|
|
15401
|
+
},
|
|
15402
|
+
{
|
|
15403
|
+
method: "get",
|
|
15404
|
+
path: "/api/v1/projects/{name}/ads/insights",
|
|
15405
|
+
summary: "Daily paid-performance rollups (spend in integer micros; ctr/cpc derived server-side)",
|
|
15406
|
+
tags: ["ads"],
|
|
15407
|
+
parameters: [
|
|
15408
|
+
nameParameter,
|
|
15409
|
+
{ in: "query", name: "level", required: false, description: "campaign | ad_group", schema: stringSchema },
|
|
15410
|
+
{ in: "query", name: "entityId", required: false, description: "Scope to one upstream entity id", schema: stringSchema },
|
|
15411
|
+
{ in: "query", name: "from", required: false, description: "Inclusive start date (YYYY-MM-DD)", schema: stringSchema },
|
|
15412
|
+
{ in: "query", name: "to", required: false, description: "Inclusive end date (YYYY-MM-DD)", schema: stringSchema }
|
|
15413
|
+
],
|
|
15414
|
+
responses: {
|
|
15415
|
+
200: jsonResponse("Daily rollup rows.", "AdsInsightsResponse"),
|
|
15416
|
+
400: errorResponse("Invalid level filter."),
|
|
15417
|
+
404: errorResponse("Project not found.")
|
|
15418
|
+
}
|
|
15419
|
+
},
|
|
15420
|
+
{
|
|
15421
|
+
method: "get",
|
|
15422
|
+
path: "/api/v1/projects/{name}/ads/summary",
|
|
15423
|
+
summary: "Composite paid-performance summary (campaign-level totals; all derived metrics)",
|
|
15424
|
+
tags: ["ads"],
|
|
15425
|
+
parameters: [nameParameter],
|
|
15426
|
+
responses: {
|
|
15427
|
+
200: jsonResponse("Summary returned.", "AdsSummaryDto"),
|
|
15428
|
+
404: errorResponse("Project not found.")
|
|
15429
|
+
}
|
|
15430
|
+
},
|
|
15064
15431
|
{
|
|
15065
15432
|
method: "post",
|
|
15066
15433
|
path: "/api/v1/projects/{name}/bing/connect",
|
|
@@ -16385,7 +16752,8 @@ var routeCatalog = [
|
|
|
16385
16752
|
tags: ["backlinks"],
|
|
16386
16753
|
parameters: [
|
|
16387
16754
|
nameParameter,
|
|
16388
|
-
{ name: "release", in: "query", description: "Release id filter.", schema: stringSchema }
|
|
16755
|
+
{ name: "release", in: "query", description: "Release id filter.", schema: stringSchema },
|
|
16756
|
+
{ name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
|
|
16389
16757
|
],
|
|
16390
16758
|
responses: {
|
|
16391
16759
|
200: rawJsonResponse("Summary returned, or null when no backlinks exist.", {
|
|
@@ -16403,7 +16771,8 @@ var routeCatalog = [
|
|
|
16403
16771
|
nameParameter,
|
|
16404
16772
|
{ name: "release", in: "query", description: "Release id filter.", schema: stringSchema },
|
|
16405
16773
|
{ name: "limit", in: "query", description: "Max results (1-500).", schema: stringSchema },
|
|
16406
|
-
{ name: "offset", in: "query", description: "Pagination offset.", schema: stringSchema }
|
|
16774
|
+
{ name: "offset", in: "query", description: "Pagination offset.", schema: stringSchema },
|
|
16775
|
+
{ name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
|
|
16407
16776
|
],
|
|
16408
16777
|
responses: {
|
|
16409
16778
|
200: jsonResponse("Domain list returned.", "BacklinkListResponse"),
|
|
@@ -16415,12 +16784,44 @@ var routeCatalog = [
|
|
|
16415
16784
|
path: "/api/v1/projects/{name}/backlinks/history",
|
|
16416
16785
|
summary: "Get per-release backlink summaries for a project",
|
|
16417
16786
|
tags: ["backlinks"],
|
|
16418
|
-
parameters: [
|
|
16787
|
+
parameters: [
|
|
16788
|
+
nameParameter,
|
|
16789
|
+
{ name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
|
|
16790
|
+
],
|
|
16419
16791
|
responses: {
|
|
16420
16792
|
200: jsonArrayResponse("History returned oldest-first by queriedAt.", "BacklinkHistoryEntry"),
|
|
16421
16793
|
404: errorResponse("Project not found.")
|
|
16422
16794
|
}
|
|
16423
16795
|
},
|
|
16796
|
+
{
|
|
16797
|
+
method: "get",
|
|
16798
|
+
path: "/api/v1/projects/{name}/backlinks/sources",
|
|
16799
|
+
summary: "Report per-source backlink availability for a project",
|
|
16800
|
+
description: "Returns connection + data availability for every backlink source (commoncrawl, bing-webmaster) so callers can degrade gracefully across CC-only / Bing-only / both / neither.",
|
|
16801
|
+
tags: ["backlinks"],
|
|
16802
|
+
parameters: [
|
|
16803
|
+
nameParameter,
|
|
16804
|
+
{ name: "excludeCrawlers", in: "query", description: 'When "1"/"true", count linking domains excluding crawler/proxy hosts (matches the dashboard). Default off.', schema: stringSchema }
|
|
16805
|
+
],
|
|
16806
|
+
responses: {
|
|
16807
|
+
200: jsonResponse("Per-source availability returned.", "BacklinkSourcesResponse"),
|
|
16808
|
+
404: errorResponse("Project not found.")
|
|
16809
|
+
}
|
|
16810
|
+
},
|
|
16811
|
+
{
|
|
16812
|
+
method: "post",
|
|
16813
|
+
path: "/api/v1/projects/{name}/backlinks/bing-sync",
|
|
16814
|
+
summary: "Sync a project's inbound links from Bing Webmaster Tools",
|
|
16815
|
+
description: 'Creates a tracking run and pulls inbound links live from the connected Bing Webmaster account, writing source="bing-webmaster" backlink rows. Requires a Bing connection for the project domain.',
|
|
16816
|
+
tags: ["backlinks"],
|
|
16817
|
+
parameters: [nameParameter],
|
|
16818
|
+
responses: {
|
|
16819
|
+
201: jsonResponse("Bing sync run queued.", "RunDto"),
|
|
16820
|
+
400: errorResponse("No Bing Webmaster connection for this project."),
|
|
16821
|
+
404: errorResponse("Project not found."),
|
|
16822
|
+
422: errorResponse("Bing backlinks sync is not available on this deployment.")
|
|
16823
|
+
}
|
|
16824
|
+
},
|
|
16424
16825
|
{
|
|
16425
16826
|
method: "post",
|
|
16426
16827
|
path: "/api/v1/projects/{name}/traffic/connect/cloud-run",
|
|
@@ -20351,15 +20752,276 @@ async function googleRoutes(app, opts) {
|
|
|
20351
20752
|
});
|
|
20352
20753
|
}
|
|
20353
20754
|
|
|
20354
|
-
// ../api-routes/src/
|
|
20755
|
+
// ../api-routes/src/ads.ts
|
|
20355
20756
|
import crypto18 from "crypto";
|
|
20356
|
-
import { eq as eq21, and as and14,
|
|
20757
|
+
import { eq as eq21, and as and14, asc as asc2, gte as gte3, lte as lte2, inArray as inArray10 } from "drizzle-orm";
|
|
20758
|
+
function statusDto(row) {
|
|
20759
|
+
if (!row) return { connected: false };
|
|
20760
|
+
return {
|
|
20761
|
+
connected: true,
|
|
20762
|
+
adAccountId: row.adAccountId,
|
|
20763
|
+
displayName: row.displayName,
|
|
20764
|
+
currencyCode: row.currencyCode,
|
|
20765
|
+
timezone: row.timezone,
|
|
20766
|
+
status: row.status,
|
|
20767
|
+
lastSyncedAt: row.lastSyncedAt
|
|
20768
|
+
};
|
|
20769
|
+
}
|
|
20770
|
+
function creativeDto(raw) {
|
|
20771
|
+
if (raw == null || typeof raw !== "object") return null;
|
|
20772
|
+
const c = raw;
|
|
20773
|
+
return {
|
|
20774
|
+
type: typeof c.type === "string" ? c.type : null,
|
|
20775
|
+
title: typeof c.title === "string" ? c.title : null,
|
|
20776
|
+
body: typeof c.body === "string" ? c.body : null,
|
|
20777
|
+
targetUrl: typeof c.target_url === "string" ? c.target_url : null
|
|
20778
|
+
};
|
|
20779
|
+
}
|
|
20780
|
+
async function adsRoutes(app, opts) {
|
|
20781
|
+
app.post(
|
|
20782
|
+
"/projects/:name/ads/connect",
|
|
20783
|
+
async (request) => {
|
|
20784
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20785
|
+
const parsed = adsConnectRequestSchema.safeParse(request.body ?? {});
|
|
20786
|
+
if (!parsed.success) throw validationError('"apiKey" is required');
|
|
20787
|
+
if (!opts.adsCredentialStore || !opts.verifyAdsAccount) {
|
|
20788
|
+
throw validationError("Ads credential storage is not configured for this deployment");
|
|
20789
|
+
}
|
|
20790
|
+
let account;
|
|
20791
|
+
try {
|
|
20792
|
+
account = await opts.verifyAdsAccount(parsed.data.apiKey);
|
|
20793
|
+
} catch (err) {
|
|
20794
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
20795
|
+
throw validationError(`OpenAI Ads API rejected the key: ${message}`);
|
|
20796
|
+
}
|
|
20797
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20798
|
+
const existingCfg = opts.adsCredentialStore.getConnection(project.name);
|
|
20799
|
+
opts.adsCredentialStore.upsertConnection({
|
|
20800
|
+
projectName: project.name,
|
|
20801
|
+
apiKey: parsed.data.apiKey,
|
|
20802
|
+
adAccountId: account.id,
|
|
20803
|
+
createdAt: existingCfg?.createdAt ?? now,
|
|
20804
|
+
updatedAt: now
|
|
20805
|
+
});
|
|
20806
|
+
const existingRow = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20807
|
+
app.db.transaction((tx) => {
|
|
20808
|
+
if (existingRow) {
|
|
20809
|
+
tx.update(adsConnections).set({
|
|
20810
|
+
adAccountId: account.id,
|
|
20811
|
+
displayName: account.name,
|
|
20812
|
+
currencyCode: account.currencyCode,
|
|
20813
|
+
timezone: account.timezone,
|
|
20814
|
+
status: account.status,
|
|
20815
|
+
updatedAt: now
|
|
20816
|
+
}).where(eq21(adsConnections.id, existingRow.id)).run();
|
|
20817
|
+
} else {
|
|
20818
|
+
tx.insert(adsConnections).values({
|
|
20819
|
+
id: crypto18.randomUUID(),
|
|
20820
|
+
projectId: project.id,
|
|
20821
|
+
adAccountId: account.id,
|
|
20822
|
+
displayName: account.name,
|
|
20823
|
+
currencyCode: account.currencyCode,
|
|
20824
|
+
timezone: account.timezone,
|
|
20825
|
+
status: account.status,
|
|
20826
|
+
createdAt: now,
|
|
20827
|
+
updatedAt: now
|
|
20828
|
+
}).run();
|
|
20829
|
+
}
|
|
20830
|
+
writeAuditLog(tx, auditFromRequest(request, {
|
|
20831
|
+
projectId: project.id,
|
|
20832
|
+
actor: "api",
|
|
20833
|
+
action: "ads.connected",
|
|
20834
|
+
entityType: "ads-connection",
|
|
20835
|
+
entityId: account.id
|
|
20836
|
+
}));
|
|
20837
|
+
});
|
|
20838
|
+
const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20839
|
+
return statusDto(row);
|
|
20840
|
+
}
|
|
20841
|
+
);
|
|
20842
|
+
app.delete("/projects/:name/ads/connection", async (request) => {
|
|
20843
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20844
|
+
const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20845
|
+
if (row) {
|
|
20846
|
+
app.db.transaction((tx) => {
|
|
20847
|
+
tx.delete(adsConnections).where(eq21(adsConnections.id, row.id)).run();
|
|
20848
|
+
writeAuditLog(tx, auditFromRequest(request, {
|
|
20849
|
+
projectId: project.id,
|
|
20850
|
+
actor: "api",
|
|
20851
|
+
action: "ads.disconnected",
|
|
20852
|
+
entityType: "ads-connection",
|
|
20853
|
+
entityId: row.adAccountId
|
|
20854
|
+
}));
|
|
20855
|
+
});
|
|
20856
|
+
}
|
|
20857
|
+
const removedFromConfig = opts.adsCredentialStore?.removeConnection(project.name) ?? false;
|
|
20858
|
+
const response = { disconnected: Boolean(row) || removedFromConfig };
|
|
20859
|
+
return response;
|
|
20860
|
+
});
|
|
20861
|
+
app.get("/projects/:name/ads/status", async (request) => {
|
|
20862
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20863
|
+
const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20864
|
+
return statusDto(row);
|
|
20865
|
+
});
|
|
20866
|
+
app.post("/projects/:name/ads/sync", async (request) => {
|
|
20867
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20868
|
+
const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20869
|
+
if (!row) {
|
|
20870
|
+
throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
|
|
20871
|
+
}
|
|
20872
|
+
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and14(
|
|
20873
|
+
eq21(runs.projectId, project.id),
|
|
20874
|
+
eq21(runs.kind, RunKinds["ads-sync"]),
|
|
20875
|
+
inArray10(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
20876
|
+
)).get();
|
|
20877
|
+
if (inFlight) {
|
|
20878
|
+
const existing = { runId: inFlight.id, status: inFlight.status };
|
|
20879
|
+
return existing;
|
|
20880
|
+
}
|
|
20881
|
+
const runId = crypto18.randomUUID();
|
|
20882
|
+
app.db.insert(runs).values({
|
|
20883
|
+
id: runId,
|
|
20884
|
+
projectId: project.id,
|
|
20885
|
+
kind: RunKinds["ads-sync"],
|
|
20886
|
+
status: RunStatuses.queued,
|
|
20887
|
+
trigger: RunTriggers.manual,
|
|
20888
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20889
|
+
}).run();
|
|
20890
|
+
opts.onAdsSyncRequested?.(runId, project.id);
|
|
20891
|
+
const response = { runId, status: RunStatuses.queued };
|
|
20892
|
+
return response;
|
|
20893
|
+
});
|
|
20894
|
+
app.get("/projects/:name/ads/campaigns", async (request) => {
|
|
20895
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20896
|
+
const campaignRows = app.db.select().from(adsCampaigns).where(eq21(adsCampaigns.projectId, project.id)).all();
|
|
20897
|
+
const groupRows = app.db.select().from(adsAdGroups).where(eq21(adsAdGroups.projectId, project.id)).all();
|
|
20898
|
+
const adRows = app.db.select().from(adsAds).where(eq21(adsAds.projectId, project.id)).all();
|
|
20899
|
+
const adsByGroup = /* @__PURE__ */ new Map();
|
|
20900
|
+
for (const ad of adRows) {
|
|
20901
|
+
const dto = {
|
|
20902
|
+
id: ad.id,
|
|
20903
|
+
adGroupId: ad.adGroupId,
|
|
20904
|
+
name: ad.name,
|
|
20905
|
+
status: ad.status,
|
|
20906
|
+
reviewStatus: ad.reviewStatus,
|
|
20907
|
+
creative: creativeDto(ad.creative)
|
|
20908
|
+
};
|
|
20909
|
+
const list = adsByGroup.get(ad.adGroupId) ?? [];
|
|
20910
|
+
list.push(dto);
|
|
20911
|
+
adsByGroup.set(ad.adGroupId, list);
|
|
20912
|
+
}
|
|
20913
|
+
const groupsByCampaign = /* @__PURE__ */ new Map();
|
|
20914
|
+
for (const group of groupRows) {
|
|
20915
|
+
const dto = {
|
|
20916
|
+
id: group.id,
|
|
20917
|
+
campaignId: group.campaignId,
|
|
20918
|
+
name: group.name,
|
|
20919
|
+
status: group.status,
|
|
20920
|
+
billingEventType: group.billingEventType,
|
|
20921
|
+
maxBidMicros: group.maxBidMicros,
|
|
20922
|
+
contextHints: group.contextHints,
|
|
20923
|
+
ads: adsByGroup.get(group.id) ?? []
|
|
20924
|
+
};
|
|
20925
|
+
const list = groupsByCampaign.get(group.campaignId) ?? [];
|
|
20926
|
+
list.push(dto);
|
|
20927
|
+
groupsByCampaign.set(group.campaignId, list);
|
|
20928
|
+
}
|
|
20929
|
+
const campaigns = campaignRows.map((campaign) => ({
|
|
20930
|
+
id: campaign.id,
|
|
20931
|
+
name: campaign.name,
|
|
20932
|
+
status: campaign.status,
|
|
20933
|
+
biddingType: campaign.biddingType,
|
|
20934
|
+
dailySpendLimitMicros: campaign.dailySpendLimitMicros,
|
|
20935
|
+
lifetimeSpendLimitMicros: campaign.lifetimeSpendLimitMicros,
|
|
20936
|
+
adGroups: groupsByCampaign.get(campaign.id) ?? []
|
|
20937
|
+
}));
|
|
20938
|
+
const response = { campaigns };
|
|
20939
|
+
return response;
|
|
20940
|
+
});
|
|
20941
|
+
app.get("/projects/:name/ads/insights", async (request) => {
|
|
20942
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20943
|
+
const { level, entityId, from, to } = request.query;
|
|
20944
|
+
let parsedLevel;
|
|
20945
|
+
if (level !== void 0) {
|
|
20946
|
+
const result = adsInsightLevelSchema.safeParse(level);
|
|
20947
|
+
if (!result.success) {
|
|
20948
|
+
throw validationError('"level" must be one of: campaign, ad_group');
|
|
20949
|
+
}
|
|
20950
|
+
parsedLevel = result.data;
|
|
20951
|
+
}
|
|
20952
|
+
const conditions = [eq21(adsInsightsDaily.projectId, project.id)];
|
|
20953
|
+
if (parsedLevel) conditions.push(eq21(adsInsightsDaily.level, parsedLevel));
|
|
20954
|
+
if (entityId) conditions.push(eq21(adsInsightsDaily.entityId, entityId));
|
|
20955
|
+
if (from) conditions.push(gte3(adsInsightsDaily.date, from));
|
|
20956
|
+
if (to) conditions.push(lte2(adsInsightsDaily.date, to));
|
|
20957
|
+
const rows = app.db.select().from(adsInsightsDaily).where(and14(...conditions)).orderBy(asc2(adsInsightsDaily.date)).all();
|
|
20958
|
+
const dtoRows = rows.map((row) => ({
|
|
20959
|
+
level: row.level,
|
|
20960
|
+
entityId: row.entityId,
|
|
20961
|
+
date: row.date,
|
|
20962
|
+
impressions: row.impressions,
|
|
20963
|
+
clicks: row.clicks,
|
|
20964
|
+
spendMicros: row.spendMicros,
|
|
20965
|
+
ctr: adsCtr(row.clicks, row.impressions),
|
|
20966
|
+
cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
|
|
20967
|
+
}));
|
|
20968
|
+
const conn = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20969
|
+
const response = { rows: dtoRows, currencyCode: conn?.currencyCode ?? null };
|
|
20970
|
+
return response;
|
|
20971
|
+
});
|
|
20972
|
+
app.get("/projects/:name/ads/summary", async (request) => {
|
|
20973
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20974
|
+
const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20975
|
+
const campaignCount = app.db.select().from(adsCampaigns).where(eq21(adsCampaigns.projectId, project.id)).all().length;
|
|
20976
|
+
const adGroupCount = app.db.select().from(adsAdGroups).where(eq21(adsAdGroups.projectId, project.id)).all().length;
|
|
20977
|
+
const adCount = app.db.select().from(adsAds).where(eq21(adsAds.projectId, project.id)).all().length;
|
|
20978
|
+
const campaignInsights = app.db.select().from(adsInsightsDaily).where(and14(
|
|
20979
|
+
eq21(adsInsightsDaily.projectId, project.id),
|
|
20980
|
+
eq21(adsInsightsDaily.level, "campaign")
|
|
20981
|
+
)).all();
|
|
20982
|
+
let impressions = 0;
|
|
20983
|
+
let clicks = 0;
|
|
20984
|
+
let spendMicros = 0;
|
|
20985
|
+
let fromDate = null;
|
|
20986
|
+
let toDate = null;
|
|
20987
|
+
for (const insight of campaignInsights) {
|
|
20988
|
+
impressions += insight.impressions;
|
|
20989
|
+
clicks += insight.clicks;
|
|
20990
|
+
spendMicros += insight.spendMicros;
|
|
20991
|
+
if (fromDate === null || insight.date < fromDate) fromDate = insight.date;
|
|
20992
|
+
if (toDate === null || insight.date > toDate) toDate = insight.date;
|
|
20993
|
+
}
|
|
20994
|
+
const response = {
|
|
20995
|
+
connected: Boolean(row),
|
|
20996
|
+
displayName: row?.displayName ?? null,
|
|
20997
|
+
currencyCode: row?.currencyCode ?? null,
|
|
20998
|
+
lastSyncedAt: row?.lastSyncedAt ?? null,
|
|
20999
|
+
campaignCount,
|
|
21000
|
+
adGroupCount,
|
|
21001
|
+
adCount,
|
|
21002
|
+
window: { from: fromDate, to: toDate },
|
|
21003
|
+
totals: {
|
|
21004
|
+
impressions,
|
|
21005
|
+
clicks,
|
|
21006
|
+
spendMicros,
|
|
21007
|
+
ctr: adsCtr(clicks, impressions),
|
|
21008
|
+
cpcMicros: adsCpcMicros(spendMicros, clicks)
|
|
21009
|
+
}
|
|
21010
|
+
};
|
|
21011
|
+
return response;
|
|
21012
|
+
});
|
|
21013
|
+
}
|
|
21014
|
+
|
|
21015
|
+
// ../api-routes/src/bing.ts
|
|
21016
|
+
import crypto19 from "crypto";
|
|
21017
|
+
import { eq as eq22, and as and15, desc as desc11 } from "drizzle-orm";
|
|
20357
21018
|
|
|
20358
21019
|
// ../integration-bing/src/constants.ts
|
|
20359
21020
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
20360
21021
|
var BING_SUBMIT_URL_BATCH_LIMIT = 500;
|
|
20361
21022
|
var BING_SUBMIT_URL_DAILY_LIMIT = 1e4;
|
|
20362
21023
|
var BING_REQUEST_TIMEOUT_MS = 3e4;
|
|
21024
|
+
var BING_LINKS_MAX_PAGES = 20;
|
|
20363
21025
|
|
|
20364
21026
|
// ../integration-bing/src/types.ts
|
|
20365
21027
|
var BingApiError = class extends Error {
|
|
@@ -20523,6 +21185,51 @@ async function getCrawlIssues(apiKey, siteUrl) {
|
|
|
20523
21185
|
const data = await bingFetch(apiKey, `GetCrawlIssues?siteUrl=${encodedSite}`);
|
|
20524
21186
|
return data ?? [];
|
|
20525
21187
|
}
|
|
21188
|
+
async function getLinkCounts(apiKey, siteUrl, opts = {}) {
|
|
21189
|
+
validateApiKey(apiKey);
|
|
21190
|
+
validateSiteUrl2(siteUrl);
|
|
21191
|
+
const encodedSite = encodeURIComponent(siteUrl);
|
|
21192
|
+
const maxPages = Math.max(1, opts.maxPages ?? BING_LINKS_MAX_PAGES);
|
|
21193
|
+
const out = [];
|
|
21194
|
+
let page = 0;
|
|
21195
|
+
let totalPages = 1;
|
|
21196
|
+
while (page < totalPages && page < maxPages) {
|
|
21197
|
+
const data = await bingFetch(apiKey, `GetLinkCounts?siteUrl=${encodedSite}&page=${page}`);
|
|
21198
|
+
for (const link of data?.Links ?? []) {
|
|
21199
|
+
if (link && typeof link.Url === "string") {
|
|
21200
|
+
out.push({ Url: link.Url, Count: Number(link.Count ?? 0) });
|
|
21201
|
+
}
|
|
21202
|
+
}
|
|
21203
|
+
totalPages = Number(data?.TotalPages ?? 1) || 1;
|
|
21204
|
+
page++;
|
|
21205
|
+
}
|
|
21206
|
+
return out;
|
|
21207
|
+
}
|
|
21208
|
+
async function getUrlLinks(apiKey, siteUrl, link, opts = {}) {
|
|
21209
|
+
validateApiKey(apiKey);
|
|
21210
|
+
validateSiteUrl2(siteUrl);
|
|
21211
|
+
validateUrl2(link);
|
|
21212
|
+
const encodedSite = encodeURIComponent(siteUrl);
|
|
21213
|
+
const encodedLink = encodeURIComponent(link);
|
|
21214
|
+
const maxPages = Math.max(1, opts.maxPages ?? BING_LINKS_MAX_PAGES);
|
|
21215
|
+
const out = [];
|
|
21216
|
+
let page = 0;
|
|
21217
|
+
let totalPages = 1;
|
|
21218
|
+
while (page < totalPages && page < maxPages) {
|
|
21219
|
+
const data = await bingFetch(
|
|
21220
|
+
apiKey,
|
|
21221
|
+
`GetUrlLinks?siteUrl=${encodedSite}&link=${encodedLink}&page=${page}`
|
|
21222
|
+
);
|
|
21223
|
+
for (const detail of data?.Details ?? []) {
|
|
21224
|
+
if (detail && typeof detail.Url === "string") {
|
|
21225
|
+
out.push({ Url: detail.Url, AnchorText: detail.AnchorText });
|
|
21226
|
+
}
|
|
21227
|
+
}
|
|
21228
|
+
totalPages = Number(data?.TotalPages ?? 1) || 1;
|
|
21229
|
+
page++;
|
|
21230
|
+
}
|
|
21231
|
+
return out;
|
|
21232
|
+
}
|
|
20526
21233
|
|
|
20527
21234
|
// ../api-routes/src/bing.ts
|
|
20528
21235
|
function parseBingDate(value) {
|
|
@@ -20681,7 +21388,7 @@ async function bingRoutes(app, opts) {
|
|
|
20681
21388
|
const store = requireConnectionStore();
|
|
20682
21389
|
const project = resolveProject(app.db, request.params.name);
|
|
20683
21390
|
requireConnection(store, project.canonicalDomain);
|
|
20684
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
21391
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq22(bingUrlInspections.projectId, project.id)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
|
|
20685
21392
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
20686
21393
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
20687
21394
|
for (const row of allInspections) {
|
|
@@ -20738,7 +21445,7 @@ async function bingRoutes(app, opts) {
|
|
|
20738
21445
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
20739
21446
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20740
21447
|
app.db.insert(bingCoverageSnapshots).values({
|
|
20741
|
-
id:
|
|
21448
|
+
id: crypto19.randomUUID(),
|
|
20742
21449
|
projectId: project.id,
|
|
20743
21450
|
syncRunId: snapshotRunId,
|
|
20744
21451
|
date: snapshotDate,
|
|
@@ -20770,7 +21477,7 @@ async function bingRoutes(app, opts) {
|
|
|
20770
21477
|
const project = resolveProject(app.db, request.params.name);
|
|
20771
21478
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
20772
21479
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
20773
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
21480
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq22(bingCoverageSnapshots.projectId, project.id)).orderBy(desc11(bingCoverageSnapshots.date)).limit(limit).all();
|
|
20774
21481
|
return rows.map((r) => ({
|
|
20775
21482
|
date: r.date,
|
|
20776
21483
|
indexed: r.indexed,
|
|
@@ -20782,7 +21489,7 @@ async function bingRoutes(app, opts) {
|
|
|
20782
21489
|
requireConnectionStore();
|
|
20783
21490
|
const project = resolveProject(app.db, request.params.name);
|
|
20784
21491
|
const { url, limit } = request.query;
|
|
20785
|
-
const whereClause = url ?
|
|
21492
|
+
const whereClause = url ? and15(eq22(bingUrlInspections.projectId, project.id), eq22(bingUrlInspections.url, url)) : eq22(bingUrlInspections.projectId, project.id);
|
|
20786
21493
|
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc11(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
20787
21494
|
return filtered.map((r) => ({
|
|
20788
21495
|
id: r.id,
|
|
@@ -20809,7 +21516,7 @@ async function bingRoutes(app, opts) {
|
|
|
20809
21516
|
throw validationError("url is required");
|
|
20810
21517
|
}
|
|
20811
21518
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20812
|
-
const runId =
|
|
21519
|
+
const runId = crypto19.randomUUID();
|
|
20813
21520
|
app.db.insert(runs).values({
|
|
20814
21521
|
id: runId,
|
|
20815
21522
|
projectId: project.id,
|
|
@@ -20830,7 +21537,7 @@ async function bingRoutes(app, opts) {
|
|
|
20830
21537
|
discoveryDate: result.DiscoveryDate ?? null
|
|
20831
21538
|
});
|
|
20832
21539
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20833
|
-
const id =
|
|
21540
|
+
const id = crypto19.randomUUID();
|
|
20834
21541
|
const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
|
|
20835
21542
|
const lastCrawledDate = parseBingDate(result.LastCrawledDate);
|
|
20836
21543
|
const inIndexDate = parseBingDate(result.InIndexDate);
|
|
@@ -20872,7 +21579,7 @@ async function bingRoutes(app, opts) {
|
|
|
20872
21579
|
anchorCount: result.AnchorCount ?? null,
|
|
20873
21580
|
discoveryDate
|
|
20874
21581
|
}).run();
|
|
20875
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
21582
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq22(runs.id, runId)).run();
|
|
20876
21583
|
return {
|
|
20877
21584
|
id,
|
|
20878
21585
|
url,
|
|
@@ -20888,7 +21595,7 @@ async function bingRoutes(app, opts) {
|
|
|
20888
21595
|
} catch (e) {
|
|
20889
21596
|
const msg = e instanceof Error ? e.message : String(e);
|
|
20890
21597
|
bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
|
|
20891
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
21598
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
|
|
20892
21599
|
throw e;
|
|
20893
21600
|
}
|
|
20894
21601
|
});
|
|
@@ -20900,7 +21607,7 @@ async function bingRoutes(app, opts) {
|
|
|
20900
21607
|
throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
|
|
20901
21608
|
}
|
|
20902
21609
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20903
|
-
const runId =
|
|
21610
|
+
const runId = crypto19.randomUUID();
|
|
20904
21611
|
app.db.insert(runs).values({
|
|
20905
21612
|
id: runId,
|
|
20906
21613
|
projectId: project.id,
|
|
@@ -20915,7 +21622,7 @@ async function bingRoutes(app, opts) {
|
|
|
20915
21622
|
} else {
|
|
20916
21623
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
20917
21624
|
}
|
|
20918
|
-
const run = app.db.select().from(runs).where(
|
|
21625
|
+
const run = app.db.select().from(runs).where(eq22(runs.id, runId)).get();
|
|
20919
21626
|
return run;
|
|
20920
21627
|
});
|
|
20921
21628
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -20927,7 +21634,7 @@ async function bingRoutes(app, opts) {
|
|
|
20927
21634
|
}
|
|
20928
21635
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
20929
21636
|
if (request.body?.allUnindexed) {
|
|
20930
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
21637
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq22(bingUrlInspections.projectId, project.id)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
|
|
20931
21638
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
20932
21639
|
for (const row of allInspections) {
|
|
20933
21640
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -21014,14 +21721,14 @@ async function bingRoutes(app, opts) {
|
|
|
21014
21721
|
import fs from "fs";
|
|
21015
21722
|
import path from "path";
|
|
21016
21723
|
import os from "os";
|
|
21017
|
-
import { eq as
|
|
21724
|
+
import { eq as eq23, and as and16 } from "drizzle-orm";
|
|
21018
21725
|
function getScreenshotDir() {
|
|
21019
21726
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
21020
21727
|
}
|
|
21021
21728
|
async function cdpRoutes(app, opts) {
|
|
21022
21729
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
21023
21730
|
const { snapshotId } = request.params;
|
|
21024
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(
|
|
21731
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq23(querySnapshots.id, snapshotId)).get();
|
|
21025
21732
|
if (!snapshot?.screenshotPath) {
|
|
21026
21733
|
const err = notFound("Screenshot", snapshotId);
|
|
21027
21734
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -21088,7 +21795,7 @@ async function cdpRoutes(app, opts) {
|
|
|
21088
21795
|
async (request, reply) => {
|
|
21089
21796
|
const project = resolveProject(app.db, request.params.name);
|
|
21090
21797
|
const { runId } = request.params;
|
|
21091
|
-
const run = app.db.select().from(runs).where(
|
|
21798
|
+
const run = app.db.select().from(runs).where(and16(eq23(runs.id, runId), eq23(runs.projectId, project.id))).get();
|
|
21092
21799
|
if (!run) {
|
|
21093
21800
|
const err = notFound("Run", runId);
|
|
21094
21801
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -21101,8 +21808,8 @@ async function cdpRoutes(app, opts) {
|
|
|
21101
21808
|
citedDomains: querySnapshots.citedDomains,
|
|
21102
21809
|
screenshotPath: querySnapshots.screenshotPath,
|
|
21103
21810
|
rawResponse: querySnapshots.rawResponse
|
|
21104
|
-
}).from(querySnapshots).where(
|
|
21105
|
-
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
21811
|
+
}).from(querySnapshots).where(eq23(querySnapshots.runId, runId)).all());
|
|
21812
|
+
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq23(queries.projectId, project.id)).all();
|
|
21106
21813
|
const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
|
|
21107
21814
|
const byQuery = /* @__PURE__ */ new Map();
|
|
21108
21815
|
for (const snap of snapshots) {
|
|
@@ -21184,8 +21891,8 @@ async function cdpRoutes(app, opts) {
|
|
|
21184
21891
|
}
|
|
21185
21892
|
|
|
21186
21893
|
// ../api-routes/src/ga.ts
|
|
21187
|
-
import
|
|
21188
|
-
import { eq as
|
|
21894
|
+
import crypto20 from "crypto";
|
|
21895
|
+
import { eq as eq24, desc as desc12, and as and17, sql as sql9 } from "drizzle-orm";
|
|
21189
21896
|
function gaLog(level, action, ctx) {
|
|
21190
21897
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
21191
21898
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -21392,10 +22099,10 @@ async function ga4Routes(app, opts) {
|
|
|
21392
22099
|
if (!saConn && !oauthConn) {
|
|
21393
22100
|
throw notFound("GA4 connection", project.name);
|
|
21394
22101
|
}
|
|
21395
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
21396
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
21397
|
-
app.db.delete(gaAiReferrals).where(
|
|
21398
|
-
app.db.delete(gaSocialReferrals).where(
|
|
22102
|
+
app.db.delete(gaTrafficSnapshots).where(eq24(gaTrafficSnapshots.projectId, project.id)).run();
|
|
22103
|
+
app.db.delete(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).run();
|
|
22104
|
+
app.db.delete(gaAiReferrals).where(eq24(gaAiReferrals.projectId, project.id)).run();
|
|
22105
|
+
app.db.delete(gaSocialReferrals).where(eq24(gaSocialReferrals.projectId, project.id)).run();
|
|
21399
22106
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
21400
22107
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
21401
22108
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -21416,7 +22123,7 @@ async function ga4Routes(app, opts) {
|
|
|
21416
22123
|
if (!connected) {
|
|
21417
22124
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
21418
22125
|
}
|
|
21419
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
22126
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).orderBy(desc12(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
21420
22127
|
return {
|
|
21421
22128
|
connected: true,
|
|
21422
22129
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -21440,7 +22147,7 @@ async function ga4Routes(app, opts) {
|
|
|
21440
22147
|
const syncAi = !only || only === "ai";
|
|
21441
22148
|
const syncSocial = !only || only === "social";
|
|
21442
22149
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21443
|
-
const runId =
|
|
22150
|
+
const runId = crypto20.randomUUID();
|
|
21444
22151
|
app.db.insert(runs).values({
|
|
21445
22152
|
id: runId,
|
|
21446
22153
|
projectId: project.id,
|
|
@@ -21480,15 +22187,15 @@ async function ga4Routes(app, opts) {
|
|
|
21480
22187
|
app.db.transaction((tx) => {
|
|
21481
22188
|
if (syncTraffic) {
|
|
21482
22189
|
tx.delete(gaTrafficSnapshots).where(
|
|
21483
|
-
|
|
21484
|
-
|
|
22190
|
+
and17(
|
|
22191
|
+
eq24(gaTrafficSnapshots.projectId, project.id),
|
|
21485
22192
|
sql9`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
21486
22193
|
sql9`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
21487
22194
|
)
|
|
21488
22195
|
).run();
|
|
21489
22196
|
for (const row of rows) {
|
|
21490
22197
|
tx.insert(gaTrafficSnapshots).values({
|
|
21491
|
-
id:
|
|
22198
|
+
id: crypto20.randomUUID(),
|
|
21492
22199
|
projectId: project.id,
|
|
21493
22200
|
date: row.date,
|
|
21494
22201
|
landingPage: row.landingPage,
|
|
@@ -21504,15 +22211,15 @@ async function ga4Routes(app, opts) {
|
|
|
21504
22211
|
}
|
|
21505
22212
|
if (syncAi) {
|
|
21506
22213
|
tx.delete(gaAiReferrals).where(
|
|
21507
|
-
|
|
21508
|
-
|
|
22214
|
+
and17(
|
|
22215
|
+
eq24(gaAiReferrals.projectId, project.id),
|
|
21509
22216
|
sql9`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
21510
22217
|
sql9`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
21511
22218
|
)
|
|
21512
22219
|
).run();
|
|
21513
22220
|
for (const row of aiReferrals) {
|
|
21514
22221
|
tx.insert(gaAiReferrals).values({
|
|
21515
|
-
id:
|
|
22222
|
+
id: crypto20.randomUUID(),
|
|
21516
22223
|
projectId: project.id,
|
|
21517
22224
|
date: row.date,
|
|
21518
22225
|
source: row.source,
|
|
@@ -21530,15 +22237,15 @@ async function ga4Routes(app, opts) {
|
|
|
21530
22237
|
}
|
|
21531
22238
|
if (syncSocial) {
|
|
21532
22239
|
tx.delete(gaSocialReferrals).where(
|
|
21533
|
-
|
|
21534
|
-
|
|
22240
|
+
and17(
|
|
22241
|
+
eq24(gaSocialReferrals.projectId, project.id),
|
|
21535
22242
|
sql9`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
21536
22243
|
sql9`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
21537
22244
|
)
|
|
21538
22245
|
).run();
|
|
21539
22246
|
for (const row of socialReferrals) {
|
|
21540
22247
|
tx.insert(gaSocialReferrals).values({
|
|
21541
|
-
id:
|
|
22248
|
+
id: crypto20.randomUUID(),
|
|
21542
22249
|
projectId: project.id,
|
|
21543
22250
|
date: row.date,
|
|
21544
22251
|
source: row.source,
|
|
@@ -21552,9 +22259,9 @@ async function ga4Routes(app, opts) {
|
|
|
21552
22259
|
}
|
|
21553
22260
|
}
|
|
21554
22261
|
if (syncSummary) {
|
|
21555
|
-
tx.delete(gaTrafficSummaries).where(
|
|
22262
|
+
tx.delete(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).run();
|
|
21556
22263
|
tx.insert(gaTrafficSummaries).values({
|
|
21557
|
-
id:
|
|
22264
|
+
id: crypto20.randomUUID(),
|
|
21558
22265
|
projectId: project.id,
|
|
21559
22266
|
periodStart: summary.periodStart,
|
|
21560
22267
|
periodEnd: summary.periodEnd,
|
|
@@ -21564,10 +22271,10 @@ async function ga4Routes(app, opts) {
|
|
|
21564
22271
|
syncedAt: now,
|
|
21565
22272
|
syncRunId: runId
|
|
21566
22273
|
}).run();
|
|
21567
|
-
tx.delete(gaTrafficWindowSummaries).where(
|
|
22274
|
+
tx.delete(gaTrafficWindowSummaries).where(eq24(gaTrafficWindowSummaries.projectId, project.id)).run();
|
|
21568
22275
|
for (const ws of windowSummaries) {
|
|
21569
22276
|
tx.insert(gaTrafficWindowSummaries).values({
|
|
21570
|
-
id:
|
|
22277
|
+
id: crypto20.randomUUID(),
|
|
21571
22278
|
projectId: project.id,
|
|
21572
22279
|
windowKey: ws.windowKey,
|
|
21573
22280
|
periodStart: ws.periodStart,
|
|
@@ -21582,7 +22289,7 @@ async function ga4Routes(app, opts) {
|
|
|
21582
22289
|
}
|
|
21583
22290
|
}
|
|
21584
22291
|
});
|
|
21585
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
22292
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq24(runs.id, runId)).run();
|
|
21586
22293
|
const syncedComponents = only ? [
|
|
21587
22294
|
...syncTraffic ? ["traffic"] : [],
|
|
21588
22295
|
...syncSummary ? ["summary"] : [],
|
|
@@ -21611,7 +22318,7 @@ async function ga4Routes(app, opts) {
|
|
|
21611
22318
|
} catch (e) {
|
|
21612
22319
|
const msg = e instanceof Error ? e.message : String(e);
|
|
21613
22320
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
|
|
21614
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
22321
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
|
|
21615
22322
|
throw e;
|
|
21616
22323
|
}
|
|
21617
22324
|
});
|
|
@@ -21622,11 +22329,11 @@ async function ga4Routes(app, opts) {
|
|
|
21622
22329
|
const window = parseWindow(request.query.window);
|
|
21623
22330
|
const cutoff = windowCutoff(window);
|
|
21624
22331
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
21625
|
-
const snapshotConditions = [
|
|
22332
|
+
const snapshotConditions = [eq24(gaTrafficSnapshots.projectId, project.id)];
|
|
21626
22333
|
if (cutoffDate) snapshotConditions.push(sql9`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
21627
|
-
const aiConditions = [
|
|
22334
|
+
const aiConditions = [eq24(gaAiReferrals.projectId, project.id)];
|
|
21628
22335
|
if (cutoffDate) aiConditions.push(sql9`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
21629
|
-
const socialConditions = [
|
|
22336
|
+
const socialConditions = [eq24(gaSocialReferrals.projectId, project.id)];
|
|
21630
22337
|
if (cutoffDate) socialConditions.push(sql9`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
21631
22338
|
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
21632
22339
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
@@ -21634,42 +22341,42 @@ async function ga4Routes(app, opts) {
|
|
|
21634
22341
|
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
21635
22342
|
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
21636
22343
|
}).from(gaTrafficWindowSummaries).where(
|
|
21637
|
-
|
|
21638
|
-
|
|
21639
|
-
|
|
22344
|
+
and17(
|
|
22345
|
+
eq24(gaTrafficWindowSummaries.projectId, project.id),
|
|
22346
|
+
eq24(gaTrafficWindowSummaries.windowKey, window)
|
|
21640
22347
|
)
|
|
21641
22348
|
).get() : null;
|
|
21642
22349
|
const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
|
|
21643
22350
|
totalSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
21644
22351
|
totalOrganicSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
21645
22352
|
totalUsers: sql9`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
21646
|
-
}).from(gaTrafficSnapshots).where(
|
|
22353
|
+
}).from(gaTrafficSnapshots).where(and17(...snapshotConditions)).get() : null;
|
|
21647
22354
|
const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
21648
22355
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
21649
22356
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
21650
22357
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
21651
|
-
}).from(gaTrafficSummaries).where(
|
|
22358
|
+
}).from(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).get();
|
|
21652
22359
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
21653
22360
|
totalDirectSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
21654
|
-
}).from(gaTrafficSnapshots).where(
|
|
22361
|
+
}).from(gaTrafficSnapshots).where(and17(...snapshotConditions)).get();
|
|
21655
22362
|
const summaryMeta = app.db.select({
|
|
21656
22363
|
periodStart: gaTrafficSummaries.periodStart,
|
|
21657
22364
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
21658
|
-
}).from(gaTrafficSummaries).where(
|
|
22365
|
+
}).from(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).get();
|
|
21659
22366
|
const rows = app.db.select({
|
|
21660
22367
|
landingPage: sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
21661
22368
|
sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
|
|
21662
22369
|
organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
21663
22370
|
directSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
|
|
21664
22371
|
users: sql9`SUM(${gaTrafficSnapshots.users})`
|
|
21665
|
-
}).from(gaTrafficSnapshots).where(
|
|
22372
|
+
}).from(gaTrafficSnapshots).where(and17(...snapshotConditions)).groupBy(sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql9`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
21666
22373
|
const aiReferralRows = app.db.select({
|
|
21667
22374
|
source: gaAiReferrals.source,
|
|
21668
22375
|
medium: gaAiReferrals.medium,
|
|
21669
22376
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
21670
22377
|
sessions: sql9`SUM(${gaAiReferrals.sessions})`,
|
|
21671
22378
|
users: sql9`SUM(${gaAiReferrals.users})`
|
|
21672
|
-
}).from(gaAiReferrals).where(
|
|
22379
|
+
}).from(gaAiReferrals).where(and17(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
|
|
21673
22380
|
const aiReferralLandingPageRows = app.db.select({
|
|
21674
22381
|
source: gaAiReferrals.source,
|
|
21675
22382
|
medium: gaAiReferrals.medium,
|
|
@@ -21677,7 +22384,7 @@ async function ga4Routes(app, opts) {
|
|
|
21677
22384
|
landingPage: sql9`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
21678
22385
|
sessions: sql9`SUM(${gaAiReferrals.sessions})`,
|
|
21679
22386
|
users: sql9`SUM(${gaAiReferrals.users})`
|
|
21680
|
-
}).from(gaAiReferrals).where(
|
|
22387
|
+
}).from(gaAiReferrals).where(and17(...aiConditions)).groupBy(
|
|
21681
22388
|
gaAiReferrals.source,
|
|
21682
22389
|
gaAiReferrals.medium,
|
|
21683
22390
|
gaAiReferrals.sourceDimension,
|
|
@@ -21714,7 +22421,7 @@ async function ga4Routes(app, opts) {
|
|
|
21714
22421
|
channelGroup: gaAiReferrals.channelGroup,
|
|
21715
22422
|
sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
|
|
21716
22423
|
users: sql9`COALESCE(SUM(${gaAiReferrals.users}), 0)`
|
|
21717
|
-
}).from(gaAiReferrals).where(
|
|
22424
|
+
}).from(gaAiReferrals).where(and17(...aiConditions, eq24(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
|
|
21718
22425
|
const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
|
|
21719
22426
|
let aiBySessionUsers = 0;
|
|
21720
22427
|
for (const row of aiBySessionRows) {
|
|
@@ -21728,12 +22435,12 @@ async function ga4Routes(app, opts) {
|
|
|
21728
22435
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
21729
22436
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`,
|
|
21730
22437
|
users: sql9`SUM(${gaSocialReferrals.users})`
|
|
21731
|
-
}).from(gaSocialReferrals).where(
|
|
22438
|
+
}).from(gaSocialReferrals).where(and17(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql9`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
21732
22439
|
const socialTotals = app.db.select({
|
|
21733
22440
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`,
|
|
21734
22441
|
users: sql9`SUM(${gaSocialReferrals.users})`
|
|
21735
|
-
}).from(gaSocialReferrals).where(
|
|
21736
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
22442
|
+
}).from(gaSocialReferrals).where(and17(...socialConditions)).get();
|
|
22443
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).orderBy(desc12(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
21737
22444
|
const total = summaryRow?.totalSessions ?? 0;
|
|
21738
22445
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
21739
22446
|
const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
|
|
@@ -21813,7 +22520,7 @@ async function ga4Routes(app, opts) {
|
|
|
21813
22520
|
const project = resolveProject(app.db, request.params.name);
|
|
21814
22521
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
21815
22522
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
21816
|
-
const conditions = [
|
|
22523
|
+
const conditions = [eq24(gaAiReferrals.projectId, project.id)];
|
|
21817
22524
|
if (cutoffDate) conditions.push(sql9`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
21818
22525
|
const rows = app.db.select({
|
|
21819
22526
|
date: gaAiReferrals.date,
|
|
@@ -21823,7 +22530,7 @@ async function ga4Routes(app, opts) {
|
|
|
21823
22530
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
21824
22531
|
sessions: sql9`SUM(${gaAiReferrals.sessions})`,
|
|
21825
22532
|
users: sql9`SUM(${gaAiReferrals.users})`
|
|
21826
|
-
}).from(gaAiReferrals).where(
|
|
22533
|
+
}).from(gaAiReferrals).where(and17(...conditions)).groupBy(
|
|
21827
22534
|
gaAiReferrals.date,
|
|
21828
22535
|
gaAiReferrals.source,
|
|
21829
22536
|
gaAiReferrals.medium,
|
|
@@ -21836,7 +22543,7 @@ async function ga4Routes(app, opts) {
|
|
|
21836
22543
|
const project = resolveProject(app.db, request.params.name);
|
|
21837
22544
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
21838
22545
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
21839
|
-
const conditions = [
|
|
22546
|
+
const conditions = [eq24(gaSocialReferrals.projectId, project.id)];
|
|
21840
22547
|
if (cutoffDate) conditions.push(sql9`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
21841
22548
|
const rows = app.db.select({
|
|
21842
22549
|
date: gaSocialReferrals.date,
|
|
@@ -21845,7 +22552,7 @@ async function ga4Routes(app, opts) {
|
|
|
21845
22552
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
21846
22553
|
sessions: gaSocialReferrals.sessions,
|
|
21847
22554
|
users: gaSocialReferrals.users
|
|
21848
|
-
}).from(gaSocialReferrals).where(
|
|
22555
|
+
}).from(gaSocialReferrals).where(and17(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
21849
22556
|
return rows;
|
|
21850
22557
|
});
|
|
21851
22558
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -21858,8 +22565,8 @@ async function ga4Routes(app, opts) {
|
|
|
21858
22565
|
d.setDate(d.getDate() - n);
|
|
21859
22566
|
return fmt(d);
|
|
21860
22567
|
};
|
|
21861
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
21862
|
-
|
|
22568
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and17(
|
|
22569
|
+
eq24(gaSocialReferrals.projectId, project.id),
|
|
21863
22570
|
sql9`${gaSocialReferrals.date} >= ${from}`,
|
|
21864
22571
|
sql9`${gaSocialReferrals.date} < ${to}`
|
|
21865
22572
|
)).get();
|
|
@@ -21871,16 +22578,16 @@ async function ga4Routes(app, opts) {
|
|
|
21871
22578
|
const sourceCurrent = app.db.select({
|
|
21872
22579
|
source: gaSocialReferrals.source,
|
|
21873
22580
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`
|
|
21874
|
-
}).from(gaSocialReferrals).where(
|
|
21875
|
-
|
|
22581
|
+
}).from(gaSocialReferrals).where(and17(
|
|
22582
|
+
eq24(gaSocialReferrals.projectId, project.id),
|
|
21876
22583
|
sql9`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
|
|
21877
22584
|
sql9`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
21878
22585
|
)).groupBy(gaSocialReferrals.source).all();
|
|
21879
22586
|
const sourcePrev = app.db.select({
|
|
21880
22587
|
source: gaSocialReferrals.source,
|
|
21881
22588
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`
|
|
21882
|
-
}).from(gaSocialReferrals).where(
|
|
21883
|
-
|
|
22589
|
+
}).from(gaSocialReferrals).where(and17(
|
|
22590
|
+
eq24(gaSocialReferrals.projectId, project.id),
|
|
21884
22591
|
sql9`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
|
|
21885
22592
|
sql9`${gaSocialReferrals.date} < ${daysAgo(7)}`
|
|
21886
22593
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -21921,16 +22628,16 @@ async function ga4Routes(app, opts) {
|
|
|
21921
22628
|
return fmt(d);
|
|
21922
22629
|
};
|
|
21923
22630
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
21924
|
-
const sumTotal = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
21925
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
21926
|
-
const sumDirect = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
21927
|
-
const sumAi = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
21928
|
-
|
|
22631
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and17(eq24(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
22632
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and17(eq24(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
22633
|
+
const sumDirect = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and17(eq24(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
22634
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and17(
|
|
22635
|
+
eq24(gaAiReferrals.projectId, project.id),
|
|
21929
22636
|
sql9`${gaAiReferrals.date} >= ${from}`,
|
|
21930
22637
|
sql9`${gaAiReferrals.date} < ${to}`,
|
|
21931
|
-
|
|
22638
|
+
eq24(gaAiReferrals.sourceDimension, "session")
|
|
21932
22639
|
)).get();
|
|
21933
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
22640
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and17(eq24(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${from}`, sql9`${gaSocialReferrals.date} < ${to}`)).get();
|
|
21934
22641
|
const todayStr = fmt(today);
|
|
21935
22642
|
const buildTrend = (sum) => {
|
|
21936
22643
|
const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
|
|
@@ -21939,17 +22646,17 @@ async function ga4Routes(app, opts) {
|
|
|
21939
22646
|
const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
|
|
21940
22647
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
21941
22648
|
};
|
|
21942
|
-
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
21943
|
-
|
|
22649
|
+
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and17(
|
|
22650
|
+
eq24(gaAiReferrals.projectId, project.id),
|
|
21944
22651
|
sql9`${gaAiReferrals.date} >= ${daysAgo(7)}`,
|
|
21945
22652
|
sql9`${gaAiReferrals.date} < ${todayStr}`,
|
|
21946
|
-
|
|
22653
|
+
eq24(gaAiReferrals.sourceDimension, "session")
|
|
21947
22654
|
)).groupBy(gaAiReferrals.source).all();
|
|
21948
|
-
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
21949
|
-
|
|
22655
|
+
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and17(
|
|
22656
|
+
eq24(gaAiReferrals.projectId, project.id),
|
|
21950
22657
|
sql9`${gaAiReferrals.date} >= ${daysAgo(14)}`,
|
|
21951
22658
|
sql9`${gaAiReferrals.date} < ${daysAgo(7)}`,
|
|
21952
|
-
|
|
22659
|
+
eq24(gaAiReferrals.sourceDimension, "session")
|
|
21953
22660
|
)).groupBy(gaAiReferrals.source).all();
|
|
21954
22661
|
const findBiggestMover = (current, prev) => {
|
|
21955
22662
|
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
@@ -21965,8 +22672,8 @@ async function ga4Routes(app, opts) {
|
|
|
21965
22672
|
}
|
|
21966
22673
|
return mover;
|
|
21967
22674
|
};
|
|
21968
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
21969
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
22675
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and17(eq24(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql9`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
22676
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and17(eq24(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql9`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
21970
22677
|
return {
|
|
21971
22678
|
total: buildTrend(sumTotal),
|
|
21972
22679
|
organic: buildTrend(sumOrganic),
|
|
@@ -21981,14 +22688,14 @@ async function ga4Routes(app, opts) {
|
|
|
21981
22688
|
const project = resolveProject(app.db, request.params.name);
|
|
21982
22689
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
21983
22690
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
21984
|
-
const conditions = [
|
|
22691
|
+
const conditions = [eq24(gaTrafficSnapshots.projectId, project.id)];
|
|
21985
22692
|
if (cutoffDate) conditions.push(sql9`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
21986
22693
|
const rows = app.db.select({
|
|
21987
22694
|
date: gaTrafficSnapshots.date,
|
|
21988
22695
|
sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
|
|
21989
22696
|
organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
21990
22697
|
users: sql9`SUM(${gaTrafficSnapshots.users})`
|
|
21991
|
-
}).from(gaTrafficSnapshots).where(
|
|
22698
|
+
}).from(gaTrafficSnapshots).where(and17(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
21992
22699
|
return rows.map((r) => ({
|
|
21993
22700
|
date: r.date,
|
|
21994
22701
|
sessions: r.sessions ?? 0,
|
|
@@ -22004,7 +22711,7 @@ async function ga4Routes(app, opts) {
|
|
|
22004
22711
|
sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
|
|
22005
22712
|
organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
22006
22713
|
users: sql9`SUM(${gaTrafficSnapshots.users})`
|
|
22007
|
-
}).from(gaTrafficSnapshots).where(
|
|
22714
|
+
}).from(gaTrafficSnapshots).where(eq24(gaTrafficSnapshots.projectId, project.id)).groupBy(sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql9`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
22008
22715
|
return {
|
|
22009
22716
|
pages: trafficPages.map((r) => ({
|
|
22010
22717
|
landingPage: r.landingPage,
|
|
@@ -22138,7 +22845,7 @@ function parseSchemaPageEntry(entry) {
|
|
|
22138
22845
|
}
|
|
22139
22846
|
|
|
22140
22847
|
// ../integration-wordpress/src/wordpress-client.ts
|
|
22141
|
-
import
|
|
22848
|
+
import crypto21 from "crypto";
|
|
22142
22849
|
function validateUsername(username) {
|
|
22143
22850
|
if (!username || typeof username !== "string" || username.trim().length === 0) {
|
|
22144
22851
|
throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
|
|
@@ -22351,7 +23058,7 @@ function buildSnippet(content) {
|
|
|
22351
23058
|
return `${text2.slice(0, 157)}...`;
|
|
22352
23059
|
}
|
|
22353
23060
|
function contentHash(content) {
|
|
22354
|
-
return
|
|
23061
|
+
return crypto21.createHash("sha256").update(content).digest("hex");
|
|
22355
23062
|
}
|
|
22356
23063
|
function buildAmbiguousSlugMessage(slug, pages) {
|
|
22357
23064
|
const candidates = pages.map((page) => {
|
|
@@ -23651,8 +24358,8 @@ async function wordpressRoutes(app, opts) {
|
|
|
23651
24358
|
}
|
|
23652
24359
|
|
|
23653
24360
|
// ../api-routes/src/backlinks.ts
|
|
23654
|
-
import
|
|
23655
|
-
import { and as
|
|
24361
|
+
import crypto22 from "crypto";
|
|
24362
|
+
import { and as and19, asc as asc3, desc as desc13, eq as eq25, sql as sql10 } from "drizzle-orm";
|
|
23656
24363
|
|
|
23657
24364
|
// ../integration-commoncrawl/src/constants.ts
|
|
23658
24365
|
import os2 from "os";
|
|
@@ -24055,7 +24762,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
24055
24762
|
}
|
|
24056
24763
|
|
|
24057
24764
|
// ../api-routes/src/backlinks-filter.ts
|
|
24058
|
-
import { and as
|
|
24765
|
+
import { and as and18, ne as ne3, notLike } from "drizzle-orm";
|
|
24059
24766
|
var BACKLINK_FILTER_PATTERNS = [
|
|
24060
24767
|
"*.google.com",
|
|
24061
24768
|
"*.googleusercontent.com",
|
|
@@ -24078,7 +24785,7 @@ function backlinkCrawlerExclusionClause() {
|
|
|
24078
24785
|
conditions.push(ne3(backlinkDomains.linkingDomain, pattern));
|
|
24079
24786
|
}
|
|
24080
24787
|
}
|
|
24081
|
-
const combined =
|
|
24788
|
+
const combined = and18(...conditions);
|
|
24082
24789
|
if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
|
|
24083
24790
|
return combined;
|
|
24084
24791
|
}
|
|
@@ -24121,9 +24828,18 @@ function mapSummaryRow(row) {
|
|
|
24121
24828
|
totalLinkingDomains: row.totalLinkingDomains,
|
|
24122
24829
|
totalHosts: row.totalHosts,
|
|
24123
24830
|
top10HostsShare: row.top10HostsShare,
|
|
24124
|
-
queriedAt: row.queriedAt
|
|
24831
|
+
queriedAt: row.queriedAt,
|
|
24832
|
+
source: row.source
|
|
24125
24833
|
};
|
|
24126
24834
|
}
|
|
24835
|
+
function parseSourceParam(value) {
|
|
24836
|
+
if (value === void 0 || value === "") return BacklinkSources.commoncrawl;
|
|
24837
|
+
const parsed = backlinkSourceSchema.safeParse(value);
|
|
24838
|
+
if (!parsed.success) {
|
|
24839
|
+
throw validationError(`Invalid source "${value}". Expected one of: ${Object.values(BacklinkSources).join(", ")}.`);
|
|
24840
|
+
}
|
|
24841
|
+
return parsed.data;
|
|
24842
|
+
}
|
|
24127
24843
|
function mapRunRow(row) {
|
|
24128
24844
|
return {
|
|
24129
24845
|
id: row.id,
|
|
@@ -24138,9 +24854,13 @@ function mapRunRow(row) {
|
|
|
24138
24854
|
createdAt: row.createdAt
|
|
24139
24855
|
};
|
|
24140
24856
|
}
|
|
24141
|
-
function latestSummaryForProject(db, projectId, release) {
|
|
24142
|
-
const
|
|
24143
|
-
|
|
24857
|
+
function latestSummaryForProject(db, projectId, source, release) {
|
|
24858
|
+
const conditions = [
|
|
24859
|
+
eq25(backlinkSummaries.projectId, projectId),
|
|
24860
|
+
eq25(backlinkSummaries.source, source)
|
|
24861
|
+
];
|
|
24862
|
+
if (release) conditions.push(eq25(backlinkSummaries.release, release));
|
|
24863
|
+
return db.select().from(backlinkSummaries).where(and19(...conditions)).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
|
|
24144
24864
|
}
|
|
24145
24865
|
function parseExcludeCrawlers(value) {
|
|
24146
24866
|
if (!value) return false;
|
|
@@ -24148,11 +24868,12 @@ function parseExcludeCrawlers(value) {
|
|
|
24148
24868
|
return lower === "1" || lower === "true" || lower === "yes";
|
|
24149
24869
|
}
|
|
24150
24870
|
function computeFilteredSummary(db, base) {
|
|
24151
|
-
const baseDomainCondition =
|
|
24152
|
-
|
|
24153
|
-
|
|
24871
|
+
const baseDomainCondition = and19(
|
|
24872
|
+
eq25(backlinkDomains.projectId, base.projectId),
|
|
24873
|
+
eq25(backlinkDomains.source, base.source),
|
|
24874
|
+
eq25(backlinkDomains.release, base.release)
|
|
24154
24875
|
);
|
|
24155
|
-
const filteredCondition =
|
|
24876
|
+
const filteredCondition = and19(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
24156
24877
|
const unfilteredAgg = db.select({
|
|
24157
24878
|
count: sql10`count(*)`,
|
|
24158
24879
|
total: sql10`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
@@ -24176,10 +24897,48 @@ function computeFilteredSummary(db, base) {
|
|
|
24176
24897
|
totalHosts,
|
|
24177
24898
|
top10HostsShare: top10Share.toFixed(6),
|
|
24178
24899
|
queriedAt: base.queriedAt,
|
|
24900
|
+
source: base.source,
|
|
24179
24901
|
excludedLinkingDomains: Math.max(0, unfilteredLinkingDomains - totalLinkingDomains),
|
|
24180
24902
|
excludedHosts: Math.max(0, unfilteredHosts - totalHosts)
|
|
24181
24903
|
};
|
|
24182
24904
|
}
|
|
24905
|
+
function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
|
|
24906
|
+
const summary = db.select().from(backlinkSummaries).where(and19(eq25(backlinkSummaries.projectId, projectId), eq25(backlinkSummaries.source, source))).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
|
|
24907
|
+
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
24908
|
+
if (summary && excludeCrawlers) {
|
|
24909
|
+
const filtered = db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(and19(
|
|
24910
|
+
eq25(backlinkDomains.projectId, projectId),
|
|
24911
|
+
eq25(backlinkDomains.source, source),
|
|
24912
|
+
eq25(backlinkDomains.release, summary.release),
|
|
24913
|
+
backlinkCrawlerExclusionClause()
|
|
24914
|
+
)).get();
|
|
24915
|
+
totalLinkingDomains = Number(filtered?.count ?? 0);
|
|
24916
|
+
}
|
|
24917
|
+
return {
|
|
24918
|
+
source,
|
|
24919
|
+
connected,
|
|
24920
|
+
hasData: !!summary,
|
|
24921
|
+
latestRelease: summary?.release ?? null,
|
|
24922
|
+
totalLinkingDomains,
|
|
24923
|
+
lastSyncedAt: summary?.queriedAt ?? null
|
|
24924
|
+
};
|
|
24925
|
+
}
|
|
24926
|
+
function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
|
|
24927
|
+
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
24928
|
+
const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
|
|
24929
|
+
const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
|
|
24930
|
+
const sources = [
|
|
24931
|
+
buildSourceAvailability(db, project.id, BacklinkSources.commoncrawl, ccConnected, excludeCrawlers),
|
|
24932
|
+
buildSourceAvailability(db, project.id, BacklinkSources["bing-webmaster"], bingConnected, excludeCrawlers)
|
|
24933
|
+
];
|
|
24934
|
+
return {
|
|
24935
|
+
projectId: project.id,
|
|
24936
|
+
targetDomain: project.canonicalDomain,
|
|
24937
|
+
sources,
|
|
24938
|
+
anyConnected: sources.some((s) => s.connected),
|
|
24939
|
+
anyData: sources.some((s) => s.hasData)
|
|
24940
|
+
};
|
|
24941
|
+
}
|
|
24183
24942
|
async function backlinksRoutes(app, opts) {
|
|
24184
24943
|
app.get("/backlinks/status", async (_request, reply) => {
|
|
24185
24944
|
if (!opts.getBacklinksStatus) {
|
|
@@ -24221,7 +24980,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
24221
24980
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
24222
24981
|
);
|
|
24223
24982
|
}
|
|
24224
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
24983
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.release, release)).get();
|
|
24225
24984
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24226
24985
|
if (existing) {
|
|
24227
24986
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -24232,12 +24991,12 @@ async function backlinksRoutes(app, opts) {
|
|
|
24232
24991
|
phaseDetail: null,
|
|
24233
24992
|
error: null,
|
|
24234
24993
|
updatedAt: now
|
|
24235
|
-
}).where(
|
|
24994
|
+
}).where(eq25(ccReleaseSyncs.id, existing.id)).run();
|
|
24236
24995
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
24237
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
24996
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.id, existing.id)).get();
|
|
24238
24997
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
24239
24998
|
}
|
|
24240
|
-
const id =
|
|
24999
|
+
const id = crypto22.randomUUID();
|
|
24241
25000
|
app.db.insert(ccReleaseSyncs).values({
|
|
24242
25001
|
id,
|
|
24243
25002
|
release,
|
|
@@ -24246,7 +25005,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
24246
25005
|
updatedAt: now
|
|
24247
25006
|
}).run();
|
|
24248
25007
|
opts.onReleaseSyncRequested(id, release);
|
|
24249
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
25008
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.id, id)).get();
|
|
24250
25009
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
24251
25010
|
});
|
|
24252
25011
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
@@ -24294,7 +25053,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
24294
25053
|
throw validationError("Invalid release id");
|
|
24295
25054
|
}
|
|
24296
25055
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24297
|
-
const runId =
|
|
25056
|
+
const runId = crypto22.randomUUID();
|
|
24298
25057
|
app.db.insert(runs).values({
|
|
24299
25058
|
id: runId,
|
|
24300
25059
|
projectId: project.id,
|
|
@@ -24304,14 +25063,15 @@ async function backlinksRoutes(app, opts) {
|
|
|
24304
25063
|
createdAt: now
|
|
24305
25064
|
}).run();
|
|
24306
25065
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
24307
|
-
const run = app.db.select().from(runs).where(
|
|
25066
|
+
const run = app.db.select().from(runs).where(eq25(runs.id, runId)).get();
|
|
24308
25067
|
return reply.status(201).send(mapRunRow(run));
|
|
24309
25068
|
});
|
|
24310
25069
|
app.get(
|
|
24311
25070
|
"/projects/:name/backlinks/summary",
|
|
24312
25071
|
async (request, reply) => {
|
|
24313
25072
|
const project = resolveProject(app.db, request.params.name);
|
|
24314
|
-
const
|
|
25073
|
+
const source = parseSourceParam(request.query.source);
|
|
25074
|
+
const row = latestSummaryForProject(app.db, project.id, source, request.query.release);
|
|
24315
25075
|
if (!row) return reply.send(null);
|
|
24316
25076
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
24317
25077
|
return reply.send(excludeCrawlers ? computeFilteredSummary(app.db, row) : mapSummaryRow(row));
|
|
@@ -24319,30 +25079,34 @@ async function backlinksRoutes(app, opts) {
|
|
|
24319
25079
|
);
|
|
24320
25080
|
app.get("/projects/:name/backlinks/domains", async (request, reply) => {
|
|
24321
25081
|
const project = resolveProject(app.db, request.params.name);
|
|
24322
|
-
const
|
|
25082
|
+
const source = parseSourceParam(request.query.source);
|
|
25083
|
+
const summaryRow = latestSummaryForProject(app.db, project.id, source, request.query.release);
|
|
24323
25084
|
const targetRelease = request.query.release ?? summaryRow?.release;
|
|
24324
25085
|
if (!targetRelease) {
|
|
24325
|
-
const response2 = { summary: null, total: 0, rows: [] };
|
|
25086
|
+
const response2 = { source, summary: null, total: 0, rows: [] };
|
|
24326
25087
|
return reply.send(response2);
|
|
24327
25088
|
}
|
|
24328
25089
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
24329
25090
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
24330
25091
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
24331
|
-
const baseDomainCondition =
|
|
24332
|
-
|
|
24333
|
-
|
|
25092
|
+
const baseDomainCondition = and19(
|
|
25093
|
+
eq25(backlinkDomains.projectId, project.id),
|
|
25094
|
+
eq25(backlinkDomains.source, source),
|
|
25095
|
+
eq25(backlinkDomains.release, targetRelease)
|
|
24334
25096
|
);
|
|
24335
|
-
const domainCondition = excludeCrawlers ?
|
|
25097
|
+
const domainCondition = excludeCrawlers ? and19(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
24336
25098
|
const totalRow = app.db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
24337
25099
|
const rows = app.db.select({
|
|
24338
25100
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
24339
|
-
numHosts: backlinkDomains.numHosts
|
|
25101
|
+
numHosts: backlinkDomains.numHosts,
|
|
25102
|
+
source: backlinkDomains.source
|
|
24340
25103
|
}).from(backlinkDomains).where(domainCondition).orderBy(desc13(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
|
|
24341
25104
|
let summary = null;
|
|
24342
25105
|
if (summaryRow) {
|
|
24343
25106
|
summary = excludeCrawlers ? computeFilteredSummary(app.db, summaryRow) : mapSummaryRow(summaryRow);
|
|
24344
25107
|
}
|
|
24345
25108
|
const response = {
|
|
25109
|
+
source,
|
|
24346
25110
|
summary,
|
|
24347
25111
|
total: Number(totalRow?.count ?? 0),
|
|
24348
25112
|
rows
|
|
@@ -24353,26 +25117,67 @@ async function backlinksRoutes(app, opts) {
|
|
|
24353
25117
|
"/projects/:name/backlinks/history",
|
|
24354
25118
|
async (request, reply) => {
|
|
24355
25119
|
const project = resolveProject(app.db, request.params.name);
|
|
24356
|
-
const
|
|
25120
|
+
const source = parseSourceParam(request.query.source);
|
|
25121
|
+
const rows = app.db.select().from(backlinkSummaries).where(and19(eq25(backlinkSummaries.projectId, project.id), eq25(backlinkSummaries.source, source))).orderBy(asc3(backlinkSummaries.queriedAt)).all();
|
|
24357
25122
|
const response = rows.map((r) => ({
|
|
24358
25123
|
release: r.release,
|
|
24359
25124
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
24360
25125
|
totalHosts: r.totalHosts,
|
|
24361
25126
|
top10HostsShare: r.top10HostsShare,
|
|
24362
|
-
queriedAt: r.queriedAt
|
|
25127
|
+
queriedAt: r.queriedAt,
|
|
25128
|
+
source: r.source
|
|
24363
25129
|
}));
|
|
24364
25130
|
return reply.send(response);
|
|
24365
25131
|
}
|
|
24366
25132
|
);
|
|
25133
|
+
app.get(
|
|
25134
|
+
"/projects/:name/backlinks/sources",
|
|
25135
|
+
async (request, reply) => {
|
|
25136
|
+
const project = resolveProject(app.db, request.params.name);
|
|
25137
|
+
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
25138
|
+
const response = computeSourceAvailability(app.db, project, opts.bingConnectionStore, excludeCrawlers);
|
|
25139
|
+
return reply.send(response);
|
|
25140
|
+
}
|
|
25141
|
+
);
|
|
25142
|
+
app.post(
|
|
25143
|
+
"/projects/:name/backlinks/bing-sync",
|
|
25144
|
+
async (request, reply) => {
|
|
25145
|
+
const project = resolveProject(app.db, request.params.name);
|
|
25146
|
+
if (!opts.onBingBacklinkSyncRequested) {
|
|
25147
|
+
throw missingDependency(
|
|
25148
|
+
"Bing backlinks sync is only available from a local canonry install with Bing Webmaster connected."
|
|
25149
|
+
);
|
|
25150
|
+
}
|
|
25151
|
+
const conn = opts.bingConnectionStore?.getConnection(project.canonicalDomain);
|
|
25152
|
+
if (!conn) {
|
|
25153
|
+
throw validationError(
|
|
25154
|
+
`No Bing Webmaster connection for "${project.name}". Run \`canonry bing connect ${project.name} --api-key <key>\` first.`
|
|
25155
|
+
);
|
|
25156
|
+
}
|
|
25157
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
25158
|
+
const runId = crypto22.randomUUID();
|
|
25159
|
+
app.db.insert(runs).values({
|
|
25160
|
+
id: runId,
|
|
25161
|
+
projectId: project.id,
|
|
25162
|
+
kind: RunKinds["backlink-extract"],
|
|
25163
|
+
status: RunStatuses.queued,
|
|
25164
|
+
trigger: RunTriggers.manual,
|
|
25165
|
+
createdAt: now
|
|
25166
|
+
}).run();
|
|
25167
|
+
opts.onBingBacklinkSyncRequested(runId, project.id);
|
|
25168
|
+
const run = app.db.select().from(runs).where(eq25(runs.id, runId)).get();
|
|
25169
|
+
return reply.status(201).send(mapRunRow(run));
|
|
25170
|
+
}
|
|
25171
|
+
);
|
|
24367
25172
|
}
|
|
24368
25173
|
|
|
24369
25174
|
// ../api-routes/src/traffic.ts
|
|
24370
|
-
import
|
|
25175
|
+
import crypto24 from "crypto";
|
|
24371
25176
|
import { Agent as UndiciAgent } from "undici";
|
|
24372
|
-
import { and as
|
|
25177
|
+
import { and as and20, desc as desc14, eq as eq26, gte as gte4, lte as lte3, sql as sql11 } from "drizzle-orm";
|
|
24373
25178
|
|
|
24374
25179
|
// ../integration-cloud-run/src/auth.ts
|
|
24375
|
-
import
|
|
25180
|
+
import crypto23 from "crypto";
|
|
24376
25181
|
var GOOGLE_TOKEN_URL3 = "https://oauth2.googleapis.com/token";
|
|
24377
25182
|
var CLOUD_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
|
|
24378
25183
|
var TOKEN_REQUEST_TIMEOUT_MS = 3e4;
|
|
@@ -24403,7 +25208,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
|
|
|
24403
25208
|
const headerB64 = encode(header);
|
|
24404
25209
|
const payloadB64 = encode(payload);
|
|
24405
25210
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
24406
|
-
const sign =
|
|
25211
|
+
const sign = crypto23.createSign("RSA-SHA256");
|
|
24407
25212
|
sign.update(signingInput);
|
|
24408
25213
|
const signature = sign.sign(privateKey, "base64url");
|
|
24409
25214
|
return `${signingInput}.${signature}`;
|
|
@@ -28186,8 +28991,8 @@ async function runBackfillTask(options) {
|
|
|
28186
28991
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28187
28992
|
try {
|
|
28188
28993
|
app.db.transaction((tx) => {
|
|
28189
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
28190
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
28994
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq26(runs.id, runId)).run();
|
|
28995
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq26(trafficSources.id, sourceRow.id)).run();
|
|
28191
28996
|
});
|
|
28192
28997
|
} catch {
|
|
28193
28998
|
}
|
|
@@ -28202,7 +29007,7 @@ async function runBackfillTask(options) {
|
|
|
28202
29007
|
if (allEvents.length === 0) {
|
|
28203
29008
|
const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
28204
29009
|
try {
|
|
28205
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(
|
|
29010
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq26(runs.id, runId)).run();
|
|
28206
29011
|
} catch {
|
|
28207
29012
|
}
|
|
28208
29013
|
return;
|
|
@@ -28224,31 +29029,31 @@ async function runBackfillTask(options) {
|
|
|
28224
29029
|
try {
|
|
28225
29030
|
app.db.transaction((tx) => {
|
|
28226
29031
|
tx.delete(crawlerEventsHourly).where(
|
|
28227
|
-
|
|
28228
|
-
|
|
28229
|
-
|
|
28230
|
-
|
|
29032
|
+
and20(
|
|
29033
|
+
eq26(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
29034
|
+
gte4(crawlerEventsHourly.tsHour, windowStartIso),
|
|
29035
|
+
lte3(crawlerEventsHourly.tsHour, windowEndIso)
|
|
28231
29036
|
)
|
|
28232
29037
|
).run();
|
|
28233
29038
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
28234
|
-
|
|
28235
|
-
|
|
28236
|
-
|
|
28237
|
-
|
|
29039
|
+
and20(
|
|
29040
|
+
eq26(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
29041
|
+
gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
29042
|
+
lte3(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
28238
29043
|
)
|
|
28239
29044
|
).run();
|
|
28240
29045
|
tx.delete(aiReferralEventsHourly).where(
|
|
28241
|
-
|
|
28242
|
-
|
|
28243
|
-
|
|
28244
|
-
|
|
29046
|
+
and20(
|
|
29047
|
+
eq26(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
29048
|
+
gte4(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
29049
|
+
lte3(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
28245
29050
|
)
|
|
28246
29051
|
).run();
|
|
28247
29052
|
tx.delete(rawEventSamples).where(
|
|
28248
|
-
|
|
28249
|
-
|
|
28250
|
-
|
|
28251
|
-
|
|
29053
|
+
and20(
|
|
29054
|
+
eq26(rawEventSamples.sourceId, sourceRow.id),
|
|
29055
|
+
gte4(rawEventSamples.ts, windowStartIso),
|
|
29056
|
+
lte3(rawEventSamples.ts, windowEndIso)
|
|
28252
29057
|
)
|
|
28253
29058
|
).run();
|
|
28254
29059
|
for (const bucket of report.crawlerEventsHourly) {
|
|
@@ -28311,7 +29116,7 @@ async function runBackfillTask(options) {
|
|
|
28311
29116
|
}
|
|
28312
29117
|
})();
|
|
28313
29118
|
tx.insert(rawEventSamples).values({
|
|
28314
|
-
id:
|
|
29119
|
+
id: crypto24.randomUUID(),
|
|
28315
29120
|
projectId: project.id,
|
|
28316
29121
|
sourceId: sourceRow.id,
|
|
28317
29122
|
ts: sample.observedAt,
|
|
@@ -28335,8 +29140,8 @@ async function runBackfillTask(options) {
|
|
|
28335
29140
|
lastError: null,
|
|
28336
29141
|
lastEventIds: newRingBuffer,
|
|
28337
29142
|
updatedAt: finishedAt
|
|
28338
|
-
}).where(
|
|
28339
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
29143
|
+
}).where(eq26(trafficSources.id, sourceRow.id)).run();
|
|
29144
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq26(runs.id, runId)).run();
|
|
28340
29145
|
});
|
|
28341
29146
|
} catch (e) {
|
|
28342
29147
|
markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -28418,7 +29223,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28418
29223
|
createdAt: existing?.createdAt ?? now,
|
|
28419
29224
|
updatedAt: now
|
|
28420
29225
|
});
|
|
28421
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
29226
|
+
const activeSource = app.db.select().from(trafficSources).where(eq26(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
|
|
28422
29227
|
const config = {
|
|
28423
29228
|
gcpProjectId,
|
|
28424
29229
|
serviceName: serviceName ?? null,
|
|
@@ -28434,10 +29239,10 @@ async function trafficRoutes(app, opts) {
|
|
|
28434
29239
|
lastError: null,
|
|
28435
29240
|
configJson: config,
|
|
28436
29241
|
updatedAt: now
|
|
28437
|
-
}).where(
|
|
28438
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29242
|
+
}).where(eq26(trafficSources.id, activeSource.id)).run();
|
|
29243
|
+
sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, activeSource.id)).get();
|
|
28439
29244
|
} else {
|
|
28440
|
-
const newId =
|
|
29245
|
+
const newId = crypto24.randomUUID();
|
|
28441
29246
|
app.db.insert(trafficSources).values({
|
|
28442
29247
|
id: newId,
|
|
28443
29248
|
projectId: project.id,
|
|
@@ -28452,7 +29257,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28452
29257
|
createdAt: now,
|
|
28453
29258
|
updatedAt: now
|
|
28454
29259
|
}).run();
|
|
28455
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29260
|
+
sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, newId)).get();
|
|
28456
29261
|
}
|
|
28457
29262
|
writeAuditLog(app.db, {
|
|
28458
29263
|
projectId: project.id,
|
|
@@ -28504,7 +29309,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28504
29309
|
createdAt: existing?.createdAt ?? now,
|
|
28505
29310
|
updatedAt: now
|
|
28506
29311
|
});
|
|
28507
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
29312
|
+
const activeSource = app.db.select().from(trafficSources).where(eq26(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
|
|
28508
29313
|
const config = { baseUrl, username };
|
|
28509
29314
|
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
28510
29315
|
let sourceRow;
|
|
@@ -28515,10 +29320,10 @@ async function trafficRoutes(app, opts) {
|
|
|
28515
29320
|
lastError: null,
|
|
28516
29321
|
configJson: config,
|
|
28517
29322
|
updatedAt: now
|
|
28518
|
-
}).where(
|
|
28519
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29323
|
+
}).where(eq26(trafficSources.id, activeSource.id)).run();
|
|
29324
|
+
sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, activeSource.id)).get();
|
|
28520
29325
|
} else {
|
|
28521
|
-
const newId =
|
|
29326
|
+
const newId = crypto24.randomUUID();
|
|
28522
29327
|
app.db.insert(trafficSources).values({
|
|
28523
29328
|
id: newId,
|
|
28524
29329
|
projectId: project.id,
|
|
@@ -28533,7 +29338,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28533
29338
|
createdAt: now,
|
|
28534
29339
|
updatedAt: now
|
|
28535
29340
|
}).run();
|
|
28536
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29341
|
+
sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, newId)).get();
|
|
28537
29342
|
}
|
|
28538
29343
|
writeAuditLog(app.db, {
|
|
28539
29344
|
projectId: project.id,
|
|
@@ -28587,7 +29392,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28587
29392
|
createdAt: existing?.createdAt ?? now,
|
|
28588
29393
|
updatedAt: now
|
|
28589
29394
|
});
|
|
28590
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
29395
|
+
const activeSource = app.db.select().from(trafficSources).where(eq26(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
|
|
28591
29396
|
const config = { projectId, teamId, environment };
|
|
28592
29397
|
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
28593
29398
|
const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
|
|
@@ -28599,10 +29404,10 @@ async function trafficRoutes(app, opts) {
|
|
|
28599
29404
|
lastError: null,
|
|
28600
29405
|
configJson: config,
|
|
28601
29406
|
updatedAt: now
|
|
28602
|
-
}).where(
|
|
28603
|
-
row = tx.select().from(trafficSources).where(
|
|
29407
|
+
}).where(eq26(trafficSources.id, activeSource.id)).run();
|
|
29408
|
+
row = tx.select().from(trafficSources).where(eq26(trafficSources.id, activeSource.id)).get();
|
|
28604
29409
|
} else {
|
|
28605
|
-
const newId =
|
|
29410
|
+
const newId = crypto24.randomUUID();
|
|
28606
29411
|
tx.insert(trafficSources).values({
|
|
28607
29412
|
id: newId,
|
|
28608
29413
|
projectId: project.id,
|
|
@@ -28625,18 +29430,18 @@ async function trafficRoutes(app, opts) {
|
|
|
28625
29430
|
createdAt: now,
|
|
28626
29431
|
updatedAt: now
|
|
28627
29432
|
}).run();
|
|
28628
|
-
row = tx.select().from(trafficSources).where(
|
|
29433
|
+
row = tx.select().from(trafficSources).where(eq26(trafficSources.id, newId)).get();
|
|
28629
29434
|
}
|
|
28630
29435
|
const existingSchedule = tx.select().from(schedules).where(
|
|
28631
|
-
|
|
28632
|
-
|
|
28633
|
-
|
|
29436
|
+
and20(
|
|
29437
|
+
eq26(schedules.projectId, project.id),
|
|
29438
|
+
eq26(schedules.kind, SchedulableRunKinds["traffic-sync"])
|
|
28634
29439
|
)
|
|
28635
29440
|
).get();
|
|
28636
29441
|
let created = false;
|
|
28637
29442
|
if (!existingSchedule) {
|
|
28638
29443
|
tx.insert(schedules).values({
|
|
28639
|
-
id:
|
|
29444
|
+
id: crypto24.randomUUID(),
|
|
28640
29445
|
projectId: project.id,
|
|
28641
29446
|
kind: SchedulableRunKinds["traffic-sync"],
|
|
28642
29447
|
cronExpr: DEFAULT_TRAFFIC_SYNC_CRON,
|
|
@@ -28679,7 +29484,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28679
29484
|
});
|
|
28680
29485
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
28681
29486
|
const project = resolveProject(app.db, request.params.name);
|
|
28682
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
29487
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, request.params.id)).get();
|
|
28683
29488
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
28684
29489
|
throw notFound("Traffic source", request.params.id);
|
|
28685
29490
|
}
|
|
@@ -28691,7 +29496,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28691
29496
|
const windowEnd = /* @__PURE__ */ new Date();
|
|
28692
29497
|
const startedAt = windowEnd.toISOString();
|
|
28693
29498
|
const syncStartedAtMs = windowEnd.getTime();
|
|
28694
|
-
const runId =
|
|
29499
|
+
const runId = crypto24.randomUUID();
|
|
28695
29500
|
app.db.insert(runs).values({
|
|
28696
29501
|
id: runId,
|
|
28697
29502
|
projectId: project.id,
|
|
@@ -28705,8 +29510,8 @@ async function trafficRoutes(app, opts) {
|
|
|
28705
29510
|
const markFailed = (msg, errorCode) => {
|
|
28706
29511
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28707
29512
|
app.db.transaction((tx) => {
|
|
28708
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
28709
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
29513
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq26(runs.id, runId)).run();
|
|
29514
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq26(trafficSources.id, sourceRow.id)).run();
|
|
28710
29515
|
});
|
|
28711
29516
|
try {
|
|
28712
29517
|
opts.onTrafficSynced?.({
|
|
@@ -28787,7 +29592,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28787
29592
|
}
|
|
28788
29593
|
const credential = credentialStore.getConnection(project.name);
|
|
28789
29594
|
if (!credential) {
|
|
28790
|
-
app.db.delete(runs).where(
|
|
29595
|
+
app.db.delete(runs).where(eq26(runs.id, runId)).run();
|
|
28791
29596
|
throw validationError(
|
|
28792
29597
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
28793
29598
|
);
|
|
@@ -28836,12 +29641,12 @@ async function trafficRoutes(app, opts) {
|
|
|
28836
29641
|
auditAction = "traffic.vercel.synced";
|
|
28837
29642
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
28838
29643
|
if (!credentialStore) {
|
|
28839
|
-
app.db.delete(runs).where(
|
|
29644
|
+
app.db.delete(runs).where(eq26(runs.id, runId)).run();
|
|
28840
29645
|
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
28841
29646
|
}
|
|
28842
29647
|
const credential = credentialStore.getConnection(project.name);
|
|
28843
29648
|
if (!credential) {
|
|
28844
|
-
app.db.delete(runs).where(
|
|
29649
|
+
app.db.delete(runs).where(eq26(runs.id, runId)).run();
|
|
28845
29650
|
throw validationError(
|
|
28846
29651
|
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
28847
29652
|
);
|
|
@@ -28941,7 +29746,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28941
29746
|
let aiReferralHitsCount = 0;
|
|
28942
29747
|
let unknownHitsCount = 0;
|
|
28943
29748
|
app.db.transaction((tx) => {
|
|
28944
|
-
const latestRow = tx.select().from(trafficSources).where(
|
|
29749
|
+
const latestRow = tx.select().from(trafficSources).where(eq26(trafficSources.id, sourceRow.id)).get();
|
|
28945
29750
|
const previousIds = latestRow.lastEventIds ?? [];
|
|
28946
29751
|
const seenEventIds = new Set(previousIds);
|
|
28947
29752
|
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
@@ -29074,7 +29879,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29074
29879
|
}
|
|
29075
29880
|
})();
|
|
29076
29881
|
tx.insert(rawEventSamples).values({
|
|
29077
|
-
id:
|
|
29882
|
+
id: crypto24.randomUUID(),
|
|
29078
29883
|
projectId: project.id,
|
|
29079
29884
|
sourceId: sourceRow.id,
|
|
29080
29885
|
ts: sample.observedAt,
|
|
@@ -29109,8 +29914,8 @@ async function trafficRoutes(app, opts) {
|
|
|
29109
29914
|
if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
29110
29915
|
sourceUpdate.lastCursor = nextCursor ?? null;
|
|
29111
29916
|
}
|
|
29112
|
-
tx.update(trafficSources).set(sourceUpdate).where(
|
|
29113
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
29917
|
+
tx.update(trafficSources).set(sourceUpdate).where(eq26(trafficSources.id, sourceRow.id)).run();
|
|
29918
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq26(runs.id, runId)).run();
|
|
29114
29919
|
});
|
|
29115
29920
|
writeAuditLog(app.db, {
|
|
29116
29921
|
projectId: project.id,
|
|
@@ -29162,7 +29967,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29162
29967
|
});
|
|
29163
29968
|
app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
|
|
29164
29969
|
const project = resolveProject(app.db, request.params.name);
|
|
29165
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
29970
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, request.params.id)).get();
|
|
29166
29971
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
29167
29972
|
throw notFound("Traffic source", request.params.id);
|
|
29168
29973
|
}
|
|
@@ -29312,7 +30117,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29312
30117
|
};
|
|
29313
30118
|
}
|
|
29314
30119
|
const startedAt = windowEnd.toISOString();
|
|
29315
|
-
const runId =
|
|
30120
|
+
const runId = crypto24.randomUUID();
|
|
29316
30121
|
app.db.insert(runs).values({
|
|
29317
30122
|
id: runId,
|
|
29318
30123
|
projectId: project.id,
|
|
@@ -29347,34 +30152,34 @@ async function trafficRoutes(app, opts) {
|
|
|
29347
30152
|
});
|
|
29348
30153
|
function buildSourceDetail(projectId, row, since) {
|
|
29349
30154
|
const crawlerTotals = app.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
29350
|
-
|
|
29351
|
-
|
|
29352
|
-
|
|
30155
|
+
and20(
|
|
30156
|
+
eq26(crawlerEventsHourly.sourceId, row.id),
|
|
30157
|
+
gte4(crawlerEventsHourly.tsHour, since)
|
|
29353
30158
|
)
|
|
29354
30159
|
).get();
|
|
29355
30160
|
const aiUserFetchTotals = app.db.select({ total: sql11`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
29356
|
-
|
|
29357
|
-
|
|
29358
|
-
|
|
30161
|
+
and20(
|
|
30162
|
+
eq26(aiUserFetchEventsHourly.sourceId, row.id),
|
|
30163
|
+
gte4(aiUserFetchEventsHourly.tsHour, since)
|
|
29359
30164
|
)
|
|
29360
30165
|
).get();
|
|
29361
30166
|
const aiTotals = app.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
29362
|
-
|
|
29363
|
-
|
|
29364
|
-
|
|
30167
|
+
and20(
|
|
30168
|
+
eq26(aiReferralEventsHourly.sourceId, row.id),
|
|
30169
|
+
gte4(aiReferralEventsHourly.tsHour, since)
|
|
29365
30170
|
)
|
|
29366
30171
|
).get();
|
|
29367
30172
|
const sampleTotals = app.db.select({ total: sql11`COUNT(*)` }).from(rawEventSamples).where(
|
|
29368
|
-
|
|
29369
|
-
|
|
29370
|
-
|
|
30173
|
+
and20(
|
|
30174
|
+
eq26(rawEventSamples.sourceId, row.id),
|
|
30175
|
+
gte4(rawEventSamples.ts, since)
|
|
29371
30176
|
)
|
|
29372
30177
|
).get();
|
|
29373
30178
|
const latestRun = app.db.select().from(runs).where(
|
|
29374
|
-
|
|
29375
|
-
|
|
29376
|
-
|
|
29377
|
-
|
|
30179
|
+
and20(
|
|
30180
|
+
eq26(runs.projectId, projectId),
|
|
30181
|
+
eq26(runs.kind, RunKinds["traffic-sync"]),
|
|
30182
|
+
eq26(runs.sourceId, row.id)
|
|
29378
30183
|
)
|
|
29379
30184
|
).orderBy(desc14(runs.startedAt)).limit(1).get();
|
|
29380
30185
|
return {
|
|
@@ -29402,7 +30207,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29402
30207
|
"`advanceToNow` must be `true`. There is no implicit reset."
|
|
29403
30208
|
);
|
|
29404
30209
|
}
|
|
29405
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
30210
|
+
const sourceRow = app.db.select().from(trafficSources).where(and20(eq26(trafficSources.projectId, project.id), eq26(trafficSources.id, request.params.id))).get();
|
|
29406
30211
|
if (!sourceRow) {
|
|
29407
30212
|
throw notFound("traffic source", request.params.id);
|
|
29408
30213
|
}
|
|
@@ -29419,7 +30224,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29419
30224
|
status: TrafficSourceStatuses.connected,
|
|
29420
30225
|
lastError: null,
|
|
29421
30226
|
updatedAt: now
|
|
29422
|
-
}).where(
|
|
30227
|
+
}).where(eq26(trafficSources.id, sourceRow.id)).run();
|
|
29423
30228
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
29424
30229
|
projectId: project.id,
|
|
29425
30230
|
actor: "api",
|
|
@@ -29427,20 +30232,20 @@ async function trafficRoutes(app, opts) {
|
|
|
29427
30232
|
entityType: "traffic_source",
|
|
29428
30233
|
entityId: sourceRow.id
|
|
29429
30234
|
}));
|
|
29430
|
-
updatedRow = tx.select().from(trafficSources).where(
|
|
30235
|
+
updatedRow = tx.select().from(trafficSources).where(eq26(trafficSources.id, sourceRow.id)).get();
|
|
29431
30236
|
});
|
|
29432
30237
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
29433
30238
|
});
|
|
29434
30239
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
29435
30240
|
const project = resolveProject(app.db, request.params.name);
|
|
29436
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
30241
|
+
const rows = app.db.select().from(trafficSources).where(eq26(trafficSources.projectId, project.id)).orderBy(desc14(trafficSources.createdAt)).all();
|
|
29437
30242
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
29438
30243
|
const response = { sources };
|
|
29439
30244
|
return response;
|
|
29440
30245
|
});
|
|
29441
30246
|
app.get("/projects/:name/traffic/status", async (request) => {
|
|
29442
30247
|
const project = resolveProject(app.db, request.params.name);
|
|
29443
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
30248
|
+
const rows = app.db.select().from(trafficSources).where(eq26(trafficSources.projectId, project.id)).orderBy(desc14(trafficSources.createdAt)).all();
|
|
29444
30249
|
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
29445
30250
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
29446
30251
|
const response = { sources };
|
|
@@ -29450,7 +30255,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29450
30255
|
"/projects/:name/traffic/sources/:id",
|
|
29451
30256
|
async (request) => {
|
|
29452
30257
|
const project = resolveProject(app.db, request.params.name);
|
|
29453
|
-
const row = app.db.select().from(trafficSources).where(
|
|
30258
|
+
const row = app.db.select().from(trafficSources).where(eq26(trafficSources.id, request.params.id)).get();
|
|
29454
30259
|
if (!row || row.projectId !== project.id) {
|
|
29455
30260
|
throw notFound("Traffic source", request.params.id);
|
|
29456
30261
|
}
|
|
@@ -29501,12 +30306,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29501
30306
|
let aiReferralTotal = 0;
|
|
29502
30307
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
29503
30308
|
const crawlerFilters = [
|
|
29504
|
-
|
|
29505
|
-
|
|
29506
|
-
|
|
30309
|
+
eq26(crawlerEventsHourly.projectId, project.id),
|
|
30310
|
+
gte4(crawlerEventsHourly.tsHour, sinceIso),
|
|
30311
|
+
lte3(crawlerEventsHourly.tsHour, untilIso)
|
|
29507
30312
|
];
|
|
29508
|
-
if (sourceIdParam) crawlerFilters.push(
|
|
29509
|
-
const crawlerWhere =
|
|
30313
|
+
if (sourceIdParam) crawlerFilters.push(eq26(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
30314
|
+
const crawlerWhere = and20(...crawlerFilters);
|
|
29510
30315
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
|
|
29511
30316
|
crawlerTotal = Number(total?.total ?? 0);
|
|
29512
30317
|
const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc14(crawlerEventsHourly.tsHour)).limit(limit).all();
|
|
@@ -29526,12 +30331,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29526
30331
|
}
|
|
29527
30332
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
29528
30333
|
const userFetchFilters = [
|
|
29529
|
-
|
|
29530
|
-
|
|
29531
|
-
|
|
30334
|
+
eq26(aiUserFetchEventsHourly.projectId, project.id),
|
|
30335
|
+
gte4(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
30336
|
+
lte3(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
29532
30337
|
];
|
|
29533
|
-
if (sourceIdParam) userFetchFilters.push(
|
|
29534
|
-
const userFetchWhere =
|
|
30338
|
+
if (sourceIdParam) userFetchFilters.push(eq26(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
30339
|
+
const userFetchWhere = and20(...userFetchFilters);
|
|
29535
30340
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
29536
30341
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
29537
30342
|
const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(desc14(aiUserFetchEventsHourly.tsHour)).limit(limit).all();
|
|
@@ -29551,12 +30356,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29551
30356
|
}
|
|
29552
30357
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
29553
30358
|
const aiFilters = [
|
|
29554
|
-
|
|
29555
|
-
|
|
29556
|
-
|
|
30359
|
+
eq26(aiReferralEventsHourly.projectId, project.id),
|
|
30360
|
+
gte4(aiReferralEventsHourly.tsHour, sinceIso),
|
|
30361
|
+
lte3(aiReferralEventsHourly.tsHour, untilIso)
|
|
29557
30362
|
];
|
|
29558
|
-
if (sourceIdParam) aiFilters.push(
|
|
29559
|
-
const aiWhere =
|
|
30363
|
+
if (sourceIdParam) aiFilters.push(eq26(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
30364
|
+
const aiWhere = and20(...aiFilters);
|
|
29560
30365
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
29561
30366
|
aiReferralTotal = Number(total?.total ?? 0);
|
|
29562
30367
|
const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc14(aiReferralEventsHourly.tsHour)).limit(limit).all();
|
|
@@ -29592,7 +30397,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29592
30397
|
}
|
|
29593
30398
|
|
|
29594
30399
|
// ../api-routes/src/doctor/checks/agent.ts
|
|
29595
|
-
import
|
|
30400
|
+
import crypto25 from "crypto";
|
|
29596
30401
|
import fs6 from "fs";
|
|
29597
30402
|
import path7 from "path";
|
|
29598
30403
|
var REQUIRED_SKILLS = ["canonry", "aero"];
|
|
@@ -29745,7 +30550,7 @@ function isInstalled(dir) {
|
|
|
29745
30550
|
}
|
|
29746
30551
|
function hashInstalledFile(filePath) {
|
|
29747
30552
|
try {
|
|
29748
|
-
return
|
|
30553
|
+
return crypto25.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
|
|
29749
30554
|
} catch {
|
|
29750
30555
|
return void 0;
|
|
29751
30556
|
}
|
|
@@ -29759,6 +30564,54 @@ function readInstalledManifest(skillDir) {
|
|
|
29759
30564
|
}
|
|
29760
30565
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
29761
30566
|
|
|
30567
|
+
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
30568
|
+
import { and as and21, eq as eq27 } from "drizzle-orm";
|
|
30569
|
+
function skippedNoProject() {
|
|
30570
|
+
return {
|
|
30571
|
+
status: CheckStatuses.skipped,
|
|
30572
|
+
code: "backlinks.source.no-project",
|
|
30573
|
+
summary: "Project context required."
|
|
30574
|
+
};
|
|
30575
|
+
}
|
|
30576
|
+
var BACKLINKS_CHECKS = [
|
|
30577
|
+
{
|
|
30578
|
+
id: "backlinks.source.connected",
|
|
30579
|
+
category: CheckCategories.integrations,
|
|
30580
|
+
scope: CheckScopes.project,
|
|
30581
|
+
title: "Backlinks source connected",
|
|
30582
|
+
run: (ctx) => {
|
|
30583
|
+
if (!ctx.project) return skippedNoProject();
|
|
30584
|
+
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq27(projects.id, ctx.project.id)).get();
|
|
30585
|
+
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
30586
|
+
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
30587
|
+
const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
|
|
30588
|
+
const connected = [];
|
|
30589
|
+
if (ccConnected) connected.push(BacklinkSources.commoncrawl);
|
|
30590
|
+
if (bingConnected) connected.push(BacklinkSources["bing-webmaster"]);
|
|
30591
|
+
if (connected.length === 0) {
|
|
30592
|
+
return {
|
|
30593
|
+
status: CheckStatuses.warn,
|
|
30594
|
+
code: "backlinks.source.none",
|
|
30595
|
+
summary: `No backlink source is set up for ${ctx.project.name}.`,
|
|
30596
|
+
remediation: `Enable Common Crawl (set autoExtractBacklinks on the project + run \`canonry backlinks sync\`) or connect Bing Webmaster (\`canonry bing connect ${ctx.project.name} --api-key <key>\` then \`canonry backlinks bing-sync ${ctx.project.name}\`).`,
|
|
30597
|
+
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
|
|
30598
|
+
};
|
|
30599
|
+
}
|
|
30600
|
+
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and21(
|
|
30601
|
+
eq27(backlinkSummaries.projectId, ctx.project.id),
|
|
30602
|
+
eq27(backlinkSummaries.source, BacklinkSources.commoncrawl)
|
|
30603
|
+
)).limit(1).get() : false;
|
|
30604
|
+
return {
|
|
30605
|
+
status: CheckStatuses.ok,
|
|
30606
|
+
code: "backlinks.source.connected",
|
|
30607
|
+
summary: `${connected.length} backlink source${connected.length === 1 ? "" : "s"} set up: ${connected.join(", ")}.`,
|
|
30608
|
+
remediation: ccConnected && !ccHasData ? `Common Crawl is ready but no backlinks have been extracted for ${ctx.project.name} yet \u2014 run \`canonry backlinks extract ${ctx.project.name}\`.` : null,
|
|
30609
|
+
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected, connected, commoncrawlHasData: ccHasData }
|
|
30610
|
+
};
|
|
30611
|
+
}
|
|
30612
|
+
}
|
|
30613
|
+
];
|
|
30614
|
+
|
|
29762
30615
|
// ../api-routes/src/doctor/checks/bing-auth.ts
|
|
29763
30616
|
var BING_AUTH_CHECKS = [
|
|
29764
30617
|
{
|
|
@@ -29906,10 +30759,10 @@ var BING_AUTH_CHECKS = [
|
|
|
29906
30759
|
];
|
|
29907
30760
|
|
|
29908
30761
|
// ../api-routes/src/doctor/checks/content.ts
|
|
29909
|
-
import { eq as
|
|
30762
|
+
import { eq as eq28 } from "drizzle-orm";
|
|
29910
30763
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
29911
30764
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
29912
|
-
function
|
|
30765
|
+
function skippedNoProject2() {
|
|
29913
30766
|
return {
|
|
29914
30767
|
status: CheckStatuses.skipped,
|
|
29915
30768
|
code: "content.winnability.no-project",
|
|
@@ -29919,7 +30772,7 @@ function skippedNoProject() {
|
|
|
29919
30772
|
}
|
|
29920
30773
|
function loadProject(ctx) {
|
|
29921
30774
|
if (!ctx.project) return null;
|
|
29922
|
-
return ctx.db.select().from(projects).where(
|
|
30775
|
+
return ctx.db.select().from(projects).where(eq28(projects.id, ctx.project.id)).get() ?? null;
|
|
29923
30776
|
}
|
|
29924
30777
|
function percent(value) {
|
|
29925
30778
|
return Math.round(value * 100);
|
|
@@ -29930,7 +30783,7 @@ var winnabilityCoverageCheck = {
|
|
|
29930
30783
|
scope: CheckScopes.project,
|
|
29931
30784
|
title: "Content winnability classification coverage",
|
|
29932
30785
|
run: (ctx) => {
|
|
29933
|
-
if (!ctx.project) return
|
|
30786
|
+
if (!ctx.project) return skippedNoProject2();
|
|
29934
30787
|
const project = loadProject(ctx);
|
|
29935
30788
|
if (!project) {
|
|
29936
30789
|
return {
|
|
@@ -30010,6 +30863,123 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
30010
30863
|
CONTENT_CHECKS.map((check) => [check.id, check])
|
|
30011
30864
|
);
|
|
30012
30865
|
|
|
30866
|
+
// ../api-routes/src/doctor/checks/ads.ts
|
|
30867
|
+
import { eq as eq29 } from "drizzle-orm";
|
|
30868
|
+
var RECENT_SYNC_WARN_DAYS = 7;
|
|
30869
|
+
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
30870
|
+
var adsConnectionCheck = {
|
|
30871
|
+
id: "ads.auth.connection",
|
|
30872
|
+
category: CheckCategories.auth,
|
|
30873
|
+
scope: CheckScopes.project,
|
|
30874
|
+
title: "OpenAI ads connection",
|
|
30875
|
+
run: (ctx) => {
|
|
30876
|
+
if (!ctx.project) {
|
|
30877
|
+
return {
|
|
30878
|
+
status: CheckStatuses.skipped,
|
|
30879
|
+
code: "ads.auth.no-project",
|
|
30880
|
+
summary: "Project context required.",
|
|
30881
|
+
remediation: null
|
|
30882
|
+
};
|
|
30883
|
+
}
|
|
30884
|
+
const row = ctx.db.select().from(adsConnections).where(eq29(adsConnections.projectId, ctx.project.id)).get();
|
|
30885
|
+
if (!row) {
|
|
30886
|
+
return {
|
|
30887
|
+
status: CheckStatuses.skipped,
|
|
30888
|
+
code: "ads.auth.not-connected",
|
|
30889
|
+
summary: "No OpenAI ads connection for this project.",
|
|
30890
|
+
remediation: null
|
|
30891
|
+
};
|
|
30892
|
+
}
|
|
30893
|
+
if (!ctx.adsCredentialStore) {
|
|
30894
|
+
return {
|
|
30895
|
+
status: CheckStatuses.skipped,
|
|
30896
|
+
code: "ads.auth.store-unavailable",
|
|
30897
|
+
summary: "No ads credential store configured for this deployment.",
|
|
30898
|
+
remediation: null
|
|
30899
|
+
};
|
|
30900
|
+
}
|
|
30901
|
+
const cfg = ctx.adsCredentialStore.getConnection(ctx.project.name);
|
|
30902
|
+
if (!cfg?.apiKey) {
|
|
30903
|
+
return {
|
|
30904
|
+
status: CheckStatuses.fail,
|
|
30905
|
+
code: "ads.auth.missing-key",
|
|
30906
|
+
summary: "An ads connection row exists but no SDK key is stored in the local config.",
|
|
30907
|
+
remediation: `Re-run \`canonry ads connect ${ctx.project.name} --api-key <sdk-key>\` to restore the credential.`,
|
|
30908
|
+
details: { adAccountId: row.adAccountId }
|
|
30909
|
+
};
|
|
30910
|
+
}
|
|
30911
|
+
return {
|
|
30912
|
+
status: CheckStatuses.ok,
|
|
30913
|
+
code: "ads.auth.ok",
|
|
30914
|
+
summary: `Connected to ad account ${row.displayName ?? row.adAccountId}.`,
|
|
30915
|
+
remediation: null,
|
|
30916
|
+
details: { adAccountId: row.adAccountId }
|
|
30917
|
+
};
|
|
30918
|
+
}
|
|
30919
|
+
};
|
|
30920
|
+
var adsRecentSyncCheck = {
|
|
30921
|
+
id: "ads.data.recent-sync",
|
|
30922
|
+
category: CheckCategories.integrations,
|
|
30923
|
+
scope: CheckScopes.project,
|
|
30924
|
+
title: "OpenAI ads recent sync",
|
|
30925
|
+
run: (ctx) => {
|
|
30926
|
+
if (!ctx.project) {
|
|
30927
|
+
return {
|
|
30928
|
+
status: CheckStatuses.skipped,
|
|
30929
|
+
code: "ads.data.no-project",
|
|
30930
|
+
summary: "Project context required.",
|
|
30931
|
+
remediation: null
|
|
30932
|
+
};
|
|
30933
|
+
}
|
|
30934
|
+
const row = ctx.db.select().from(adsConnections).where(eq29(adsConnections.projectId, ctx.project.id)).get();
|
|
30935
|
+
if (!row) {
|
|
30936
|
+
return {
|
|
30937
|
+
status: CheckStatuses.skipped,
|
|
30938
|
+
code: "ads.data.not-connected",
|
|
30939
|
+
summary: "No OpenAI ads connection for this project.",
|
|
30940
|
+
remediation: null
|
|
30941
|
+
};
|
|
30942
|
+
}
|
|
30943
|
+
if (!row.lastSyncedAt) {
|
|
30944
|
+
return {
|
|
30945
|
+
status: CheckStatuses.warn,
|
|
30946
|
+
code: "ads.data.never-synced",
|
|
30947
|
+
summary: "The connected ad account has never been synced.",
|
|
30948
|
+
remediation: `Run \`canonry ads sync ${ctx.project.name}\` (and schedule it: \`canonry schedule set ${ctx.project.name} --kind ads-sync --preset daily\`).`
|
|
30949
|
+
};
|
|
30950
|
+
}
|
|
30951
|
+
const syncedAtMs = new Date(row.lastSyncedAt).getTime();
|
|
30952
|
+
const ageDays = (Date.now() - syncedAtMs) / (1e3 * 60 * 60 * 24);
|
|
30953
|
+
const details = { lastSyncedAt: row.lastSyncedAt, ageDays: Math.round(ageDays) };
|
|
30954
|
+
if (ageDays > RECENT_SYNC_FAIL_DAYS) {
|
|
30955
|
+
return {
|
|
30956
|
+
status: CheckStatuses.fail,
|
|
30957
|
+
code: "ads.data.stale",
|
|
30958
|
+
summary: `Last ads sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_FAIL_DAYS}d).`,
|
|
30959
|
+
remediation: `Run \`canonry ads sync ${ctx.project.name}\` and check the ads-sync schedule.`,
|
|
30960
|
+
details
|
|
30961
|
+
};
|
|
30962
|
+
}
|
|
30963
|
+
if (ageDays > RECENT_SYNC_WARN_DAYS) {
|
|
30964
|
+
return {
|
|
30965
|
+
status: CheckStatuses.warn,
|
|
30966
|
+
code: "ads.data.aging",
|
|
30967
|
+
summary: `Last ads sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_WARN_DAYS}d).`,
|
|
30968
|
+
remediation: `Schedule daily syncs: \`canonry schedule set ${ctx.project.name} --kind ads-sync --preset daily\`.`,
|
|
30969
|
+
details
|
|
30970
|
+
};
|
|
30971
|
+
}
|
|
30972
|
+
return {
|
|
30973
|
+
status: CheckStatuses.ok,
|
|
30974
|
+
code: "ads.data.ok",
|
|
30975
|
+
summary: `Last ads sync ${Math.round(ageDays)} day(s) ago.`,
|
|
30976
|
+
remediation: null,
|
|
30977
|
+
details
|
|
30978
|
+
};
|
|
30979
|
+
}
|
|
30980
|
+
};
|
|
30981
|
+
var ADS_CHECKS = [adsConnectionCheck, adsRecentSyncCheck];
|
|
30982
|
+
|
|
30013
30983
|
// ../api-routes/src/doctor/checks/ga-auth.ts
|
|
30014
30984
|
async function checkServiceAccount(conn) {
|
|
30015
30985
|
if (!conn.propertyId) {
|
|
@@ -30152,10 +31122,10 @@ var ga4ConnectionCheck = {
|
|
|
30152
31122
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
30153
31123
|
|
|
30154
31124
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
30155
|
-
import { and as
|
|
30156
|
-
var
|
|
30157
|
-
var
|
|
30158
|
-
function
|
|
31125
|
+
import { and as and22, eq as eq30 } from "drizzle-orm";
|
|
31126
|
+
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
31127
|
+
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
31128
|
+
function skippedNoProject3() {
|
|
30159
31129
|
return {
|
|
30160
31130
|
status: CheckStatuses.skipped,
|
|
30161
31131
|
code: "gbp.auth.no-project",
|
|
@@ -30172,7 +31142,7 @@ function storeUnavailable() {
|
|
|
30172
31142
|
};
|
|
30173
31143
|
}
|
|
30174
31144
|
async function resolveGbpToken(ctx) {
|
|
30175
|
-
if (!ctx.project) return { ok: false, output:
|
|
31145
|
+
if (!ctx.project) return { ok: false, output: skippedNoProject3() };
|
|
30176
31146
|
const store = ctx.googleConnectionStore;
|
|
30177
31147
|
if (!store) return { ok: false, output: storeUnavailable() };
|
|
30178
31148
|
const auth = ctx.getGoogleAuthConfig?.() ?? {};
|
|
@@ -30250,7 +31220,7 @@ var scopesCheck = {
|
|
|
30250
31220
|
scope: CheckScopes.project,
|
|
30251
31221
|
title: "GBP granted scopes",
|
|
30252
31222
|
run: async (ctx) => {
|
|
30253
|
-
if (!ctx.project) return
|
|
31223
|
+
if (!ctx.project) return skippedNoProject3();
|
|
30254
31224
|
const store = ctx.googleConnectionStore;
|
|
30255
31225
|
if (!store) return storeUnavailable();
|
|
30256
31226
|
const conn = store.getConnection(ctx.project.canonicalDomain, "gbp");
|
|
@@ -30287,7 +31257,7 @@ var accountAccessCheck = {
|
|
|
30287
31257
|
scope: CheckScopes.project,
|
|
30288
31258
|
title: "GBP account access",
|
|
30289
31259
|
run: async (ctx) => {
|
|
30290
|
-
if (!ctx.project) return
|
|
31260
|
+
if (!ctx.project) return skippedNoProject3();
|
|
30291
31261
|
const store = ctx.googleConnectionStore;
|
|
30292
31262
|
if (!store) return storeUnavailable();
|
|
30293
31263
|
const conn = store.getConnection(ctx.project.canonicalDomain, "gbp");
|
|
@@ -30384,8 +31354,8 @@ var recentSyncCheck = {
|
|
|
30384
31354
|
scope: CheckScopes.project,
|
|
30385
31355
|
title: "GBP recent sync",
|
|
30386
31356
|
run: (ctx) => {
|
|
30387
|
-
if (!ctx.project) return
|
|
30388
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(
|
|
31357
|
+
if (!ctx.project) return skippedNoProject3();
|
|
31358
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and22(eq30(gbpLocations.projectId, ctx.project.id), eq30(gbpLocations.selected, true))).all();
|
|
30389
31359
|
if (selected.length === 0) {
|
|
30390
31360
|
return {
|
|
30391
31361
|
status: CheckStatuses.skipped,
|
|
@@ -30407,20 +31377,20 @@ var recentSyncCheck = {
|
|
|
30407
31377
|
const newest = Math.max(...syncTimes);
|
|
30408
31378
|
const ageDays = (Date.now() - newest) / (1e3 * 60 * 60 * 24);
|
|
30409
31379
|
const details = { selectedLocations: selected.length, newestSyncAgeDays: Math.round(ageDays) };
|
|
30410
|
-
if (ageDays >
|
|
31380
|
+
if (ageDays > RECENT_SYNC_FAIL_DAYS2) {
|
|
30411
31381
|
return {
|
|
30412
31382
|
status: CheckStatuses.fail,
|
|
30413
31383
|
code: "gbp.data.stale",
|
|
30414
|
-
summary: `Most recent GBP sync was ${Math.round(ageDays)} days ago (> ${
|
|
31384
|
+
summary: `Most recent GBP sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_FAIL_DAYS2}d).`,
|
|
30415
31385
|
remediation: `Run \`canonry gbp sync ${ctx.project.name}\` or set a gbp-sync schedule.`,
|
|
30416
31386
|
details
|
|
30417
31387
|
};
|
|
30418
31388
|
}
|
|
30419
|
-
if (ageDays >
|
|
31389
|
+
if (ageDays > RECENT_SYNC_WARN_DAYS2) {
|
|
30420
31390
|
return {
|
|
30421
31391
|
status: CheckStatuses.warn,
|
|
30422
31392
|
code: "gbp.data.aging",
|
|
30423
|
-
summary: `Most recent GBP sync was ${Math.round(ageDays)} days ago (> ${
|
|
31393
|
+
summary: `Most recent GBP sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_WARN_DAYS2}d).`,
|
|
30424
31394
|
remediation: `Run \`canonry gbp sync ${ctx.project.name}\` or set a gbp-sync schedule to keep data fresh.`,
|
|
30425
31395
|
details
|
|
30426
31396
|
};
|
|
@@ -30445,7 +31415,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
30445
31415
|
);
|
|
30446
31416
|
|
|
30447
31417
|
// ../api-routes/src/doctor/checks/places.ts
|
|
30448
|
-
import { eq as
|
|
31418
|
+
import { eq as eq31 } from "drizzle-orm";
|
|
30449
31419
|
var apiKeyCheck = {
|
|
30450
31420
|
id: "gbp.places.api-key",
|
|
30451
31421
|
category: CheckCategories.auth,
|
|
@@ -30490,7 +31460,7 @@ var apiKeyCheck = {
|
|
|
30490
31460
|
details: { tier: cfg.tier }
|
|
30491
31461
|
};
|
|
30492
31462
|
}
|
|
30493
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
31463
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq31(gbpLocations.projectId, ctx.project.id)).all();
|
|
30494
31464
|
const selected = rows.filter((r) => r.selected);
|
|
30495
31465
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
30496
31466
|
const details = {
|
|
@@ -30526,7 +31496,7 @@ var PLACES_CHECK_BY_ID = Object.fromEntries(
|
|
|
30526
31496
|
var REQUIRED_GSC_SCOPES = [GSC_SCOPE, INDEXING_SCOPE];
|
|
30527
31497
|
async function resolveAccessToken(ctx) {
|
|
30528
31498
|
if (!ctx.project) {
|
|
30529
|
-
return { ok: false, output:
|
|
31499
|
+
return { ok: false, output: skippedNoProject4() };
|
|
30530
31500
|
}
|
|
30531
31501
|
const store = ctx.googleConnectionStore;
|
|
30532
31502
|
if (!store) {
|
|
@@ -30593,7 +31563,7 @@ async function resolveAccessToken(ctx) {
|
|
|
30593
31563
|
};
|
|
30594
31564
|
}
|
|
30595
31565
|
}
|
|
30596
|
-
function
|
|
31566
|
+
function skippedNoProject4() {
|
|
30597
31567
|
return {
|
|
30598
31568
|
status: CheckStatuses.skipped,
|
|
30599
31569
|
code: "google.auth.no-project",
|
|
@@ -30623,7 +31593,7 @@ var propertyAccessCheck = {
|
|
|
30623
31593
|
scope: CheckScopes.project,
|
|
30624
31594
|
title: "GSC property access",
|
|
30625
31595
|
run: async (ctx) => {
|
|
30626
|
-
if (!ctx.project) return
|
|
31596
|
+
if (!ctx.project) return skippedNoProject4();
|
|
30627
31597
|
const store = ctx.googleConnectionStore;
|
|
30628
31598
|
if (!store) {
|
|
30629
31599
|
return {
|
|
@@ -30724,7 +31694,7 @@ var redirectUriCheck = {
|
|
|
30724
31694
|
scope: CheckScopes.project,
|
|
30725
31695
|
title: "OAuth redirect URI",
|
|
30726
31696
|
run: async (ctx) => {
|
|
30727
|
-
if (!ctx.project) return
|
|
31697
|
+
if (!ctx.project) return skippedNoProject4();
|
|
30728
31698
|
const auth = ctx.getGoogleAuthConfig?.() ?? {};
|
|
30729
31699
|
if (!auth.clientId || !auth.clientSecret) {
|
|
30730
31700
|
return {
|
|
@@ -30778,7 +31748,7 @@ var scopesCheck2 = {
|
|
|
30778
31748
|
scope: CheckScopes.project,
|
|
30779
31749
|
title: "GSC granted scopes",
|
|
30780
31750
|
run: async (ctx) => {
|
|
30781
|
-
if (!ctx.project) return
|
|
31751
|
+
if (!ctx.project) return skippedNoProject4();
|
|
30782
31752
|
const store = ctx.googleConnectionStore;
|
|
30783
31753
|
if (!store) {
|
|
30784
31754
|
return {
|
|
@@ -30941,10 +31911,10 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
30941
31911
|
];
|
|
30942
31912
|
|
|
30943
31913
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
30944
|
-
import { and as
|
|
31914
|
+
import { and as and23, eq as eq32, gte as gte5, ne as ne4, sql as sql12 } from "drizzle-orm";
|
|
30945
31915
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
30946
31916
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
30947
|
-
function
|
|
31917
|
+
function skippedNoProject5() {
|
|
30948
31918
|
return {
|
|
30949
31919
|
status: CheckStatuses.skipped,
|
|
30950
31920
|
code: "traffic.no-project",
|
|
@@ -30955,8 +31925,8 @@ function skippedNoProject4() {
|
|
|
30955
31925
|
function loadProbes(ctx) {
|
|
30956
31926
|
if (!ctx.project) return [];
|
|
30957
31927
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
30958
|
-
|
|
30959
|
-
|
|
31928
|
+
and23(
|
|
31929
|
+
eq32(trafficSources.projectId, ctx.project.id),
|
|
30960
31930
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
30961
31931
|
)
|
|
30962
31932
|
).all();
|
|
@@ -30978,7 +31948,7 @@ var sourceConnectedCheck = {
|
|
|
30978
31948
|
scope: CheckScopes.project,
|
|
30979
31949
|
title: "Traffic source connected",
|
|
30980
31950
|
run: (ctx) => {
|
|
30981
|
-
if (!ctx.project) return
|
|
31951
|
+
if (!ctx.project) return skippedNoProject5();
|
|
30982
31952
|
const sources = loadProbes(ctx);
|
|
30983
31953
|
if (sources.length === 0) {
|
|
30984
31954
|
return {
|
|
@@ -31022,7 +31992,7 @@ var recentDataCheck = {
|
|
|
31022
31992
|
scope: CheckScopes.project,
|
|
31023
31993
|
title: "Traffic source recent data",
|
|
31024
31994
|
run: (ctx) => {
|
|
31025
|
-
if (!ctx.project) return
|
|
31995
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31026
31996
|
const sources = loadProbes(ctx);
|
|
31027
31997
|
if (sources.length === 0) {
|
|
31028
31998
|
return {
|
|
@@ -31036,17 +32006,17 @@ var recentDataCheck = {
|
|
|
31036
32006
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
31037
32007
|
const recentCrawlers = Number(
|
|
31038
32008
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
31039
|
-
|
|
31040
|
-
|
|
31041
|
-
|
|
32009
|
+
and23(
|
|
32010
|
+
eq32(crawlerEventsHourly.projectId, ctx.project.id),
|
|
32011
|
+
gte5(crawlerEventsHourly.tsHour, warnCutoff)
|
|
31042
32012
|
)
|
|
31043
32013
|
).get()?.total ?? 0
|
|
31044
32014
|
);
|
|
31045
32015
|
const recentReferrals = Number(
|
|
31046
32016
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31047
|
-
|
|
31048
|
-
|
|
31049
|
-
|
|
32017
|
+
and23(
|
|
32018
|
+
eq32(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
32019
|
+
gte5(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
31050
32020
|
)
|
|
31051
32021
|
).get()?.total ?? 0
|
|
31052
32022
|
);
|
|
@@ -31060,17 +32030,17 @@ var recentDataCheck = {
|
|
|
31060
32030
|
}
|
|
31061
32031
|
const olderCrawlers = Number(
|
|
31062
32032
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
31063
|
-
|
|
31064
|
-
|
|
31065
|
-
|
|
32033
|
+
and23(
|
|
32034
|
+
eq32(crawlerEventsHourly.projectId, ctx.project.id),
|
|
32035
|
+
gte5(crawlerEventsHourly.tsHour, failCutoff)
|
|
31066
32036
|
)
|
|
31067
32037
|
).get()?.total ?? 0
|
|
31068
32038
|
);
|
|
31069
32039
|
const olderReferrals = Number(
|
|
31070
32040
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31071
|
-
|
|
31072
|
-
|
|
31073
|
-
|
|
32041
|
+
and23(
|
|
32042
|
+
eq32(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
32043
|
+
gte5(aiReferralEventsHourly.tsHour, failCutoff)
|
|
31074
32044
|
)
|
|
31075
32045
|
).get()?.total ?? 0
|
|
31076
32046
|
);
|
|
@@ -31184,7 +32154,7 @@ var credentialsCheck = {
|
|
|
31184
32154
|
scope: CheckScopes.project,
|
|
31185
32155
|
title: "Traffic source credentials",
|
|
31186
32156
|
run: async (ctx) => {
|
|
31187
|
-
if (!ctx.project) return
|
|
32157
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31188
32158
|
const sources = loadProbes(ctx);
|
|
31189
32159
|
if (sources.length === 0) {
|
|
31190
32160
|
return {
|
|
@@ -31213,7 +32183,7 @@ var scopesCheck3 = {
|
|
|
31213
32183
|
scope: CheckScopes.project,
|
|
31214
32184
|
title: "Traffic source scopes",
|
|
31215
32185
|
run: async (ctx) => {
|
|
31216
|
-
if (!ctx.project) return
|
|
32186
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31217
32187
|
const sources = loadProbes(ctx);
|
|
31218
32188
|
if (sources.length === 0) {
|
|
31219
32189
|
return {
|
|
@@ -31242,7 +32212,7 @@ var cacheBlindSpotCheck = {
|
|
|
31242
32212
|
scope: CheckScopes.project,
|
|
31243
32213
|
title: "WordPress traffic cache blind spot",
|
|
31244
32214
|
run: (ctx) => {
|
|
31245
|
-
if (!ctx.project) return
|
|
32215
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31246
32216
|
const wpSources = loadProbes(ctx).filter(
|
|
31247
32217
|
(s) => s.sourceType === TrafficSourceTypes.wordpress
|
|
31248
32218
|
);
|
|
@@ -31354,8 +32324,10 @@ var ALL_CHECKS = [
|
|
|
31354
32324
|
...BING_AUTH_CHECKS,
|
|
31355
32325
|
...WORDPRESS_PUBLISH_CHECKS,
|
|
31356
32326
|
...GA_AUTH_CHECKS,
|
|
32327
|
+
...ADS_CHECKS,
|
|
31357
32328
|
...PROVIDERS_CHECKS,
|
|
31358
32329
|
...TRAFFIC_SOURCE_CHECKS,
|
|
32330
|
+
...BACKLINKS_CHECKS,
|
|
31359
32331
|
...CONTENT_CHECKS,
|
|
31360
32332
|
...AGENT_CHECKS
|
|
31361
32333
|
];
|
|
@@ -31440,6 +32412,7 @@ async function doctorRoutes(app, opts) {
|
|
|
31440
32412
|
bingConnectionStore: opts.bingConnectionStore,
|
|
31441
32413
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
31442
32414
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
32415
|
+
adsCredentialStore: opts.adsCredentialStore,
|
|
31443
32416
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
31444
32417
|
getPlacesConfig: opts.getPlacesConfig,
|
|
31445
32418
|
redirectUri,
|
|
@@ -31465,6 +32438,7 @@ async function doctorRoutes(app, opts) {
|
|
|
31465
32438
|
bingConnectionStore: opts.bingConnectionStore,
|
|
31466
32439
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
31467
32440
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
32441
|
+
adsCredentialStore: opts.adsCredentialStore,
|
|
31468
32442
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
31469
32443
|
getPlacesConfig: opts.getPlacesConfig,
|
|
31470
32444
|
redirectUri,
|
|
@@ -31478,8 +32452,8 @@ async function doctorRoutes(app, opts) {
|
|
|
31478
32452
|
}
|
|
31479
32453
|
|
|
31480
32454
|
// ../api-routes/src/discovery/routes.ts
|
|
31481
|
-
import
|
|
31482
|
-
import { and as
|
|
32455
|
+
import crypto26 from "crypto";
|
|
32456
|
+
import { and as and24, desc as desc15, eq as eq33, gte as gte6, inArray as inArray11 } from "drizzle-orm";
|
|
31483
32457
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
31484
32458
|
async function discoveryRoutes(app, opts) {
|
|
31485
32459
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -31511,21 +32485,21 @@ async function discoveryRoutes(app, opts) {
|
|
|
31511
32485
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
31512
32486
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
31513
32487
|
const decision = app.db.transaction((tx) => {
|
|
31514
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
31515
|
-
|
|
31516
|
-
|
|
31517
|
-
|
|
32488
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and24(
|
|
32489
|
+
eq33(discoverySessions.projectId, project.id),
|
|
32490
|
+
eq33(discoverySessions.icpDescription, icpDescription),
|
|
32491
|
+
inArray11(discoverySessions.status, [
|
|
31518
32492
|
DiscoverySessionStatuses.queued,
|
|
31519
32493
|
DiscoverySessionStatuses.seeding,
|
|
31520
32494
|
DiscoverySessionStatuses.probing
|
|
31521
32495
|
]),
|
|
31522
|
-
|
|
32496
|
+
gte6(discoverySessions.createdAt, ageFloorIso)
|
|
31523
32497
|
)).orderBy(desc15(discoverySessions.createdAt)).get();
|
|
31524
32498
|
if (existing && existing.runId) {
|
|
31525
32499
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
31526
32500
|
}
|
|
31527
|
-
const sessionId =
|
|
31528
|
-
const runId =
|
|
32501
|
+
const sessionId = crypto26.randomUUID();
|
|
32502
|
+
const runId = crypto26.randomUUID();
|
|
31529
32503
|
tx.insert(discoverySessions).values({
|
|
31530
32504
|
id: sessionId,
|
|
31531
32505
|
projectId: project.id,
|
|
@@ -31583,7 +32557,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31583
32557
|
const project = resolveProject(app.db, request.params.name);
|
|
31584
32558
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
31585
32559
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
31586
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
32560
|
+
const rows = app.db.select().from(discoverySessions).where(eq33(discoverySessions.projectId, project.id)).orderBy(desc15(discoverySessions.createdAt)).limit(limit).all();
|
|
31587
32561
|
return reply.send(rows.map(serializeSession));
|
|
31588
32562
|
}
|
|
31589
32563
|
);
|
|
@@ -31591,11 +32565,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
31591
32565
|
"/projects/:name/discover/sessions/:id",
|
|
31592
32566
|
async (request, reply) => {
|
|
31593
32567
|
const project = resolveProject(app.db, request.params.name);
|
|
31594
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32568
|
+
const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
|
|
31595
32569
|
if (!session || session.projectId !== project.id) {
|
|
31596
32570
|
throw notFound("Discovery session", request.params.id);
|
|
31597
32571
|
}
|
|
31598
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32572
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
|
|
31599
32573
|
const detail = {
|
|
31600
32574
|
...serializeSession(session),
|
|
31601
32575
|
probes: probeRows.map(serializeProbe)
|
|
@@ -31607,12 +32581,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
31607
32581
|
"/projects/:name/discover/sessions/:id/promote",
|
|
31608
32582
|
async (request, reply) => {
|
|
31609
32583
|
const project = resolveProject(app.db, request.params.name);
|
|
31610
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32584
|
+
const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
|
|
31611
32585
|
if (!session || session.projectId !== project.id) {
|
|
31612
32586
|
throw notFound("Discovery session", request.params.id);
|
|
31613
32587
|
}
|
|
31614
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
31615
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32588
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
|
|
32589
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
31616
32590
|
const seenCompetitors = new Set(existingCompetitors);
|
|
31617
32591
|
const cited = /* @__PURE__ */ new Set();
|
|
31618
32592
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -31641,7 +32615,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31641
32615
|
);
|
|
31642
32616
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
31643
32617
|
const project = resolveProject(app.db, request.params.name);
|
|
31644
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32618
|
+
const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
|
|
31645
32619
|
if (!session || session.projectId !== project.id) {
|
|
31646
32620
|
throw notFound("Discovery session", request.params.id);
|
|
31647
32621
|
}
|
|
@@ -31664,7 +32638,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31664
32638
|
const bucketSet = new Set(buckets);
|
|
31665
32639
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
31666
32640
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
31667
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32641
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
|
|
31668
32642
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
31669
32643
|
for (const probe of probeRows) {
|
|
31670
32644
|
if (!probe.bucket) continue;
|
|
@@ -31672,7 +32646,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31672
32646
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
31673
32647
|
}
|
|
31674
32648
|
const existingQueries = new Set(
|
|
31675
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
32649
|
+
app.db.select({ query: queries.query }).from(queries).where(eq33(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
31676
32650
|
);
|
|
31677
32651
|
const promotedQueries = [];
|
|
31678
32652
|
const skippedQueries = [];
|
|
@@ -31688,7 +32662,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31688
32662
|
const skippedCompetitors = [];
|
|
31689
32663
|
if (includeCompetitors) {
|
|
31690
32664
|
const existingCompetitors = new Set(
|
|
31691
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32665
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
31692
32666
|
);
|
|
31693
32667
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
31694
32668
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -31707,7 +32681,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31707
32681
|
app.db.transaction((tx) => {
|
|
31708
32682
|
for (const query of promotedQueries) {
|
|
31709
32683
|
tx.insert(queries).values({
|
|
31710
|
-
id:
|
|
32684
|
+
id: crypto26.randomUUID(),
|
|
31711
32685
|
projectId: project.id,
|
|
31712
32686
|
query,
|
|
31713
32687
|
provenance,
|
|
@@ -31716,7 +32690,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31716
32690
|
}
|
|
31717
32691
|
for (const domain of promotedCompetitors) {
|
|
31718
32692
|
tx.insert(competitors).values({
|
|
31719
|
-
id:
|
|
32693
|
+
id: crypto26.randomUUID(),
|
|
31720
32694
|
projectId: project.id,
|
|
31721
32695
|
domain,
|
|
31722
32696
|
provenance,
|
|
@@ -31791,8 +32765,8 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
31791
32765
|
}
|
|
31792
32766
|
|
|
31793
32767
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
31794
|
-
import
|
|
31795
|
-
import { eq as
|
|
32768
|
+
import crypto27 from "crypto";
|
|
32769
|
+
import { eq as eq34 } from "drizzle-orm";
|
|
31796
32770
|
var DEFAULT_MAX_PROBES = 100;
|
|
31797
32771
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
31798
32772
|
function classifyProbeBucket(input) {
|
|
@@ -31846,7 +32820,7 @@ async function executeDiscovery(opts) {
|
|
|
31846
32820
|
status: DiscoverySessionStatuses.seeding,
|
|
31847
32821
|
dedupThreshold,
|
|
31848
32822
|
startedAt
|
|
31849
|
-
}).where(
|
|
32823
|
+
}).where(eq34(discoverySessions.id, opts.sessionId)).run();
|
|
31850
32824
|
const seedResult = await opts.deps.seed({
|
|
31851
32825
|
project: opts.project,
|
|
31852
32826
|
icpDescription: opts.icpDescription,
|
|
@@ -31872,7 +32846,7 @@ async function executeDiscovery(opts) {
|
|
|
31872
32846
|
seedCountRaw,
|
|
31873
32847
|
seedCount,
|
|
31874
32848
|
warning
|
|
31875
|
-
}).where(
|
|
32849
|
+
}).where(eq34(discoverySessions.id, opts.sessionId)).run();
|
|
31876
32850
|
const probeRows = [];
|
|
31877
32851
|
const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
|
|
31878
32852
|
for (const query of probedCanonicals) {
|
|
@@ -31885,7 +32859,7 @@ async function executeDiscovery(opts) {
|
|
|
31885
32859
|
probeRows.push({ citedDomains: probe.citedDomains, bucket });
|
|
31886
32860
|
buckets[bucket]++;
|
|
31887
32861
|
opts.db.insert(discoveryProbes).values({
|
|
31888
|
-
id:
|
|
32862
|
+
id: crypto27.randomUUID(),
|
|
31889
32863
|
sessionId: opts.sessionId,
|
|
31890
32864
|
projectId: opts.project.id,
|
|
31891
32865
|
query,
|
|
@@ -31912,7 +32886,7 @@ async function executeDiscovery(opts) {
|
|
|
31912
32886
|
wastedCount: buckets["wasted-surface"],
|
|
31913
32887
|
competitorMap,
|
|
31914
32888
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
31915
|
-
}).where(
|
|
32889
|
+
}).where(eq34(discoverySessions.id, opts.sessionId)).run();
|
|
31916
32890
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
31917
32891
|
return {
|
|
31918
32892
|
buckets,
|
|
@@ -31929,7 +32903,7 @@ function upsertDomainClassifications(db, projectId, sessionId, competitorMap) {
|
|
|
31929
32903
|
const domain = normalizeDomain(entry.domain);
|
|
31930
32904
|
if (!domain) continue;
|
|
31931
32905
|
db.insert(domainClassifications).values({
|
|
31932
|
-
id:
|
|
32906
|
+
id: crypto27.randomUUID(),
|
|
31933
32907
|
projectId,
|
|
31934
32908
|
domain,
|
|
31935
32909
|
competitorType: entry.competitorType,
|
|
@@ -31952,7 +32926,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
31952
32926
|
status: DiscoverySessionStatuses.failed,
|
|
31953
32927
|
error,
|
|
31954
32928
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
31955
|
-
}).where(
|
|
32929
|
+
}).where(eq34(discoverySessions.id, sessionId)).run();
|
|
31956
32930
|
}
|
|
31957
32931
|
function dedupeStrings(input) {
|
|
31958
32932
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -31969,8 +32943,8 @@ function dedupeStrings(input) {
|
|
|
31969
32943
|
}
|
|
31970
32944
|
|
|
31971
32945
|
// ../api-routes/src/technical-aeo.ts
|
|
31972
|
-
import
|
|
31973
|
-
import { and as
|
|
32946
|
+
import crypto28 from "crypto";
|
|
32947
|
+
import { and as and25, asc as asc4, count, desc as desc16, eq as eq35, inArray as inArray12 } from "drizzle-orm";
|
|
31974
32948
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
31975
32949
|
function emptyScore(projectName) {
|
|
31976
32950
|
return {
|
|
@@ -32002,10 +32976,10 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
32002
32976
|
async function technicalAeoRoutes(app, opts) {
|
|
32003
32977
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
32004
32978
|
const project = resolveProject(app.db, request.params.name);
|
|
32005
|
-
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs,
|
|
32006
|
-
|
|
32007
|
-
|
|
32008
|
-
|
|
32979
|
+
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
|
|
32980
|
+
eq35(siteAuditSnapshots.projectId, project.id),
|
|
32981
|
+
eq35(runs.kind, RunKinds["site-audit"]),
|
|
32982
|
+
inArray12(runs.status, SURFACEABLE_STATUSES),
|
|
32009
32983
|
notProbeRun()
|
|
32010
32984
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
32011
32985
|
const latest = rows[0];
|
|
@@ -32037,24 +33011,24 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32037
33011
|
});
|
|
32038
33012
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
32039
33013
|
const project = resolveProject(app.db, request.params.name);
|
|
32040
|
-
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs,
|
|
32041
|
-
|
|
32042
|
-
|
|
32043
|
-
|
|
33014
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
|
|
33015
|
+
eq35(siteAuditSnapshots.projectId, project.id),
|
|
33016
|
+
eq35(runs.kind, RunKinds["site-audit"]),
|
|
33017
|
+
inArray12(runs.status, SURFACEABLE_STATUSES),
|
|
32044
33018
|
notProbeRun()
|
|
32045
33019
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
32046
33020
|
if (!latest) {
|
|
32047
33021
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
32048
33022
|
}
|
|
32049
33023
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
32050
|
-
const conds = [
|
|
32051
|
-
if (statusFilter) conds.push(
|
|
32052
|
-
const where =
|
|
33024
|
+
const conds = [eq35(siteAuditPages.runId, latest.runId)];
|
|
33025
|
+
if (statusFilter) conds.push(eq35(siteAuditPages.status, statusFilter));
|
|
33026
|
+
const where = and25(...conds);
|
|
32053
33027
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
32054
33028
|
const total = totalRow?.value ?? 0;
|
|
32055
33029
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
32056
33030
|
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
32057
|
-
const orderBy = request.query.sort === "score-desc" ? desc16(siteAuditPages.overallScore) : request.query.sort === "url" ?
|
|
33031
|
+
const orderBy = request.query.sort === "score-desc" ? desc16(siteAuditPages.overallScore) : request.query.sort === "url" ? asc4(siteAuditPages.url) : asc4(siteAuditPages.overallScore);
|
|
32058
33032
|
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
32059
33033
|
const pages = rows.map((row) => ({
|
|
32060
33034
|
url: row.url,
|
|
@@ -32073,10 +33047,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32073
33047
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
32074
33048
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
32075
33049
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
32076
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
32077
|
-
|
|
32078
|
-
|
|
32079
|
-
|
|
33050
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
|
|
33051
|
+
eq35(siteAuditSnapshots.projectId, project.id),
|
|
33052
|
+
eq35(runs.kind, RunKinds["site-audit"]),
|
|
33053
|
+
inArray12(runs.status, SURFACEABLE_STATUSES),
|
|
32080
33054
|
notProbeRun()
|
|
32081
33055
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
32082
33056
|
return { project: project.name, points: rows.reverse() };
|
|
@@ -32087,16 +33061,16 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32087
33061
|
if (!parsed.success) {
|
|
32088
33062
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
32089
33063
|
}
|
|
32090
|
-
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
32091
|
-
|
|
32092
|
-
|
|
32093
|
-
|
|
33064
|
+
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and25(
|
|
33065
|
+
eq35(runs.projectId, project.id),
|
|
33066
|
+
eq35(runs.kind, RunKinds["site-audit"]),
|
|
33067
|
+
inArray12(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
32094
33068
|
)).get();
|
|
32095
33069
|
if (existing) {
|
|
32096
33070
|
return { runId: existing.id, status: existing.status };
|
|
32097
33071
|
}
|
|
32098
33072
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32099
|
-
const runId =
|
|
33073
|
+
const runId = crypto28.randomUUID();
|
|
32100
33074
|
app.db.insert(runs).values({
|
|
32101
33075
|
id: runId,
|
|
32102
33076
|
projectId: project.id,
|
|
@@ -32223,6 +33197,11 @@ async function apiRoutes(app, opts) {
|
|
|
32223
33197
|
getTelemetryStatus: opts.getTelemetryStatus,
|
|
32224
33198
|
setTelemetryEnabled: opts.setTelemetryEnabled
|
|
32225
33199
|
});
|
|
33200
|
+
await api.register(adsRoutes, {
|
|
33201
|
+
adsCredentialStore: opts.adsCredentialStore,
|
|
33202
|
+
verifyAdsAccount: opts.verifyAdsAccount,
|
|
33203
|
+
onAdsSyncRequested: opts.onAdsSyncRequested
|
|
33204
|
+
});
|
|
32226
33205
|
await api.register(bingRoutes, {
|
|
32227
33206
|
bingConnectionStore: opts.bingConnectionStore,
|
|
32228
33207
|
onInspectSitemapRequested: opts.onBingInspectSitemapRequested
|
|
@@ -32271,6 +33250,8 @@ async function apiRoutes(app, opts) {
|
|
|
32271
33250
|
onInstallBacklinks: opts.onInstallBacklinks,
|
|
32272
33251
|
onReleaseSyncRequested: opts.onReleaseSyncRequested,
|
|
32273
33252
|
onBacklinkExtractRequested: opts.onBacklinkExtractRequested,
|
|
33253
|
+
onBingBacklinkSyncRequested: opts.onBingBacklinkSyncRequested,
|
|
33254
|
+
bingConnectionStore: opts.bingConnectionStore,
|
|
32274
33255
|
onBacklinksPruneCache: opts.onBacklinksPruneCache,
|
|
32275
33256
|
listCachedReleases: opts.listCachedReleases,
|
|
32276
33257
|
discoverLatestRelease: opts.discoverLatestRelease
|
|
@@ -32286,6 +33267,7 @@ async function apiRoutes(app, opts) {
|
|
|
32286
33267
|
bingConnectionStore: opts.bingConnectionStore,
|
|
32287
33268
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
32288
33269
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
33270
|
+
adsCredentialStore: opts.adsCredentialStore,
|
|
32289
33271
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
32290
33272
|
getPlacesConfig: opts.getPlacesConfig,
|
|
32291
33273
|
publicUrl: opts.publicUrl,
|
|
@@ -32436,7 +33418,7 @@ function buildTrafficSourceValidators(opts) {
|
|
|
32436
33418
|
}
|
|
32437
33419
|
|
|
32438
33420
|
// src/intelligence-service.ts
|
|
32439
|
-
import
|
|
33421
|
+
import crypto29 from "crypto";
|
|
32440
33422
|
|
|
32441
33423
|
// src/logger.ts
|
|
32442
33424
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -32668,9 +33650,9 @@ var IntelligenceService = class {
|
|
|
32668
33650
|
*/
|
|
32669
33651
|
analyzeAndPersist(runId, projectId) {
|
|
32670
33652
|
const recentRuns = this.db.select().from(runs).where(
|
|
32671
|
-
|
|
32672
|
-
|
|
32673
|
-
or5(
|
|
33653
|
+
and26(
|
|
33654
|
+
eq36(runs.projectId, projectId),
|
|
33655
|
+
or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
|
|
32674
33656
|
// Defensive: RunCoordinator already skips probes before this is
|
|
32675
33657
|
// called, but if a future call site invokes analyzeAndPersist
|
|
32676
33658
|
// directly for a probe, probes still must not pollute the
|
|
@@ -32752,7 +33734,7 @@ var IntelligenceService = class {
|
|
|
32752
33734
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
32753
33735
|
*/
|
|
32754
33736
|
analyzeAndPersistGbp(runId, projectId) {
|
|
32755
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
33737
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq36(runs.id, runId)).get();
|
|
32756
33738
|
if (!runRow) {
|
|
32757
33739
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
32758
33740
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -32760,11 +33742,11 @@ var IntelligenceService = class {
|
|
|
32760
33742
|
}
|
|
32761
33743
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
32762
33744
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
32763
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
32764
|
-
|
|
32765
|
-
|
|
32766
|
-
|
|
32767
|
-
|
|
33745
|
+
const selected = this.db.select().from(gbpLocations).where(and26(
|
|
33746
|
+
eq36(gbpLocations.projectId, projectId),
|
|
33747
|
+
eq36(gbpLocations.selected, true),
|
|
33748
|
+
gte7(gbpLocations.syncedAt, windowStart),
|
|
33749
|
+
lte4(gbpLocations.syncedAt, windowEnd)
|
|
32768
33750
|
)).all();
|
|
32769
33751
|
if (selected.length === 0) {
|
|
32770
33752
|
log.info("gbp-intelligence.skip", { runId, reason: "no locations synced during run" });
|
|
@@ -32797,10 +33779,10 @@ var IntelligenceService = class {
|
|
|
32797
33779
|
}
|
|
32798
33780
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
32799
33781
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
32800
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
32801
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
32802
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
32803
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(
|
|
33782
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and26(eq36(gbpDailyMetrics.projectId, projectId), eq36(gbpDailyMetrics.locationName, locationName))).all();
|
|
33783
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and26(eq36(gbpPlaceActions.projectId, projectId), eq36(gbpPlaceActions.locationName, locationName))).all();
|
|
33784
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and26(eq36(gbpLodgingSnapshots.projectId, projectId), eq36(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
33785
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and26(eq36(gbpPlaceDetails.projectId, projectId), eq36(gbpPlaceDetails.locationName, locationName))).orderBy(desc17(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
32804
33786
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
32805
33787
|
const summary = buildGbpSummary({
|
|
32806
33788
|
locationName,
|
|
@@ -32832,7 +33814,7 @@ var IntelligenceService = class {
|
|
|
32832
33814
|
/** Build the month-over-month keyword series for a location from the
|
|
32833
33815
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
32834
33816
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
32835
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
33817
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and26(eq36(gbpKeywordMonthly.projectId, projectId), eq36(gbpKeywordMonthly.locationName, locationName))).all();
|
|
32836
33818
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
32837
33819
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
32838
33820
|
const recentMonth = months[0] ?? null;
|
|
@@ -32863,7 +33845,7 @@ var IntelligenceService = class {
|
|
|
32863
33845
|
*/
|
|
32864
33846
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
32865
33847
|
const covered = new Set(coveredLocationNames);
|
|
32866
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
33848
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and26(eq36(insights.projectId, projectId), eq36(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
32867
33849
|
const staleIds = [];
|
|
32868
33850
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
32869
33851
|
for (const row of existing) {
|
|
@@ -32874,7 +33856,7 @@ var IntelligenceService = class {
|
|
|
32874
33856
|
}
|
|
32875
33857
|
this.db.transaction((tx) => {
|
|
32876
33858
|
for (const id of staleIds) {
|
|
32877
|
-
tx.delete(insights).where(
|
|
33859
|
+
tx.delete(insights).where(eq36(insights.id, id)).run();
|
|
32878
33860
|
}
|
|
32879
33861
|
for (const insight of gbpInsights) {
|
|
32880
33862
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -32952,7 +33934,7 @@ var IntelligenceService = class {
|
|
|
32952
33934
|
* create per run + aggregate). DB is left untouched.
|
|
32953
33935
|
*/
|
|
32954
33936
|
backfill(projectName, opts, onProgress) {
|
|
32955
|
-
const project = this.db.select().from(projects).where(
|
|
33937
|
+
const project = this.db.select().from(projects).where(eq36(projects.name, projectName)).get();
|
|
32956
33938
|
if (!project) {
|
|
32957
33939
|
throw new Error(`Project "${projectName}" not found`);
|
|
32958
33940
|
}
|
|
@@ -32965,13 +33947,13 @@ var IntelligenceService = class {
|
|
|
32965
33947
|
sinceTimestamp = parsed;
|
|
32966
33948
|
}
|
|
32967
33949
|
const allRuns = this.db.select().from(runs).where(
|
|
32968
|
-
|
|
32969
|
-
|
|
32970
|
-
or5(
|
|
33950
|
+
and26(
|
|
33951
|
+
eq36(runs.projectId, project.id),
|
|
33952
|
+
or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
|
|
32971
33953
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
32972
33954
|
ne5(runs.trigger, RunTriggers.probe)
|
|
32973
33955
|
)
|
|
32974
|
-
).orderBy(
|
|
33956
|
+
).orderBy(asc5(runs.finishedAt)).all();
|
|
32975
33957
|
let startIdx = 0;
|
|
32976
33958
|
let endIdx = allRuns.length;
|
|
32977
33959
|
if (opts?.fromRunId) {
|
|
@@ -33000,7 +33982,7 @@ var IntelligenceService = class {
|
|
|
33000
33982
|
let wouldDeleteTotal = 0;
|
|
33001
33983
|
const existingByRunId = /* @__PURE__ */ new Map();
|
|
33002
33984
|
if (isDryRun && targetRuns.length > 0) {
|
|
33003
|
-
const rows = this.db.select({ runId: insights.runId }).from(insights).where(
|
|
33985
|
+
const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray13(insights.runId, targetRuns.map((r) => r.id))).all();
|
|
33004
33986
|
for (const r of rows) {
|
|
33005
33987
|
if (r.runId == null) continue;
|
|
33006
33988
|
existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
|
|
@@ -33046,7 +34028,7 @@ var IntelligenceService = class {
|
|
|
33046
34028
|
return { processed, skipped, totalInsights };
|
|
33047
34029
|
}
|
|
33048
34030
|
loadTrackedCompetitors(projectId) {
|
|
33049
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
34031
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
33050
34032
|
}
|
|
33051
34033
|
/**
|
|
33052
34034
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -33067,15 +34049,15 @@ var IntelligenceService = class {
|
|
|
33067
34049
|
}
|
|
33068
34050
|
persistResult(result, runId, projectId) {
|
|
33069
34051
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
33070
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
34052
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq36(insights.runId, runId)).all();
|
|
33071
34053
|
for (const row of existingInsights) {
|
|
33072
34054
|
if (row.dismissed) {
|
|
33073
34055
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
33074
34056
|
}
|
|
33075
34057
|
}
|
|
33076
34058
|
this.db.transaction((tx) => {
|
|
33077
|
-
tx.delete(insights).where(
|
|
33078
|
-
tx.delete(healthSnapshots).where(
|
|
34059
|
+
tx.delete(insights).where(eq36(insights.runId, runId)).run();
|
|
34060
|
+
tx.delete(healthSnapshots).where(eq36(healthSnapshots.runId, runId)).run();
|
|
33079
34061
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33080
34062
|
for (const insight of result.insights) {
|
|
33081
34063
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -33095,7 +34077,7 @@ var IntelligenceService = class {
|
|
|
33095
34077
|
}).run();
|
|
33096
34078
|
}
|
|
33097
34079
|
tx.insert(healthSnapshots).values({
|
|
33098
|
-
id:
|
|
34080
|
+
id: crypto29.randomUUID(),
|
|
33099
34081
|
projectId,
|
|
33100
34082
|
runId,
|
|
33101
34083
|
overallCitedRate: String(result.health.overallCitedRate),
|
|
@@ -33126,24 +34108,24 @@ var IntelligenceService = class {
|
|
|
33126
34108
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
33127
34109
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
33128
34110
|
if (regressions.length === 0) return rawInsights;
|
|
33129
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
34111
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq36(gscSearchData.projectId, projectId)).all();
|
|
33130
34112
|
const gscConnected = gscRows.length > 0;
|
|
33131
34113
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
33132
34114
|
for (const row of gscRows) {
|
|
33133
34115
|
const key = row.query.toLowerCase();
|
|
33134
34116
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
33135
34117
|
}
|
|
33136
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
34118
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq36(projects.id, projectId)).get();
|
|
33137
34119
|
const locationCount = Math.max(
|
|
33138
34120
|
1,
|
|
33139
34121
|
(projectRow?.locations ?? []).length
|
|
33140
34122
|
);
|
|
33141
34123
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
33142
34124
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
33143
|
-
|
|
33144
|
-
|
|
33145
|
-
|
|
33146
|
-
or5(
|
|
34125
|
+
and26(
|
|
34126
|
+
eq36(runs.projectId, projectId),
|
|
34127
|
+
eq36(runs.kind, RunKinds["answer-visibility"]),
|
|
34128
|
+
or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
|
|
33147
34129
|
// Defensive — see top of file.
|
|
33148
34130
|
ne5(runs.trigger, RunTriggers.probe)
|
|
33149
34131
|
)
|
|
@@ -33163,7 +34145,7 @@ var IntelligenceService = class {
|
|
|
33163
34145
|
const haveHistory = recentRunIds.length > 0;
|
|
33164
34146
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
33165
34147
|
if (haveHistory) {
|
|
33166
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
34148
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and26(eq36(insights.type, "regression"), inArray13(insights.runId, recentRunIds))).all();
|
|
33167
34149
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
33168
34150
|
for (const row of priorRows) {
|
|
33169
34151
|
if (!row.runId) continue;
|
|
@@ -33192,7 +34174,7 @@ var IntelligenceService = class {
|
|
|
33192
34174
|
});
|
|
33193
34175
|
}
|
|
33194
34176
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
33195
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
34177
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq36(projects.id, projectId)).get();
|
|
33196
34178
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
33197
34179
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
33198
34180
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -33208,7 +34190,7 @@ var IntelligenceService = class {
|
|
|
33208
34190
|
citedDomains: querySnapshots.citedDomains,
|
|
33209
34191
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
33210
34192
|
snapshotLocation: querySnapshots.location
|
|
33211
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
34193
|
+
}).from(querySnapshots).leftJoin(queries, eq36(querySnapshots.queryId, queries.id)).where(eq36(querySnapshots.runId, runId)).all();
|
|
33212
34194
|
const snapshots = [];
|
|
33213
34195
|
let orphanCount = 0;
|
|
33214
34196
|
for (const r of rows) {
|
|
@@ -33283,6 +34265,11 @@ export {
|
|
|
33283
34265
|
gbpPlaceActions,
|
|
33284
34266
|
gbpLodgingSnapshots,
|
|
33285
34267
|
gbpPlaceDetails,
|
|
34268
|
+
adsConnections,
|
|
34269
|
+
adsCampaigns,
|
|
34270
|
+
adsAdGroups,
|
|
34271
|
+
adsAds,
|
|
34272
|
+
adsInsightsDaily,
|
|
33286
34273
|
createClient,
|
|
33287
34274
|
parseJsonColumn,
|
|
33288
34275
|
extractLegacyCredentials,
|
|
@@ -33315,6 +34302,8 @@ export {
|
|
|
33315
34302
|
hashLodging,
|
|
33316
34303
|
getUrlInfo,
|
|
33317
34304
|
getCrawlIssues,
|
|
34305
|
+
getLinkCounts,
|
|
34306
|
+
getUrlLinks,
|
|
33318
34307
|
PLUGIN_DIR,
|
|
33319
34308
|
DUCKDB_SPEC,
|
|
33320
34309
|
CC_CACHE_DIR,
|