@ainyc/canonry 4.77.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-BPZWX7YI.js → chunk-CXIGHPBE.js} +1006 -324
- package/dist/{chunk-FB43IMZT.js → chunk-LCABGFYN.js} +724 -286
- package/dist/cli.js +372 -148
- package/dist/index.d.ts +17 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-C76ZRMF5.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;
|
|
@@ -24383,6 +24924,8 @@ var CloudRunAuthError = class extends Error {
|
|
|
24383
24924
|
this.body = body;
|
|
24384
24925
|
this.name = "CloudRunAuthError";
|
|
24385
24926
|
}
|
|
24927
|
+
httpStatus;
|
|
24928
|
+
body;
|
|
24386
24929
|
};
|
|
24387
24930
|
function createServiceAccountJwt2(clientEmail, privateKey, scope) {
|
|
24388
24931
|
if (!clientEmail) throw new CloudRunAuthError("clientEmail is required");
|
|
@@ -24401,7 +24944,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
|
|
|
24401
24944
|
const headerB64 = encode(header);
|
|
24402
24945
|
const payloadB64 = encode(payload);
|
|
24403
24946
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
24404
|
-
const sign =
|
|
24947
|
+
const sign = crypto23.createSign("RSA-SHA256");
|
|
24405
24948
|
sign.update(signingInput);
|
|
24406
24949
|
const signature = sign.sign(privateKey, "base64url");
|
|
24407
24950
|
return `${signingInput}.${signature}`;
|
|
@@ -24559,6 +25102,8 @@ var CloudRunLoggingApiError = class extends Error {
|
|
|
24559
25102
|
this.body = body;
|
|
24560
25103
|
this.name = "CloudRunLoggingApiError";
|
|
24561
25104
|
}
|
|
25105
|
+
status;
|
|
25106
|
+
body;
|
|
24562
25107
|
};
|
|
24563
25108
|
function validateAccessToken3(accessToken) {
|
|
24564
25109
|
if (!accessToken.trim()) {
|
|
@@ -27613,6 +28158,8 @@ var WordpressTrafficApiError = class extends Error {
|
|
|
27613
28158
|
this.body = body;
|
|
27614
28159
|
this.name = "WordpressTrafficApiError";
|
|
27615
28160
|
}
|
|
28161
|
+
status;
|
|
28162
|
+
body;
|
|
27616
28163
|
};
|
|
27617
28164
|
function trimRequired(name, value) {
|
|
27618
28165
|
const trimmed = value.trim();
|
|
@@ -27814,6 +28361,9 @@ var VercelLogsApiError = class extends Error {
|
|
|
27814
28361
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
27815
28362
|
this.name = "VercelLogsApiError";
|
|
27816
28363
|
}
|
|
28364
|
+
status;
|
|
28365
|
+
body;
|
|
28366
|
+
retryAfterSeconds;
|
|
27817
28367
|
};
|
|
27818
28368
|
function parseRetryAfter2(headerValue) {
|
|
27819
28369
|
if (!headerValue) return void 0;
|
|
@@ -28177,8 +28727,8 @@ async function runBackfillTask(options) {
|
|
|
28177
28727
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28178
28728
|
try {
|
|
28179
28729
|
app.db.transaction((tx) => {
|
|
28180
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
28181
|
-
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();
|
|
28182
28732
|
});
|
|
28183
28733
|
} catch {
|
|
28184
28734
|
}
|
|
@@ -28193,7 +28743,7 @@ async function runBackfillTask(options) {
|
|
|
28193
28743
|
if (allEvents.length === 0) {
|
|
28194
28744
|
const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
28195
28745
|
try {
|
|
28196
|
-
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();
|
|
28197
28747
|
} catch {
|
|
28198
28748
|
}
|
|
28199
28749
|
return;
|
|
@@ -28215,31 +28765,31 @@ async function runBackfillTask(options) {
|
|
|
28215
28765
|
try {
|
|
28216
28766
|
app.db.transaction((tx) => {
|
|
28217
28767
|
tx.delete(crawlerEventsHourly).where(
|
|
28218
|
-
|
|
28219
|
-
|
|
28220
|
-
|
|
28221
|
-
|
|
28768
|
+
and20(
|
|
28769
|
+
eq26(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
28770
|
+
gte4(crawlerEventsHourly.tsHour, windowStartIso),
|
|
28771
|
+
lte3(crawlerEventsHourly.tsHour, windowEndIso)
|
|
28222
28772
|
)
|
|
28223
28773
|
).run();
|
|
28224
28774
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
28225
|
-
|
|
28226
|
-
|
|
28227
|
-
|
|
28228
|
-
|
|
28775
|
+
and20(
|
|
28776
|
+
eq26(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
28777
|
+
gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
28778
|
+
lte3(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
28229
28779
|
)
|
|
28230
28780
|
).run();
|
|
28231
28781
|
tx.delete(aiReferralEventsHourly).where(
|
|
28232
|
-
|
|
28233
|
-
|
|
28234
|
-
|
|
28235
|
-
|
|
28782
|
+
and20(
|
|
28783
|
+
eq26(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
28784
|
+
gte4(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
28785
|
+
lte3(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
28236
28786
|
)
|
|
28237
28787
|
).run();
|
|
28238
28788
|
tx.delete(rawEventSamples).where(
|
|
28239
|
-
|
|
28240
|
-
|
|
28241
|
-
|
|
28242
|
-
|
|
28789
|
+
and20(
|
|
28790
|
+
eq26(rawEventSamples.sourceId, sourceRow.id),
|
|
28791
|
+
gte4(rawEventSamples.ts, windowStartIso),
|
|
28792
|
+
lte3(rawEventSamples.ts, windowEndIso)
|
|
28243
28793
|
)
|
|
28244
28794
|
).run();
|
|
28245
28795
|
for (const bucket of report.crawlerEventsHourly) {
|
|
@@ -28302,7 +28852,7 @@ async function runBackfillTask(options) {
|
|
|
28302
28852
|
}
|
|
28303
28853
|
})();
|
|
28304
28854
|
tx.insert(rawEventSamples).values({
|
|
28305
|
-
id:
|
|
28855
|
+
id: crypto24.randomUUID(),
|
|
28306
28856
|
projectId: project.id,
|
|
28307
28857
|
sourceId: sourceRow.id,
|
|
28308
28858
|
ts: sample.observedAt,
|
|
@@ -28326,8 +28876,8 @@ async function runBackfillTask(options) {
|
|
|
28326
28876
|
lastError: null,
|
|
28327
28877
|
lastEventIds: newRingBuffer,
|
|
28328
28878
|
updatedAt: finishedAt
|
|
28329
|
-
}).where(
|
|
28330
|
-
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();
|
|
28331
28881
|
});
|
|
28332
28882
|
} catch (e) {
|
|
28333
28883
|
markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -28409,7 +28959,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28409
28959
|
createdAt: existing?.createdAt ?? now,
|
|
28410
28960
|
updatedAt: now
|
|
28411
28961
|
});
|
|
28412
|
-
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);
|
|
28413
28963
|
const config = {
|
|
28414
28964
|
gcpProjectId,
|
|
28415
28965
|
serviceName: serviceName ?? null,
|
|
@@ -28425,10 +28975,10 @@ async function trafficRoutes(app, opts) {
|
|
|
28425
28975
|
lastError: null,
|
|
28426
28976
|
configJson: config,
|
|
28427
28977
|
updatedAt: now
|
|
28428
|
-
}).where(
|
|
28429
|
-
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();
|
|
28430
28980
|
} else {
|
|
28431
|
-
const newId =
|
|
28981
|
+
const newId = crypto24.randomUUID();
|
|
28432
28982
|
app.db.insert(trafficSources).values({
|
|
28433
28983
|
id: newId,
|
|
28434
28984
|
projectId: project.id,
|
|
@@ -28443,7 +28993,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28443
28993
|
createdAt: now,
|
|
28444
28994
|
updatedAt: now
|
|
28445
28995
|
}).run();
|
|
28446
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
28996
|
+
sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, newId)).get();
|
|
28447
28997
|
}
|
|
28448
28998
|
writeAuditLog(app.db, {
|
|
28449
28999
|
projectId: project.id,
|
|
@@ -28495,7 +29045,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28495
29045
|
createdAt: existing?.createdAt ?? now,
|
|
28496
29046
|
updatedAt: now
|
|
28497
29047
|
});
|
|
28498
|
-
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);
|
|
28499
29049
|
const config = { baseUrl, username };
|
|
28500
29050
|
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
28501
29051
|
let sourceRow;
|
|
@@ -28506,10 +29056,10 @@ async function trafficRoutes(app, opts) {
|
|
|
28506
29056
|
lastError: null,
|
|
28507
29057
|
configJson: config,
|
|
28508
29058
|
updatedAt: now
|
|
28509
|
-
}).where(
|
|
28510
|
-
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();
|
|
28511
29061
|
} else {
|
|
28512
|
-
const newId =
|
|
29062
|
+
const newId = crypto24.randomUUID();
|
|
28513
29063
|
app.db.insert(trafficSources).values({
|
|
28514
29064
|
id: newId,
|
|
28515
29065
|
projectId: project.id,
|
|
@@ -28524,7 +29074,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28524
29074
|
createdAt: now,
|
|
28525
29075
|
updatedAt: now
|
|
28526
29076
|
}).run();
|
|
28527
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29077
|
+
sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, newId)).get();
|
|
28528
29078
|
}
|
|
28529
29079
|
writeAuditLog(app.db, {
|
|
28530
29080
|
projectId: project.id,
|
|
@@ -28578,7 +29128,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28578
29128
|
createdAt: existing?.createdAt ?? now,
|
|
28579
29129
|
updatedAt: now
|
|
28580
29130
|
});
|
|
28581
|
-
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);
|
|
28582
29132
|
const config = { projectId, teamId, environment };
|
|
28583
29133
|
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
28584
29134
|
const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
|
|
@@ -28590,10 +29140,10 @@ async function trafficRoutes(app, opts) {
|
|
|
28590
29140
|
lastError: null,
|
|
28591
29141
|
configJson: config,
|
|
28592
29142
|
updatedAt: now
|
|
28593
|
-
}).where(
|
|
28594
|
-
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();
|
|
28595
29145
|
} else {
|
|
28596
|
-
const newId =
|
|
29146
|
+
const newId = crypto24.randomUUID();
|
|
28597
29147
|
tx.insert(trafficSources).values({
|
|
28598
29148
|
id: newId,
|
|
28599
29149
|
projectId: project.id,
|
|
@@ -28616,18 +29166,18 @@ async function trafficRoutes(app, opts) {
|
|
|
28616
29166
|
createdAt: now,
|
|
28617
29167
|
updatedAt: now
|
|
28618
29168
|
}).run();
|
|
28619
|
-
row = tx.select().from(trafficSources).where(
|
|
29169
|
+
row = tx.select().from(trafficSources).where(eq26(trafficSources.id, newId)).get();
|
|
28620
29170
|
}
|
|
28621
29171
|
const existingSchedule = tx.select().from(schedules).where(
|
|
28622
|
-
|
|
28623
|
-
|
|
28624
|
-
|
|
29172
|
+
and20(
|
|
29173
|
+
eq26(schedules.projectId, project.id),
|
|
29174
|
+
eq26(schedules.kind, SchedulableRunKinds["traffic-sync"])
|
|
28625
29175
|
)
|
|
28626
29176
|
).get();
|
|
28627
29177
|
let created = false;
|
|
28628
29178
|
if (!existingSchedule) {
|
|
28629
29179
|
tx.insert(schedules).values({
|
|
28630
|
-
id:
|
|
29180
|
+
id: crypto24.randomUUID(),
|
|
28631
29181
|
projectId: project.id,
|
|
28632
29182
|
kind: SchedulableRunKinds["traffic-sync"],
|
|
28633
29183
|
cronExpr: DEFAULT_TRAFFIC_SYNC_CRON,
|
|
@@ -28670,7 +29220,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28670
29220
|
});
|
|
28671
29221
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
28672
29222
|
const project = resolveProject(app.db, request.params.name);
|
|
28673
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
29223
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, request.params.id)).get();
|
|
28674
29224
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
28675
29225
|
throw notFound("Traffic source", request.params.id);
|
|
28676
29226
|
}
|
|
@@ -28682,7 +29232,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28682
29232
|
const windowEnd = /* @__PURE__ */ new Date();
|
|
28683
29233
|
const startedAt = windowEnd.toISOString();
|
|
28684
29234
|
const syncStartedAtMs = windowEnd.getTime();
|
|
28685
|
-
const runId =
|
|
29235
|
+
const runId = crypto24.randomUUID();
|
|
28686
29236
|
app.db.insert(runs).values({
|
|
28687
29237
|
id: runId,
|
|
28688
29238
|
projectId: project.id,
|
|
@@ -28696,8 +29246,8 @@ async function trafficRoutes(app, opts) {
|
|
|
28696
29246
|
const markFailed = (msg, errorCode) => {
|
|
28697
29247
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28698
29248
|
app.db.transaction((tx) => {
|
|
28699
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
28700
|
-
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();
|
|
28701
29251
|
});
|
|
28702
29252
|
try {
|
|
28703
29253
|
opts.onTrafficSynced?.({
|
|
@@ -28778,7 +29328,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28778
29328
|
}
|
|
28779
29329
|
const credential = credentialStore.getConnection(project.name);
|
|
28780
29330
|
if (!credential) {
|
|
28781
|
-
app.db.delete(runs).where(
|
|
29331
|
+
app.db.delete(runs).where(eq26(runs.id, runId)).run();
|
|
28782
29332
|
throw validationError(
|
|
28783
29333
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
28784
29334
|
);
|
|
@@ -28827,12 +29377,12 @@ async function trafficRoutes(app, opts) {
|
|
|
28827
29377
|
auditAction = "traffic.vercel.synced";
|
|
28828
29378
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
28829
29379
|
if (!credentialStore) {
|
|
28830
|
-
app.db.delete(runs).where(
|
|
29380
|
+
app.db.delete(runs).where(eq26(runs.id, runId)).run();
|
|
28831
29381
|
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
28832
29382
|
}
|
|
28833
29383
|
const credential = credentialStore.getConnection(project.name);
|
|
28834
29384
|
if (!credential) {
|
|
28835
|
-
app.db.delete(runs).where(
|
|
29385
|
+
app.db.delete(runs).where(eq26(runs.id, runId)).run();
|
|
28836
29386
|
throw validationError(
|
|
28837
29387
|
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
28838
29388
|
);
|
|
@@ -28932,7 +29482,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28932
29482
|
let aiReferralHitsCount = 0;
|
|
28933
29483
|
let unknownHitsCount = 0;
|
|
28934
29484
|
app.db.transaction((tx) => {
|
|
28935
|
-
const latestRow = tx.select().from(trafficSources).where(
|
|
29485
|
+
const latestRow = tx.select().from(trafficSources).where(eq26(trafficSources.id, sourceRow.id)).get();
|
|
28936
29486
|
const previousIds = latestRow.lastEventIds ?? [];
|
|
28937
29487
|
const seenEventIds = new Set(previousIds);
|
|
28938
29488
|
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
@@ -29065,7 +29615,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29065
29615
|
}
|
|
29066
29616
|
})();
|
|
29067
29617
|
tx.insert(rawEventSamples).values({
|
|
29068
|
-
id:
|
|
29618
|
+
id: crypto24.randomUUID(),
|
|
29069
29619
|
projectId: project.id,
|
|
29070
29620
|
sourceId: sourceRow.id,
|
|
29071
29621
|
ts: sample.observedAt,
|
|
@@ -29100,8 +29650,8 @@ async function trafficRoutes(app, opts) {
|
|
|
29100
29650
|
if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
29101
29651
|
sourceUpdate.lastCursor = nextCursor ?? null;
|
|
29102
29652
|
}
|
|
29103
|
-
tx.update(trafficSources).set(sourceUpdate).where(
|
|
29104
|
-
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();
|
|
29105
29655
|
});
|
|
29106
29656
|
writeAuditLog(app.db, {
|
|
29107
29657
|
projectId: project.id,
|
|
@@ -29153,7 +29703,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29153
29703
|
});
|
|
29154
29704
|
app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
|
|
29155
29705
|
const project = resolveProject(app.db, request.params.name);
|
|
29156
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
29706
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, request.params.id)).get();
|
|
29157
29707
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
29158
29708
|
throw notFound("Traffic source", request.params.id);
|
|
29159
29709
|
}
|
|
@@ -29303,7 +29853,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29303
29853
|
};
|
|
29304
29854
|
}
|
|
29305
29855
|
const startedAt = windowEnd.toISOString();
|
|
29306
|
-
const runId =
|
|
29856
|
+
const runId = crypto24.randomUUID();
|
|
29307
29857
|
app.db.insert(runs).values({
|
|
29308
29858
|
id: runId,
|
|
29309
29859
|
projectId: project.id,
|
|
@@ -29338,34 +29888,34 @@ async function trafficRoutes(app, opts) {
|
|
|
29338
29888
|
});
|
|
29339
29889
|
function buildSourceDetail(projectId, row, since) {
|
|
29340
29890
|
const crawlerTotals = app.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
29341
|
-
|
|
29342
|
-
|
|
29343
|
-
|
|
29891
|
+
and20(
|
|
29892
|
+
eq26(crawlerEventsHourly.sourceId, row.id),
|
|
29893
|
+
gte4(crawlerEventsHourly.tsHour, since)
|
|
29344
29894
|
)
|
|
29345
29895
|
).get();
|
|
29346
29896
|
const aiUserFetchTotals = app.db.select({ total: sql11`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
29347
|
-
|
|
29348
|
-
|
|
29349
|
-
|
|
29897
|
+
and20(
|
|
29898
|
+
eq26(aiUserFetchEventsHourly.sourceId, row.id),
|
|
29899
|
+
gte4(aiUserFetchEventsHourly.tsHour, since)
|
|
29350
29900
|
)
|
|
29351
29901
|
).get();
|
|
29352
29902
|
const aiTotals = app.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
29353
|
-
|
|
29354
|
-
|
|
29355
|
-
|
|
29903
|
+
and20(
|
|
29904
|
+
eq26(aiReferralEventsHourly.sourceId, row.id),
|
|
29905
|
+
gte4(aiReferralEventsHourly.tsHour, since)
|
|
29356
29906
|
)
|
|
29357
29907
|
).get();
|
|
29358
29908
|
const sampleTotals = app.db.select({ total: sql11`COUNT(*)` }).from(rawEventSamples).where(
|
|
29359
|
-
|
|
29360
|
-
|
|
29361
|
-
|
|
29909
|
+
and20(
|
|
29910
|
+
eq26(rawEventSamples.sourceId, row.id),
|
|
29911
|
+
gte4(rawEventSamples.ts, since)
|
|
29362
29912
|
)
|
|
29363
29913
|
).get();
|
|
29364
29914
|
const latestRun = app.db.select().from(runs).where(
|
|
29365
|
-
|
|
29366
|
-
|
|
29367
|
-
|
|
29368
|
-
|
|
29915
|
+
and20(
|
|
29916
|
+
eq26(runs.projectId, projectId),
|
|
29917
|
+
eq26(runs.kind, RunKinds["traffic-sync"]),
|
|
29918
|
+
eq26(runs.sourceId, row.id)
|
|
29369
29919
|
)
|
|
29370
29920
|
).orderBy(desc14(runs.startedAt)).limit(1).get();
|
|
29371
29921
|
return {
|
|
@@ -29393,7 +29943,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29393
29943
|
"`advanceToNow` must be `true`. There is no implicit reset."
|
|
29394
29944
|
);
|
|
29395
29945
|
}
|
|
29396
|
-
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();
|
|
29397
29947
|
if (!sourceRow) {
|
|
29398
29948
|
throw notFound("traffic source", request.params.id);
|
|
29399
29949
|
}
|
|
@@ -29410,7 +29960,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29410
29960
|
status: TrafficSourceStatuses.connected,
|
|
29411
29961
|
lastError: null,
|
|
29412
29962
|
updatedAt: now
|
|
29413
|
-
}).where(
|
|
29963
|
+
}).where(eq26(trafficSources.id, sourceRow.id)).run();
|
|
29414
29964
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
29415
29965
|
projectId: project.id,
|
|
29416
29966
|
actor: "api",
|
|
@@ -29418,20 +29968,20 @@ async function trafficRoutes(app, opts) {
|
|
|
29418
29968
|
entityType: "traffic_source",
|
|
29419
29969
|
entityId: sourceRow.id
|
|
29420
29970
|
}));
|
|
29421
|
-
updatedRow = tx.select().from(trafficSources).where(
|
|
29971
|
+
updatedRow = tx.select().from(trafficSources).where(eq26(trafficSources.id, sourceRow.id)).get();
|
|
29422
29972
|
});
|
|
29423
29973
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
29424
29974
|
});
|
|
29425
29975
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
29426
29976
|
const project = resolveProject(app.db, request.params.name);
|
|
29427
|
-
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();
|
|
29428
29978
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
29429
29979
|
const response = { sources };
|
|
29430
29980
|
return response;
|
|
29431
29981
|
});
|
|
29432
29982
|
app.get("/projects/:name/traffic/status", async (request) => {
|
|
29433
29983
|
const project = resolveProject(app.db, request.params.name);
|
|
29434
|
-
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();
|
|
29435
29985
|
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
29436
29986
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
29437
29987
|
const response = { sources };
|
|
@@ -29441,7 +29991,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29441
29991
|
"/projects/:name/traffic/sources/:id",
|
|
29442
29992
|
async (request) => {
|
|
29443
29993
|
const project = resolveProject(app.db, request.params.name);
|
|
29444
|
-
const row = app.db.select().from(trafficSources).where(
|
|
29994
|
+
const row = app.db.select().from(trafficSources).where(eq26(trafficSources.id, request.params.id)).get();
|
|
29445
29995
|
if (!row || row.projectId !== project.id) {
|
|
29446
29996
|
throw notFound("Traffic source", request.params.id);
|
|
29447
29997
|
}
|
|
@@ -29492,12 +30042,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29492
30042
|
let aiReferralTotal = 0;
|
|
29493
30043
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
29494
30044
|
const crawlerFilters = [
|
|
29495
|
-
|
|
29496
|
-
|
|
29497
|
-
|
|
30045
|
+
eq26(crawlerEventsHourly.projectId, project.id),
|
|
30046
|
+
gte4(crawlerEventsHourly.tsHour, sinceIso),
|
|
30047
|
+
lte3(crawlerEventsHourly.tsHour, untilIso)
|
|
29498
30048
|
];
|
|
29499
|
-
if (sourceIdParam) crawlerFilters.push(
|
|
29500
|
-
const crawlerWhere =
|
|
30049
|
+
if (sourceIdParam) crawlerFilters.push(eq26(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
30050
|
+
const crawlerWhere = and20(...crawlerFilters);
|
|
29501
30051
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
|
|
29502
30052
|
crawlerTotal = Number(total?.total ?? 0);
|
|
29503
30053
|
const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc14(crawlerEventsHourly.tsHour)).limit(limit).all();
|
|
@@ -29517,12 +30067,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29517
30067
|
}
|
|
29518
30068
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
29519
30069
|
const userFetchFilters = [
|
|
29520
|
-
|
|
29521
|
-
|
|
29522
|
-
|
|
30070
|
+
eq26(aiUserFetchEventsHourly.projectId, project.id),
|
|
30071
|
+
gte4(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
30072
|
+
lte3(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
29523
30073
|
];
|
|
29524
|
-
if (sourceIdParam) userFetchFilters.push(
|
|
29525
|
-
const userFetchWhere =
|
|
30074
|
+
if (sourceIdParam) userFetchFilters.push(eq26(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
30075
|
+
const userFetchWhere = and20(...userFetchFilters);
|
|
29526
30076
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
29527
30077
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
29528
30078
|
const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(desc14(aiUserFetchEventsHourly.tsHour)).limit(limit).all();
|
|
@@ -29542,12 +30092,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29542
30092
|
}
|
|
29543
30093
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
29544
30094
|
const aiFilters = [
|
|
29545
|
-
|
|
29546
|
-
|
|
29547
|
-
|
|
30095
|
+
eq26(aiReferralEventsHourly.projectId, project.id),
|
|
30096
|
+
gte4(aiReferralEventsHourly.tsHour, sinceIso),
|
|
30097
|
+
lte3(aiReferralEventsHourly.tsHour, untilIso)
|
|
29548
30098
|
];
|
|
29549
|
-
if (sourceIdParam) aiFilters.push(
|
|
29550
|
-
const aiWhere =
|
|
30099
|
+
if (sourceIdParam) aiFilters.push(eq26(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
30100
|
+
const aiWhere = and20(...aiFilters);
|
|
29551
30101
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
29552
30102
|
aiReferralTotal = Number(total?.total ?? 0);
|
|
29553
30103
|
const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc14(aiReferralEventsHourly.tsHour)).limit(limit).all();
|
|
@@ -29583,7 +30133,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29583
30133
|
}
|
|
29584
30134
|
|
|
29585
30135
|
// ../api-routes/src/doctor/checks/agent.ts
|
|
29586
|
-
import
|
|
30136
|
+
import crypto25 from "crypto";
|
|
29587
30137
|
import fs6 from "fs";
|
|
29588
30138
|
import path7 from "path";
|
|
29589
30139
|
var REQUIRED_SKILLS = ["canonry", "aero"];
|
|
@@ -29736,7 +30286,7 @@ function isInstalled(dir) {
|
|
|
29736
30286
|
}
|
|
29737
30287
|
function hashInstalledFile(filePath) {
|
|
29738
30288
|
try {
|
|
29739
|
-
return
|
|
30289
|
+
return crypto25.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
|
|
29740
30290
|
} catch {
|
|
29741
30291
|
return void 0;
|
|
29742
30292
|
}
|
|
@@ -29897,7 +30447,7 @@ var BING_AUTH_CHECKS = [
|
|
|
29897
30447
|
];
|
|
29898
30448
|
|
|
29899
30449
|
// ../api-routes/src/doctor/checks/content.ts
|
|
29900
|
-
import { eq as
|
|
30450
|
+
import { eq as eq27 } from "drizzle-orm";
|
|
29901
30451
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
29902
30452
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
29903
30453
|
function skippedNoProject() {
|
|
@@ -29910,7 +30460,7 @@ function skippedNoProject() {
|
|
|
29910
30460
|
}
|
|
29911
30461
|
function loadProject(ctx) {
|
|
29912
30462
|
if (!ctx.project) return null;
|
|
29913
|
-
return ctx.db.select().from(projects).where(
|
|
30463
|
+
return ctx.db.select().from(projects).where(eq27(projects.id, ctx.project.id)).get() ?? null;
|
|
29914
30464
|
}
|
|
29915
30465
|
function percent(value) {
|
|
29916
30466
|
return Math.round(value * 100);
|
|
@@ -30001,6 +30551,123 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
30001
30551
|
CONTENT_CHECKS.map((check) => [check.id, check])
|
|
30002
30552
|
);
|
|
30003
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
|
+
|
|
30004
30671
|
// ../api-routes/src/doctor/checks/ga-auth.ts
|
|
30005
30672
|
async function checkServiceAccount(conn) {
|
|
30006
30673
|
if (!conn.propertyId) {
|
|
@@ -30143,9 +30810,9 @@ var ga4ConnectionCheck = {
|
|
|
30143
30810
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
30144
30811
|
|
|
30145
30812
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
30146
|
-
import { and as
|
|
30147
|
-
var
|
|
30148
|
-
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;
|
|
30149
30816
|
function skippedNoProject2() {
|
|
30150
30817
|
return {
|
|
30151
30818
|
status: CheckStatuses.skipped,
|
|
@@ -30376,7 +31043,7 @@ var recentSyncCheck = {
|
|
|
30376
31043
|
title: "GBP recent sync",
|
|
30377
31044
|
run: (ctx) => {
|
|
30378
31045
|
if (!ctx.project) return skippedNoProject2();
|
|
30379
|
-
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();
|
|
30380
31047
|
if (selected.length === 0) {
|
|
30381
31048
|
return {
|
|
30382
31049
|
status: CheckStatuses.skipped,
|
|
@@ -30398,20 +31065,20 @@ var recentSyncCheck = {
|
|
|
30398
31065
|
const newest = Math.max(...syncTimes);
|
|
30399
31066
|
const ageDays = (Date.now() - newest) / (1e3 * 60 * 60 * 24);
|
|
30400
31067
|
const details = { selectedLocations: selected.length, newestSyncAgeDays: Math.round(ageDays) };
|
|
30401
|
-
if (ageDays >
|
|
31068
|
+
if (ageDays > RECENT_SYNC_FAIL_DAYS2) {
|
|
30402
31069
|
return {
|
|
30403
31070
|
status: CheckStatuses.fail,
|
|
30404
31071
|
code: "gbp.data.stale",
|
|
30405
|
-
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).`,
|
|
30406
31073
|
remediation: `Run \`canonry gbp sync ${ctx.project.name}\` or set a gbp-sync schedule.`,
|
|
30407
31074
|
details
|
|
30408
31075
|
};
|
|
30409
31076
|
}
|
|
30410
|
-
if (ageDays >
|
|
31077
|
+
if (ageDays > RECENT_SYNC_WARN_DAYS2) {
|
|
30411
31078
|
return {
|
|
30412
31079
|
status: CheckStatuses.warn,
|
|
30413
31080
|
code: "gbp.data.aging",
|
|
30414
|
-
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).`,
|
|
30415
31082
|
remediation: `Run \`canonry gbp sync ${ctx.project.name}\` or set a gbp-sync schedule to keep data fresh.`,
|
|
30416
31083
|
details
|
|
30417
31084
|
};
|
|
@@ -30436,7 +31103,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
30436
31103
|
);
|
|
30437
31104
|
|
|
30438
31105
|
// ../api-routes/src/doctor/checks/places.ts
|
|
30439
|
-
import { eq as
|
|
31106
|
+
import { eq as eq30 } from "drizzle-orm";
|
|
30440
31107
|
var apiKeyCheck = {
|
|
30441
31108
|
id: "gbp.places.api-key",
|
|
30442
31109
|
category: CheckCategories.auth,
|
|
@@ -30481,7 +31148,7 @@ var apiKeyCheck = {
|
|
|
30481
31148
|
details: { tier: cfg.tier }
|
|
30482
31149
|
};
|
|
30483
31150
|
}
|
|
30484
|
-
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();
|
|
30485
31152
|
const selected = rows.filter((r) => r.selected);
|
|
30486
31153
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
30487
31154
|
const details = {
|
|
@@ -30932,7 +31599,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
30932
31599
|
];
|
|
30933
31600
|
|
|
30934
31601
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
30935
|
-
import { and as
|
|
31602
|
+
import { and as and22, eq as eq31, gte as gte5, ne as ne4, sql as sql12 } from "drizzle-orm";
|
|
30936
31603
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
30937
31604
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
30938
31605
|
function skippedNoProject4() {
|
|
@@ -30946,8 +31613,8 @@ function skippedNoProject4() {
|
|
|
30946
31613
|
function loadProbes(ctx) {
|
|
30947
31614
|
if (!ctx.project) return [];
|
|
30948
31615
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
30949
|
-
|
|
30950
|
-
|
|
31616
|
+
and22(
|
|
31617
|
+
eq31(trafficSources.projectId, ctx.project.id),
|
|
30951
31618
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
30952
31619
|
)
|
|
30953
31620
|
).all();
|
|
@@ -31027,17 +31694,17 @@ var recentDataCheck = {
|
|
|
31027
31694
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
31028
31695
|
const recentCrawlers = Number(
|
|
31029
31696
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
31030
|
-
|
|
31031
|
-
|
|
31032
|
-
|
|
31697
|
+
and22(
|
|
31698
|
+
eq31(crawlerEventsHourly.projectId, ctx.project.id),
|
|
31699
|
+
gte5(crawlerEventsHourly.tsHour, warnCutoff)
|
|
31033
31700
|
)
|
|
31034
31701
|
).get()?.total ?? 0
|
|
31035
31702
|
);
|
|
31036
31703
|
const recentReferrals = Number(
|
|
31037
31704
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31038
|
-
|
|
31039
|
-
|
|
31040
|
-
|
|
31705
|
+
and22(
|
|
31706
|
+
eq31(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
31707
|
+
gte5(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
31041
31708
|
)
|
|
31042
31709
|
).get()?.total ?? 0
|
|
31043
31710
|
);
|
|
@@ -31051,17 +31718,17 @@ var recentDataCheck = {
|
|
|
31051
31718
|
}
|
|
31052
31719
|
const olderCrawlers = Number(
|
|
31053
31720
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
31054
|
-
|
|
31055
|
-
|
|
31056
|
-
|
|
31721
|
+
and22(
|
|
31722
|
+
eq31(crawlerEventsHourly.projectId, ctx.project.id),
|
|
31723
|
+
gte5(crawlerEventsHourly.tsHour, failCutoff)
|
|
31057
31724
|
)
|
|
31058
31725
|
).get()?.total ?? 0
|
|
31059
31726
|
);
|
|
31060
31727
|
const olderReferrals = Number(
|
|
31061
31728
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31062
|
-
|
|
31063
|
-
|
|
31064
|
-
|
|
31729
|
+
and22(
|
|
31730
|
+
eq31(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
31731
|
+
gte5(aiReferralEventsHourly.tsHour, failCutoff)
|
|
31065
31732
|
)
|
|
31066
31733
|
).get()?.total ?? 0
|
|
31067
31734
|
);
|
|
@@ -31345,6 +32012,7 @@ var ALL_CHECKS = [
|
|
|
31345
32012
|
...BING_AUTH_CHECKS,
|
|
31346
32013
|
...WORDPRESS_PUBLISH_CHECKS,
|
|
31347
32014
|
...GA_AUTH_CHECKS,
|
|
32015
|
+
...ADS_CHECKS,
|
|
31348
32016
|
...PROVIDERS_CHECKS,
|
|
31349
32017
|
...TRAFFIC_SOURCE_CHECKS,
|
|
31350
32018
|
...CONTENT_CHECKS,
|
|
@@ -31431,6 +32099,7 @@ async function doctorRoutes(app, opts) {
|
|
|
31431
32099
|
bingConnectionStore: opts.bingConnectionStore,
|
|
31432
32100
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
31433
32101
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
32102
|
+
adsCredentialStore: opts.adsCredentialStore,
|
|
31434
32103
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
31435
32104
|
getPlacesConfig: opts.getPlacesConfig,
|
|
31436
32105
|
redirectUri,
|
|
@@ -31456,6 +32125,7 @@ async function doctorRoutes(app, opts) {
|
|
|
31456
32125
|
bingConnectionStore: opts.bingConnectionStore,
|
|
31457
32126
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
31458
32127
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
32128
|
+
adsCredentialStore: opts.adsCredentialStore,
|
|
31459
32129
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
31460
32130
|
getPlacesConfig: opts.getPlacesConfig,
|
|
31461
32131
|
redirectUri,
|
|
@@ -31469,8 +32139,8 @@ async function doctorRoutes(app, opts) {
|
|
|
31469
32139
|
}
|
|
31470
32140
|
|
|
31471
32141
|
// ../api-routes/src/discovery/routes.ts
|
|
31472
|
-
import
|
|
31473
|
-
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";
|
|
31474
32144
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
31475
32145
|
async function discoveryRoutes(app, opts) {
|
|
31476
32146
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -31502,21 +32172,21 @@ async function discoveryRoutes(app, opts) {
|
|
|
31502
32172
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
31503
32173
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
31504
32174
|
const decision = app.db.transaction((tx) => {
|
|
31505
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
31506
|
-
|
|
31507
|
-
|
|
31508
|
-
|
|
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, [
|
|
31509
32179
|
DiscoverySessionStatuses.queued,
|
|
31510
32180
|
DiscoverySessionStatuses.seeding,
|
|
31511
32181
|
DiscoverySessionStatuses.probing
|
|
31512
32182
|
]),
|
|
31513
|
-
|
|
32183
|
+
gte6(discoverySessions.createdAt, ageFloorIso)
|
|
31514
32184
|
)).orderBy(desc15(discoverySessions.createdAt)).get();
|
|
31515
32185
|
if (existing && existing.runId) {
|
|
31516
32186
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
31517
32187
|
}
|
|
31518
|
-
const sessionId =
|
|
31519
|
-
const runId =
|
|
32188
|
+
const sessionId = crypto26.randomUUID();
|
|
32189
|
+
const runId = crypto26.randomUUID();
|
|
31520
32190
|
tx.insert(discoverySessions).values({
|
|
31521
32191
|
id: sessionId,
|
|
31522
32192
|
projectId: project.id,
|
|
@@ -31574,7 +32244,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31574
32244
|
const project = resolveProject(app.db, request.params.name);
|
|
31575
32245
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
31576
32246
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
31577
|
-
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();
|
|
31578
32248
|
return reply.send(rows.map(serializeSession));
|
|
31579
32249
|
}
|
|
31580
32250
|
);
|
|
@@ -31582,11 +32252,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
31582
32252
|
"/projects/:name/discover/sessions/:id",
|
|
31583
32253
|
async (request, reply) => {
|
|
31584
32254
|
const project = resolveProject(app.db, request.params.name);
|
|
31585
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32255
|
+
const session = app.db.select().from(discoverySessions).where(eq32(discoverySessions.id, request.params.id)).get();
|
|
31586
32256
|
if (!session || session.projectId !== project.id) {
|
|
31587
32257
|
throw notFound("Discovery session", request.params.id);
|
|
31588
32258
|
}
|
|
31589
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32259
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq32(discoveryProbes.sessionId, session.id)).all();
|
|
31590
32260
|
const detail = {
|
|
31591
32261
|
...serializeSession(session),
|
|
31592
32262
|
probes: probeRows.map(serializeProbe)
|
|
@@ -31598,12 +32268,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
31598
32268
|
"/projects/:name/discover/sessions/:id/promote",
|
|
31599
32269
|
async (request, reply) => {
|
|
31600
32270
|
const project = resolveProject(app.db, request.params.name);
|
|
31601
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32271
|
+
const session = app.db.select().from(discoverySessions).where(eq32(discoverySessions.id, request.params.id)).get();
|
|
31602
32272
|
if (!session || session.projectId !== project.id) {
|
|
31603
32273
|
throw notFound("Discovery session", request.params.id);
|
|
31604
32274
|
}
|
|
31605
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
31606
|
-
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());
|
|
31607
32277
|
const seenCompetitors = new Set(existingCompetitors);
|
|
31608
32278
|
const cited = /* @__PURE__ */ new Set();
|
|
31609
32279
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -31632,7 +32302,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31632
32302
|
);
|
|
31633
32303
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
31634
32304
|
const project = resolveProject(app.db, request.params.name);
|
|
31635
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32305
|
+
const session = app.db.select().from(discoverySessions).where(eq32(discoverySessions.id, request.params.id)).get();
|
|
31636
32306
|
if (!session || session.projectId !== project.id) {
|
|
31637
32307
|
throw notFound("Discovery session", request.params.id);
|
|
31638
32308
|
}
|
|
@@ -31655,7 +32325,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31655
32325
|
const bucketSet = new Set(buckets);
|
|
31656
32326
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
31657
32327
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
31658
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32328
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq32(discoveryProbes.sessionId, session.id)).all();
|
|
31659
32329
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
31660
32330
|
for (const probe of probeRows) {
|
|
31661
32331
|
if (!probe.bucket) continue;
|
|
@@ -31663,7 +32333,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31663
32333
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
31664
32334
|
}
|
|
31665
32335
|
const existingQueries = new Set(
|
|
31666
|
-
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())
|
|
31667
32337
|
);
|
|
31668
32338
|
const promotedQueries = [];
|
|
31669
32339
|
const skippedQueries = [];
|
|
@@ -31679,7 +32349,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31679
32349
|
const skippedCompetitors = [];
|
|
31680
32350
|
if (includeCompetitors) {
|
|
31681
32351
|
const existingCompetitors = new Set(
|
|
31682
|
-
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())
|
|
31683
32353
|
);
|
|
31684
32354
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
31685
32355
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -31698,7 +32368,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31698
32368
|
app.db.transaction((tx) => {
|
|
31699
32369
|
for (const query of promotedQueries) {
|
|
31700
32370
|
tx.insert(queries).values({
|
|
31701
|
-
id:
|
|
32371
|
+
id: crypto26.randomUUID(),
|
|
31702
32372
|
projectId: project.id,
|
|
31703
32373
|
query,
|
|
31704
32374
|
provenance,
|
|
@@ -31707,7 +32377,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
31707
32377
|
}
|
|
31708
32378
|
for (const domain of promotedCompetitors) {
|
|
31709
32379
|
tx.insert(competitors).values({
|
|
31710
|
-
id:
|
|
32380
|
+
id: crypto26.randomUUID(),
|
|
31711
32381
|
projectId: project.id,
|
|
31712
32382
|
domain,
|
|
31713
32383
|
provenance,
|
|
@@ -31782,8 +32452,8 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
31782
32452
|
}
|
|
31783
32453
|
|
|
31784
32454
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
31785
|
-
import
|
|
31786
|
-
import { eq as
|
|
32455
|
+
import crypto27 from "crypto";
|
|
32456
|
+
import { eq as eq33 } from "drizzle-orm";
|
|
31787
32457
|
var DEFAULT_MAX_PROBES = 100;
|
|
31788
32458
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
31789
32459
|
function classifyProbeBucket(input) {
|
|
@@ -31837,7 +32507,7 @@ async function executeDiscovery(opts) {
|
|
|
31837
32507
|
status: DiscoverySessionStatuses.seeding,
|
|
31838
32508
|
dedupThreshold,
|
|
31839
32509
|
startedAt
|
|
31840
|
-
}).where(
|
|
32510
|
+
}).where(eq33(discoverySessions.id, opts.sessionId)).run();
|
|
31841
32511
|
const seedResult = await opts.deps.seed({
|
|
31842
32512
|
project: opts.project,
|
|
31843
32513
|
icpDescription: opts.icpDescription,
|
|
@@ -31863,7 +32533,7 @@ async function executeDiscovery(opts) {
|
|
|
31863
32533
|
seedCountRaw,
|
|
31864
32534
|
seedCount,
|
|
31865
32535
|
warning
|
|
31866
|
-
}).where(
|
|
32536
|
+
}).where(eq33(discoverySessions.id, opts.sessionId)).run();
|
|
31867
32537
|
const probeRows = [];
|
|
31868
32538
|
const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
|
|
31869
32539
|
for (const query of probedCanonicals) {
|
|
@@ -31876,7 +32546,7 @@ async function executeDiscovery(opts) {
|
|
|
31876
32546
|
probeRows.push({ citedDomains: probe.citedDomains, bucket });
|
|
31877
32547
|
buckets[bucket]++;
|
|
31878
32548
|
opts.db.insert(discoveryProbes).values({
|
|
31879
|
-
id:
|
|
32549
|
+
id: crypto27.randomUUID(),
|
|
31880
32550
|
sessionId: opts.sessionId,
|
|
31881
32551
|
projectId: opts.project.id,
|
|
31882
32552
|
query,
|
|
@@ -31903,7 +32573,7 @@ async function executeDiscovery(opts) {
|
|
|
31903
32573
|
wastedCount: buckets["wasted-surface"],
|
|
31904
32574
|
competitorMap,
|
|
31905
32575
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
31906
|
-
}).where(
|
|
32576
|
+
}).where(eq33(discoverySessions.id, opts.sessionId)).run();
|
|
31907
32577
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
31908
32578
|
return {
|
|
31909
32579
|
buckets,
|
|
@@ -31920,7 +32590,7 @@ function upsertDomainClassifications(db, projectId, sessionId, competitorMap) {
|
|
|
31920
32590
|
const domain = normalizeDomain(entry.domain);
|
|
31921
32591
|
if (!domain) continue;
|
|
31922
32592
|
db.insert(domainClassifications).values({
|
|
31923
|
-
id:
|
|
32593
|
+
id: crypto27.randomUUID(),
|
|
31924
32594
|
projectId,
|
|
31925
32595
|
domain,
|
|
31926
32596
|
competitorType: entry.competitorType,
|
|
@@ -31943,7 +32613,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
31943
32613
|
status: DiscoverySessionStatuses.failed,
|
|
31944
32614
|
error,
|
|
31945
32615
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
31946
|
-
}).where(
|
|
32616
|
+
}).where(eq33(discoverySessions.id, sessionId)).run();
|
|
31947
32617
|
}
|
|
31948
32618
|
function dedupeStrings(input) {
|
|
31949
32619
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -31960,8 +32630,8 @@ function dedupeStrings(input) {
|
|
|
31960
32630
|
}
|
|
31961
32631
|
|
|
31962
32632
|
// ../api-routes/src/technical-aeo.ts
|
|
31963
|
-
import
|
|
31964
|
-
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";
|
|
31965
32635
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
31966
32636
|
function emptyScore(projectName) {
|
|
31967
32637
|
return {
|
|
@@ -31993,10 +32663,10 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
31993
32663
|
async function technicalAeoRoutes(app, opts) {
|
|
31994
32664
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
31995
32665
|
const project = resolveProject(app.db, request.params.name);
|
|
31996
|
-
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs,
|
|
31997
|
-
|
|
31998
|
-
|
|
31999
|
-
|
|
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),
|
|
32000
32670
|
notProbeRun()
|
|
32001
32671
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
32002
32672
|
const latest = rows[0];
|
|
@@ -32028,24 +32698,24 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32028
32698
|
});
|
|
32029
32699
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
32030
32700
|
const project = resolveProject(app.db, request.params.name);
|
|
32031
|
-
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs,
|
|
32032
|
-
|
|
32033
|
-
|
|
32034
|
-
|
|
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),
|
|
32035
32705
|
notProbeRun()
|
|
32036
32706
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
32037
32707
|
if (!latest) {
|
|
32038
32708
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
32039
32709
|
}
|
|
32040
32710
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
32041
|
-
const conds = [
|
|
32042
|
-
if (statusFilter) conds.push(
|
|
32043
|
-
const where =
|
|
32711
|
+
const conds = [eq34(siteAuditPages.runId, latest.runId)];
|
|
32712
|
+
if (statusFilter) conds.push(eq34(siteAuditPages.status, statusFilter));
|
|
32713
|
+
const where = and24(...conds);
|
|
32044
32714
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
32045
32715
|
const total = totalRow?.value ?? 0;
|
|
32046
32716
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
32047
32717
|
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
32048
|
-
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);
|
|
32049
32719
|
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
32050
32720
|
const pages = rows.map((row) => ({
|
|
32051
32721
|
url: row.url,
|
|
@@ -32064,10 +32734,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32064
32734
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
32065
32735
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
32066
32736
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
32067
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
32068
|
-
|
|
32069
|
-
|
|
32070
|
-
|
|
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),
|
|
32071
32741
|
notProbeRun()
|
|
32072
32742
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
32073
32743
|
return { project: project.name, points: rows.reverse() };
|
|
@@ -32078,16 +32748,16 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32078
32748
|
if (!parsed.success) {
|
|
32079
32749
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
32080
32750
|
}
|
|
32081
|
-
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
32082
|
-
|
|
32083
|
-
|
|
32084
|
-
|
|
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])
|
|
32085
32755
|
)).get();
|
|
32086
32756
|
if (existing) {
|
|
32087
32757
|
return { runId: existing.id, status: existing.status };
|
|
32088
32758
|
}
|
|
32089
32759
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32090
|
-
const runId =
|
|
32760
|
+
const runId = crypto28.randomUUID();
|
|
32091
32761
|
app.db.insert(runs).values({
|
|
32092
32762
|
id: runId,
|
|
32093
32763
|
projectId: project.id,
|
|
@@ -32214,6 +32884,11 @@ async function apiRoutes(app, opts) {
|
|
|
32214
32884
|
getTelemetryStatus: opts.getTelemetryStatus,
|
|
32215
32885
|
setTelemetryEnabled: opts.setTelemetryEnabled
|
|
32216
32886
|
});
|
|
32887
|
+
await api.register(adsRoutes, {
|
|
32888
|
+
adsCredentialStore: opts.adsCredentialStore,
|
|
32889
|
+
verifyAdsAccount: opts.verifyAdsAccount,
|
|
32890
|
+
onAdsSyncRequested: opts.onAdsSyncRequested
|
|
32891
|
+
});
|
|
32217
32892
|
await api.register(bingRoutes, {
|
|
32218
32893
|
bingConnectionStore: opts.bingConnectionStore,
|
|
32219
32894
|
onInspectSitemapRequested: opts.onBingInspectSitemapRequested
|
|
@@ -32277,6 +32952,7 @@ async function apiRoutes(app, opts) {
|
|
|
32277
32952
|
bingConnectionStore: opts.bingConnectionStore,
|
|
32278
32953
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
32279
32954
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
32955
|
+
adsCredentialStore: opts.adsCredentialStore,
|
|
32280
32956
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
32281
32957
|
getPlacesConfig: opts.getPlacesConfig,
|
|
32282
32958
|
publicUrl: opts.publicUrl,
|
|
@@ -32427,7 +33103,7 @@ function buildTrafficSourceValidators(opts) {
|
|
|
32427
33103
|
}
|
|
32428
33104
|
|
|
32429
33105
|
// src/intelligence-service.ts
|
|
32430
|
-
import
|
|
33106
|
+
import crypto29 from "crypto";
|
|
32431
33107
|
|
|
32432
33108
|
// src/logger.ts
|
|
32433
33109
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -32651,6 +33327,7 @@ var IntelligenceService = class {
|
|
|
32651
33327
|
constructor(db) {
|
|
32652
33328
|
this.db = db;
|
|
32653
33329
|
}
|
|
33330
|
+
db;
|
|
32654
33331
|
/**
|
|
32655
33332
|
* Analyze a completed run and persist insights + health snapshot.
|
|
32656
33333
|
* Idempotent: deletes prior results for the same runId before inserting.
|
|
@@ -32658,9 +33335,9 @@ var IntelligenceService = class {
|
|
|
32658
33335
|
*/
|
|
32659
33336
|
analyzeAndPersist(runId, projectId) {
|
|
32660
33337
|
const recentRuns = this.db.select().from(runs).where(
|
|
32661
|
-
|
|
32662
|
-
|
|
32663
|
-
or5(
|
|
33338
|
+
and25(
|
|
33339
|
+
eq35(runs.projectId, projectId),
|
|
33340
|
+
or5(eq35(runs.status, "completed"), eq35(runs.status, "partial")),
|
|
32664
33341
|
// Defensive: RunCoordinator already skips probes before this is
|
|
32665
33342
|
// called, but if a future call site invokes analyzeAndPersist
|
|
32666
33343
|
// directly for a probe, probes still must not pollute the
|
|
@@ -32742,7 +33419,7 @@ var IntelligenceService = class {
|
|
|
32742
33419
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
32743
33420
|
*/
|
|
32744
33421
|
analyzeAndPersistGbp(runId, projectId) {
|
|
32745
|
-
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();
|
|
32746
33423
|
if (!runRow) {
|
|
32747
33424
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
32748
33425
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -32750,11 +33427,11 @@ var IntelligenceService = class {
|
|
|
32750
33427
|
}
|
|
32751
33428
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
32752
33429
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
32753
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
32754
|
-
|
|
32755
|
-
|
|
32756
|
-
|
|
32757
|
-
|
|
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)
|
|
32758
33435
|
)).all();
|
|
32759
33436
|
if (selected.length === 0) {
|
|
32760
33437
|
log.info("gbp-intelligence.skip", { runId, reason: "no locations synced during run" });
|
|
@@ -32787,10 +33464,10 @@ var IntelligenceService = class {
|
|
|
32787
33464
|
}
|
|
32788
33465
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
32789
33466
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
32790
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
32791
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
32792
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
32793
|
-
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();
|
|
32794
33471
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
32795
33472
|
const summary = buildGbpSummary({
|
|
32796
33473
|
locationName,
|
|
@@ -32822,7 +33499,7 @@ var IntelligenceService = class {
|
|
|
32822
33499
|
/** Build the month-over-month keyword series for a location from the
|
|
32823
33500
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
32824
33501
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
32825
|
-
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();
|
|
32826
33503
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
32827
33504
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
32828
33505
|
const recentMonth = months[0] ?? null;
|
|
@@ -32853,7 +33530,7 @@ var IntelligenceService = class {
|
|
|
32853
33530
|
*/
|
|
32854
33531
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
32855
33532
|
const covered = new Set(coveredLocationNames);
|
|
32856
|
-
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();
|
|
32857
33534
|
const staleIds = [];
|
|
32858
33535
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
32859
33536
|
for (const row of existing) {
|
|
@@ -32864,7 +33541,7 @@ var IntelligenceService = class {
|
|
|
32864
33541
|
}
|
|
32865
33542
|
this.db.transaction((tx) => {
|
|
32866
33543
|
for (const id of staleIds) {
|
|
32867
|
-
tx.delete(insights).where(
|
|
33544
|
+
tx.delete(insights).where(eq35(insights.id, id)).run();
|
|
32868
33545
|
}
|
|
32869
33546
|
for (const insight of gbpInsights) {
|
|
32870
33547
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -32942,7 +33619,7 @@ var IntelligenceService = class {
|
|
|
32942
33619
|
* create per run + aggregate). DB is left untouched.
|
|
32943
33620
|
*/
|
|
32944
33621
|
backfill(projectName, opts, onProgress) {
|
|
32945
|
-
const project = this.db.select().from(projects).where(
|
|
33622
|
+
const project = this.db.select().from(projects).where(eq35(projects.name, projectName)).get();
|
|
32946
33623
|
if (!project) {
|
|
32947
33624
|
throw new Error(`Project "${projectName}" not found`);
|
|
32948
33625
|
}
|
|
@@ -32955,13 +33632,13 @@ var IntelligenceService = class {
|
|
|
32955
33632
|
sinceTimestamp = parsed;
|
|
32956
33633
|
}
|
|
32957
33634
|
const allRuns = this.db.select().from(runs).where(
|
|
32958
|
-
|
|
32959
|
-
|
|
32960
|
-
or5(
|
|
33635
|
+
and25(
|
|
33636
|
+
eq35(runs.projectId, project.id),
|
|
33637
|
+
or5(eq35(runs.status, "completed"), eq35(runs.status, "partial")),
|
|
32961
33638
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
32962
33639
|
ne5(runs.trigger, RunTriggers.probe)
|
|
32963
33640
|
)
|
|
32964
|
-
).orderBy(
|
|
33641
|
+
).orderBy(asc5(runs.finishedAt)).all();
|
|
32965
33642
|
let startIdx = 0;
|
|
32966
33643
|
let endIdx = allRuns.length;
|
|
32967
33644
|
if (opts?.fromRunId) {
|
|
@@ -32990,7 +33667,7 @@ var IntelligenceService = class {
|
|
|
32990
33667
|
let wouldDeleteTotal = 0;
|
|
32991
33668
|
const existingByRunId = /* @__PURE__ */ new Map();
|
|
32992
33669
|
if (isDryRun && targetRuns.length > 0) {
|
|
32993
|
-
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();
|
|
32994
33671
|
for (const r of rows) {
|
|
32995
33672
|
if (r.runId == null) continue;
|
|
32996
33673
|
existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
|
|
@@ -33036,7 +33713,7 @@ var IntelligenceService = class {
|
|
|
33036
33713
|
return { processed, skipped, totalInsights };
|
|
33037
33714
|
}
|
|
33038
33715
|
loadTrackedCompetitors(projectId) {
|
|
33039
|
-
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);
|
|
33040
33717
|
}
|
|
33041
33718
|
/**
|
|
33042
33719
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -33057,15 +33734,15 @@ var IntelligenceService = class {
|
|
|
33057
33734
|
}
|
|
33058
33735
|
persistResult(result, runId, projectId) {
|
|
33059
33736
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
33060
|
-
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();
|
|
33061
33738
|
for (const row of existingInsights) {
|
|
33062
33739
|
if (row.dismissed) {
|
|
33063
33740
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
33064
33741
|
}
|
|
33065
33742
|
}
|
|
33066
33743
|
this.db.transaction((tx) => {
|
|
33067
|
-
tx.delete(insights).where(
|
|
33068
|
-
tx.delete(healthSnapshots).where(
|
|
33744
|
+
tx.delete(insights).where(eq35(insights.runId, runId)).run();
|
|
33745
|
+
tx.delete(healthSnapshots).where(eq35(healthSnapshots.runId, runId)).run();
|
|
33069
33746
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33070
33747
|
for (const insight of result.insights) {
|
|
33071
33748
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -33085,7 +33762,7 @@ var IntelligenceService = class {
|
|
|
33085
33762
|
}).run();
|
|
33086
33763
|
}
|
|
33087
33764
|
tx.insert(healthSnapshots).values({
|
|
33088
|
-
id:
|
|
33765
|
+
id: crypto29.randomUUID(),
|
|
33089
33766
|
projectId,
|
|
33090
33767
|
runId,
|
|
33091
33768
|
overallCitedRate: String(result.health.overallCitedRate),
|
|
@@ -33116,24 +33793,24 @@ var IntelligenceService = class {
|
|
|
33116
33793
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
33117
33794
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
33118
33795
|
if (regressions.length === 0) return rawInsights;
|
|
33119
|
-
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();
|
|
33120
33797
|
const gscConnected = gscRows.length > 0;
|
|
33121
33798
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
33122
33799
|
for (const row of gscRows) {
|
|
33123
33800
|
const key = row.query.toLowerCase();
|
|
33124
33801
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
33125
33802
|
}
|
|
33126
|
-
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();
|
|
33127
33804
|
const locationCount = Math.max(
|
|
33128
33805
|
1,
|
|
33129
33806
|
(projectRow?.locations ?? []).length
|
|
33130
33807
|
);
|
|
33131
33808
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
33132
33809
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
33133
|
-
|
|
33134
|
-
|
|
33135
|
-
|
|
33136
|
-
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")),
|
|
33137
33814
|
// Defensive — see top of file.
|
|
33138
33815
|
ne5(runs.trigger, RunTriggers.probe)
|
|
33139
33816
|
)
|
|
@@ -33153,7 +33830,7 @@ var IntelligenceService = class {
|
|
|
33153
33830
|
const haveHistory = recentRunIds.length > 0;
|
|
33154
33831
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
33155
33832
|
if (haveHistory) {
|
|
33156
|
-
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();
|
|
33157
33834
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
33158
33835
|
for (const row of priorRows) {
|
|
33159
33836
|
if (!row.runId) continue;
|
|
@@ -33182,7 +33859,7 @@ var IntelligenceService = class {
|
|
|
33182
33859
|
});
|
|
33183
33860
|
}
|
|
33184
33861
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
33185
|
-
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();
|
|
33186
33863
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
33187
33864
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
33188
33865
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -33198,7 +33875,7 @@ var IntelligenceService = class {
|
|
|
33198
33875
|
citedDomains: querySnapshots.citedDomains,
|
|
33199
33876
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
33200
33877
|
snapshotLocation: querySnapshots.location
|
|
33201
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
33878
|
+
}).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(eq35(querySnapshots.runId, runId)).all();
|
|
33202
33879
|
const snapshots = [];
|
|
33203
33880
|
let orphanCount = 0;
|
|
33204
33881
|
for (const r of rows) {
|
|
@@ -33273,6 +33950,11 @@ export {
|
|
|
33273
33950
|
gbpPlaceActions,
|
|
33274
33951
|
gbpLodgingSnapshots,
|
|
33275
33952
|
gbpPlaceDetails,
|
|
33953
|
+
adsConnections,
|
|
33954
|
+
adsCampaigns,
|
|
33955
|
+
adsAdGroups,
|
|
33956
|
+
adsAds,
|
|
33957
|
+
adsInsightsDaily,
|
|
33276
33958
|
createClient,
|
|
33277
33959
|
parseJsonColumn,
|
|
33278
33960
|
extractLegacyCredentials,
|