@ainyc/canonry 4.78.0 → 4.80.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 +18 -0
- package/assets/assets/{BacklinksPage-CwXveumn.js → BacklinksPage-dRc62jAY.js} +1 -1
- package/assets/assets/{ChartPrimitives-DntKGI5J.js → ChartPrimitives-D2_IvTkk.js} +1 -1
- package/assets/assets/{ProjectPage-CVudiU8X.js → ProjectPage-DSuvRUIf.js} +1 -1
- package/assets/assets/{RunRow-DMtYXaxG.js → RunRow-C0MA3yuQ.js} +1 -1
- package/assets/assets/{RunsPage-Cz-YlucO.js → RunsPage-4uxTYgGy.js} +1 -1
- package/assets/assets/{SettingsPage-BCuG3C-0.js → SettingsPage-3-SLhcJ7.js} +1 -1
- package/assets/assets/{TrafficPage-DV8X47wa.js → TrafficPage-DZ50qwme.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-BmYhK9jm.js → TrafficSourceDetailPage-CzK5TMFp.js} +1 -1
- package/assets/assets/{arrow-left-CUmHyNnF.js → arrow-left-BaZIkAXX.js} +1 -1
- package/assets/assets/{extract-error-message-DFjy9_zi.js → extract-error-message-cpvfuFqW.js} +1 -1
- package/assets/assets/{index-D9smxU6R.js → index-EnY_OBRd.js} +70 -70
- package/assets/assets/{trash-2-B_UtEEm8.js → trash-2-JpcztiS5.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-XI6YSTGE.js → chunk-2QBSRHSN.js} +187 -1
- package/dist/{chunk-KPN22EWK.js → chunk-AVN6Q6LM.js} +138 -2
- package/dist/{chunk-3WXARKUE.js → chunk-CXIGHPBE.js} +996 -324
- package/dist/{chunk-QKTIP6GC.js → chunk-LCABGFYN.js} +713 -286
- package/dist/cli.js +369 -148
- package/dist/index.d.ts +17 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-CDVUUG7O.js → intelligence-service-ZWW3I3NL.js} +2 -2
- package/dist/mcp.js +9 -3
- package/package.json +11 -10
|
@@ -34,6 +34,16 @@ import {
|
|
|
34
34
|
__export,
|
|
35
35
|
absolutizeProjectUrl,
|
|
36
36
|
actionConfidenceLabel,
|
|
37
|
+
adsCampaignListResponseSchema,
|
|
38
|
+
adsConnectRequestSchema,
|
|
39
|
+
adsConnectionStatusDtoSchema,
|
|
40
|
+
adsCpcMicros,
|
|
41
|
+
adsCtr,
|
|
42
|
+
adsDisconnectResponseSchema,
|
|
43
|
+
adsInsightLevelSchema,
|
|
44
|
+
adsInsightsResponseSchema,
|
|
45
|
+
adsSummaryDtoSchema,
|
|
46
|
+
adsSyncResponseSchema,
|
|
37
47
|
agentProvidersResponseDtoSchema,
|
|
38
48
|
apiKeyDtoSchema,
|
|
39
49
|
apiKeyListDtoSchema,
|
|
@@ -232,10 +242,10 @@ import {
|
|
|
232
242
|
wordpressSchemaDeployResultDtoSchema,
|
|
233
243
|
wordpressSchemaStatusResultDtoSchema,
|
|
234
244
|
wordpressStatusDtoSchema
|
|
235
|
-
} from "./chunk-
|
|
245
|
+
} from "./chunk-AVN6Q6LM.js";
|
|
236
246
|
|
|
237
247
|
// src/intelligence-service.ts
|
|
238
|
-
import { eq as
|
|
248
|
+
import { eq as eq35, desc as desc17, asc as asc5, and as and25, ne as ne5, or as or5, inArray as inArray13, gte as gte7, lte as lte4 } from "drizzle-orm";
|
|
239
249
|
|
|
240
250
|
// ../db/src/client.ts
|
|
241
251
|
import { mkdirSync } from "fs";
|
|
@@ -246,6 +256,11 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
|
246
256
|
// ../db/src/schema.ts
|
|
247
257
|
var schema_exports = {};
|
|
248
258
|
__export(schema_exports, {
|
|
259
|
+
adsAdGroups: () => adsAdGroups,
|
|
260
|
+
adsAds: () => adsAds,
|
|
261
|
+
adsCampaigns: () => adsCampaigns,
|
|
262
|
+
adsConnections: () => adsConnections,
|
|
263
|
+
adsInsightsDaily: () => adsInsightsDaily,
|
|
249
264
|
agentMemory: () => agentMemory,
|
|
250
265
|
agentSessions: () => agentSessions,
|
|
251
266
|
aiReferralEventsHourly: () => aiReferralEventsHourly,
|
|
@@ -1219,6 +1234,83 @@ var gbpPlaceDetails = sqliteTable("gbp_place_details", {
|
|
|
1219
1234
|
}, (table) => [
|
|
1220
1235
|
index("idx_gbp_place_details_loc").on(table.projectId, table.locationName, table.syncedAt)
|
|
1221
1236
|
]);
|
|
1237
|
+
var adsConnections = sqliteTable("ads_connections", {
|
|
1238
|
+
id: text("id").primaryKey(),
|
|
1239
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1240
|
+
adAccountId: text("ad_account_id").notNull(),
|
|
1241
|
+
displayName: text("display_name"),
|
|
1242
|
+
currencyCode: text("currency_code"),
|
|
1243
|
+
timezone: text("timezone"),
|
|
1244
|
+
status: text("status"),
|
|
1245
|
+
lastSyncedAt: text("last_synced_at"),
|
|
1246
|
+
createdAt: text("created_at").notNull(),
|
|
1247
|
+
updatedAt: text("updated_at").notNull()
|
|
1248
|
+
}, (table) => [
|
|
1249
|
+
uniqueIndex("idx_ads_conn_project").on(table.projectId)
|
|
1250
|
+
]);
|
|
1251
|
+
var adsCampaigns = sqliteTable("ads_campaigns", {
|
|
1252
|
+
id: text("id").primaryKey(),
|
|
1253
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1254
|
+
name: text("name").notNull(),
|
|
1255
|
+
status: text("status").notNull(),
|
|
1256
|
+
biddingType: text("bidding_type"),
|
|
1257
|
+
dailySpendLimitMicros: integer("daily_spend_limit_micros"),
|
|
1258
|
+
lifetimeSpendLimitMicros: integer("lifetime_spend_limit_micros"),
|
|
1259
|
+
targeting: text("targeting", { mode: "json" }).$type(),
|
|
1260
|
+
upstreamCreatedAt: integer("upstream_created_at"),
|
|
1261
|
+
upstreamUpdatedAt: integer("upstream_updated_at"),
|
|
1262
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" }),
|
|
1263
|
+
syncedAt: text("synced_at").notNull()
|
|
1264
|
+
}, (table) => [
|
|
1265
|
+
index("idx_ads_campaigns_project").on(table.projectId)
|
|
1266
|
+
]);
|
|
1267
|
+
var adsAdGroups = sqliteTable("ads_ad_groups", {
|
|
1268
|
+
id: text("id").primaryKey(),
|
|
1269
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1270
|
+
campaignId: text("campaign_id").notNull().references(() => adsCampaigns.id, { onDelete: "cascade" }),
|
|
1271
|
+
name: text("name").notNull(),
|
|
1272
|
+
status: text("status").notNull(),
|
|
1273
|
+
billingEventType: text("billing_event_type"),
|
|
1274
|
+
maxBidMicros: integer("max_bid_micros"),
|
|
1275
|
+
contextHints: text("context_hints", { mode: "json" }).$type().notNull().default([]),
|
|
1276
|
+
upstreamCreatedAt: integer("upstream_created_at"),
|
|
1277
|
+
upstreamUpdatedAt: integer("upstream_updated_at"),
|
|
1278
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" }),
|
|
1279
|
+
syncedAt: text("synced_at").notNull()
|
|
1280
|
+
}, (table) => [
|
|
1281
|
+
index("idx_ads_ad_groups_project").on(table.projectId),
|
|
1282
|
+
index("idx_ads_ad_groups_campaign").on(table.campaignId)
|
|
1283
|
+
]);
|
|
1284
|
+
var adsAds = sqliteTable("ads_ads", {
|
|
1285
|
+
id: text("id").primaryKey(),
|
|
1286
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1287
|
+
adGroupId: text("ad_group_id").notNull().references(() => adsAdGroups.id, { onDelete: "cascade" }),
|
|
1288
|
+
name: text("name").notNull(),
|
|
1289
|
+
status: text("status").notNull(),
|
|
1290
|
+
creative: text("creative", { mode: "json" }).$type(),
|
|
1291
|
+
reviewStatus: text("review_status"),
|
|
1292
|
+
upstreamCreatedAt: integer("upstream_created_at"),
|
|
1293
|
+
upstreamUpdatedAt: integer("upstream_updated_at"),
|
|
1294
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" }),
|
|
1295
|
+
syncedAt: text("synced_at").notNull()
|
|
1296
|
+
}, (table) => [
|
|
1297
|
+
index("idx_ads_ads_project").on(table.projectId),
|
|
1298
|
+
index("idx_ads_ads_group").on(table.adGroupId)
|
|
1299
|
+
]);
|
|
1300
|
+
var adsInsightsDaily = sqliteTable("ads_insights_daily", {
|
|
1301
|
+
id: text("id").primaryKey(),
|
|
1302
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1303
|
+
level: text("level").notNull(),
|
|
1304
|
+
entityId: text("entity_id").notNull(),
|
|
1305
|
+
date: text("date").notNull(),
|
|
1306
|
+
impressions: integer("impressions").notNull().default(0),
|
|
1307
|
+
clicks: integer("clicks").notNull().default(0),
|
|
1308
|
+
spendMicros: integer("spend_micros").notNull().default(0),
|
|
1309
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" })
|
|
1310
|
+
}, (table) => [
|
|
1311
|
+
uniqueIndex("uniq_ads_insights_daily").on(table.projectId, table.level, table.entityId, table.date),
|
|
1312
|
+
index("idx_ads_insights_project_date").on(table.projectId, table.date)
|
|
1313
|
+
]);
|
|
1222
1314
|
|
|
1223
1315
|
// ../db/src/client.ts
|
|
1224
1316
|
function createClient(databasePath) {
|
|
@@ -2876,6 +2968,89 @@ var MIGRATION_VERSIONS = [
|
|
|
2876
2968
|
statements: [
|
|
2877
2969
|
`ALTER TABLE discovery_sessions ADD COLUMN warning TEXT`
|
|
2878
2970
|
]
|
|
2971
|
+
},
|
|
2972
|
+
{
|
|
2973
|
+
// OpenAI Advertiser API (ChatGPT ads) — connection metadata, entity
|
|
2974
|
+
// snapshots (campaigns / ad groups / ads), and daily paid-performance
|
|
2975
|
+
// rollups. One connection per project (ad accounts are not domain-bound).
|
|
2976
|
+
// Money columns are integer micros; ads-sync normalizes the insights
|
|
2977
|
+
// API's decimal-dollar spend at ingest. Credentials live in config.yaml.
|
|
2978
|
+
version: 77,
|
|
2979
|
+
name: "openai-ads-tables",
|
|
2980
|
+
statements: [
|
|
2981
|
+
`CREATE TABLE IF NOT EXISTS ads_connections (
|
|
2982
|
+
id TEXT PRIMARY KEY,
|
|
2983
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
2984
|
+
ad_account_id TEXT NOT NULL,
|
|
2985
|
+
display_name TEXT,
|
|
2986
|
+
currency_code TEXT,
|
|
2987
|
+
timezone TEXT,
|
|
2988
|
+
status TEXT,
|
|
2989
|
+
last_synced_at TEXT,
|
|
2990
|
+
created_at TEXT NOT NULL,
|
|
2991
|
+
updated_at TEXT NOT NULL
|
|
2992
|
+
)`,
|
|
2993
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_ads_conn_project ON ads_connections(project_id)`,
|
|
2994
|
+
`CREATE TABLE IF NOT EXISTS ads_campaigns (
|
|
2995
|
+
id TEXT PRIMARY KEY,
|
|
2996
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
2997
|
+
name TEXT NOT NULL,
|
|
2998
|
+
status TEXT NOT NULL,
|
|
2999
|
+
bidding_type TEXT,
|
|
3000
|
+
daily_spend_limit_micros INTEGER,
|
|
3001
|
+
lifetime_spend_limit_micros INTEGER,
|
|
3002
|
+
targeting TEXT,
|
|
3003
|
+
upstream_created_at INTEGER,
|
|
3004
|
+
upstream_updated_at INTEGER,
|
|
3005
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
|
|
3006
|
+
synced_at TEXT NOT NULL
|
|
3007
|
+
)`,
|
|
3008
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_campaigns_project ON ads_campaigns(project_id)`,
|
|
3009
|
+
`CREATE TABLE IF NOT EXISTS ads_ad_groups (
|
|
3010
|
+
id TEXT PRIMARY KEY,
|
|
3011
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3012
|
+
campaign_id TEXT NOT NULL REFERENCES ads_campaigns(id) ON DELETE CASCADE,
|
|
3013
|
+
name TEXT NOT NULL,
|
|
3014
|
+
status TEXT NOT NULL,
|
|
3015
|
+
billing_event_type TEXT,
|
|
3016
|
+
max_bid_micros INTEGER,
|
|
3017
|
+
context_hints TEXT NOT NULL DEFAULT '[]',
|
|
3018
|
+
upstream_created_at INTEGER,
|
|
3019
|
+
upstream_updated_at INTEGER,
|
|
3020
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
|
|
3021
|
+
synced_at TEXT NOT NULL
|
|
3022
|
+
)`,
|
|
3023
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_ad_groups_project ON ads_ad_groups(project_id)`,
|
|
3024
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_ad_groups_campaign ON ads_ad_groups(campaign_id)`,
|
|
3025
|
+
`CREATE TABLE IF NOT EXISTS ads_ads (
|
|
3026
|
+
id TEXT PRIMARY KEY,
|
|
3027
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3028
|
+
ad_group_id TEXT NOT NULL REFERENCES ads_ad_groups(id) ON DELETE CASCADE,
|
|
3029
|
+
name TEXT NOT NULL,
|
|
3030
|
+
status TEXT NOT NULL,
|
|
3031
|
+
creative TEXT,
|
|
3032
|
+
review_status TEXT,
|
|
3033
|
+
upstream_created_at INTEGER,
|
|
3034
|
+
upstream_updated_at INTEGER,
|
|
3035
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
|
|
3036
|
+
synced_at TEXT NOT NULL
|
|
3037
|
+
)`,
|
|
3038
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_ads_project ON ads_ads(project_id)`,
|
|
3039
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_ads_group ON ads_ads(ad_group_id)`,
|
|
3040
|
+
`CREATE TABLE IF NOT EXISTS ads_insights_daily (
|
|
3041
|
+
id TEXT PRIMARY KEY,
|
|
3042
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3043
|
+
level TEXT NOT NULL,
|
|
3044
|
+
entity_id TEXT NOT NULL,
|
|
3045
|
+
date TEXT NOT NULL,
|
|
3046
|
+
impressions INTEGER NOT NULL DEFAULT 0,
|
|
3047
|
+
clicks INTEGER NOT NULL DEFAULT 0,
|
|
3048
|
+
spend_micros INTEGER NOT NULL DEFAULT 0,
|
|
3049
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL
|
|
3050
|
+
)`,
|
|
3051
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS uniq_ads_insights_daily ON ads_insights_daily(project_id, level, entity_id, date)`,
|
|
3052
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_insights_project_date ON ads_insights_daily(project_id, date)`
|
|
3053
|
+
]
|
|
2879
3054
|
}
|
|
2880
3055
|
];
|
|
2881
3056
|
function isDuplicateColumnError(err) {
|
|
@@ -13020,6 +13195,12 @@ function makeSnippet(text2, query) {
|
|
|
13020
13195
|
import { z } from "zod";
|
|
13021
13196
|
var SCHEMA_TABLE = {
|
|
13022
13197
|
AgentProvidersResponseDto: agentProvidersResponseDtoSchema,
|
|
13198
|
+
AdsCampaignListResponse: adsCampaignListResponseSchema,
|
|
13199
|
+
AdsConnectionStatusDto: adsConnectionStatusDtoSchema,
|
|
13200
|
+
AdsDisconnectResponse: adsDisconnectResponseSchema,
|
|
13201
|
+
AdsInsightsResponse: adsInsightsResponseSchema,
|
|
13202
|
+
AdsSummaryDto: adsSummaryDtoSchema,
|
|
13203
|
+
AdsSyncResponse: adsSyncResponseSchema,
|
|
13023
13204
|
ApiKeyDto: apiKeyDtoSchema,
|
|
13024
13205
|
ApiKeyListDto: apiKeyListDtoSchema,
|
|
13025
13206
|
AuditLogEntry: auditLogEntrySchema,
|
|
@@ -15061,6 +15242,106 @@ var routeCatalog = [
|
|
|
15061
15242
|
404: errorResponse("Project not found.")
|
|
15062
15243
|
}
|
|
15063
15244
|
},
|
|
15245
|
+
{
|
|
15246
|
+
method: "post",
|
|
15247
|
+
path: "/api/v1/projects/{name}/ads/connect",
|
|
15248
|
+
summary: "Connect an OpenAI ad account (ChatGPT ads) with an Ads Manager SDK key",
|
|
15249
|
+
tags: ["ads"],
|
|
15250
|
+
parameters: [nameParameter],
|
|
15251
|
+
requestBody: {
|
|
15252
|
+
required: true,
|
|
15253
|
+
content: {
|
|
15254
|
+
"application/json": {
|
|
15255
|
+
schema: {
|
|
15256
|
+
type: "object",
|
|
15257
|
+
required: ["apiKey"],
|
|
15258
|
+
properties: {
|
|
15259
|
+
apiKey: stringSchema
|
|
15260
|
+
}
|
|
15261
|
+
}
|
|
15262
|
+
}
|
|
15263
|
+
}
|
|
15264
|
+
},
|
|
15265
|
+
responses: {
|
|
15266
|
+
200: jsonResponse("Connected; key validated against the upstream ad account.", "AdsConnectionStatusDto"),
|
|
15267
|
+
400: errorResponse("Missing/invalid key or credential storage unavailable."),
|
|
15268
|
+
404: errorResponse("Project not found.")
|
|
15269
|
+
}
|
|
15270
|
+
},
|
|
15271
|
+
{
|
|
15272
|
+
method: "delete",
|
|
15273
|
+
path: "/api/v1/projects/{name}/ads/connection",
|
|
15274
|
+
summary: "Disconnect the OpenAI ad account (removes the stored credential)",
|
|
15275
|
+
tags: ["ads"],
|
|
15276
|
+
parameters: [nameParameter],
|
|
15277
|
+
responses: {
|
|
15278
|
+
200: jsonResponse("Disconnected (idempotent).", "AdsDisconnectResponse"),
|
|
15279
|
+
404: errorResponse("Project not found.")
|
|
15280
|
+
}
|
|
15281
|
+
},
|
|
15282
|
+
{
|
|
15283
|
+
method: "get",
|
|
15284
|
+
path: "/api/v1/projects/{name}/ads/status",
|
|
15285
|
+
summary: "OpenAI ads connection status and last sync time",
|
|
15286
|
+
tags: ["ads"],
|
|
15287
|
+
parameters: [nameParameter],
|
|
15288
|
+
responses: {
|
|
15289
|
+
200: jsonResponse("Connection status.", "AdsConnectionStatusDto"),
|
|
15290
|
+
404: errorResponse("Project not found.")
|
|
15291
|
+
}
|
|
15292
|
+
},
|
|
15293
|
+
{
|
|
15294
|
+
method: "post",
|
|
15295
|
+
path: "/api/v1/projects/{name}/ads/sync",
|
|
15296
|
+
summary: "Trigger an ads-sync run (entity snapshots + daily paid-performance rollups)",
|
|
15297
|
+
tags: ["ads"],
|
|
15298
|
+
parameters: [nameParameter],
|
|
15299
|
+
responses: {
|
|
15300
|
+
200: jsonResponse("Sync run queued.", "AdsSyncResponse"),
|
|
15301
|
+
400: errorResponse("No ads connection for this project."),
|
|
15302
|
+
404: errorResponse("Project not found.")
|
|
15303
|
+
}
|
|
15304
|
+
},
|
|
15305
|
+
{
|
|
15306
|
+
method: "get",
|
|
15307
|
+
path: "/api/v1/projects/{name}/ads/campaigns",
|
|
15308
|
+
summary: "Synced campaign snapshots with nested ad groups (context hints) and ads",
|
|
15309
|
+
tags: ["ads"],
|
|
15310
|
+
parameters: [nameParameter],
|
|
15311
|
+
responses: {
|
|
15312
|
+
200: jsonResponse("Campaign snapshots.", "AdsCampaignListResponse"),
|
|
15313
|
+
404: errorResponse("Project not found.")
|
|
15314
|
+
}
|
|
15315
|
+
},
|
|
15316
|
+
{
|
|
15317
|
+
method: "get",
|
|
15318
|
+
path: "/api/v1/projects/{name}/ads/insights",
|
|
15319
|
+
summary: "Daily paid-performance rollups (spend in integer micros; ctr/cpc derived server-side)",
|
|
15320
|
+
tags: ["ads"],
|
|
15321
|
+
parameters: [
|
|
15322
|
+
nameParameter,
|
|
15323
|
+
{ in: "query", name: "level", required: false, description: "campaign | ad_group", schema: stringSchema },
|
|
15324
|
+
{ in: "query", name: "entityId", required: false, description: "Scope to one upstream entity id", schema: stringSchema },
|
|
15325
|
+
{ in: "query", name: "from", required: false, description: "Inclusive start date (YYYY-MM-DD)", schema: stringSchema },
|
|
15326
|
+
{ in: "query", name: "to", required: false, description: "Inclusive end date (YYYY-MM-DD)", schema: stringSchema }
|
|
15327
|
+
],
|
|
15328
|
+
responses: {
|
|
15329
|
+
200: jsonResponse("Daily rollup rows.", "AdsInsightsResponse"),
|
|
15330
|
+
400: errorResponse("Invalid level filter."),
|
|
15331
|
+
404: errorResponse("Project not found.")
|
|
15332
|
+
}
|
|
15333
|
+
},
|
|
15334
|
+
{
|
|
15335
|
+
method: "get",
|
|
15336
|
+
path: "/api/v1/projects/{name}/ads/summary",
|
|
15337
|
+
summary: "Composite paid-performance summary (campaign-level totals; all derived metrics)",
|
|
15338
|
+
tags: ["ads"],
|
|
15339
|
+
parameters: [nameParameter],
|
|
15340
|
+
responses: {
|
|
15341
|
+
200: jsonResponse("Summary returned.", "AdsSummaryDto"),
|
|
15342
|
+
404: errorResponse("Project not found.")
|
|
15343
|
+
}
|
|
15344
|
+
},
|
|
15064
15345
|
{
|
|
15065
15346
|
method: "post",
|
|
15066
15347
|
path: "/api/v1/projects/{name}/bing/connect",
|
|
@@ -20351,9 +20632,269 @@ async function googleRoutes(app, opts) {
|
|
|
20351
20632
|
});
|
|
20352
20633
|
}
|
|
20353
20634
|
|
|
20354
|
-
// ../api-routes/src/
|
|
20635
|
+
// ../api-routes/src/ads.ts
|
|
20355
20636
|
import crypto18 from "crypto";
|
|
20356
|
-
import { eq as eq21, and as and14,
|
|
20637
|
+
import { eq as eq21, and as and14, asc as asc2, gte as gte3, lte as lte2, inArray as inArray10 } from "drizzle-orm";
|
|
20638
|
+
function statusDto(row) {
|
|
20639
|
+
if (!row) return { connected: false };
|
|
20640
|
+
return {
|
|
20641
|
+
connected: true,
|
|
20642
|
+
adAccountId: row.adAccountId,
|
|
20643
|
+
displayName: row.displayName,
|
|
20644
|
+
currencyCode: row.currencyCode,
|
|
20645
|
+
timezone: row.timezone,
|
|
20646
|
+
status: row.status,
|
|
20647
|
+
lastSyncedAt: row.lastSyncedAt
|
|
20648
|
+
};
|
|
20649
|
+
}
|
|
20650
|
+
function creativeDto(raw) {
|
|
20651
|
+
if (raw == null || typeof raw !== "object") return null;
|
|
20652
|
+
const c = raw;
|
|
20653
|
+
return {
|
|
20654
|
+
type: typeof c.type === "string" ? c.type : null,
|
|
20655
|
+
title: typeof c.title === "string" ? c.title : null,
|
|
20656
|
+
body: typeof c.body === "string" ? c.body : null,
|
|
20657
|
+
targetUrl: typeof c.target_url === "string" ? c.target_url : null
|
|
20658
|
+
};
|
|
20659
|
+
}
|
|
20660
|
+
async function adsRoutes(app, opts) {
|
|
20661
|
+
app.post(
|
|
20662
|
+
"/projects/:name/ads/connect",
|
|
20663
|
+
async (request) => {
|
|
20664
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20665
|
+
const parsed = adsConnectRequestSchema.safeParse(request.body ?? {});
|
|
20666
|
+
if (!parsed.success) throw validationError('"apiKey" is required');
|
|
20667
|
+
if (!opts.adsCredentialStore || !opts.verifyAdsAccount) {
|
|
20668
|
+
throw validationError("Ads credential storage is not configured for this deployment");
|
|
20669
|
+
}
|
|
20670
|
+
let account;
|
|
20671
|
+
try {
|
|
20672
|
+
account = await opts.verifyAdsAccount(parsed.data.apiKey);
|
|
20673
|
+
} catch (err) {
|
|
20674
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
20675
|
+
throw validationError(`OpenAI Ads API rejected the key: ${message}`);
|
|
20676
|
+
}
|
|
20677
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20678
|
+
const existingCfg = opts.adsCredentialStore.getConnection(project.name);
|
|
20679
|
+
opts.adsCredentialStore.upsertConnection({
|
|
20680
|
+
projectName: project.name,
|
|
20681
|
+
apiKey: parsed.data.apiKey,
|
|
20682
|
+
adAccountId: account.id,
|
|
20683
|
+
createdAt: existingCfg?.createdAt ?? now,
|
|
20684
|
+
updatedAt: now
|
|
20685
|
+
});
|
|
20686
|
+
const existingRow = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20687
|
+
app.db.transaction((tx) => {
|
|
20688
|
+
if (existingRow) {
|
|
20689
|
+
tx.update(adsConnections).set({
|
|
20690
|
+
adAccountId: account.id,
|
|
20691
|
+
displayName: account.name,
|
|
20692
|
+
currencyCode: account.currencyCode,
|
|
20693
|
+
timezone: account.timezone,
|
|
20694
|
+
status: account.status,
|
|
20695
|
+
updatedAt: now
|
|
20696
|
+
}).where(eq21(adsConnections.id, existingRow.id)).run();
|
|
20697
|
+
} else {
|
|
20698
|
+
tx.insert(adsConnections).values({
|
|
20699
|
+
id: crypto18.randomUUID(),
|
|
20700
|
+
projectId: project.id,
|
|
20701
|
+
adAccountId: account.id,
|
|
20702
|
+
displayName: account.name,
|
|
20703
|
+
currencyCode: account.currencyCode,
|
|
20704
|
+
timezone: account.timezone,
|
|
20705
|
+
status: account.status,
|
|
20706
|
+
createdAt: now,
|
|
20707
|
+
updatedAt: now
|
|
20708
|
+
}).run();
|
|
20709
|
+
}
|
|
20710
|
+
writeAuditLog(tx, auditFromRequest(request, {
|
|
20711
|
+
projectId: project.id,
|
|
20712
|
+
actor: "api",
|
|
20713
|
+
action: "ads.connected",
|
|
20714
|
+
entityType: "ads-connection",
|
|
20715
|
+
entityId: account.id
|
|
20716
|
+
}));
|
|
20717
|
+
});
|
|
20718
|
+
const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20719
|
+
return statusDto(row);
|
|
20720
|
+
}
|
|
20721
|
+
);
|
|
20722
|
+
app.delete("/projects/:name/ads/connection", async (request) => {
|
|
20723
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20724
|
+
const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20725
|
+
if (row) {
|
|
20726
|
+
app.db.transaction((tx) => {
|
|
20727
|
+
tx.delete(adsConnections).where(eq21(adsConnections.id, row.id)).run();
|
|
20728
|
+
writeAuditLog(tx, auditFromRequest(request, {
|
|
20729
|
+
projectId: project.id,
|
|
20730
|
+
actor: "api",
|
|
20731
|
+
action: "ads.disconnected",
|
|
20732
|
+
entityType: "ads-connection",
|
|
20733
|
+
entityId: row.adAccountId
|
|
20734
|
+
}));
|
|
20735
|
+
});
|
|
20736
|
+
}
|
|
20737
|
+
const removedFromConfig = opts.adsCredentialStore?.removeConnection(project.name) ?? false;
|
|
20738
|
+
const response = { disconnected: Boolean(row) || removedFromConfig };
|
|
20739
|
+
return response;
|
|
20740
|
+
});
|
|
20741
|
+
app.get("/projects/:name/ads/status", async (request) => {
|
|
20742
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20743
|
+
const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20744
|
+
return statusDto(row);
|
|
20745
|
+
});
|
|
20746
|
+
app.post("/projects/:name/ads/sync", async (request) => {
|
|
20747
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20748
|
+
const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20749
|
+
if (!row) {
|
|
20750
|
+
throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
|
|
20751
|
+
}
|
|
20752
|
+
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and14(
|
|
20753
|
+
eq21(runs.projectId, project.id),
|
|
20754
|
+
eq21(runs.kind, RunKinds["ads-sync"]),
|
|
20755
|
+
inArray10(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
20756
|
+
)).get();
|
|
20757
|
+
if (inFlight) {
|
|
20758
|
+
const existing = { runId: inFlight.id, status: inFlight.status };
|
|
20759
|
+
return existing;
|
|
20760
|
+
}
|
|
20761
|
+
const runId = crypto18.randomUUID();
|
|
20762
|
+
app.db.insert(runs).values({
|
|
20763
|
+
id: runId,
|
|
20764
|
+
projectId: project.id,
|
|
20765
|
+
kind: RunKinds["ads-sync"],
|
|
20766
|
+
status: RunStatuses.queued,
|
|
20767
|
+
trigger: RunTriggers.manual,
|
|
20768
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20769
|
+
}).run();
|
|
20770
|
+
opts.onAdsSyncRequested?.(runId, project.id);
|
|
20771
|
+
const response = { runId, status: RunStatuses.queued };
|
|
20772
|
+
return response;
|
|
20773
|
+
});
|
|
20774
|
+
app.get("/projects/:name/ads/campaigns", async (request) => {
|
|
20775
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20776
|
+
const campaignRows = app.db.select().from(adsCampaigns).where(eq21(adsCampaigns.projectId, project.id)).all();
|
|
20777
|
+
const groupRows = app.db.select().from(adsAdGroups).where(eq21(adsAdGroups.projectId, project.id)).all();
|
|
20778
|
+
const adRows = app.db.select().from(adsAds).where(eq21(adsAds.projectId, project.id)).all();
|
|
20779
|
+
const adsByGroup = /* @__PURE__ */ new Map();
|
|
20780
|
+
for (const ad of adRows) {
|
|
20781
|
+
const dto = {
|
|
20782
|
+
id: ad.id,
|
|
20783
|
+
adGroupId: ad.adGroupId,
|
|
20784
|
+
name: ad.name,
|
|
20785
|
+
status: ad.status,
|
|
20786
|
+
reviewStatus: ad.reviewStatus,
|
|
20787
|
+
creative: creativeDto(ad.creative)
|
|
20788
|
+
};
|
|
20789
|
+
const list = adsByGroup.get(ad.adGroupId) ?? [];
|
|
20790
|
+
list.push(dto);
|
|
20791
|
+
adsByGroup.set(ad.adGroupId, list);
|
|
20792
|
+
}
|
|
20793
|
+
const groupsByCampaign = /* @__PURE__ */ new Map();
|
|
20794
|
+
for (const group of groupRows) {
|
|
20795
|
+
const dto = {
|
|
20796
|
+
id: group.id,
|
|
20797
|
+
campaignId: group.campaignId,
|
|
20798
|
+
name: group.name,
|
|
20799
|
+
status: group.status,
|
|
20800
|
+
billingEventType: group.billingEventType,
|
|
20801
|
+
maxBidMicros: group.maxBidMicros,
|
|
20802
|
+
contextHints: group.contextHints,
|
|
20803
|
+
ads: adsByGroup.get(group.id) ?? []
|
|
20804
|
+
};
|
|
20805
|
+
const list = groupsByCampaign.get(group.campaignId) ?? [];
|
|
20806
|
+
list.push(dto);
|
|
20807
|
+
groupsByCampaign.set(group.campaignId, list);
|
|
20808
|
+
}
|
|
20809
|
+
const campaigns = campaignRows.map((campaign) => ({
|
|
20810
|
+
id: campaign.id,
|
|
20811
|
+
name: campaign.name,
|
|
20812
|
+
status: campaign.status,
|
|
20813
|
+
biddingType: campaign.biddingType,
|
|
20814
|
+
dailySpendLimitMicros: campaign.dailySpendLimitMicros,
|
|
20815
|
+
lifetimeSpendLimitMicros: campaign.lifetimeSpendLimitMicros,
|
|
20816
|
+
adGroups: groupsByCampaign.get(campaign.id) ?? []
|
|
20817
|
+
}));
|
|
20818
|
+
const response = { campaigns };
|
|
20819
|
+
return response;
|
|
20820
|
+
});
|
|
20821
|
+
app.get("/projects/:name/ads/insights", async (request) => {
|
|
20822
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20823
|
+
const { level, entityId, from, to } = request.query;
|
|
20824
|
+
let parsedLevel;
|
|
20825
|
+
if (level !== void 0) {
|
|
20826
|
+
const result = adsInsightLevelSchema.safeParse(level);
|
|
20827
|
+
if (!result.success) {
|
|
20828
|
+
throw validationError('"level" must be one of: campaign, ad_group');
|
|
20829
|
+
}
|
|
20830
|
+
parsedLevel = result.data;
|
|
20831
|
+
}
|
|
20832
|
+
const conditions = [eq21(adsInsightsDaily.projectId, project.id)];
|
|
20833
|
+
if (parsedLevel) conditions.push(eq21(adsInsightsDaily.level, parsedLevel));
|
|
20834
|
+
if (entityId) conditions.push(eq21(adsInsightsDaily.entityId, entityId));
|
|
20835
|
+
if (from) conditions.push(gte3(adsInsightsDaily.date, from));
|
|
20836
|
+
if (to) conditions.push(lte2(adsInsightsDaily.date, to));
|
|
20837
|
+
const rows = app.db.select().from(adsInsightsDaily).where(and14(...conditions)).orderBy(asc2(adsInsightsDaily.date)).all();
|
|
20838
|
+
const dtoRows = rows.map((row) => ({
|
|
20839
|
+
level: row.level,
|
|
20840
|
+
entityId: row.entityId,
|
|
20841
|
+
date: row.date,
|
|
20842
|
+
impressions: row.impressions,
|
|
20843
|
+
clicks: row.clicks,
|
|
20844
|
+
spendMicros: row.spendMicros,
|
|
20845
|
+
ctr: adsCtr(row.clicks, row.impressions),
|
|
20846
|
+
cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
|
|
20847
|
+
}));
|
|
20848
|
+
const conn = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20849
|
+
const response = { rows: dtoRows, currencyCode: conn?.currencyCode ?? null };
|
|
20850
|
+
return response;
|
|
20851
|
+
});
|
|
20852
|
+
app.get("/projects/:name/ads/summary", async (request) => {
|
|
20853
|
+
const project = resolveProject(app.db, request.params.name);
|
|
20854
|
+
const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
|
|
20855
|
+
const campaignCount = app.db.select().from(adsCampaigns).where(eq21(adsCampaigns.projectId, project.id)).all().length;
|
|
20856
|
+
const adGroupCount = app.db.select().from(adsAdGroups).where(eq21(adsAdGroups.projectId, project.id)).all().length;
|
|
20857
|
+
const adCount = app.db.select().from(adsAds).where(eq21(adsAds.projectId, project.id)).all().length;
|
|
20858
|
+
const campaignInsights = app.db.select().from(adsInsightsDaily).where(and14(
|
|
20859
|
+
eq21(adsInsightsDaily.projectId, project.id),
|
|
20860
|
+
eq21(adsInsightsDaily.level, "campaign")
|
|
20861
|
+
)).all();
|
|
20862
|
+
let impressions = 0;
|
|
20863
|
+
let clicks = 0;
|
|
20864
|
+
let spendMicros = 0;
|
|
20865
|
+
let fromDate = null;
|
|
20866
|
+
let toDate = null;
|
|
20867
|
+
for (const insight of campaignInsights) {
|
|
20868
|
+
impressions += insight.impressions;
|
|
20869
|
+
clicks += insight.clicks;
|
|
20870
|
+
spendMicros += insight.spendMicros;
|
|
20871
|
+
if (fromDate === null || insight.date < fromDate) fromDate = insight.date;
|
|
20872
|
+
if (toDate === null || insight.date > toDate) toDate = insight.date;
|
|
20873
|
+
}
|
|
20874
|
+
const response = {
|
|
20875
|
+
connected: Boolean(row),
|
|
20876
|
+
displayName: row?.displayName ?? null,
|
|
20877
|
+
currencyCode: row?.currencyCode ?? null,
|
|
20878
|
+
lastSyncedAt: row?.lastSyncedAt ?? null,
|
|
20879
|
+
campaignCount,
|
|
20880
|
+
adGroupCount,
|
|
20881
|
+
adCount,
|
|
20882
|
+
window: { from: fromDate, to: toDate },
|
|
20883
|
+
totals: {
|
|
20884
|
+
impressions,
|
|
20885
|
+
clicks,
|
|
20886
|
+
spendMicros,
|
|
20887
|
+
ctr: adsCtr(clicks, impressions),
|
|
20888
|
+
cpcMicros: adsCpcMicros(spendMicros, clicks)
|
|
20889
|
+
}
|
|
20890
|
+
};
|
|
20891
|
+
return response;
|
|
20892
|
+
});
|
|
20893
|
+
}
|
|
20894
|
+
|
|
20895
|
+
// ../api-routes/src/bing.ts
|
|
20896
|
+
import crypto19 from "crypto";
|
|
20897
|
+
import { eq as eq22, and as and15, desc as desc11 } from "drizzle-orm";
|
|
20357
20898
|
|
|
20358
20899
|
// ../integration-bing/src/constants.ts
|
|
20359
20900
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -20681,7 +21222,7 @@ async function bingRoutes(app, opts) {
|
|
|
20681
21222
|
const store = requireConnectionStore();
|
|
20682
21223
|
const project = resolveProject(app.db, request.params.name);
|
|
20683
21224
|
requireConnection(store, project.canonicalDomain);
|
|
20684
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
21225
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq22(bingUrlInspections.projectId, project.id)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
|
|
20685
21226
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
20686
21227
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
20687
21228
|
for (const row of allInspections) {
|
|
@@ -20738,7 +21279,7 @@ async function bingRoutes(app, opts) {
|
|
|
20738
21279
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
20739
21280
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20740
21281
|
app.db.insert(bingCoverageSnapshots).values({
|
|
20741
|
-
id:
|
|
21282
|
+
id: crypto19.randomUUID(),
|
|
20742
21283
|
projectId: project.id,
|
|
20743
21284
|
syncRunId: snapshotRunId,
|
|
20744
21285
|
date: snapshotDate,
|
|
@@ -20770,7 +21311,7 @@ async function bingRoutes(app, opts) {
|
|
|
20770
21311
|
const project = resolveProject(app.db, request.params.name);
|
|
20771
21312
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
20772
21313
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
20773
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
21314
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq22(bingCoverageSnapshots.projectId, project.id)).orderBy(desc11(bingCoverageSnapshots.date)).limit(limit).all();
|
|
20774
21315
|
return rows.map((r) => ({
|
|
20775
21316
|
date: r.date,
|
|
20776
21317
|
indexed: r.indexed,
|
|
@@ -20782,7 +21323,7 @@ async function bingRoutes(app, opts) {
|
|
|
20782
21323
|
requireConnectionStore();
|
|
20783
21324
|
const project = resolveProject(app.db, request.params.name);
|
|
20784
21325
|
const { url, limit } = request.query;
|
|
20785
|
-
const whereClause = url ?
|
|
21326
|
+
const whereClause = url ? and15(eq22(bingUrlInspections.projectId, project.id), eq22(bingUrlInspections.url, url)) : eq22(bingUrlInspections.projectId, project.id);
|
|
20786
21327
|
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
21328
|
return filtered.map((r) => ({
|
|
20788
21329
|
id: r.id,
|
|
@@ -20809,7 +21350,7 @@ async function bingRoutes(app, opts) {
|
|
|
20809
21350
|
throw validationError("url is required");
|
|
20810
21351
|
}
|
|
20811
21352
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20812
|
-
const runId =
|
|
21353
|
+
const runId = crypto19.randomUUID();
|
|
20813
21354
|
app.db.insert(runs).values({
|
|
20814
21355
|
id: runId,
|
|
20815
21356
|
projectId: project.id,
|
|
@@ -20830,7 +21371,7 @@ async function bingRoutes(app, opts) {
|
|
|
20830
21371
|
discoveryDate: result.DiscoveryDate ?? null
|
|
20831
21372
|
});
|
|
20832
21373
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20833
|
-
const id =
|
|
21374
|
+
const id = crypto19.randomUUID();
|
|
20834
21375
|
const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
|
|
20835
21376
|
const lastCrawledDate = parseBingDate(result.LastCrawledDate);
|
|
20836
21377
|
const inIndexDate = parseBingDate(result.InIndexDate);
|
|
@@ -20872,7 +21413,7 @@ async function bingRoutes(app, opts) {
|
|
|
20872
21413
|
anchorCount: result.AnchorCount ?? null,
|
|
20873
21414
|
discoveryDate
|
|
20874
21415
|
}).run();
|
|
20875
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
21416
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq22(runs.id, runId)).run();
|
|
20876
21417
|
return {
|
|
20877
21418
|
id,
|
|
20878
21419
|
url,
|
|
@@ -20888,7 +21429,7 @@ async function bingRoutes(app, opts) {
|
|
|
20888
21429
|
} catch (e) {
|
|
20889
21430
|
const msg = e instanceof Error ? e.message : String(e);
|
|
20890
21431
|
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(
|
|
21432
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
|
|
20892
21433
|
throw e;
|
|
20893
21434
|
}
|
|
20894
21435
|
});
|
|
@@ -20900,7 +21441,7 @@ async function bingRoutes(app, opts) {
|
|
|
20900
21441
|
throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
|
|
20901
21442
|
}
|
|
20902
21443
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20903
|
-
const runId =
|
|
21444
|
+
const runId = crypto19.randomUUID();
|
|
20904
21445
|
app.db.insert(runs).values({
|
|
20905
21446
|
id: runId,
|
|
20906
21447
|
projectId: project.id,
|
|
@@ -20915,7 +21456,7 @@ async function bingRoutes(app, opts) {
|
|
|
20915
21456
|
} else {
|
|
20916
21457
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
20917
21458
|
}
|
|
20918
|
-
const run = app.db.select().from(runs).where(
|
|
21459
|
+
const run = app.db.select().from(runs).where(eq22(runs.id, runId)).get();
|
|
20919
21460
|
return run;
|
|
20920
21461
|
});
|
|
20921
21462
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -20927,7 +21468,7 @@ async function bingRoutes(app, opts) {
|
|
|
20927
21468
|
}
|
|
20928
21469
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
20929
21470
|
if (request.body?.allUnindexed) {
|
|
20930
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
21471
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq22(bingUrlInspections.projectId, project.id)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
|
|
20931
21472
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
20932
21473
|
for (const row of allInspections) {
|
|
20933
21474
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -21014,14 +21555,14 @@ async function bingRoutes(app, opts) {
|
|
|
21014
21555
|
import fs from "fs";
|
|
21015
21556
|
import path from "path";
|
|
21016
21557
|
import os from "os";
|
|
21017
|
-
import { eq as
|
|
21558
|
+
import { eq as eq23, and as and16 } from "drizzle-orm";
|
|
21018
21559
|
function getScreenshotDir() {
|
|
21019
21560
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
21020
21561
|
}
|
|
21021
21562
|
async function cdpRoutes(app, opts) {
|
|
21022
21563
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
21023
21564
|
const { snapshotId } = request.params;
|
|
21024
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(
|
|
21565
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq23(querySnapshots.id, snapshotId)).get();
|
|
21025
21566
|
if (!snapshot?.screenshotPath) {
|
|
21026
21567
|
const err = notFound("Screenshot", snapshotId);
|
|
21027
21568
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -21088,7 +21629,7 @@ async function cdpRoutes(app, opts) {
|
|
|
21088
21629
|
async (request, reply) => {
|
|
21089
21630
|
const project = resolveProject(app.db, request.params.name);
|
|
21090
21631
|
const { runId } = request.params;
|
|
21091
|
-
const run = app.db.select().from(runs).where(
|
|
21632
|
+
const run = app.db.select().from(runs).where(and16(eq23(runs.id, runId), eq23(runs.projectId, project.id))).get();
|
|
21092
21633
|
if (!run) {
|
|
21093
21634
|
const err = notFound("Run", runId);
|
|
21094
21635
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -21101,8 +21642,8 @@ async function cdpRoutes(app, opts) {
|
|
|
21101
21642
|
citedDomains: querySnapshots.citedDomains,
|
|
21102
21643
|
screenshotPath: querySnapshots.screenshotPath,
|
|
21103
21644
|
rawResponse: querySnapshots.rawResponse
|
|
21104
|
-
}).from(querySnapshots).where(
|
|
21105
|
-
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
21645
|
+
}).from(querySnapshots).where(eq23(querySnapshots.runId, runId)).all());
|
|
21646
|
+
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq23(queries.projectId, project.id)).all();
|
|
21106
21647
|
const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
|
|
21107
21648
|
const byQuery = /* @__PURE__ */ new Map();
|
|
21108
21649
|
for (const snap of snapshots) {
|
|
@@ -21184,8 +21725,8 @@ async function cdpRoutes(app, opts) {
|
|
|
21184
21725
|
}
|
|
21185
21726
|
|
|
21186
21727
|
// ../api-routes/src/ga.ts
|
|
21187
|
-
import
|
|
21188
|
-
import { eq as
|
|
21728
|
+
import crypto20 from "crypto";
|
|
21729
|
+
import { eq as eq24, desc as desc12, and as and17, sql as sql9 } from "drizzle-orm";
|
|
21189
21730
|
function gaLog(level, action, ctx) {
|
|
21190
21731
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
21191
21732
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -21392,10 +21933,10 @@ async function ga4Routes(app, opts) {
|
|
|
21392
21933
|
if (!saConn && !oauthConn) {
|
|
21393
21934
|
throw notFound("GA4 connection", project.name);
|
|
21394
21935
|
}
|
|
21395
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
21396
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
21397
|
-
app.db.delete(gaAiReferrals).where(
|
|
21398
|
-
app.db.delete(gaSocialReferrals).where(
|
|
21936
|
+
app.db.delete(gaTrafficSnapshots).where(eq24(gaTrafficSnapshots.projectId, project.id)).run();
|
|
21937
|
+
app.db.delete(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).run();
|
|
21938
|
+
app.db.delete(gaAiReferrals).where(eq24(gaAiReferrals.projectId, project.id)).run();
|
|
21939
|
+
app.db.delete(gaSocialReferrals).where(eq24(gaSocialReferrals.projectId, project.id)).run();
|
|
21399
21940
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
21400
21941
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
21401
21942
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -21416,7 +21957,7 @@ async function ga4Routes(app, opts) {
|
|
|
21416
21957
|
if (!connected) {
|
|
21417
21958
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
21418
21959
|
}
|
|
21419
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
21960
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).orderBy(desc12(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
21420
21961
|
return {
|
|
21421
21962
|
connected: true,
|
|
21422
21963
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -21440,7 +21981,7 @@ async function ga4Routes(app, opts) {
|
|
|
21440
21981
|
const syncAi = !only || only === "ai";
|
|
21441
21982
|
const syncSocial = !only || only === "social";
|
|
21442
21983
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21443
|
-
const runId =
|
|
21984
|
+
const runId = crypto20.randomUUID();
|
|
21444
21985
|
app.db.insert(runs).values({
|
|
21445
21986
|
id: runId,
|
|
21446
21987
|
projectId: project.id,
|
|
@@ -21480,15 +22021,15 @@ async function ga4Routes(app, opts) {
|
|
|
21480
22021
|
app.db.transaction((tx) => {
|
|
21481
22022
|
if (syncTraffic) {
|
|
21482
22023
|
tx.delete(gaTrafficSnapshots).where(
|
|
21483
|
-
|
|
21484
|
-
|
|
22024
|
+
and17(
|
|
22025
|
+
eq24(gaTrafficSnapshots.projectId, project.id),
|
|
21485
22026
|
sql9`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
21486
22027
|
sql9`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
21487
22028
|
)
|
|
21488
22029
|
).run();
|
|
21489
22030
|
for (const row of rows) {
|
|
21490
22031
|
tx.insert(gaTrafficSnapshots).values({
|
|
21491
|
-
id:
|
|
22032
|
+
id: crypto20.randomUUID(),
|
|
21492
22033
|
projectId: project.id,
|
|
21493
22034
|
date: row.date,
|
|
21494
22035
|
landingPage: row.landingPage,
|
|
@@ -21504,15 +22045,15 @@ async function ga4Routes(app, opts) {
|
|
|
21504
22045
|
}
|
|
21505
22046
|
if (syncAi) {
|
|
21506
22047
|
tx.delete(gaAiReferrals).where(
|
|
21507
|
-
|
|
21508
|
-
|
|
22048
|
+
and17(
|
|
22049
|
+
eq24(gaAiReferrals.projectId, project.id),
|
|
21509
22050
|
sql9`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
21510
22051
|
sql9`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
21511
22052
|
)
|
|
21512
22053
|
).run();
|
|
21513
22054
|
for (const row of aiReferrals) {
|
|
21514
22055
|
tx.insert(gaAiReferrals).values({
|
|
21515
|
-
id:
|
|
22056
|
+
id: crypto20.randomUUID(),
|
|
21516
22057
|
projectId: project.id,
|
|
21517
22058
|
date: row.date,
|
|
21518
22059
|
source: row.source,
|
|
@@ -21530,15 +22071,15 @@ async function ga4Routes(app, opts) {
|
|
|
21530
22071
|
}
|
|
21531
22072
|
if (syncSocial) {
|
|
21532
22073
|
tx.delete(gaSocialReferrals).where(
|
|
21533
|
-
|
|
21534
|
-
|
|
22074
|
+
and17(
|
|
22075
|
+
eq24(gaSocialReferrals.projectId, project.id),
|
|
21535
22076
|
sql9`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
21536
22077
|
sql9`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
21537
22078
|
)
|
|
21538
22079
|
).run();
|
|
21539
22080
|
for (const row of socialReferrals) {
|
|
21540
22081
|
tx.insert(gaSocialReferrals).values({
|
|
21541
|
-
id:
|
|
22082
|
+
id: crypto20.randomUUID(),
|
|
21542
22083
|
projectId: project.id,
|
|
21543
22084
|
date: row.date,
|
|
21544
22085
|
source: row.source,
|
|
@@ -21552,9 +22093,9 @@ async function ga4Routes(app, opts) {
|
|
|
21552
22093
|
}
|
|
21553
22094
|
}
|
|
21554
22095
|
if (syncSummary) {
|
|
21555
|
-
tx.delete(gaTrafficSummaries).where(
|
|
22096
|
+
tx.delete(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).run();
|
|
21556
22097
|
tx.insert(gaTrafficSummaries).values({
|
|
21557
|
-
id:
|
|
22098
|
+
id: crypto20.randomUUID(),
|
|
21558
22099
|
projectId: project.id,
|
|
21559
22100
|
periodStart: summary.periodStart,
|
|
21560
22101
|
periodEnd: summary.periodEnd,
|
|
@@ -21564,10 +22105,10 @@ async function ga4Routes(app, opts) {
|
|
|
21564
22105
|
syncedAt: now,
|
|
21565
22106
|
syncRunId: runId
|
|
21566
22107
|
}).run();
|
|
21567
|
-
tx.delete(gaTrafficWindowSummaries).where(
|
|
22108
|
+
tx.delete(gaTrafficWindowSummaries).where(eq24(gaTrafficWindowSummaries.projectId, project.id)).run();
|
|
21568
22109
|
for (const ws of windowSummaries) {
|
|
21569
22110
|
tx.insert(gaTrafficWindowSummaries).values({
|
|
21570
|
-
id:
|
|
22111
|
+
id: crypto20.randomUUID(),
|
|
21571
22112
|
projectId: project.id,
|
|
21572
22113
|
windowKey: ws.windowKey,
|
|
21573
22114
|
periodStart: ws.periodStart,
|
|
@@ -21582,7 +22123,7 @@ async function ga4Routes(app, opts) {
|
|
|
21582
22123
|
}
|
|
21583
22124
|
}
|
|
21584
22125
|
});
|
|
21585
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
22126
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq24(runs.id, runId)).run();
|
|
21586
22127
|
const syncedComponents = only ? [
|
|
21587
22128
|
...syncTraffic ? ["traffic"] : [],
|
|
21588
22129
|
...syncSummary ? ["summary"] : [],
|
|
@@ -21611,7 +22152,7 @@ async function ga4Routes(app, opts) {
|
|
|
21611
22152
|
} catch (e) {
|
|
21612
22153
|
const msg = e instanceof Error ? e.message : String(e);
|
|
21613
22154
|
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(
|
|
22155
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
|
|
21615
22156
|
throw e;
|
|
21616
22157
|
}
|
|
21617
22158
|
});
|
|
@@ -21622,11 +22163,11 @@ async function ga4Routes(app, opts) {
|
|
|
21622
22163
|
const window = parseWindow(request.query.window);
|
|
21623
22164
|
const cutoff = windowCutoff(window);
|
|
21624
22165
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
21625
|
-
const snapshotConditions = [
|
|
22166
|
+
const snapshotConditions = [eq24(gaTrafficSnapshots.projectId, project.id)];
|
|
21626
22167
|
if (cutoffDate) snapshotConditions.push(sql9`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
21627
|
-
const aiConditions = [
|
|
22168
|
+
const aiConditions = [eq24(gaAiReferrals.projectId, project.id)];
|
|
21628
22169
|
if (cutoffDate) aiConditions.push(sql9`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
21629
|
-
const socialConditions = [
|
|
22170
|
+
const socialConditions = [eq24(gaSocialReferrals.projectId, project.id)];
|
|
21630
22171
|
if (cutoffDate) socialConditions.push(sql9`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
21631
22172
|
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
21632
22173
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
@@ -21634,42 +22175,42 @@ async function ga4Routes(app, opts) {
|
|
|
21634
22175
|
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
21635
22176
|
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
21636
22177
|
}).from(gaTrafficWindowSummaries).where(
|
|
21637
|
-
|
|
21638
|
-
|
|
21639
|
-
|
|
22178
|
+
and17(
|
|
22179
|
+
eq24(gaTrafficWindowSummaries.projectId, project.id),
|
|
22180
|
+
eq24(gaTrafficWindowSummaries.windowKey, window)
|
|
21640
22181
|
)
|
|
21641
22182
|
).get() : null;
|
|
21642
22183
|
const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
|
|
21643
22184
|
totalSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
21644
22185
|
totalOrganicSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
21645
22186
|
totalUsers: sql9`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
21646
|
-
}).from(gaTrafficSnapshots).where(
|
|
22187
|
+
}).from(gaTrafficSnapshots).where(and17(...snapshotConditions)).get() : null;
|
|
21647
22188
|
const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
21648
22189
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
21649
22190
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
21650
22191
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
21651
|
-
}).from(gaTrafficSummaries).where(
|
|
22192
|
+
}).from(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).get();
|
|
21652
22193
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
21653
22194
|
totalDirectSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
21654
|
-
}).from(gaTrafficSnapshots).where(
|
|
22195
|
+
}).from(gaTrafficSnapshots).where(and17(...snapshotConditions)).get();
|
|
21655
22196
|
const summaryMeta = app.db.select({
|
|
21656
22197
|
periodStart: gaTrafficSummaries.periodStart,
|
|
21657
22198
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
21658
|
-
}).from(gaTrafficSummaries).where(
|
|
22199
|
+
}).from(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).get();
|
|
21659
22200
|
const rows = app.db.select({
|
|
21660
22201
|
landingPage: sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
21661
22202
|
sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
|
|
21662
22203
|
organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
21663
22204
|
directSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
|
|
21664
22205
|
users: sql9`SUM(${gaTrafficSnapshots.users})`
|
|
21665
|
-
}).from(gaTrafficSnapshots).where(
|
|
22206
|
+
}).from(gaTrafficSnapshots).where(and17(...snapshotConditions)).groupBy(sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql9`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
21666
22207
|
const aiReferralRows = app.db.select({
|
|
21667
22208
|
source: gaAiReferrals.source,
|
|
21668
22209
|
medium: gaAiReferrals.medium,
|
|
21669
22210
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
21670
22211
|
sessions: sql9`SUM(${gaAiReferrals.sessions})`,
|
|
21671
22212
|
users: sql9`SUM(${gaAiReferrals.users})`
|
|
21672
|
-
}).from(gaAiReferrals).where(
|
|
22213
|
+
}).from(gaAiReferrals).where(and17(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
|
|
21673
22214
|
const aiReferralLandingPageRows = app.db.select({
|
|
21674
22215
|
source: gaAiReferrals.source,
|
|
21675
22216
|
medium: gaAiReferrals.medium,
|
|
@@ -21677,7 +22218,7 @@ async function ga4Routes(app, opts) {
|
|
|
21677
22218
|
landingPage: sql9`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
21678
22219
|
sessions: sql9`SUM(${gaAiReferrals.sessions})`,
|
|
21679
22220
|
users: sql9`SUM(${gaAiReferrals.users})`
|
|
21680
|
-
}).from(gaAiReferrals).where(
|
|
22221
|
+
}).from(gaAiReferrals).where(and17(...aiConditions)).groupBy(
|
|
21681
22222
|
gaAiReferrals.source,
|
|
21682
22223
|
gaAiReferrals.medium,
|
|
21683
22224
|
gaAiReferrals.sourceDimension,
|
|
@@ -21714,7 +22255,7 @@ async function ga4Routes(app, opts) {
|
|
|
21714
22255
|
channelGroup: gaAiReferrals.channelGroup,
|
|
21715
22256
|
sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
|
|
21716
22257
|
users: sql9`COALESCE(SUM(${gaAiReferrals.users}), 0)`
|
|
21717
|
-
}).from(gaAiReferrals).where(
|
|
22258
|
+
}).from(gaAiReferrals).where(and17(...aiConditions, eq24(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
|
|
21718
22259
|
const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
|
|
21719
22260
|
let aiBySessionUsers = 0;
|
|
21720
22261
|
for (const row of aiBySessionRows) {
|
|
@@ -21728,12 +22269,12 @@ async function ga4Routes(app, opts) {
|
|
|
21728
22269
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
21729
22270
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`,
|
|
21730
22271
|
users: sql9`SUM(${gaSocialReferrals.users})`
|
|
21731
|
-
}).from(gaSocialReferrals).where(
|
|
22272
|
+
}).from(gaSocialReferrals).where(and17(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql9`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
21732
22273
|
const socialTotals = app.db.select({
|
|
21733
22274
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`,
|
|
21734
22275
|
users: sql9`SUM(${gaSocialReferrals.users})`
|
|
21735
|
-
}).from(gaSocialReferrals).where(
|
|
21736
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
22276
|
+
}).from(gaSocialReferrals).where(and17(...socialConditions)).get();
|
|
22277
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).orderBy(desc12(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
21737
22278
|
const total = summaryRow?.totalSessions ?? 0;
|
|
21738
22279
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
21739
22280
|
const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
|
|
@@ -21813,7 +22354,7 @@ async function ga4Routes(app, opts) {
|
|
|
21813
22354
|
const project = resolveProject(app.db, request.params.name);
|
|
21814
22355
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
21815
22356
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
21816
|
-
const conditions = [
|
|
22357
|
+
const conditions = [eq24(gaAiReferrals.projectId, project.id)];
|
|
21817
22358
|
if (cutoffDate) conditions.push(sql9`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
21818
22359
|
const rows = app.db.select({
|
|
21819
22360
|
date: gaAiReferrals.date,
|
|
@@ -21823,7 +22364,7 @@ async function ga4Routes(app, opts) {
|
|
|
21823
22364
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
21824
22365
|
sessions: sql9`SUM(${gaAiReferrals.sessions})`,
|
|
21825
22366
|
users: sql9`SUM(${gaAiReferrals.users})`
|
|
21826
|
-
}).from(gaAiReferrals).where(
|
|
22367
|
+
}).from(gaAiReferrals).where(and17(...conditions)).groupBy(
|
|
21827
22368
|
gaAiReferrals.date,
|
|
21828
22369
|
gaAiReferrals.source,
|
|
21829
22370
|
gaAiReferrals.medium,
|
|
@@ -21836,7 +22377,7 @@ async function ga4Routes(app, opts) {
|
|
|
21836
22377
|
const project = resolveProject(app.db, request.params.name);
|
|
21837
22378
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
21838
22379
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
21839
|
-
const conditions = [
|
|
22380
|
+
const conditions = [eq24(gaSocialReferrals.projectId, project.id)];
|
|
21840
22381
|
if (cutoffDate) conditions.push(sql9`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
21841
22382
|
const rows = app.db.select({
|
|
21842
22383
|
date: gaSocialReferrals.date,
|
|
@@ -21845,7 +22386,7 @@ async function ga4Routes(app, opts) {
|
|
|
21845
22386
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
21846
22387
|
sessions: gaSocialReferrals.sessions,
|
|
21847
22388
|
users: gaSocialReferrals.users
|
|
21848
|
-
}).from(gaSocialReferrals).where(
|
|
22389
|
+
}).from(gaSocialReferrals).where(and17(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
21849
22390
|
return rows;
|
|
21850
22391
|
});
|
|
21851
22392
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -21858,8 +22399,8 @@ async function ga4Routes(app, opts) {
|
|
|
21858
22399
|
d.setDate(d.getDate() - n);
|
|
21859
22400
|
return fmt(d);
|
|
21860
22401
|
};
|
|
21861
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
21862
|
-
|
|
22402
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and17(
|
|
22403
|
+
eq24(gaSocialReferrals.projectId, project.id),
|
|
21863
22404
|
sql9`${gaSocialReferrals.date} >= ${from}`,
|
|
21864
22405
|
sql9`${gaSocialReferrals.date} < ${to}`
|
|
21865
22406
|
)).get();
|
|
@@ -21871,16 +22412,16 @@ async function ga4Routes(app, opts) {
|
|
|
21871
22412
|
const sourceCurrent = app.db.select({
|
|
21872
22413
|
source: gaSocialReferrals.source,
|
|
21873
22414
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`
|
|
21874
|
-
}).from(gaSocialReferrals).where(
|
|
21875
|
-
|
|
22415
|
+
}).from(gaSocialReferrals).where(and17(
|
|
22416
|
+
eq24(gaSocialReferrals.projectId, project.id),
|
|
21876
22417
|
sql9`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
|
|
21877
22418
|
sql9`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
21878
22419
|
)).groupBy(gaSocialReferrals.source).all();
|
|
21879
22420
|
const sourcePrev = app.db.select({
|
|
21880
22421
|
source: gaSocialReferrals.source,
|
|
21881
22422
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`
|
|
21882
|
-
}).from(gaSocialReferrals).where(
|
|
21883
|
-
|
|
22423
|
+
}).from(gaSocialReferrals).where(and17(
|
|
22424
|
+
eq24(gaSocialReferrals.projectId, project.id),
|
|
21884
22425
|
sql9`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
|
|
21885
22426
|
sql9`${gaSocialReferrals.date} < ${daysAgo(7)}`
|
|
21886
22427
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -21921,16 +22462,16 @@ async function ga4Routes(app, opts) {
|
|
|
21921
22462
|
return fmt(d);
|
|
21922
22463
|
};
|
|
21923
22464
|
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
|
-
|
|
22465
|
+
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();
|
|
22466
|
+
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();
|
|
22467
|
+
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();
|
|
22468
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and17(
|
|
22469
|
+
eq24(gaAiReferrals.projectId, project.id),
|
|
21929
22470
|
sql9`${gaAiReferrals.date} >= ${from}`,
|
|
21930
22471
|
sql9`${gaAiReferrals.date} < ${to}`,
|
|
21931
|
-
|
|
22472
|
+
eq24(gaAiReferrals.sourceDimension, "session")
|
|
21932
22473
|
)).get();
|
|
21933
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
22474
|
+
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
22475
|
const todayStr = fmt(today);
|
|
21935
22476
|
const buildTrend = (sum) => {
|
|
21936
22477
|
const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
|
|
@@ -21939,17 +22480,17 @@ async function ga4Routes(app, opts) {
|
|
|
21939
22480
|
const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
|
|
21940
22481
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
21941
22482
|
};
|
|
21942
|
-
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
21943
|
-
|
|
22483
|
+
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and17(
|
|
22484
|
+
eq24(gaAiReferrals.projectId, project.id),
|
|
21944
22485
|
sql9`${gaAiReferrals.date} >= ${daysAgo(7)}`,
|
|
21945
22486
|
sql9`${gaAiReferrals.date} < ${todayStr}`,
|
|
21946
|
-
|
|
22487
|
+
eq24(gaAiReferrals.sourceDimension, "session")
|
|
21947
22488
|
)).groupBy(gaAiReferrals.source).all();
|
|
21948
|
-
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
21949
|
-
|
|
22489
|
+
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and17(
|
|
22490
|
+
eq24(gaAiReferrals.projectId, project.id),
|
|
21950
22491
|
sql9`${gaAiReferrals.date} >= ${daysAgo(14)}`,
|
|
21951
22492
|
sql9`${gaAiReferrals.date} < ${daysAgo(7)}`,
|
|
21952
|
-
|
|
22493
|
+
eq24(gaAiReferrals.sourceDimension, "session")
|
|
21953
22494
|
)).groupBy(gaAiReferrals.source).all();
|
|
21954
22495
|
const findBiggestMover = (current, prev) => {
|
|
21955
22496
|
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
@@ -21965,8 +22506,8 @@ async function ga4Routes(app, opts) {
|
|
|
21965
22506
|
}
|
|
21966
22507
|
return mover;
|
|
21967
22508
|
};
|
|
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(
|
|
22509
|
+
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();
|
|
22510
|
+
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
22511
|
return {
|
|
21971
22512
|
total: buildTrend(sumTotal),
|
|
21972
22513
|
organic: buildTrend(sumOrganic),
|
|
@@ -21981,14 +22522,14 @@ async function ga4Routes(app, opts) {
|
|
|
21981
22522
|
const project = resolveProject(app.db, request.params.name);
|
|
21982
22523
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
21983
22524
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
21984
|
-
const conditions = [
|
|
22525
|
+
const conditions = [eq24(gaTrafficSnapshots.projectId, project.id)];
|
|
21985
22526
|
if (cutoffDate) conditions.push(sql9`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
21986
22527
|
const rows = app.db.select({
|
|
21987
22528
|
date: gaTrafficSnapshots.date,
|
|
21988
22529
|
sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
|
|
21989
22530
|
organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
21990
22531
|
users: sql9`SUM(${gaTrafficSnapshots.users})`
|
|
21991
|
-
}).from(gaTrafficSnapshots).where(
|
|
22532
|
+
}).from(gaTrafficSnapshots).where(and17(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
21992
22533
|
return rows.map((r) => ({
|
|
21993
22534
|
date: r.date,
|
|
21994
22535
|
sessions: r.sessions ?? 0,
|
|
@@ -22004,7 +22545,7 @@ async function ga4Routes(app, opts) {
|
|
|
22004
22545
|
sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
|
|
22005
22546
|
organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
22006
22547
|
users: sql9`SUM(${gaTrafficSnapshots.users})`
|
|
22007
|
-
}).from(gaTrafficSnapshots).where(
|
|
22548
|
+
}).from(gaTrafficSnapshots).where(eq24(gaTrafficSnapshots.projectId, project.id)).groupBy(sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql9`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
22008
22549
|
return {
|
|
22009
22550
|
pages: trafficPages.map((r) => ({
|
|
22010
22551
|
landingPage: r.landingPage,
|
|
@@ -22138,7 +22679,7 @@ function parseSchemaPageEntry(entry) {
|
|
|
22138
22679
|
}
|
|
22139
22680
|
|
|
22140
22681
|
// ../integration-wordpress/src/wordpress-client.ts
|
|
22141
|
-
import
|
|
22682
|
+
import crypto21 from "crypto";
|
|
22142
22683
|
function validateUsername(username) {
|
|
22143
22684
|
if (!username || typeof username !== "string" || username.trim().length === 0) {
|
|
22144
22685
|
throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
|
|
@@ -22351,7 +22892,7 @@ function buildSnippet(content) {
|
|
|
22351
22892
|
return `${text2.slice(0, 157)}...`;
|
|
22352
22893
|
}
|
|
22353
22894
|
function contentHash(content) {
|
|
22354
|
-
return
|
|
22895
|
+
return crypto21.createHash("sha256").update(content).digest("hex");
|
|
22355
22896
|
}
|
|
22356
22897
|
function buildAmbiguousSlugMessage(slug, pages) {
|
|
22357
22898
|
const candidates = pages.map((page) => {
|
|
@@ -23651,8 +24192,8 @@ async function wordpressRoutes(app, opts) {
|
|
|
23651
24192
|
}
|
|
23652
24193
|
|
|
23653
24194
|
// ../api-routes/src/backlinks.ts
|
|
23654
|
-
import
|
|
23655
|
-
import { and as
|
|
24195
|
+
import crypto22 from "crypto";
|
|
24196
|
+
import { and as and19, asc as asc3, desc as desc13, eq as eq25, sql as sql10 } from "drizzle-orm";
|
|
23656
24197
|
|
|
23657
24198
|
// ../integration-commoncrawl/src/constants.ts
|
|
23658
24199
|
import os2 from "os";
|
|
@@ -24055,7 +24596,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
24055
24596
|
}
|
|
24056
24597
|
|
|
24057
24598
|
// ../api-routes/src/backlinks-filter.ts
|
|
24058
|
-
import { and as
|
|
24599
|
+
import { and as and18, ne as ne3, notLike } from "drizzle-orm";
|
|
24059
24600
|
var BACKLINK_FILTER_PATTERNS = [
|
|
24060
24601
|
"*.google.com",
|
|
24061
24602
|
"*.googleusercontent.com",
|
|
@@ -24078,7 +24619,7 @@ function backlinkCrawlerExclusionClause() {
|
|
|
24078
24619
|
conditions.push(ne3(backlinkDomains.linkingDomain, pattern));
|
|
24079
24620
|
}
|
|
24080
24621
|
}
|
|
24081
|
-
const combined =
|
|
24622
|
+
const combined = and18(...conditions);
|
|
24082
24623
|
if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
|
|
24083
24624
|
return combined;
|
|
24084
24625
|
}
|
|
@@ -24139,7 +24680,7 @@ function mapRunRow(row) {
|
|
|
24139
24680
|
};
|
|
24140
24681
|
}
|
|
24141
24682
|
function latestSummaryForProject(db, projectId, release) {
|
|
24142
|
-
const condition = release ?
|
|
24683
|
+
const condition = release ? and19(eq25(backlinkSummaries.projectId, projectId), eq25(backlinkSummaries.release, release)) : eq25(backlinkSummaries.projectId, projectId);
|
|
24143
24684
|
return db.select().from(backlinkSummaries).where(condition).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
|
|
24144
24685
|
}
|
|
24145
24686
|
function parseExcludeCrawlers(value) {
|
|
@@ -24148,11 +24689,11 @@ function parseExcludeCrawlers(value) {
|
|
|
24148
24689
|
return lower === "1" || lower === "true" || lower === "yes";
|
|
24149
24690
|
}
|
|
24150
24691
|
function computeFilteredSummary(db, base) {
|
|
24151
|
-
const baseDomainCondition =
|
|
24152
|
-
|
|
24153
|
-
|
|
24692
|
+
const baseDomainCondition = and19(
|
|
24693
|
+
eq25(backlinkDomains.projectId, base.projectId),
|
|
24694
|
+
eq25(backlinkDomains.release, base.release)
|
|
24154
24695
|
);
|
|
24155
|
-
const filteredCondition =
|
|
24696
|
+
const filteredCondition = and19(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
24156
24697
|
const unfilteredAgg = db.select({
|
|
24157
24698
|
count: sql10`count(*)`,
|
|
24158
24699
|
total: sql10`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
@@ -24221,7 +24762,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
24221
24762
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
24222
24763
|
);
|
|
24223
24764
|
}
|
|
24224
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
24765
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.release, release)).get();
|
|
24225
24766
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24226
24767
|
if (existing) {
|
|
24227
24768
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -24232,12 +24773,12 @@ async function backlinksRoutes(app, opts) {
|
|
|
24232
24773
|
phaseDetail: null,
|
|
24233
24774
|
error: null,
|
|
24234
24775
|
updatedAt: now
|
|
24235
|
-
}).where(
|
|
24776
|
+
}).where(eq25(ccReleaseSyncs.id, existing.id)).run();
|
|
24236
24777
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
24237
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
24778
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.id, existing.id)).get();
|
|
24238
24779
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
24239
24780
|
}
|
|
24240
|
-
const id =
|
|
24781
|
+
const id = crypto22.randomUUID();
|
|
24241
24782
|
app.db.insert(ccReleaseSyncs).values({
|
|
24242
24783
|
id,
|
|
24243
24784
|
release,
|
|
@@ -24246,7 +24787,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
24246
24787
|
updatedAt: now
|
|
24247
24788
|
}).run();
|
|
24248
24789
|
opts.onReleaseSyncRequested(id, release);
|
|
24249
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
24790
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.id, id)).get();
|
|
24250
24791
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
24251
24792
|
});
|
|
24252
24793
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
@@ -24294,7 +24835,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
24294
24835
|
throw validationError("Invalid release id");
|
|
24295
24836
|
}
|
|
24296
24837
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24297
|
-
const runId =
|
|
24838
|
+
const runId = crypto22.randomUUID();
|
|
24298
24839
|
app.db.insert(runs).values({
|
|
24299
24840
|
id: runId,
|
|
24300
24841
|
projectId: project.id,
|
|
@@ -24304,7 +24845,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
24304
24845
|
createdAt: now
|
|
24305
24846
|
}).run();
|
|
24306
24847
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
24307
|
-
const run = app.db.select().from(runs).where(
|
|
24848
|
+
const run = app.db.select().from(runs).where(eq25(runs.id, runId)).get();
|
|
24308
24849
|
return reply.status(201).send(mapRunRow(run));
|
|
24309
24850
|
});
|
|
24310
24851
|
app.get(
|
|
@@ -24328,11 +24869,11 @@ async function backlinksRoutes(app, opts) {
|
|
|
24328
24869
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
24329
24870
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
24330
24871
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
24331
|
-
const baseDomainCondition =
|
|
24332
|
-
|
|
24333
|
-
|
|
24872
|
+
const baseDomainCondition = and19(
|
|
24873
|
+
eq25(backlinkDomains.projectId, project.id),
|
|
24874
|
+
eq25(backlinkDomains.release, targetRelease)
|
|
24334
24875
|
);
|
|
24335
|
-
const domainCondition = excludeCrawlers ?
|
|
24876
|
+
const domainCondition = excludeCrawlers ? and19(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
24336
24877
|
const totalRow = app.db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
24337
24878
|
const rows = app.db.select({
|
|
24338
24879
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
@@ -24353,7 +24894,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
24353
24894
|
"/projects/:name/backlinks/history",
|
|
24354
24895
|
async (request, reply) => {
|
|
24355
24896
|
const project = resolveProject(app.db, request.params.name);
|
|
24356
|
-
const rows = app.db.select().from(backlinkSummaries).where(
|
|
24897
|
+
const rows = app.db.select().from(backlinkSummaries).where(eq25(backlinkSummaries.projectId, project.id)).orderBy(asc3(backlinkSummaries.queriedAt)).all();
|
|
24357
24898
|
const response = rows.map((r) => ({
|
|
24358
24899
|
release: r.release,
|
|
24359
24900
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -24367,12 +24908,12 @@ async function backlinksRoutes(app, opts) {
|
|
|
24367
24908
|
}
|
|
24368
24909
|
|
|
24369
24910
|
// ../api-routes/src/traffic.ts
|
|
24370
|
-
import
|
|
24911
|
+
import crypto24 from "crypto";
|
|
24371
24912
|
import { Agent as UndiciAgent } from "undici";
|
|
24372
|
-
import { and as
|
|
24913
|
+
import { and as and20, desc as desc14, eq as eq26, gte as gte4, lte as lte3, sql as sql11 } from "drizzle-orm";
|
|
24373
24914
|
|
|
24374
24915
|
// ../integration-cloud-run/src/auth.ts
|
|
24375
|
-
import
|
|
24916
|
+
import crypto23 from "crypto";
|
|
24376
24917
|
var GOOGLE_TOKEN_URL3 = "https://oauth2.googleapis.com/token";
|
|
24377
24918
|
var CLOUD_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
|
|
24378
24919
|
var TOKEN_REQUEST_TIMEOUT_MS = 3e4;
|
|
@@ -24403,7 +24944,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
|
|
|
24403
24944
|
const headerB64 = encode(header);
|
|
24404
24945
|
const payloadB64 = encode(payload);
|
|
24405
24946
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
24406
|
-
const sign =
|
|
24947
|
+
const sign = crypto23.createSign("RSA-SHA256");
|
|
24407
24948
|
sign.update(signingInput);
|
|
24408
24949
|
const signature = sign.sign(privateKey, "base64url");
|
|
24409
24950
|
return `${signingInput}.${signature}`;
|
|
@@ -28186,8 +28727,8 @@ async function runBackfillTask(options) {
|
|
|
28186
28727
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28187
28728
|
try {
|
|
28188
28729
|
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(
|
|
28730
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq26(runs.id, runId)).run();
|
|
28731
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq26(trafficSources.id, sourceRow.id)).run();
|
|
28191
28732
|
});
|
|
28192
28733
|
} catch {
|
|
28193
28734
|
}
|
|
@@ -28202,7 +28743,7 @@ async function runBackfillTask(options) {
|
|
|
28202
28743
|
if (allEvents.length === 0) {
|
|
28203
28744
|
const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
28204
28745
|
try {
|
|
28205
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(
|
|
28746
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq26(runs.id, runId)).run();
|
|
28206
28747
|
} catch {
|
|
28207
28748
|
}
|
|
28208
28749
|
return;
|
|
@@ -28224,31 +28765,31 @@ async function runBackfillTask(options) {
|
|
|
28224
28765
|
try {
|
|
28225
28766
|
app.db.transaction((tx) => {
|
|
28226
28767
|
tx.delete(crawlerEventsHourly).where(
|
|
28227
|
-
|
|
28228
|
-
|
|
28229
|
-
|
|
28230
|
-
|
|
28768
|
+
and20(
|
|
28769
|
+
eq26(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
28770
|
+
gte4(crawlerEventsHourly.tsHour, windowStartIso),
|
|
28771
|
+
lte3(crawlerEventsHourly.tsHour, windowEndIso)
|
|
28231
28772
|
)
|
|
28232
28773
|
).run();
|
|
28233
28774
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
28234
|
-
|
|
28235
|
-
|
|
28236
|
-
|
|
28237
|
-
|
|
28775
|
+
and20(
|
|
28776
|
+
eq26(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
28777
|
+
gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
28778
|
+
lte3(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
28238
28779
|
)
|
|
28239
28780
|
).run();
|
|
28240
28781
|
tx.delete(aiReferralEventsHourly).where(
|
|
28241
|
-
|
|
28242
|
-
|
|
28243
|
-
|
|
28244
|
-
|
|
28782
|
+
and20(
|
|
28783
|
+
eq26(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
28784
|
+
gte4(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
28785
|
+
lte3(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
28245
28786
|
)
|
|
28246
28787
|
).run();
|
|
28247
28788
|
tx.delete(rawEventSamples).where(
|
|
28248
|
-
|
|
28249
|
-
|
|
28250
|
-
|
|
28251
|
-
|
|
28789
|
+
and20(
|
|
28790
|
+
eq26(rawEventSamples.sourceId, sourceRow.id),
|
|
28791
|
+
gte4(rawEventSamples.ts, windowStartIso),
|
|
28792
|
+
lte3(rawEventSamples.ts, windowEndIso)
|
|
28252
28793
|
)
|
|
28253
28794
|
).run();
|
|
28254
28795
|
for (const bucket of report.crawlerEventsHourly) {
|
|
@@ -28311,7 +28852,7 @@ async function runBackfillTask(options) {
|
|
|
28311
28852
|
}
|
|
28312
28853
|
})();
|
|
28313
28854
|
tx.insert(rawEventSamples).values({
|
|
28314
|
-
id:
|
|
28855
|
+
id: crypto24.randomUUID(),
|
|
28315
28856
|
projectId: project.id,
|
|
28316
28857
|
sourceId: sourceRow.id,
|
|
28317
28858
|
ts: sample.observedAt,
|
|
@@ -28335,8 +28876,8 @@ async function runBackfillTask(options) {
|
|
|
28335
28876
|
lastError: null,
|
|
28336
28877
|
lastEventIds: newRingBuffer,
|
|
28337
28878
|
updatedAt: finishedAt
|
|
28338
|
-
}).where(
|
|
28339
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
28879
|
+
}).where(eq26(trafficSources.id, sourceRow.id)).run();
|
|
28880
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq26(runs.id, runId)).run();
|
|
28340
28881
|
});
|
|
28341
28882
|
} catch (e) {
|
|
28342
28883
|
markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -28418,7 +28959,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28418
28959
|
createdAt: existing?.createdAt ?? now,
|
|
28419
28960
|
updatedAt: now
|
|
28420
28961
|
});
|
|
28421
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
28962
|
+
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
28963
|
const config = {
|
|
28423
28964
|
gcpProjectId,
|
|
28424
28965
|
serviceName: serviceName ?? null,
|
|
@@ -28434,10 +28975,10 @@ async function trafficRoutes(app, opts) {
|
|
|
28434
28975
|
lastError: null,
|
|
28435
28976
|
configJson: config,
|
|
28436
28977
|
updatedAt: now
|
|
28437
|
-
}).where(
|
|
28438
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
28978
|
+
}).where(eq26(trafficSources.id, activeSource.id)).run();
|
|
28979
|
+
sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, activeSource.id)).get();
|
|
28439
28980
|
} else {
|
|
28440
|
-
const newId =
|
|
28981
|
+
const newId = crypto24.randomUUID();
|
|
28441
28982
|
app.db.insert(trafficSources).values({
|
|
28442
28983
|
id: newId,
|
|
28443
28984
|
projectId: project.id,
|
|
@@ -28452,7 +28993,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28452
28993
|
createdAt: now,
|
|
28453
28994
|
updatedAt: now
|
|
28454
28995
|
}).run();
|
|
28455
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
28996
|
+
sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, newId)).get();
|
|
28456
28997
|
}
|
|
28457
28998
|
writeAuditLog(app.db, {
|
|
28458
28999
|
projectId: project.id,
|
|
@@ -28504,7 +29045,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28504
29045
|
createdAt: existing?.createdAt ?? now,
|
|
28505
29046
|
updatedAt: now
|
|
28506
29047
|
});
|
|
28507
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
29048
|
+
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
29049
|
const config = { baseUrl, username };
|
|
28509
29050
|
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
28510
29051
|
let sourceRow;
|
|
@@ -28515,10 +29056,10 @@ async function trafficRoutes(app, opts) {
|
|
|
28515
29056
|
lastError: null,
|
|
28516
29057
|
configJson: config,
|
|
28517
29058
|
updatedAt: now
|
|
28518
|
-
}).where(
|
|
28519
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29059
|
+
}).where(eq26(trafficSources.id, activeSource.id)).run();
|
|
29060
|
+
sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, activeSource.id)).get();
|
|
28520
29061
|
} else {
|
|
28521
|
-
const newId =
|
|
29062
|
+
const newId = crypto24.randomUUID();
|
|
28522
29063
|
app.db.insert(trafficSources).values({
|
|
28523
29064
|
id: newId,
|
|
28524
29065
|
projectId: project.id,
|
|
@@ -28533,7 +29074,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28533
29074
|
createdAt: now,
|
|
28534
29075
|
updatedAt: now
|
|
28535
29076
|
}).run();
|
|
28536
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29077
|
+
sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, newId)).get();
|
|
28537
29078
|
}
|
|
28538
29079
|
writeAuditLog(app.db, {
|
|
28539
29080
|
projectId: project.id,
|
|
@@ -28587,7 +29128,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28587
29128
|
createdAt: existing?.createdAt ?? now,
|
|
28588
29129
|
updatedAt: now
|
|
28589
29130
|
});
|
|
28590
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
29131
|
+
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
29132
|
const config = { projectId, teamId, environment };
|
|
28592
29133
|
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
28593
29134
|
const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
|
|
@@ -28599,10 +29140,10 @@ async function trafficRoutes(app, opts) {
|
|
|
28599
29140
|
lastError: null,
|
|
28600
29141
|
configJson: config,
|
|
28601
29142
|
updatedAt: now
|
|
28602
|
-
}).where(
|
|
28603
|
-
row = tx.select().from(trafficSources).where(
|
|
29143
|
+
}).where(eq26(trafficSources.id, activeSource.id)).run();
|
|
29144
|
+
row = tx.select().from(trafficSources).where(eq26(trafficSources.id, activeSource.id)).get();
|
|
28604
29145
|
} else {
|
|
28605
|
-
const newId =
|
|
29146
|
+
const newId = crypto24.randomUUID();
|
|
28606
29147
|
tx.insert(trafficSources).values({
|
|
28607
29148
|
id: newId,
|
|
28608
29149
|
projectId: project.id,
|
|
@@ -28625,18 +29166,18 @@ async function trafficRoutes(app, opts) {
|
|
|
28625
29166
|
createdAt: now,
|
|
28626
29167
|
updatedAt: now
|
|
28627
29168
|
}).run();
|
|
28628
|
-
row = tx.select().from(trafficSources).where(
|
|
29169
|
+
row = tx.select().from(trafficSources).where(eq26(trafficSources.id, newId)).get();
|
|
28629
29170
|
}
|
|
28630
29171
|
const existingSchedule = tx.select().from(schedules).where(
|
|
28631
|
-
|
|
28632
|
-
|
|
28633
|
-
|
|
29172
|
+
and20(
|
|
29173
|
+
eq26(schedules.projectId, project.id),
|
|
29174
|
+
eq26(schedules.kind, SchedulableRunKinds["traffic-sync"])
|
|
28634
29175
|
)
|
|
28635
29176
|
).get();
|
|
28636
29177
|
let created = false;
|
|
28637
29178
|
if (!existingSchedule) {
|
|
28638
29179
|
tx.insert(schedules).values({
|
|
28639
|
-
id:
|
|
29180
|
+
id: crypto24.randomUUID(),
|
|
28640
29181
|
projectId: project.id,
|
|
28641
29182
|
kind: SchedulableRunKinds["traffic-sync"],
|
|
28642
29183
|
cronExpr: DEFAULT_TRAFFIC_SYNC_CRON,
|
|
@@ -28679,7 +29220,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28679
29220
|
});
|
|
28680
29221
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
28681
29222
|
const project = resolveProject(app.db, request.params.name);
|
|
28682
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
29223
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, request.params.id)).get();
|
|
28683
29224
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
28684
29225
|
throw notFound("Traffic source", request.params.id);
|
|
28685
29226
|
}
|
|
@@ -28691,7 +29232,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28691
29232
|
const windowEnd = /* @__PURE__ */ new Date();
|
|
28692
29233
|
const startedAt = windowEnd.toISOString();
|
|
28693
29234
|
const syncStartedAtMs = windowEnd.getTime();
|
|
28694
|
-
const runId =
|
|
29235
|
+
const runId = crypto24.randomUUID();
|
|
28695
29236
|
app.db.insert(runs).values({
|
|
28696
29237
|
id: runId,
|
|
28697
29238
|
projectId: project.id,
|
|
@@ -28705,8 +29246,8 @@ async function trafficRoutes(app, opts) {
|
|
|
28705
29246
|
const markFailed = (msg, errorCode) => {
|
|
28706
29247
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28707
29248
|
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(
|
|
29249
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq26(runs.id, runId)).run();
|
|
29250
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq26(trafficSources.id, sourceRow.id)).run();
|
|
28710
29251
|
});
|
|
28711
29252
|
try {
|
|
28712
29253
|
opts.onTrafficSynced?.({
|
|
@@ -28787,7 +29328,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28787
29328
|
}
|
|
28788
29329
|
const credential = credentialStore.getConnection(project.name);
|
|
28789
29330
|
if (!credential) {
|
|
28790
|
-
app.db.delete(runs).where(
|
|
29331
|
+
app.db.delete(runs).where(eq26(runs.id, runId)).run();
|
|
28791
29332
|
throw validationError(
|
|
28792
29333
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
28793
29334
|
);
|
|
@@ -28836,12 +29377,12 @@ async function trafficRoutes(app, opts) {
|
|
|
28836
29377
|
auditAction = "traffic.vercel.synced";
|
|
28837
29378
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
28838
29379
|
if (!credentialStore) {
|
|
28839
|
-
app.db.delete(runs).where(
|
|
29380
|
+
app.db.delete(runs).where(eq26(runs.id, runId)).run();
|
|
28840
29381
|
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
28841
29382
|
}
|
|
28842
29383
|
const credential = credentialStore.getConnection(project.name);
|
|
28843
29384
|
if (!credential) {
|
|
28844
|
-
app.db.delete(runs).where(
|
|
29385
|
+
app.db.delete(runs).where(eq26(runs.id, runId)).run();
|
|
28845
29386
|
throw validationError(
|
|
28846
29387
|
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
28847
29388
|
);
|
|
@@ -28941,7 +29482,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28941
29482
|
let aiReferralHitsCount = 0;
|
|
28942
29483
|
let unknownHitsCount = 0;
|
|
28943
29484
|
app.db.transaction((tx) => {
|
|
28944
|
-
const latestRow = tx.select().from(trafficSources).where(
|
|
29485
|
+
const latestRow = tx.select().from(trafficSources).where(eq26(trafficSources.id, sourceRow.id)).get();
|
|
28945
29486
|
const previousIds = latestRow.lastEventIds ?? [];
|
|
28946
29487
|
const seenEventIds = new Set(previousIds);
|
|
28947
29488
|
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
@@ -29074,7 +29615,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29074
29615
|
}
|
|
29075
29616
|
})();
|
|
29076
29617
|
tx.insert(rawEventSamples).values({
|
|
29077
|
-
id:
|
|
29618
|
+
id: crypto24.randomUUID(),
|
|
29078
29619
|
projectId: project.id,
|
|
29079
29620
|
sourceId: sourceRow.id,
|
|
29080
29621
|
ts: sample.observedAt,
|
|
@@ -29109,8 +29650,8 @@ async function trafficRoutes(app, opts) {
|
|
|
29109
29650
|
if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
29110
29651
|
sourceUpdate.lastCursor = nextCursor ?? null;
|
|
29111
29652
|
}
|
|
29112
|
-
tx.update(trafficSources).set(sourceUpdate).where(
|
|
29113
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
29653
|
+
tx.update(trafficSources).set(sourceUpdate).where(eq26(trafficSources.id, sourceRow.id)).run();
|
|
29654
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq26(runs.id, runId)).run();
|
|
29114
29655
|
});
|
|
29115
29656
|
writeAuditLog(app.db, {
|
|
29116
29657
|
projectId: project.id,
|
|
@@ -29162,7 +29703,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29162
29703
|
});
|
|
29163
29704
|
app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
|
|
29164
29705
|
const project = resolveProject(app.db, request.params.name);
|
|
29165
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
29706
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, request.params.id)).get();
|
|
29166
29707
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
29167
29708
|
throw notFound("Traffic source", request.params.id);
|
|
29168
29709
|
}
|
|
@@ -29312,7 +29853,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29312
29853
|
};
|
|
29313
29854
|
}
|
|
29314
29855
|
const startedAt = windowEnd.toISOString();
|
|
29315
|
-
const runId =
|
|
29856
|
+
const runId = crypto24.randomUUID();
|
|
29316
29857
|
app.db.insert(runs).values({
|
|
29317
29858
|
id: runId,
|
|
29318
29859
|
projectId: project.id,
|
|
@@ -29347,34 +29888,34 @@ async function trafficRoutes(app, opts) {
|
|
|
29347
29888
|
});
|
|
29348
29889
|
function buildSourceDetail(projectId, row, since) {
|
|
29349
29890
|
const crawlerTotals = app.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
29350
|
-
|
|
29351
|
-
|
|
29352
|
-
|
|
29891
|
+
and20(
|
|
29892
|
+
eq26(crawlerEventsHourly.sourceId, row.id),
|
|
29893
|
+
gte4(crawlerEventsHourly.tsHour, since)
|
|
29353
29894
|
)
|
|
29354
29895
|
).get();
|
|
29355
29896
|
const aiUserFetchTotals = app.db.select({ total: sql11`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
29356
|
-
|
|
29357
|
-
|
|
29358
|
-
|
|
29897
|
+
and20(
|
|
29898
|
+
eq26(aiUserFetchEventsHourly.sourceId, row.id),
|
|
29899
|
+
gte4(aiUserFetchEventsHourly.tsHour, since)
|
|
29359
29900
|
)
|
|
29360
29901
|
).get();
|
|
29361
29902
|
const aiTotals = app.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
29362
|
-
|
|
29363
|
-
|
|
29364
|
-
|
|
29903
|
+
and20(
|
|
29904
|
+
eq26(aiReferralEventsHourly.sourceId, row.id),
|
|
29905
|
+
gte4(aiReferralEventsHourly.tsHour, since)
|
|
29365
29906
|
)
|
|
29366
29907
|
).get();
|
|
29367
29908
|
const sampleTotals = app.db.select({ total: sql11`COUNT(*)` }).from(rawEventSamples).where(
|
|
29368
|
-
|
|
29369
|
-
|
|
29370
|
-
|
|
29909
|
+
and20(
|
|
29910
|
+
eq26(rawEventSamples.sourceId, row.id),
|
|
29911
|
+
gte4(rawEventSamples.ts, since)
|
|
29371
29912
|
)
|
|
29372
29913
|
).get();
|
|
29373
29914
|
const latestRun = app.db.select().from(runs).where(
|
|
29374
|
-
|
|
29375
|
-
|
|
29376
|
-
|
|
29377
|
-
|
|
29915
|
+
and20(
|
|
29916
|
+
eq26(runs.projectId, projectId),
|
|
29917
|
+
eq26(runs.kind, RunKinds["traffic-sync"]),
|
|
29918
|
+
eq26(runs.sourceId, row.id)
|
|
29378
29919
|
)
|
|
29379
29920
|
).orderBy(desc14(runs.startedAt)).limit(1).get();
|
|
29380
29921
|
return {
|
|
@@ -29402,7 +29943,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29402
29943
|
"`advanceToNow` must be `true`. There is no implicit reset."
|
|
29403
29944
|
);
|
|
29404
29945
|
}
|
|
29405
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
29946
|
+
const sourceRow = app.db.select().from(trafficSources).where(and20(eq26(trafficSources.projectId, project.id), eq26(trafficSources.id, request.params.id))).get();
|
|
29406
29947
|
if (!sourceRow) {
|
|
29407
29948
|
throw notFound("traffic source", request.params.id);
|
|
29408
29949
|
}
|
|
@@ -29419,7 +29960,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29419
29960
|
status: TrafficSourceStatuses.connected,
|
|
29420
29961
|
lastError: null,
|
|
29421
29962
|
updatedAt: now
|
|
29422
|
-
}).where(
|
|
29963
|
+
}).where(eq26(trafficSources.id, sourceRow.id)).run();
|
|
29423
29964
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
29424
29965
|
projectId: project.id,
|
|
29425
29966
|
actor: "api",
|
|
@@ -29427,20 +29968,20 @@ async function trafficRoutes(app, opts) {
|
|
|
29427
29968
|
entityType: "traffic_source",
|
|
29428
29969
|
entityId: sourceRow.id
|
|
29429
29970
|
}));
|
|
29430
|
-
updatedRow = tx.select().from(trafficSources).where(
|
|
29971
|
+
updatedRow = tx.select().from(trafficSources).where(eq26(trafficSources.id, sourceRow.id)).get();
|
|
29431
29972
|
});
|
|
29432
29973
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
29433
29974
|
});
|
|
29434
29975
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
29435
29976
|
const project = resolveProject(app.db, request.params.name);
|
|
29436
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
29977
|
+
const rows = app.db.select().from(trafficSources).where(eq26(trafficSources.projectId, project.id)).orderBy(desc14(trafficSources.createdAt)).all();
|
|
29437
29978
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
29438
29979
|
const response = { sources };
|
|
29439
29980
|
return response;
|
|
29440
29981
|
});
|
|
29441
29982
|
app.get("/projects/:name/traffic/status", async (request) => {
|
|
29442
29983
|
const project = resolveProject(app.db, request.params.name);
|
|
29443
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
29984
|
+
const rows = app.db.select().from(trafficSources).where(eq26(trafficSources.projectId, project.id)).orderBy(desc14(trafficSources.createdAt)).all();
|
|
29444
29985
|
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
29445
29986
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
29446
29987
|
const response = { sources };
|
|
@@ -29450,7 +29991,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29450
29991
|
"/projects/:name/traffic/sources/:id",
|
|
29451
29992
|
async (request) => {
|
|
29452
29993
|
const project = resolveProject(app.db, request.params.name);
|
|
29453
|
-
const row = app.db.select().from(trafficSources).where(
|
|
29994
|
+
const row = app.db.select().from(trafficSources).where(eq26(trafficSources.id, request.params.id)).get();
|
|
29454
29995
|
if (!row || row.projectId !== project.id) {
|
|
29455
29996
|
throw notFound("Traffic source", request.params.id);
|
|
29456
29997
|
}
|
|
@@ -29501,12 +30042,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29501
30042
|
let aiReferralTotal = 0;
|
|
29502
30043
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
29503
30044
|
const crawlerFilters = [
|
|
29504
|
-
|
|
29505
|
-
|
|
29506
|
-
|
|
30045
|
+
eq26(crawlerEventsHourly.projectId, project.id),
|
|
30046
|
+
gte4(crawlerEventsHourly.tsHour, sinceIso),
|
|
30047
|
+
lte3(crawlerEventsHourly.tsHour, untilIso)
|
|
29507
30048
|
];
|
|
29508
|
-
if (sourceIdParam) crawlerFilters.push(
|
|
29509
|
-
const crawlerWhere =
|
|
30049
|
+
if (sourceIdParam) crawlerFilters.push(eq26(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
30050
|
+
const crawlerWhere = and20(...crawlerFilters);
|
|
29510
30051
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
|
|
29511
30052
|
crawlerTotal = Number(total?.total ?? 0);
|
|
29512
30053
|
const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc14(crawlerEventsHourly.tsHour)).limit(limit).all();
|
|
@@ -29526,12 +30067,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29526
30067
|
}
|
|
29527
30068
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
29528
30069
|
const userFetchFilters = [
|
|
29529
|
-
|
|
29530
|
-
|
|
29531
|
-
|
|
30070
|
+
eq26(aiUserFetchEventsHourly.projectId, project.id),
|
|
30071
|
+
gte4(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
30072
|
+
lte3(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
29532
30073
|
];
|
|
29533
|
-
if (sourceIdParam) userFetchFilters.push(
|
|
29534
|
-
const userFetchWhere =
|
|
30074
|
+
if (sourceIdParam) userFetchFilters.push(eq26(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
30075
|
+
const userFetchWhere = and20(...userFetchFilters);
|
|
29535
30076
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
29536
30077
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
29537
30078
|
const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(desc14(aiUserFetchEventsHourly.tsHour)).limit(limit).all();
|
|
@@ -29551,12 +30092,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29551
30092
|
}
|
|
29552
30093
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
29553
30094
|
const aiFilters = [
|
|
29554
|
-
|
|
29555
|
-
|
|
29556
|
-
|
|
30095
|
+
eq26(aiReferralEventsHourly.projectId, project.id),
|
|
30096
|
+
gte4(aiReferralEventsHourly.tsHour, sinceIso),
|
|
30097
|
+
lte3(aiReferralEventsHourly.tsHour, untilIso)
|
|
29557
30098
|
];
|
|
29558
|
-
if (sourceIdParam) aiFilters.push(
|
|
29559
|
-
const aiWhere =
|
|
30099
|
+
if (sourceIdParam) aiFilters.push(eq26(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
30100
|
+
const aiWhere = and20(...aiFilters);
|
|
29560
30101
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
29561
30102
|
aiReferralTotal = Number(total?.total ?? 0);
|
|
29562
30103
|
const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc14(aiReferralEventsHourly.tsHour)).limit(limit).all();
|
|
@@ -29592,7 +30133,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29592
30133
|
}
|
|
29593
30134
|
|
|
29594
30135
|
// ../api-routes/src/doctor/checks/agent.ts
|
|
29595
|
-
import
|
|
30136
|
+
import crypto25 from "crypto";
|
|
29596
30137
|
import fs6 from "fs";
|
|
29597
30138
|
import path7 from "path";
|
|
29598
30139
|
var REQUIRED_SKILLS = ["canonry", "aero"];
|
|
@@ -29745,7 +30286,7 @@ function isInstalled(dir) {
|
|
|
29745
30286
|
}
|
|
29746
30287
|
function hashInstalledFile(filePath) {
|
|
29747
30288
|
try {
|
|
29748
|
-
return
|
|
30289
|
+
return crypto25.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
|
|
29749
30290
|
} catch {
|
|
29750
30291
|
return void 0;
|
|
29751
30292
|
}
|
|
@@ -29906,7 +30447,7 @@ var BING_AUTH_CHECKS = [
|
|
|
29906
30447
|
];
|
|
29907
30448
|
|
|
29908
30449
|
// ../api-routes/src/doctor/checks/content.ts
|
|
29909
|
-
import { eq as
|
|
30450
|
+
import { eq as eq27 } from "drizzle-orm";
|
|
29910
30451
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
29911
30452
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
29912
30453
|
function skippedNoProject() {
|
|
@@ -29919,7 +30460,7 @@ function skippedNoProject() {
|
|
|
29919
30460
|
}
|
|
29920
30461
|
function loadProject(ctx) {
|
|
29921
30462
|
if (!ctx.project) return null;
|
|
29922
|
-
return ctx.db.select().from(projects).where(
|
|
30463
|
+
return ctx.db.select().from(projects).where(eq27(projects.id, ctx.project.id)).get() ?? null;
|
|
29923
30464
|
}
|
|
29924
30465
|
function percent(value) {
|
|
29925
30466
|
return Math.round(value * 100);
|
|
@@ -30010,6 +30551,123 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
30010
30551
|
CONTENT_CHECKS.map((check) => [check.id, check])
|
|
30011
30552
|
);
|
|
30012
30553
|
|
|
30554
|
+
// ../api-routes/src/doctor/checks/ads.ts
|
|
30555
|
+
import { eq as eq28 } from "drizzle-orm";
|
|
30556
|
+
var RECENT_SYNC_WARN_DAYS = 7;
|
|
30557
|
+
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
30558
|
+
var adsConnectionCheck = {
|
|
30559
|
+
id: "ads.auth.connection",
|
|
30560
|
+
category: CheckCategories.auth,
|
|
30561
|
+
scope: CheckScopes.project,
|
|
30562
|
+
title: "OpenAI ads connection",
|
|
30563
|
+
run: (ctx) => {
|
|
30564
|
+
if (!ctx.project) {
|
|
30565
|
+
return {
|
|
30566
|
+
status: CheckStatuses.skipped,
|
|
30567
|
+
code: "ads.auth.no-project",
|
|
30568
|
+
summary: "Project context required.",
|
|
30569
|
+
remediation: null
|
|
30570
|
+
};
|
|
30571
|
+
}
|
|
30572
|
+
const row = ctx.db.select().from(adsConnections).where(eq28(adsConnections.projectId, ctx.project.id)).get();
|
|
30573
|
+
if (!row) {
|
|
30574
|
+
return {
|
|
30575
|
+
status: CheckStatuses.skipped,
|
|
30576
|
+
code: "ads.auth.not-connected",
|
|
30577
|
+
summary: "No OpenAI ads connection for this project.",
|
|
30578
|
+
remediation: null
|
|
30579
|
+
};
|
|
30580
|
+
}
|
|
30581
|
+
if (!ctx.adsCredentialStore) {
|
|
30582
|
+
return {
|
|
30583
|
+
status: CheckStatuses.skipped,
|
|
30584
|
+
code: "ads.auth.store-unavailable",
|
|
30585
|
+
summary: "No ads credential store configured for this deployment.",
|
|
30586
|
+
remediation: null
|
|
30587
|
+
};
|
|
30588
|
+
}
|
|
30589
|
+
const cfg = ctx.adsCredentialStore.getConnection(ctx.project.name);
|
|
30590
|
+
if (!cfg?.apiKey) {
|
|
30591
|
+
return {
|
|
30592
|
+
status: CheckStatuses.fail,
|
|
30593
|
+
code: "ads.auth.missing-key",
|
|
30594
|
+
summary: "An ads connection row exists but no SDK key is stored in the local config.",
|
|
30595
|
+
remediation: `Re-run \`canonry ads connect ${ctx.project.name} --api-key <sdk-key>\` to restore the credential.`,
|
|
30596
|
+
details: { adAccountId: row.adAccountId }
|
|
30597
|
+
};
|
|
30598
|
+
}
|
|
30599
|
+
return {
|
|
30600
|
+
status: CheckStatuses.ok,
|
|
30601
|
+
code: "ads.auth.ok",
|
|
30602
|
+
summary: `Connected to ad account ${row.displayName ?? row.adAccountId}.`,
|
|
30603
|
+
remediation: null,
|
|
30604
|
+
details: { adAccountId: row.adAccountId }
|
|
30605
|
+
};
|
|
30606
|
+
}
|
|
30607
|
+
};
|
|
30608
|
+
var adsRecentSyncCheck = {
|
|
30609
|
+
id: "ads.data.recent-sync",
|
|
30610
|
+
category: CheckCategories.integrations,
|
|
30611
|
+
scope: CheckScopes.project,
|
|
30612
|
+
title: "OpenAI ads recent sync",
|
|
30613
|
+
run: (ctx) => {
|
|
30614
|
+
if (!ctx.project) {
|
|
30615
|
+
return {
|
|
30616
|
+
status: CheckStatuses.skipped,
|
|
30617
|
+
code: "ads.data.no-project",
|
|
30618
|
+
summary: "Project context required.",
|
|
30619
|
+
remediation: null
|
|
30620
|
+
};
|
|
30621
|
+
}
|
|
30622
|
+
const row = ctx.db.select().from(adsConnections).where(eq28(adsConnections.projectId, ctx.project.id)).get();
|
|
30623
|
+
if (!row) {
|
|
30624
|
+
return {
|
|
30625
|
+
status: CheckStatuses.skipped,
|
|
30626
|
+
code: "ads.data.not-connected",
|
|
30627
|
+
summary: "No OpenAI ads connection for this project.",
|
|
30628
|
+
remediation: null
|
|
30629
|
+
};
|
|
30630
|
+
}
|
|
30631
|
+
if (!row.lastSyncedAt) {
|
|
30632
|
+
return {
|
|
30633
|
+
status: CheckStatuses.warn,
|
|
30634
|
+
code: "ads.data.never-synced",
|
|
30635
|
+
summary: "The connected ad account has never been synced.",
|
|
30636
|
+
remediation: `Run \`canonry ads sync ${ctx.project.name}\` (and schedule it: \`canonry schedule set ${ctx.project.name} --kind ads-sync --preset daily\`).`
|
|
30637
|
+
};
|
|
30638
|
+
}
|
|
30639
|
+
const syncedAtMs = new Date(row.lastSyncedAt).getTime();
|
|
30640
|
+
const ageDays = (Date.now() - syncedAtMs) / (1e3 * 60 * 60 * 24);
|
|
30641
|
+
const details = { lastSyncedAt: row.lastSyncedAt, ageDays: Math.round(ageDays) };
|
|
30642
|
+
if (ageDays > RECENT_SYNC_FAIL_DAYS) {
|
|
30643
|
+
return {
|
|
30644
|
+
status: CheckStatuses.fail,
|
|
30645
|
+
code: "ads.data.stale",
|
|
30646
|
+
summary: `Last ads sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_FAIL_DAYS}d).`,
|
|
30647
|
+
remediation: `Run \`canonry ads sync ${ctx.project.name}\` and check the ads-sync schedule.`,
|
|
30648
|
+
details
|
|
30649
|
+
};
|
|
30650
|
+
}
|
|
30651
|
+
if (ageDays > RECENT_SYNC_WARN_DAYS) {
|
|
30652
|
+
return {
|
|
30653
|
+
status: CheckStatuses.warn,
|
|
30654
|
+
code: "ads.data.aging",
|
|
30655
|
+
summary: `Last ads sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_WARN_DAYS}d).`,
|
|
30656
|
+
remediation: `Schedule daily syncs: \`canonry schedule set ${ctx.project.name} --kind ads-sync --preset daily\`.`,
|
|
30657
|
+
details
|
|
30658
|
+
};
|
|
30659
|
+
}
|
|
30660
|
+
return {
|
|
30661
|
+
status: CheckStatuses.ok,
|
|
30662
|
+
code: "ads.data.ok",
|
|
30663
|
+
summary: `Last ads sync ${Math.round(ageDays)} day(s) ago.`,
|
|
30664
|
+
remediation: null,
|
|
30665
|
+
details
|
|
30666
|
+
};
|
|
30667
|
+
}
|
|
30668
|
+
};
|
|
30669
|
+
var ADS_CHECKS = [adsConnectionCheck, adsRecentSyncCheck];
|
|
30670
|
+
|
|
30013
30671
|
// ../api-routes/src/doctor/checks/ga-auth.ts
|
|
30014
30672
|
async function checkServiceAccount(conn) {
|
|
30015
30673
|
if (!conn.propertyId) {
|
|
@@ -30152,9 +30810,9 @@ var ga4ConnectionCheck = {
|
|
|
30152
30810
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
30153
30811
|
|
|
30154
30812
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
30155
|
-
import { and as
|
|
30156
|
-
var
|
|
30157
|
-
var
|
|
30813
|
+
import { and as and21, eq as eq29 } from "drizzle-orm";
|
|
30814
|
+
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
30815
|
+
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
30158
30816
|
function skippedNoProject2() {
|
|
30159
30817
|
return {
|
|
30160
30818
|
status: CheckStatuses.skipped,
|
|
@@ -30385,7 +31043,7 @@ var recentSyncCheck = {
|
|
|
30385
31043
|
title: "GBP recent sync",
|
|
30386
31044
|
run: (ctx) => {
|
|
30387
31045
|
if (!ctx.project) return skippedNoProject2();
|
|
30388
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(
|
|
31046
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and21(eq29(gbpLocations.projectId, ctx.project.id), eq29(gbpLocations.selected, true))).all();
|
|
30389
31047
|
if (selected.length === 0) {
|
|
30390
31048
|
return {
|
|
30391
31049
|
status: CheckStatuses.skipped,
|
|
@@ -30407,20 +31065,20 @@ var recentSyncCheck = {
|
|
|
30407
31065
|
const newest = Math.max(...syncTimes);
|
|
30408
31066
|
const ageDays = (Date.now() - newest) / (1e3 * 60 * 60 * 24);
|
|
30409
31067
|
const details = { selectedLocations: selected.length, newestSyncAgeDays: Math.round(ageDays) };
|
|
30410
|
-
if (ageDays >
|
|
31068
|
+
if (ageDays > RECENT_SYNC_FAIL_DAYS2) {
|
|
30411
31069
|
return {
|
|
30412
31070
|
status: CheckStatuses.fail,
|
|
30413
31071
|
code: "gbp.data.stale",
|
|
30414
|
-
summary: `Most recent GBP sync was ${Math.round(ageDays)} days ago (> ${
|
|
31072
|
+
summary: `Most recent GBP sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_FAIL_DAYS2}d).`,
|
|
30415
31073
|
remediation: `Run \`canonry gbp sync ${ctx.project.name}\` or set a gbp-sync schedule.`,
|
|
30416
31074
|
details
|
|
30417
31075
|
};
|
|
30418
31076
|
}
|
|
30419
|
-
if (ageDays >
|
|
31077
|
+
if (ageDays > RECENT_SYNC_WARN_DAYS2) {
|
|
30420
31078
|
return {
|
|
30421
31079
|
status: CheckStatuses.warn,
|
|
30422
31080
|
code: "gbp.data.aging",
|
|
30423
|
-
summary: `Most recent GBP sync was ${Math.round(ageDays)} days ago (> ${
|
|
31081
|
+
summary: `Most recent GBP sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_WARN_DAYS2}d).`,
|
|
30424
31082
|
remediation: `Run \`canonry gbp sync ${ctx.project.name}\` or set a gbp-sync schedule to keep data fresh.`,
|
|
30425
31083
|
details
|
|
30426
31084
|
};
|
|
@@ -30445,7 +31103,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
30445
31103
|
);
|
|
30446
31104
|
|
|
30447
31105
|
// ../api-routes/src/doctor/checks/places.ts
|
|
30448
|
-
import { eq as
|
|
31106
|
+
import { eq as eq30 } from "drizzle-orm";
|
|
30449
31107
|
var apiKeyCheck = {
|
|
30450
31108
|
id: "gbp.places.api-key",
|
|
30451
31109
|
category: CheckCategories.auth,
|
|
@@ -30490,7 +31148,7 @@ var apiKeyCheck = {
|
|
|
30490
31148
|
details: { tier: cfg.tier }
|
|
30491
31149
|
};
|
|
30492
31150
|
}
|
|
30493
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
31151
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq30(gbpLocations.projectId, ctx.project.id)).all();
|
|
30494
31152
|
const selected = rows.filter((r) => r.selected);
|
|
30495
31153
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
30496
31154
|
const details = {
|
|
@@ -30941,7 +31599,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
30941
31599
|
];
|
|
30942
31600
|
|
|
30943
31601
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
30944
|
-
import { and as
|
|
31602
|
+
import { and as and22, eq as eq31, gte as gte5, ne as ne4, sql as sql12 } from "drizzle-orm";
|
|
30945
31603
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
30946
31604
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
30947
31605
|
function skippedNoProject4() {
|
|
@@ -30955,8 +31613,8 @@ function skippedNoProject4() {
|
|
|
30955
31613
|
function loadProbes(ctx) {
|
|
30956
31614
|
if (!ctx.project) return [];
|
|
30957
31615
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
30958
|
-
|
|
30959
|
-
|
|
31616
|
+
and22(
|
|
31617
|
+
eq31(trafficSources.projectId, ctx.project.id),
|
|
30960
31618
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
30961
31619
|
)
|
|
30962
31620
|
).all();
|
|
@@ -31036,17 +31694,17 @@ var recentDataCheck = {
|
|
|
31036
31694
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
31037
31695
|
const recentCrawlers = Number(
|
|
31038
31696
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
31039
|
-
|
|
31040
|
-
|
|
31041
|
-
|
|
31697
|
+
and22(
|
|
31698
|
+
eq31(crawlerEventsHourly.projectId, ctx.project.id),
|
|
31699
|
+
gte5(crawlerEventsHourly.tsHour, warnCutoff)
|
|
31042
31700
|
)
|
|
31043
31701
|
).get()?.total ?? 0
|
|
31044
31702
|
);
|
|
31045
31703
|
const recentReferrals = Number(
|
|
31046
31704
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31047
|
-
|
|
31048
|
-
|
|
31049
|
-
|
|
31705
|
+
and22(
|
|
31706
|
+
eq31(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
31707
|
+
gte5(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
31050
31708
|
)
|
|
31051
31709
|
).get()?.total ?? 0
|
|
31052
31710
|
);
|
|
@@ -31060,17 +31718,17 @@ var recentDataCheck = {
|
|
|
31060
31718
|
}
|
|
31061
31719
|
const olderCrawlers = Number(
|
|
31062
31720
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
31063
|
-
|
|
31064
|
-
|
|
31065
|
-
|
|
31721
|
+
and22(
|
|
31722
|
+
eq31(crawlerEventsHourly.projectId, ctx.project.id),
|
|
31723
|
+
gte5(crawlerEventsHourly.tsHour, failCutoff)
|
|
31066
31724
|
)
|
|
31067
31725
|
).get()?.total ?? 0
|
|
31068
31726
|
);
|
|
31069
31727
|
const olderReferrals = Number(
|
|
31070
31728
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31071
|
-
|
|
31072
|
-
|
|
31073
|
-
|
|
31729
|
+
and22(
|
|
31730
|
+
eq31(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
31731
|
+
gte5(aiReferralEventsHourly.tsHour, failCutoff)
|
|
31074
31732
|
)
|
|
31075
31733
|
).get()?.total ?? 0
|
|
31076
31734
|
);
|
|
@@ -31354,6 +32012,7 @@ var ALL_CHECKS = [
|
|
|
31354
32012
|
...BING_AUTH_CHECKS,
|
|
31355
32013
|
...WORDPRESS_PUBLISH_CHECKS,
|
|
31356
32014
|
...GA_AUTH_CHECKS,
|
|
32015
|
+
...ADS_CHECKS,
|
|
31357
32016
|
...PROVIDERS_CHECKS,
|
|
31358
32017
|
...TRAFFIC_SOURCE_CHECKS,
|
|
31359
32018
|
...CONTENT_CHECKS,
|
|
@@ -31440,6 +32099,7 @@ async function doctorRoutes(app, opts) {
|
|
|
31440
32099
|
bingConnectionStore: opts.bingConnectionStore,
|
|
31441
32100
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
31442
32101
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
32102
|
+
adsCredentialStore: opts.adsCredentialStore,
|
|
31443
32103
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
31444
32104
|
getPlacesConfig: opts.getPlacesConfig,
|
|
31445
32105
|
redirectUri,
|
|
@@ -31465,6 +32125,7 @@ async function doctorRoutes(app, opts) {
|
|
|
31465
32125
|
bingConnectionStore: opts.bingConnectionStore,
|
|
31466
32126
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
31467
32127
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
32128
|
+
adsCredentialStore: opts.adsCredentialStore,
|
|
31468
32129
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
31469
32130
|
getPlacesConfig: opts.getPlacesConfig,
|
|
31470
32131
|
redirectUri,
|
|
@@ -31478,8 +32139,8 @@ async function doctorRoutes(app, opts) {
|
|
|
31478
32139
|
}
|
|
31479
32140
|
|
|
31480
32141
|
// ../api-routes/src/discovery/routes.ts
|
|
31481
|
-
import
|
|
31482
|
-
import { and as
|
|
32142
|
+
import crypto26 from "crypto";
|
|
32143
|
+
import { and as and23, desc as desc15, eq as eq32, gte as gte6, inArray as inArray11 } from "drizzle-orm";
|
|
31483
32144
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
31484
32145
|
async function discoveryRoutes(app, opts) {
|
|
31485
32146
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -31511,21 +32172,21 @@ async function discoveryRoutes(app, opts) {
|
|
|
31511
32172
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
31512
32173
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
31513
32174
|
const decision = app.db.transaction((tx) => {
|
|
31514
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
31515
|
-
|
|
31516
|
-
|
|
31517
|
-
|
|
32175
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and23(
|
|
32176
|
+
eq32(discoverySessions.projectId, project.id),
|
|
32177
|
+
eq32(discoverySessions.icpDescription, icpDescription),
|
|
32178
|
+
inArray11(discoverySessions.status, [
|
|
31518
32179
|
DiscoverySessionStatuses.queued,
|
|
31519
32180
|
DiscoverySessionStatuses.seeding,
|
|
31520
32181
|
DiscoverySessionStatuses.probing
|
|
31521
32182
|
]),
|
|
31522
|
-
|
|
32183
|
+
gte6(discoverySessions.createdAt, ageFloorIso)
|
|
31523
32184
|
)).orderBy(desc15(discoverySessions.createdAt)).get();
|
|
31524
32185
|
if (existing && existing.runId) {
|
|
31525
32186
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
31526
32187
|
}
|
|
31527
|
-
const sessionId =
|
|
31528
|
-
const runId =
|
|
32188
|
+
const sessionId = crypto26.randomUUID();
|
|
32189
|
+
const runId = crypto26.randomUUID();
|
|
31529
32190
|
tx.insert(discoverySessions).values({
|
|
31530
32191
|
id: sessionId,
|
|
31531
32192
|
projectId: project.id,
|
|
@@ -31583,7 +32244,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31583
32244
|
const project = resolveProject(app.db, request.params.name);
|
|
31584
32245
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
31585
32246
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
31586
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
32247
|
+
const rows = app.db.select().from(discoverySessions).where(eq32(discoverySessions.projectId, project.id)).orderBy(desc15(discoverySessions.createdAt)).limit(limit).all();
|
|
31587
32248
|
return reply.send(rows.map(serializeSession));
|
|
31588
32249
|
}
|
|
31589
32250
|
);
|
|
@@ -31591,11 +32252,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
31591
32252
|
"/projects/:name/discover/sessions/:id",
|
|
31592
32253
|
async (request, reply) => {
|
|
31593
32254
|
const project = resolveProject(app.db, request.params.name);
|
|
31594
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32255
|
+
const session = app.db.select().from(discoverySessions).where(eq32(discoverySessions.id, request.params.id)).get();
|
|
31595
32256
|
if (!session || session.projectId !== project.id) {
|
|
31596
32257
|
throw notFound("Discovery session", request.params.id);
|
|
31597
32258
|
}
|
|
31598
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32259
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq32(discoveryProbes.sessionId, session.id)).all();
|
|
31599
32260
|
const detail = {
|
|
31600
32261
|
...serializeSession(session),
|
|
31601
32262
|
probes: probeRows.map(serializeProbe)
|
|
@@ -31607,12 +32268,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
31607
32268
|
"/projects/:name/discover/sessions/:id/promote",
|
|
31608
32269
|
async (request, reply) => {
|
|
31609
32270
|
const project = resolveProject(app.db, request.params.name);
|
|
31610
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32271
|
+
const session = app.db.select().from(discoverySessions).where(eq32(discoverySessions.id, request.params.id)).get();
|
|
31611
32272
|
if (!session || session.projectId !== project.id) {
|
|
31612
32273
|
throw notFound("Discovery session", request.params.id);
|
|
31613
32274
|
}
|
|
31614
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
31615
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32275
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq32(discoveryProbes.sessionId, session.id)).all();
|
|
32276
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq32(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
31616
32277
|
const seenCompetitors = new Set(existingCompetitors);
|
|
31617
32278
|
const cited = /* @__PURE__ */ new Set();
|
|
31618
32279
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -31641,7 +32302,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31641
32302
|
);
|
|
31642
32303
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
31643
32304
|
const project = resolveProject(app.db, request.params.name);
|
|
31644
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32305
|
+
const session = app.db.select().from(discoverySessions).where(eq32(discoverySessions.id, request.params.id)).get();
|
|
31645
32306
|
if (!session || session.projectId !== project.id) {
|
|
31646
32307
|
throw notFound("Discovery session", request.params.id);
|
|
31647
32308
|
}
|
|
@@ -31664,7 +32325,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31664
32325
|
const bucketSet = new Set(buckets);
|
|
31665
32326
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
31666
32327
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
31667
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32328
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq32(discoveryProbes.sessionId, session.id)).all();
|
|
31668
32329
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
31669
32330
|
for (const probe of probeRows) {
|
|
31670
32331
|
if (!probe.bucket) continue;
|
|
@@ -31672,7 +32333,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31672
32333
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
31673
32334
|
}
|
|
31674
32335
|
const existingQueries = new Set(
|
|
31675
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
32336
|
+
app.db.select({ query: queries.query }).from(queries).where(eq32(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
31676
32337
|
);
|
|
31677
32338
|
const promotedQueries = [];
|
|
31678
32339
|
const skippedQueries = [];
|
|
@@ -31688,7 +32349,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31688
32349
|
const skippedCompetitors = [];
|
|
31689
32350
|
if (includeCompetitors) {
|
|
31690
32351
|
const existingCompetitors = new Set(
|
|
31691
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32352
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq32(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
31692
32353
|
);
|
|
31693
32354
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
31694
32355
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -31707,7 +32368,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31707
32368
|
app.db.transaction((tx) => {
|
|
31708
32369
|
for (const query of promotedQueries) {
|
|
31709
32370
|
tx.insert(queries).values({
|
|
31710
|
-
id:
|
|
32371
|
+
id: crypto26.randomUUID(),
|
|
31711
32372
|
projectId: project.id,
|
|
31712
32373
|
query,
|
|
31713
32374
|
provenance,
|
|
@@ -31716,7 +32377,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31716
32377
|
}
|
|
31717
32378
|
for (const domain of promotedCompetitors) {
|
|
31718
32379
|
tx.insert(competitors).values({
|
|
31719
|
-
id:
|
|
32380
|
+
id: crypto26.randomUUID(),
|
|
31720
32381
|
projectId: project.id,
|
|
31721
32382
|
domain,
|
|
31722
32383
|
provenance,
|
|
@@ -31791,8 +32452,8 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
31791
32452
|
}
|
|
31792
32453
|
|
|
31793
32454
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
31794
|
-
import
|
|
31795
|
-
import { eq as
|
|
32455
|
+
import crypto27 from "crypto";
|
|
32456
|
+
import { eq as eq33 } from "drizzle-orm";
|
|
31796
32457
|
var DEFAULT_MAX_PROBES = 100;
|
|
31797
32458
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
31798
32459
|
function classifyProbeBucket(input) {
|
|
@@ -31846,7 +32507,7 @@ async function executeDiscovery(opts) {
|
|
|
31846
32507
|
status: DiscoverySessionStatuses.seeding,
|
|
31847
32508
|
dedupThreshold,
|
|
31848
32509
|
startedAt
|
|
31849
|
-
}).where(
|
|
32510
|
+
}).where(eq33(discoverySessions.id, opts.sessionId)).run();
|
|
31850
32511
|
const seedResult = await opts.deps.seed({
|
|
31851
32512
|
project: opts.project,
|
|
31852
32513
|
icpDescription: opts.icpDescription,
|
|
@@ -31872,7 +32533,7 @@ async function executeDiscovery(opts) {
|
|
|
31872
32533
|
seedCountRaw,
|
|
31873
32534
|
seedCount,
|
|
31874
32535
|
warning
|
|
31875
|
-
}).where(
|
|
32536
|
+
}).where(eq33(discoverySessions.id, opts.sessionId)).run();
|
|
31876
32537
|
const probeRows = [];
|
|
31877
32538
|
const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
|
|
31878
32539
|
for (const query of probedCanonicals) {
|
|
@@ -31885,7 +32546,7 @@ async function executeDiscovery(opts) {
|
|
|
31885
32546
|
probeRows.push({ citedDomains: probe.citedDomains, bucket });
|
|
31886
32547
|
buckets[bucket]++;
|
|
31887
32548
|
opts.db.insert(discoveryProbes).values({
|
|
31888
|
-
id:
|
|
32549
|
+
id: crypto27.randomUUID(),
|
|
31889
32550
|
sessionId: opts.sessionId,
|
|
31890
32551
|
projectId: opts.project.id,
|
|
31891
32552
|
query,
|
|
@@ -31912,7 +32573,7 @@ async function executeDiscovery(opts) {
|
|
|
31912
32573
|
wastedCount: buckets["wasted-surface"],
|
|
31913
32574
|
competitorMap,
|
|
31914
32575
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
31915
|
-
}).where(
|
|
32576
|
+
}).where(eq33(discoverySessions.id, opts.sessionId)).run();
|
|
31916
32577
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
31917
32578
|
return {
|
|
31918
32579
|
buckets,
|
|
@@ -31929,7 +32590,7 @@ function upsertDomainClassifications(db, projectId, sessionId, competitorMap) {
|
|
|
31929
32590
|
const domain = normalizeDomain(entry.domain);
|
|
31930
32591
|
if (!domain) continue;
|
|
31931
32592
|
db.insert(domainClassifications).values({
|
|
31932
|
-
id:
|
|
32593
|
+
id: crypto27.randomUUID(),
|
|
31933
32594
|
projectId,
|
|
31934
32595
|
domain,
|
|
31935
32596
|
competitorType: entry.competitorType,
|
|
@@ -31952,7 +32613,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
31952
32613
|
status: DiscoverySessionStatuses.failed,
|
|
31953
32614
|
error,
|
|
31954
32615
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
31955
|
-
}).where(
|
|
32616
|
+
}).where(eq33(discoverySessions.id, sessionId)).run();
|
|
31956
32617
|
}
|
|
31957
32618
|
function dedupeStrings(input) {
|
|
31958
32619
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -31969,8 +32630,8 @@ function dedupeStrings(input) {
|
|
|
31969
32630
|
}
|
|
31970
32631
|
|
|
31971
32632
|
// ../api-routes/src/technical-aeo.ts
|
|
31972
|
-
import
|
|
31973
|
-
import { and as
|
|
32633
|
+
import crypto28 from "crypto";
|
|
32634
|
+
import { and as and24, asc as asc4, count, desc as desc16, eq as eq34, inArray as inArray12 } from "drizzle-orm";
|
|
31974
32635
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
31975
32636
|
function emptyScore(projectName) {
|
|
31976
32637
|
return {
|
|
@@ -32002,10 +32663,10 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
32002
32663
|
async function technicalAeoRoutes(app, opts) {
|
|
32003
32664
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
32004
32665
|
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
|
-
|
|
32666
|
+
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq34(siteAuditSnapshots.runId, runs.id)).where(and24(
|
|
32667
|
+
eq34(siteAuditSnapshots.projectId, project.id),
|
|
32668
|
+
eq34(runs.kind, RunKinds["site-audit"]),
|
|
32669
|
+
inArray12(runs.status, SURFACEABLE_STATUSES),
|
|
32009
32670
|
notProbeRun()
|
|
32010
32671
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
32011
32672
|
const latest = rows[0];
|
|
@@ -32037,24 +32698,24 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32037
32698
|
});
|
|
32038
32699
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
32039
32700
|
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
|
-
|
|
32701
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq34(siteAuditSnapshots.runId, runs.id)).where(and24(
|
|
32702
|
+
eq34(siteAuditSnapshots.projectId, project.id),
|
|
32703
|
+
eq34(runs.kind, RunKinds["site-audit"]),
|
|
32704
|
+
inArray12(runs.status, SURFACEABLE_STATUSES),
|
|
32044
32705
|
notProbeRun()
|
|
32045
32706
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
32046
32707
|
if (!latest) {
|
|
32047
32708
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
32048
32709
|
}
|
|
32049
32710
|
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 =
|
|
32711
|
+
const conds = [eq34(siteAuditPages.runId, latest.runId)];
|
|
32712
|
+
if (statusFilter) conds.push(eq34(siteAuditPages.status, statusFilter));
|
|
32713
|
+
const where = and24(...conds);
|
|
32053
32714
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
32054
32715
|
const total = totalRow?.value ?? 0;
|
|
32055
32716
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
32056
32717
|
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" ?
|
|
32718
|
+
const orderBy = request.query.sort === "score-desc" ? desc16(siteAuditPages.overallScore) : request.query.sort === "url" ? asc4(siteAuditPages.url) : asc4(siteAuditPages.overallScore);
|
|
32058
32719
|
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
32059
32720
|
const pages = rows.map((row) => ({
|
|
32060
32721
|
url: row.url,
|
|
@@ -32073,10 +32734,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32073
32734
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
32074
32735
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
32075
32736
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
32076
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
32077
|
-
|
|
32078
|
-
|
|
32079
|
-
|
|
32737
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq34(siteAuditSnapshots.runId, runs.id)).where(and24(
|
|
32738
|
+
eq34(siteAuditSnapshots.projectId, project.id),
|
|
32739
|
+
eq34(runs.kind, RunKinds["site-audit"]),
|
|
32740
|
+
inArray12(runs.status, SURFACEABLE_STATUSES),
|
|
32080
32741
|
notProbeRun()
|
|
32081
32742
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
32082
32743
|
return { project: project.name, points: rows.reverse() };
|
|
@@ -32087,16 +32748,16 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32087
32748
|
if (!parsed.success) {
|
|
32088
32749
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
32089
32750
|
}
|
|
32090
|
-
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
32091
|
-
|
|
32092
|
-
|
|
32093
|
-
|
|
32751
|
+
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and24(
|
|
32752
|
+
eq34(runs.projectId, project.id),
|
|
32753
|
+
eq34(runs.kind, RunKinds["site-audit"]),
|
|
32754
|
+
inArray12(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
32094
32755
|
)).get();
|
|
32095
32756
|
if (existing) {
|
|
32096
32757
|
return { runId: existing.id, status: existing.status };
|
|
32097
32758
|
}
|
|
32098
32759
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32099
|
-
const runId =
|
|
32760
|
+
const runId = crypto28.randomUUID();
|
|
32100
32761
|
app.db.insert(runs).values({
|
|
32101
32762
|
id: runId,
|
|
32102
32763
|
projectId: project.id,
|
|
@@ -32223,6 +32884,11 @@ async function apiRoutes(app, opts) {
|
|
|
32223
32884
|
getTelemetryStatus: opts.getTelemetryStatus,
|
|
32224
32885
|
setTelemetryEnabled: opts.setTelemetryEnabled
|
|
32225
32886
|
});
|
|
32887
|
+
await api.register(adsRoutes, {
|
|
32888
|
+
adsCredentialStore: opts.adsCredentialStore,
|
|
32889
|
+
verifyAdsAccount: opts.verifyAdsAccount,
|
|
32890
|
+
onAdsSyncRequested: opts.onAdsSyncRequested
|
|
32891
|
+
});
|
|
32226
32892
|
await api.register(bingRoutes, {
|
|
32227
32893
|
bingConnectionStore: opts.bingConnectionStore,
|
|
32228
32894
|
onInspectSitemapRequested: opts.onBingInspectSitemapRequested
|
|
@@ -32286,6 +32952,7 @@ async function apiRoutes(app, opts) {
|
|
|
32286
32952
|
bingConnectionStore: opts.bingConnectionStore,
|
|
32287
32953
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
32288
32954
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
32955
|
+
adsCredentialStore: opts.adsCredentialStore,
|
|
32289
32956
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
32290
32957
|
getPlacesConfig: opts.getPlacesConfig,
|
|
32291
32958
|
publicUrl: opts.publicUrl,
|
|
@@ -32436,7 +33103,7 @@ function buildTrafficSourceValidators(opts) {
|
|
|
32436
33103
|
}
|
|
32437
33104
|
|
|
32438
33105
|
// src/intelligence-service.ts
|
|
32439
|
-
import
|
|
33106
|
+
import crypto29 from "crypto";
|
|
32440
33107
|
|
|
32441
33108
|
// src/logger.ts
|
|
32442
33109
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -32668,9 +33335,9 @@ var IntelligenceService = class {
|
|
|
32668
33335
|
*/
|
|
32669
33336
|
analyzeAndPersist(runId, projectId) {
|
|
32670
33337
|
const recentRuns = this.db.select().from(runs).where(
|
|
32671
|
-
|
|
32672
|
-
|
|
32673
|
-
or5(
|
|
33338
|
+
and25(
|
|
33339
|
+
eq35(runs.projectId, projectId),
|
|
33340
|
+
or5(eq35(runs.status, "completed"), eq35(runs.status, "partial")),
|
|
32674
33341
|
// Defensive: RunCoordinator already skips probes before this is
|
|
32675
33342
|
// called, but if a future call site invokes analyzeAndPersist
|
|
32676
33343
|
// directly for a probe, probes still must not pollute the
|
|
@@ -32752,7 +33419,7 @@ var IntelligenceService = class {
|
|
|
32752
33419
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
32753
33420
|
*/
|
|
32754
33421
|
analyzeAndPersistGbp(runId, projectId) {
|
|
32755
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
33422
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq35(runs.id, runId)).get();
|
|
32756
33423
|
if (!runRow) {
|
|
32757
33424
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
32758
33425
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -32760,11 +33427,11 @@ var IntelligenceService = class {
|
|
|
32760
33427
|
}
|
|
32761
33428
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
32762
33429
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
32763
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
32764
|
-
|
|
32765
|
-
|
|
32766
|
-
|
|
32767
|
-
|
|
33430
|
+
const selected = this.db.select().from(gbpLocations).where(and25(
|
|
33431
|
+
eq35(gbpLocations.projectId, projectId),
|
|
33432
|
+
eq35(gbpLocations.selected, true),
|
|
33433
|
+
gte7(gbpLocations.syncedAt, windowStart),
|
|
33434
|
+
lte4(gbpLocations.syncedAt, windowEnd)
|
|
32768
33435
|
)).all();
|
|
32769
33436
|
if (selected.length === 0) {
|
|
32770
33437
|
log.info("gbp-intelligence.skip", { runId, reason: "no locations synced during run" });
|
|
@@ -32797,10 +33464,10 @@ var IntelligenceService = class {
|
|
|
32797
33464
|
}
|
|
32798
33465
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
32799
33466
|
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(
|
|
33467
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and25(eq35(gbpDailyMetrics.projectId, projectId), eq35(gbpDailyMetrics.locationName, locationName))).all();
|
|
33468
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and25(eq35(gbpPlaceActions.projectId, projectId), eq35(gbpPlaceActions.locationName, locationName))).all();
|
|
33469
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and25(eq35(gbpLodgingSnapshots.projectId, projectId), eq35(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
33470
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and25(eq35(gbpPlaceDetails.projectId, projectId), eq35(gbpPlaceDetails.locationName, locationName))).orderBy(desc17(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
32804
33471
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
32805
33472
|
const summary = buildGbpSummary({
|
|
32806
33473
|
locationName,
|
|
@@ -32832,7 +33499,7 @@ var IntelligenceService = class {
|
|
|
32832
33499
|
/** Build the month-over-month keyword series for a location from the
|
|
32833
33500
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
32834
33501
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
32835
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
33502
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and25(eq35(gbpKeywordMonthly.projectId, projectId), eq35(gbpKeywordMonthly.locationName, locationName))).all();
|
|
32836
33503
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
32837
33504
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
32838
33505
|
const recentMonth = months[0] ?? null;
|
|
@@ -32863,7 +33530,7 @@ var IntelligenceService = class {
|
|
|
32863
33530
|
*/
|
|
32864
33531
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
32865
33532
|
const covered = new Set(coveredLocationNames);
|
|
32866
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
33533
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and25(eq35(insights.projectId, projectId), eq35(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
32867
33534
|
const staleIds = [];
|
|
32868
33535
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
32869
33536
|
for (const row of existing) {
|
|
@@ -32874,7 +33541,7 @@ var IntelligenceService = class {
|
|
|
32874
33541
|
}
|
|
32875
33542
|
this.db.transaction((tx) => {
|
|
32876
33543
|
for (const id of staleIds) {
|
|
32877
|
-
tx.delete(insights).where(
|
|
33544
|
+
tx.delete(insights).where(eq35(insights.id, id)).run();
|
|
32878
33545
|
}
|
|
32879
33546
|
for (const insight of gbpInsights) {
|
|
32880
33547
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -32952,7 +33619,7 @@ var IntelligenceService = class {
|
|
|
32952
33619
|
* create per run + aggregate). DB is left untouched.
|
|
32953
33620
|
*/
|
|
32954
33621
|
backfill(projectName, opts, onProgress) {
|
|
32955
|
-
const project = this.db.select().from(projects).where(
|
|
33622
|
+
const project = this.db.select().from(projects).where(eq35(projects.name, projectName)).get();
|
|
32956
33623
|
if (!project) {
|
|
32957
33624
|
throw new Error(`Project "${projectName}" not found`);
|
|
32958
33625
|
}
|
|
@@ -32965,13 +33632,13 @@ var IntelligenceService = class {
|
|
|
32965
33632
|
sinceTimestamp = parsed;
|
|
32966
33633
|
}
|
|
32967
33634
|
const allRuns = this.db.select().from(runs).where(
|
|
32968
|
-
|
|
32969
|
-
|
|
32970
|
-
or5(
|
|
33635
|
+
and25(
|
|
33636
|
+
eq35(runs.projectId, project.id),
|
|
33637
|
+
or5(eq35(runs.status, "completed"), eq35(runs.status, "partial")),
|
|
32971
33638
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
32972
33639
|
ne5(runs.trigger, RunTriggers.probe)
|
|
32973
33640
|
)
|
|
32974
|
-
).orderBy(
|
|
33641
|
+
).orderBy(asc5(runs.finishedAt)).all();
|
|
32975
33642
|
let startIdx = 0;
|
|
32976
33643
|
let endIdx = allRuns.length;
|
|
32977
33644
|
if (opts?.fromRunId) {
|
|
@@ -33000,7 +33667,7 @@ var IntelligenceService = class {
|
|
|
33000
33667
|
let wouldDeleteTotal = 0;
|
|
33001
33668
|
const existingByRunId = /* @__PURE__ */ new Map();
|
|
33002
33669
|
if (isDryRun && targetRuns.length > 0) {
|
|
33003
|
-
const rows = this.db.select({ runId: insights.runId }).from(insights).where(
|
|
33670
|
+
const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray13(insights.runId, targetRuns.map((r) => r.id))).all();
|
|
33004
33671
|
for (const r of rows) {
|
|
33005
33672
|
if (r.runId == null) continue;
|
|
33006
33673
|
existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
|
|
@@ -33046,7 +33713,7 @@ var IntelligenceService = class {
|
|
|
33046
33713
|
return { processed, skipped, totalInsights };
|
|
33047
33714
|
}
|
|
33048
33715
|
loadTrackedCompetitors(projectId) {
|
|
33049
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
33716
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq35(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
33050
33717
|
}
|
|
33051
33718
|
/**
|
|
33052
33719
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -33067,15 +33734,15 @@ var IntelligenceService = class {
|
|
|
33067
33734
|
}
|
|
33068
33735
|
persistResult(result, runId, projectId) {
|
|
33069
33736
|
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(
|
|
33737
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq35(insights.runId, runId)).all();
|
|
33071
33738
|
for (const row of existingInsights) {
|
|
33072
33739
|
if (row.dismissed) {
|
|
33073
33740
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
33074
33741
|
}
|
|
33075
33742
|
}
|
|
33076
33743
|
this.db.transaction((tx) => {
|
|
33077
|
-
tx.delete(insights).where(
|
|
33078
|
-
tx.delete(healthSnapshots).where(
|
|
33744
|
+
tx.delete(insights).where(eq35(insights.runId, runId)).run();
|
|
33745
|
+
tx.delete(healthSnapshots).where(eq35(healthSnapshots.runId, runId)).run();
|
|
33079
33746
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33080
33747
|
for (const insight of result.insights) {
|
|
33081
33748
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -33095,7 +33762,7 @@ var IntelligenceService = class {
|
|
|
33095
33762
|
}).run();
|
|
33096
33763
|
}
|
|
33097
33764
|
tx.insert(healthSnapshots).values({
|
|
33098
|
-
id:
|
|
33765
|
+
id: crypto29.randomUUID(),
|
|
33099
33766
|
projectId,
|
|
33100
33767
|
runId,
|
|
33101
33768
|
overallCitedRate: String(result.health.overallCitedRate),
|
|
@@ -33126,24 +33793,24 @@ var IntelligenceService = class {
|
|
|
33126
33793
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
33127
33794
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
33128
33795
|
if (regressions.length === 0) return rawInsights;
|
|
33129
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
33796
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq35(gscSearchData.projectId, projectId)).all();
|
|
33130
33797
|
const gscConnected = gscRows.length > 0;
|
|
33131
33798
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
33132
33799
|
for (const row of gscRows) {
|
|
33133
33800
|
const key = row.query.toLowerCase();
|
|
33134
33801
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
33135
33802
|
}
|
|
33136
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
33803
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq35(projects.id, projectId)).get();
|
|
33137
33804
|
const locationCount = Math.max(
|
|
33138
33805
|
1,
|
|
33139
33806
|
(projectRow?.locations ?? []).length
|
|
33140
33807
|
);
|
|
33141
33808
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
33142
33809
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
33143
|
-
|
|
33144
|
-
|
|
33145
|
-
|
|
33146
|
-
or5(
|
|
33810
|
+
and25(
|
|
33811
|
+
eq35(runs.projectId, projectId),
|
|
33812
|
+
eq35(runs.kind, RunKinds["answer-visibility"]),
|
|
33813
|
+
or5(eq35(runs.status, "completed"), eq35(runs.status, "partial")),
|
|
33147
33814
|
// Defensive — see top of file.
|
|
33148
33815
|
ne5(runs.trigger, RunTriggers.probe)
|
|
33149
33816
|
)
|
|
@@ -33163,7 +33830,7 @@ var IntelligenceService = class {
|
|
|
33163
33830
|
const haveHistory = recentRunIds.length > 0;
|
|
33164
33831
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
33165
33832
|
if (haveHistory) {
|
|
33166
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
33833
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and25(eq35(insights.type, "regression"), inArray13(insights.runId, recentRunIds))).all();
|
|
33167
33834
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
33168
33835
|
for (const row of priorRows) {
|
|
33169
33836
|
if (!row.runId) continue;
|
|
@@ -33192,7 +33859,7 @@ var IntelligenceService = class {
|
|
|
33192
33859
|
});
|
|
33193
33860
|
}
|
|
33194
33861
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
33195
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
33862
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq35(projects.id, projectId)).get();
|
|
33196
33863
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
33197
33864
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
33198
33865
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -33208,7 +33875,7 @@ var IntelligenceService = class {
|
|
|
33208
33875
|
citedDomains: querySnapshots.citedDomains,
|
|
33209
33876
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
33210
33877
|
snapshotLocation: querySnapshots.location
|
|
33211
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
33878
|
+
}).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(eq35(querySnapshots.runId, runId)).all();
|
|
33212
33879
|
const snapshots = [];
|
|
33213
33880
|
let orphanCount = 0;
|
|
33214
33881
|
for (const r of rows) {
|
|
@@ -33283,6 +33950,11 @@ export {
|
|
|
33283
33950
|
gbpPlaceActions,
|
|
33284
33951
|
gbpLodgingSnapshots,
|
|
33285
33952
|
gbpPlaceDetails,
|
|
33953
|
+
adsConnections,
|
|
33954
|
+
adsCampaigns,
|
|
33955
|
+
adsAdGroups,
|
|
33956
|
+
adsAds,
|
|
33957
|
+
adsInsightsDaily,
|
|
33286
33958
|
createClient,
|
|
33287
33959
|
parseJsonColumn,
|
|
33288
33960
|
extractLegacyCredentials,
|