@ainyc/canonry 4.101.0 → 4.102.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/assets/assets/{BacklinksPage-aysk_iGy.js → BacklinksPage-CoPo93LS.js} +1 -1
- package/assets/assets/{ChartPrimitives-CmKqxndA.js → ChartPrimitives-DqBMtYpk.js} +1 -1
- package/assets/assets/{ProjectPage-dduZrrBr.js → ProjectPage-DSJElQg_.js} +1 -1
- package/assets/assets/{RunRow-Ba77hJ4d.js → RunRow-2HNKAfK9.js} +1 -1
- package/assets/assets/{RunsPage-D3IAdQMB.js → RunsPage-Dw7ZIp-K.js} +1 -1
- package/assets/assets/{SettingsPage-Y54JtLw-.js → SettingsPage-CYmxSK3g.js} +1 -1
- package/assets/assets/{TrafficPage-DawzPWdt.js → TrafficPage-BI8BNANb.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-DAhQwFVb.js → TrafficSourceDetailPage-rdvZpta7.js} +1 -1
- package/assets/assets/{arrow-left-NsJbgqmV.js → arrow-left-C37crrqh.js} +1 -1
- package/assets/assets/{extract-error-message-u00ggxeK.js → extract-error-message-DZnbGnSd.js} +1 -1
- package/assets/assets/{index-BCuWbxUj.css → index-LsyRFLMG.css} +1 -1
- package/assets/assets/{index-4YArcfwj.js → index-sh-T-1TA.js} +57 -57
- package/assets/assets/{trash-2-BBdbbe60.js → trash-2-UvZu9cfJ.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-2YPVK75M.js → chunk-2FUODJQR.js} +1 -1
- package/dist/{chunk-O3SFVGDF.js → chunk-3PVCILSN.js} +38 -6
- package/dist/{chunk-AV3DHH6B.js → chunk-B2H2RRIT.js} +786 -611
- package/dist/{chunk-HUL5FUBD.js → chunk-EOQXB63S.js} +24 -2
- package/dist/cli.js +22 -7
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-R6QJ3KC2.js → intelligence-service-RC4BY5HF.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +8 -8
|
@@ -263,10 +263,10 @@ import {
|
|
|
263
263
|
wordpressSchemaDeployResultDtoSchema,
|
|
264
264
|
wordpressSchemaStatusResultDtoSchema,
|
|
265
265
|
wordpressStatusDtoSchema
|
|
266
|
-
} from "./chunk-
|
|
266
|
+
} from "./chunk-EOQXB63S.js";
|
|
267
267
|
|
|
268
268
|
// src/intelligence-service.ts
|
|
269
|
-
import { eq as
|
|
269
|
+
import { eq as eq38, desc as desc18, asc as asc6, and as and28, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
|
|
270
270
|
|
|
271
271
|
// ../db/src/client.ts
|
|
272
272
|
import { mkdirSync } from "fs";
|
|
@@ -318,6 +318,7 @@ __export(schema_exports, {
|
|
|
318
318
|
gbpPlaceDetails: () => gbpPlaceDetails,
|
|
319
319
|
googleConnections: () => googleConnections,
|
|
320
320
|
gscCoverageSnapshots: () => gscCoverageSnapshots,
|
|
321
|
+
gscDailyTotals: () => gscDailyTotals,
|
|
321
322
|
gscSearchData: () => gscSearchData,
|
|
322
323
|
gscUrlInspections: () => gscUrlInspections,
|
|
323
324
|
healthSnapshots: () => healthSnapshots,
|
|
@@ -468,11 +469,20 @@ var apiKeys = sqliteTable("api_keys", {
|
|
|
468
469
|
keyHash: text("key_hash").notNull().unique(),
|
|
469
470
|
keyPrefix: text("key_prefix").notNull(),
|
|
470
471
|
scopes: text("scopes", { mode: "json" }).$type().notNull().default(["*"]),
|
|
472
|
+
/**
|
|
473
|
+
* When set, the key is scoped to a SINGLE project: every project-scoped read
|
|
474
|
+
* or write is gated to this project id (enforced centrally in `auth.ts`).
|
|
475
|
+
* NULL keeps full-instance access — the historical default for every key
|
|
476
|
+
* `canonry init` / `canonry key create` writes. Cascade-deletes with the
|
|
477
|
+
* project so a stale scoped key never outlives its project.
|
|
478
|
+
*/
|
|
479
|
+
projectId: text("project_id").references(() => projects.id, { onDelete: "cascade" }),
|
|
471
480
|
createdAt: text("created_at").notNull(),
|
|
472
481
|
lastUsedAt: text("last_used_at"),
|
|
473
482
|
revokedAt: text("revoked_at")
|
|
474
483
|
}, (table) => [
|
|
475
|
-
index("idx_api_keys_prefix").on(table.keyPrefix)
|
|
484
|
+
index("idx_api_keys_prefix").on(table.keyPrefix),
|
|
485
|
+
index("idx_api_keys_project").on(table.projectId)
|
|
476
486
|
]);
|
|
477
487
|
var schedules = sqliteTable("schedules", {
|
|
478
488
|
id: text("id").primaryKey(),
|
|
@@ -548,6 +558,20 @@ var gscSearchData = sqliteTable("gsc_search_data", {
|
|
|
548
558
|
index("idx_gsc_search_query").on(table.query),
|
|
549
559
|
index("idx_gsc_search_run").on(table.syncRunId)
|
|
550
560
|
]);
|
|
561
|
+
var gscDailyTotals = sqliteTable("gsc_daily_totals", {
|
|
562
|
+
id: text("id").primaryKey(),
|
|
563
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
564
|
+
date: text("date").notNull(),
|
|
565
|
+
clicks: integer("clicks").notNull(),
|
|
566
|
+
impressions: integer("impressions").notNull(),
|
|
567
|
+
// Stored as a string like `gscSearchData.position` (parsed to a number on
|
|
568
|
+
// read). CTR is derived (clicks / impressions) and intentionally not stored.
|
|
569
|
+
position: text("position").notNull(),
|
|
570
|
+
createdAt: text("created_at").notNull()
|
|
571
|
+
}, (table) => [
|
|
572
|
+
uniqueIndex("idx_gsc_daily_totals_project_date").on(table.projectId, table.date),
|
|
573
|
+
index("idx_gsc_daily_totals_project").on(table.projectId)
|
|
574
|
+
]);
|
|
551
575
|
var gscUrlInspections = sqliteTable("gsc_url_inspections", {
|
|
552
576
|
id: text("id").primaryKey(),
|
|
553
577
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
@@ -3317,6 +3341,34 @@ var MIGRATION_VERSIONS = [
|
|
|
3317
3341
|
`CREATE INDEX IF NOT EXISTS idx_agent_tool_events_tool_created ON agent_tool_events(tool_name, created_at)`,
|
|
3318
3342
|
`CREATE INDEX IF NOT EXISTS idx_agent_tool_events_status_created ON agent_tool_events(status, created_at)`
|
|
3319
3343
|
]
|
|
3344
|
+
},
|
|
3345
|
+
{
|
|
3346
|
+
version: 86,
|
|
3347
|
+
name: "gsc-daily-totals",
|
|
3348
|
+
statements: [
|
|
3349
|
+
`CREATE TABLE IF NOT EXISTS gsc_daily_totals (
|
|
3350
|
+
id TEXT PRIMARY KEY,
|
|
3351
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3352
|
+
date TEXT NOT NULL,
|
|
3353
|
+
clicks INTEGER NOT NULL,
|
|
3354
|
+
impressions INTEGER NOT NULL,
|
|
3355
|
+
position TEXT NOT NULL,
|
|
3356
|
+
created_at TEXT NOT NULL
|
|
3357
|
+
)`,
|
|
3358
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_gsc_daily_totals_project_date ON gsc_daily_totals(project_id, date)`,
|
|
3359
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_daily_totals_project ON gsc_daily_totals(project_id)`
|
|
3360
|
+
]
|
|
3361
|
+
},
|
|
3362
|
+
{
|
|
3363
|
+
version: 87,
|
|
3364
|
+
name: "api-key-project-scope",
|
|
3365
|
+
statements: [
|
|
3366
|
+
// Opt-in single-project scoping for API keys. NULL = full-instance access
|
|
3367
|
+
// (the historical default), so this is a no-op for every existing key.
|
|
3368
|
+
// ALTER ADD COLUMN is idempotent (the runner swallows duplicate-column on retry).
|
|
3369
|
+
`ALTER TABLE api_keys ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE CASCADE`,
|
|
3370
|
+
`CREATE INDEX IF NOT EXISTS idx_api_keys_project ON api_keys(project_id)`
|
|
3371
|
+
]
|
|
3320
3372
|
}
|
|
3321
3373
|
];
|
|
3322
3374
|
function rebuildBacklinkTableWithSource(tx, table) {
|
|
@@ -5651,6 +5703,12 @@ function requireScope(request, scope) {
|
|
|
5651
5703
|
if (key.scopes.includes("*") || key.scopes.includes(scope)) return;
|
|
5652
5704
|
throw forbidden(`This action requires the "${scope}" scope on your API key.`);
|
|
5653
5705
|
}
|
|
5706
|
+
function assertProjectScope(request, projectId) {
|
|
5707
|
+
const scoped = request.apiKey?.projectId;
|
|
5708
|
+
if (scoped && scoped !== projectId) {
|
|
5709
|
+
throw forbidden("This API key is scoped to a different project.");
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5654
5712
|
function hashApiKey(key) {
|
|
5655
5713
|
return crypto2.createHash("sha256").update(key).digest("hex");
|
|
5656
5714
|
}
|
|
@@ -5711,10 +5769,20 @@ async function authPlugin(app, opts = {}) {
|
|
|
5711
5769
|
}
|
|
5712
5770
|
app.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(apiKeys.id, key.id)).run();
|
|
5713
5771
|
const scopes = Array.isArray(key.scopes) ? key.scopes : [];
|
|
5714
|
-
request.apiKey = { id: key.id, name: key.name, scopes };
|
|
5772
|
+
request.apiKey = { id: key.id, name: key.name, scopes, projectId: key.projectId ?? null };
|
|
5715
5773
|
if (isReadOnlyKey(scopes) && WRITE_METHODS.has(request.method)) {
|
|
5716
5774
|
throw forbidden("This API key is read-only and cannot perform write operations.");
|
|
5717
5775
|
}
|
|
5776
|
+
if (key.projectId) {
|
|
5777
|
+
const match = url.match(/\/projects\/([^/?#]+)/);
|
|
5778
|
+
if (match) {
|
|
5779
|
+
const projectName = decodeURIComponent(match[1]);
|
|
5780
|
+
const scoped = app.db.select({ id: projects.id }).from(projects).where(eq(projects.name, projectName)).get();
|
|
5781
|
+
if (!scoped || scoped.id !== key.projectId) {
|
|
5782
|
+
throw forbidden("This API key is scoped to a different project.");
|
|
5783
|
+
}
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5718
5786
|
});
|
|
5719
5787
|
}
|
|
5720
5788
|
|
|
@@ -5906,8 +5974,9 @@ async function projectRoutes(app, opts) {
|
|
|
5906
5974
|
const created = app.db.select().from(projects).where(eq3(projects.id, id)).get();
|
|
5907
5975
|
return reply.status(201).send(formatProject(created));
|
|
5908
5976
|
});
|
|
5909
|
-
app.get("/projects", async (
|
|
5910
|
-
const
|
|
5977
|
+
app.get("/projects", async (request, reply) => {
|
|
5978
|
+
const scoped = request.apiKey?.projectId;
|
|
5979
|
+
const rows = scoped ? app.db.select().from(projects).where(eq3(projects.id, scoped)).all() : app.db.select().from(projects).all();
|
|
5911
5980
|
return reply.send(rows.map(formatProject));
|
|
5912
5981
|
});
|
|
5913
5982
|
app.get("/projects/:name", async (request, reply) => {
|
|
@@ -6744,11 +6813,14 @@ async function runRoutes(app, opts) {
|
|
|
6744
6813
|
const filters = [gte(runs.createdAt, since)];
|
|
6745
6814
|
if (!includeProbe) filters.push(notProbeRun());
|
|
6746
6815
|
if (kind) filters.push(eq7(runs.kind, kind));
|
|
6816
|
+
const scopedProjectId = request.apiKey?.projectId;
|
|
6817
|
+
if (scopedProjectId) filters.push(eq7(runs.projectId, scopedProjectId));
|
|
6747
6818
|
const rows = app.db.select().from(runs).where(and2(...filters)).orderBy(desc(runs.createdAt), desc(runs.id)).limit(limit).all();
|
|
6748
6819
|
return reply.send(rows.map(formatRun));
|
|
6749
6820
|
});
|
|
6750
6821
|
app.post("/runs", async (request, reply) => {
|
|
6751
|
-
const
|
|
6822
|
+
const scopedProjectId = request.apiKey?.projectId;
|
|
6823
|
+
const allProjects = (scopedProjectId ? app.db.select().from(projects).where(eq7(projects.id, scopedProjectId)) : app.db.select().from(projects)).all();
|
|
6752
6824
|
if (allProjects.length === 0) {
|
|
6753
6825
|
return reply.status(207).send([]);
|
|
6754
6826
|
}
|
|
@@ -6814,6 +6886,7 @@ async function runRoutes(app, opts) {
|
|
|
6814
6886
|
app.post("/runs/:id/cancel", async (request, reply) => {
|
|
6815
6887
|
const run = app.db.select().from(runs).where(eq7(runs.id, request.params.id)).get();
|
|
6816
6888
|
if (!run) throw notFound("Run", request.params.id);
|
|
6889
|
+
assertProjectScope(request, run.projectId);
|
|
6817
6890
|
const terminalStatuses = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
|
|
6818
6891
|
if (terminalStatuses.has(run.status)) throw runNotCancellable(run.id, run.status);
|
|
6819
6892
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -6831,6 +6904,7 @@ async function runRoutes(app, opts) {
|
|
|
6831
6904
|
app.get("/runs/:id", async (request, reply) => {
|
|
6832
6905
|
const run = app.db.select().from(runs).where(eq7(runs.id, request.params.id)).get();
|
|
6833
6906
|
if (!run) throw notFound("Run", request.params.id);
|
|
6907
|
+
assertProjectScope(request, run.projectId);
|
|
6834
6908
|
return reply.send(loadRunDetail(app, run));
|
|
6835
6909
|
});
|
|
6836
6910
|
}
|
|
@@ -7286,6 +7360,13 @@ async function applyRoutes(app, opts) {
|
|
|
7286
7360
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7287
7361
|
const name = config.metadata.name;
|
|
7288
7362
|
const configQueries = resolveConfigSpecQueries(config.spec);
|
|
7363
|
+
const scopedProjectId = request.apiKey?.projectId;
|
|
7364
|
+
if (scopedProjectId) {
|
|
7365
|
+
const target = app.db.select({ id: projects.id }).from(projects).where(eq8(projects.name, name)).get();
|
|
7366
|
+
if (!target || target.id !== scopedProjectId) {
|
|
7367
|
+
throw forbidden("This API key is scoped to a single project and cannot apply this config.");
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7289
7370
|
let projectId;
|
|
7290
7371
|
let scheduleAction = null;
|
|
7291
7372
|
let aliasesChanged = false;
|
|
@@ -7549,8 +7630,9 @@ async function historyRoutes(app) {
|
|
|
7549
7630
|
const rows = app.db.select().from(auditLog).where(eq9(auditLog.projectId, project.id)).orderBy(desc2(auditLog.createdAt)).all();
|
|
7550
7631
|
return reply.send(rows.map(formatAuditEntry));
|
|
7551
7632
|
});
|
|
7552
|
-
app.get("/history", async (
|
|
7553
|
-
const
|
|
7633
|
+
app.get("/history", async (request, reply) => {
|
|
7634
|
+
const scopedProjectId = request.apiKey?.projectId;
|
|
7635
|
+
const rows = app.db.select().from(auditLog).where(scopedProjectId ? eq9(auditLog.projectId, scopedProjectId) : void 0).orderBy(desc2(auditLog.createdAt)).all();
|
|
7554
7636
|
return reply.send(rows.map(formatAuditEntry));
|
|
7555
7637
|
});
|
|
7556
7638
|
app.get("/projects/:name/snapshots", async (request, reply) => {
|
|
@@ -7731,6 +7813,15 @@ async function historyRoutes(app) {
|
|
|
7731
7813
|
if (!run1 || !run2) {
|
|
7732
7814
|
throw validationError("Both run1 and run2 query params are required");
|
|
7733
7815
|
}
|
|
7816
|
+
const requestedRunIds = [.../* @__PURE__ */ new Set([run1, run2])];
|
|
7817
|
+
const runRows = app.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(inArray2(runs.id, requestedRunIds)).all();
|
|
7818
|
+
const runsById = new Map(runRows.map((row) => [row.id, row]));
|
|
7819
|
+
for (const runId of requestedRunIds) {
|
|
7820
|
+
const run = runsById.get(runId);
|
|
7821
|
+
if (!run || run.projectId !== project.id) {
|
|
7822
|
+
throw notFound("Run", runId);
|
|
7823
|
+
}
|
|
7824
|
+
}
|
|
7734
7825
|
const snaps1 = app.db.select({
|
|
7735
7826
|
queryId: querySnapshots.queryId,
|
|
7736
7827
|
query: queries.query,
|
|
@@ -8469,7 +8560,7 @@ async function intelligenceRoutes(app) {
|
|
|
8469
8560
|
}
|
|
8470
8561
|
|
|
8471
8562
|
// ../api-routes/src/report.ts
|
|
8472
|
-
import { and as
|
|
8563
|
+
import { and as and10, desc as desc7, eq as eq15, gte as gte2, inArray as inArray6, lt, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
|
|
8473
8564
|
|
|
8474
8565
|
// ../api-routes/src/content.ts
|
|
8475
8566
|
import crypto11 from "crypto";
|
|
@@ -9204,6 +9295,38 @@ function winnabilityClassRank(winnabilityClass) {
|
|
|
9204
9295
|
return winnabilityClass === WinnabilityClasses.ownable ? 0 : 1;
|
|
9205
9296
|
}
|
|
9206
9297
|
|
|
9298
|
+
// ../api-routes/src/gsc-totals.ts
|
|
9299
|
+
import { and as and9, asc as asc2, eq as eq14, sql as sql6 } from "drizzle-orm";
|
|
9300
|
+
function readGscDailyTotals(db, projectId, startDate, endDate) {
|
|
9301
|
+
const rows = db.select({
|
|
9302
|
+
date: gscDailyTotals.date,
|
|
9303
|
+
clicks: gscDailyTotals.clicks,
|
|
9304
|
+
impressions: gscDailyTotals.impressions,
|
|
9305
|
+
position: gscDailyTotals.position
|
|
9306
|
+
}).from(gscDailyTotals).where(
|
|
9307
|
+
and9(
|
|
9308
|
+
eq14(gscDailyTotals.projectId, projectId),
|
|
9309
|
+
sql6`${gscDailyTotals.date} >= ${startDate}`,
|
|
9310
|
+
sql6`${gscDailyTotals.date} <= ${endDate}`
|
|
9311
|
+
)
|
|
9312
|
+
).orderBy(asc2(gscDailyTotals.date)).all();
|
|
9313
|
+
return rows.map((r) => {
|
|
9314
|
+
const position = Number(r.position);
|
|
9315
|
+
return {
|
|
9316
|
+
date: r.date,
|
|
9317
|
+
clicks: r.clicks,
|
|
9318
|
+
impressions: r.impressions,
|
|
9319
|
+
position: Number.isFinite(position) ? position : 0
|
|
9320
|
+
};
|
|
9321
|
+
});
|
|
9322
|
+
}
|
|
9323
|
+
function mergeGscDailyTotalsWithFallback(propertyTotals, dimensionedFallback) {
|
|
9324
|
+
const byDate = /* @__PURE__ */ new Map();
|
|
9325
|
+
for (const row of dimensionedFallback) byDate.set(row.date, row);
|
|
9326
|
+
for (const row of propertyTotals) byDate.set(row.date, row);
|
|
9327
|
+
return [...byDate.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
9328
|
+
}
|
|
9329
|
+
|
|
9207
9330
|
// ../api-routes/src/report-renderer.ts
|
|
9208
9331
|
var COLORS = {
|
|
9209
9332
|
bg: "#09090b",
|
|
@@ -11515,38 +11638,56 @@ function loadSnapshotsForRunIds(db, runIds) {
|
|
|
11515
11638
|
}));
|
|
11516
11639
|
}
|
|
11517
11640
|
function loadQueryLookup(db, projectId) {
|
|
11518
|
-
const rows = db.select().from(queries).where(
|
|
11641
|
+
const rows = db.select().from(queries).where(eq15(queries.projectId, projectId)).all();
|
|
11519
11642
|
const byId = /* @__PURE__ */ new Map();
|
|
11520
11643
|
for (const row of rows) byId.set(row.id, row.query);
|
|
11521
11644
|
return { byId };
|
|
11522
11645
|
}
|
|
11523
11646
|
function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries, windowDays) {
|
|
11524
|
-
const allRows = db.select().from(gscSearchData).where(
|
|
11525
|
-
|
|
11647
|
+
const allRows = db.select().from(gscSearchData).where(eq15(gscSearchData.projectId, projectId)).all();
|
|
11648
|
+
const allDailyTotals = readGscDailyTotals(db, projectId, "", "9999-12-31");
|
|
11526
11649
|
let maxDate = "";
|
|
11527
11650
|
for (const r of allRows) if (r.date > maxDate) maxDate = r.date;
|
|
11651
|
+
for (const r of allDailyTotals) if (r.date > maxDate) maxDate = r.date;
|
|
11652
|
+
if (!maxDate) return null;
|
|
11528
11653
|
const startDate = windowStartDate(maxDate, windowDays);
|
|
11529
11654
|
const rows = allRows.filter((r) => r.date >= startDate && r.date <= maxDate);
|
|
11530
|
-
|
|
11531
|
-
|
|
11532
|
-
let
|
|
11533
|
-
let weightedPositionSum = 0;
|
|
11655
|
+
const dailyTotals = allDailyTotals.filter((r) => r.date >= startDate && r.date <= maxDate);
|
|
11656
|
+
if (rows.length === 0 && dailyTotals.length === 0) return null;
|
|
11657
|
+
let dimensionedClicks = 0;
|
|
11534
11658
|
const queryAgg = /* @__PURE__ */ new Map();
|
|
11535
|
-
const
|
|
11659
|
+
const dimensionedTrendAgg = /* @__PURE__ */ new Map();
|
|
11536
11660
|
for (const r of rows) {
|
|
11537
|
-
|
|
11538
|
-
totalImpressions += r.impressions;
|
|
11539
|
-
weightedPositionSum += safeNum(r.position) * r.impressions;
|
|
11661
|
+
dimensionedClicks += r.clicks;
|
|
11540
11662
|
const q = queryAgg.get(r.query) ?? { clicks: 0, impressions: 0, weightedPositionSum: 0 };
|
|
11541
11663
|
q.clicks += r.clicks;
|
|
11542
11664
|
q.impressions += r.impressions;
|
|
11543
11665
|
q.weightedPositionSum += safeNum(r.position) * r.impressions;
|
|
11544
11666
|
queryAgg.set(r.query, q);
|
|
11545
|
-
const t =
|
|
11667
|
+
const t = dimensionedTrendAgg.get(r.date) ?? { clicks: 0, impressions: 0, weightedPositionSum: 0 };
|
|
11546
11668
|
t.clicks += r.clicks;
|
|
11547
11669
|
t.impressions += r.impressions;
|
|
11548
|
-
|
|
11670
|
+
t.weightedPositionSum += safeNum(r.position) * r.impressions;
|
|
11671
|
+
dimensionedTrendAgg.set(r.date, t);
|
|
11672
|
+
}
|
|
11673
|
+
const dailySeries = mergeGscDailyTotalsWithFallback(
|
|
11674
|
+
dailyTotals,
|
|
11675
|
+
[...dimensionedTrendAgg.entries()].map(([date, agg]) => ({
|
|
11676
|
+
date,
|
|
11677
|
+
clicks: agg.clicks,
|
|
11678
|
+
impressions: agg.impressions,
|
|
11679
|
+
position: agg.impressions > 0 ? agg.weightedPositionSum / agg.impressions : 0
|
|
11680
|
+
}))
|
|
11681
|
+
);
|
|
11682
|
+
let totalClicks = 0;
|
|
11683
|
+
let totalImpressions = 0;
|
|
11684
|
+
let weightedPositionSum = 0;
|
|
11685
|
+
for (const d of dailySeries) {
|
|
11686
|
+
totalClicks += d.clicks;
|
|
11687
|
+
totalImpressions += d.impressions;
|
|
11688
|
+
weightedPositionSum += d.position * d.impressions;
|
|
11549
11689
|
}
|
|
11690
|
+
const trend = dailySeries.map((d) => ({ date: d.date, clicks: d.clicks, impressions: d.impressions }));
|
|
11550
11691
|
const ctr = totalImpressions > 0 ? totalClicks / totalImpressions : 0;
|
|
11551
11692
|
const avgPosition = totalImpressions > 0 ? weightedPositionSum / totalImpressions : 0;
|
|
11552
11693
|
const topQueries = [...queryAgg.entries()].map(([query, agg]) => ({
|
|
@@ -11569,9 +11710,8 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
|
|
|
11569
11710
|
category,
|
|
11570
11711
|
clicks: agg.clicks,
|
|
11571
11712
|
impressions: agg.impressions,
|
|
11572
|
-
sharePct:
|
|
11713
|
+
sharePct: dimensionedClicks > 0 ? Math.round(agg.clicks / dimensionedClicks * 100) : 0
|
|
11573
11714
|
})).sort((a, b) => b.clicks - a.clicks);
|
|
11574
|
-
const trend = [...trendAgg.entries()].map(([date, agg]) => ({ date, clicks: agg.clicks, impressions: agg.impressions })).sort((a, b) => a.date.localeCompare(b.date));
|
|
11575
11715
|
const periodStart = trend[0]?.date ?? "";
|
|
11576
11716
|
const periodEnd = trend.at(-1)?.date ?? "";
|
|
11577
11717
|
const trackedSet = new Set(trackedQueries.map((q) => q.toLowerCase()));
|
|
@@ -11595,13 +11735,13 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
|
|
|
11595
11735
|
function buildGaSection(db, projectId, windowDays) {
|
|
11596
11736
|
const gaWindowKey = GA_WINDOW_SUMMARY_KEYS[windowDays];
|
|
11597
11737
|
const windowSummary = gaWindowKey ? db.select().from(gaTrafficWindowSummaries).where(
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
11738
|
+
and10(
|
|
11739
|
+
eq15(gaTrafficWindowSummaries.projectId, projectId),
|
|
11740
|
+
eq15(gaTrafficWindowSummaries.windowKey, gaWindowKey)
|
|
11601
11741
|
)
|
|
11602
11742
|
).limit(1).get() : void 0;
|
|
11603
|
-
const fallbackSummary = windowSummary ? null : db.select().from(gaTrafficSummaries).where(
|
|
11604
|
-
const allSnapshotRows = db.select().from(gaTrafficSnapshots).where(
|
|
11743
|
+
const fallbackSummary = windowSummary ? null : db.select().from(gaTrafficSummaries).where(eq15(gaTrafficSummaries.projectId, projectId)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
11744
|
+
const allSnapshotRows = db.select().from(gaTrafficSnapshots).where(eq15(gaTrafficSnapshots.projectId, projectId)).all();
|
|
11605
11745
|
if (!windowSummary && !fallbackSummary && allSnapshotRows.length === 0) return null;
|
|
11606
11746
|
let snapshotMaxDate = "";
|
|
11607
11747
|
for (const r of allSnapshotRows) if (r.date > snapshotMaxDate) snapshotMaxDate = r.date;
|
|
@@ -11660,7 +11800,7 @@ function buildGaSection(db, projectId, windowDays) {
|
|
|
11660
11800
|
};
|
|
11661
11801
|
}
|
|
11662
11802
|
function buildSocialReferrals(db, projectId) {
|
|
11663
|
-
const rows = db.select().from(gaSocialReferrals).where(
|
|
11803
|
+
const rows = db.select().from(gaSocialReferrals).where(eq15(gaSocialReferrals.projectId, projectId)).all();
|
|
11664
11804
|
if (rows.length === 0) return null;
|
|
11665
11805
|
let total = 0;
|
|
11666
11806
|
let organic = 0;
|
|
@@ -11692,7 +11832,7 @@ function buildSocialReferrals(db, projectId) {
|
|
|
11692
11832
|
};
|
|
11693
11833
|
}
|
|
11694
11834
|
function buildAiReferrals(db, projectId) {
|
|
11695
|
-
const rows = db.select().from(gaAiReferrals).where(
|
|
11835
|
+
const rows = db.select().from(gaAiReferrals).where(eq15(gaAiReferrals.projectId, projectId)).all();
|
|
11696
11836
|
if (rows.length === 0) return null;
|
|
11697
11837
|
const dimSessionsByTuple = /* @__PURE__ */ new Map();
|
|
11698
11838
|
for (const r of rows) {
|
|
@@ -11749,7 +11889,7 @@ function buildAiReferrals(db, projectId) {
|
|
|
11749
11889
|
return { totalSessions: total, totalUsers, bySource, trend, topLandingPages };
|
|
11750
11890
|
}
|
|
11751
11891
|
function nonSubresourceReferralPathCondition() {
|
|
11752
|
-
return
|
|
11892
|
+
return sql7`
|
|
11753
11893
|
LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/_next/static/%'
|
|
11754
11894
|
AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/assets/%'
|
|
11755
11895
|
AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/static/%'
|
|
@@ -11773,8 +11913,8 @@ function nonSubresourceReferralPathCondition() {
|
|
|
11773
11913
|
}
|
|
11774
11914
|
function buildServerActivity(db, projectId, windowDays) {
|
|
11775
11915
|
const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
|
|
11776
|
-
|
|
11777
|
-
|
|
11916
|
+
and10(
|
|
11917
|
+
eq15(trafficSources.projectId, projectId),
|
|
11778
11918
|
ne2(trafficSources.status, TrafficSourceStatuses.archived)
|
|
11779
11919
|
)
|
|
11780
11920
|
).all();
|
|
@@ -11788,19 +11928,19 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11788
11928
|
const priorStart = new Date(priorStartMs).toISOString();
|
|
11789
11929
|
const trendStart = new Date(trendStartMs).toISOString();
|
|
11790
11930
|
const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
11791
|
-
db.select({ total:
|
|
11792
|
-
|
|
11793
|
-
|
|
11794
|
-
|
|
11931
|
+
db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
11932
|
+
and10(
|
|
11933
|
+
eq15(crawlerEventsHourly.projectId, projectId),
|
|
11934
|
+
eq15(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
11795
11935
|
gte2(crawlerEventsHourly.tsHour, windowStartIso),
|
|
11796
11936
|
exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
|
|
11797
11937
|
)
|
|
11798
11938
|
).get()?.total ?? 0
|
|
11799
11939
|
);
|
|
11800
11940
|
const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
11801
|
-
db.select({ total:
|
|
11802
|
-
|
|
11803
|
-
|
|
11941
|
+
db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
11942
|
+
and10(
|
|
11943
|
+
eq15(crawlerEventsHourly.projectId, projectId),
|
|
11804
11944
|
ne2(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
11805
11945
|
gte2(crawlerEventsHourly.tsHour, windowStartIso),
|
|
11806
11946
|
exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
|
|
@@ -11808,9 +11948,9 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11808
11948
|
).get()?.total ?? 0
|
|
11809
11949
|
);
|
|
11810
11950
|
const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
11811
|
-
db.select({ total:
|
|
11812
|
-
|
|
11813
|
-
|
|
11951
|
+
db.select({ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
11952
|
+
and10(
|
|
11953
|
+
eq15(aiReferralEventsHourly.projectId, projectId),
|
|
11814
11954
|
nonSubresourceReferralPathCondition(),
|
|
11815
11955
|
gte2(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
11816
11956
|
exclusiveEnd ? lt(aiReferralEventsHourly.tsHour, windowEndIso) : lte(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
@@ -11818,9 +11958,9 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11818
11958
|
).get()?.total ?? 0
|
|
11819
11959
|
);
|
|
11820
11960
|
const sumUserFetches = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
11821
|
-
db.select({ total:
|
|
11822
|
-
|
|
11823
|
-
|
|
11961
|
+
db.select({ total: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
11962
|
+
and10(
|
|
11963
|
+
eq15(aiUserFetchEventsHourly.projectId, projectId),
|
|
11824
11964
|
gte2(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
11825
11965
|
exclusiveEnd ? lt(aiUserFetchEventsHourly.tsHour, windowEndIso) : lte(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
11826
11966
|
)
|
|
@@ -11837,31 +11977,31 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11837
11977
|
const crawlerByOperatorRows = db.select({
|
|
11838
11978
|
operator: crawlerEventsHourly.operator,
|
|
11839
11979
|
verificationStatus: crawlerEventsHourly.verificationStatus,
|
|
11840
|
-
hits:
|
|
11980
|
+
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
11841
11981
|
}).from(crawlerEventsHourly).where(
|
|
11842
|
-
|
|
11843
|
-
|
|
11982
|
+
and10(
|
|
11983
|
+
eq15(crawlerEventsHourly.projectId, projectId),
|
|
11844
11984
|
gte2(crawlerEventsHourly.tsHour, headlineStart),
|
|
11845
11985
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
11846
11986
|
)
|
|
11847
11987
|
).groupBy(crawlerEventsHourly.operator, crawlerEventsHourly.verificationStatus).all();
|
|
11848
11988
|
const crawlerByOperatorPriorRows = db.select({
|
|
11849
11989
|
operator: crawlerEventsHourly.operator,
|
|
11850
|
-
hits:
|
|
11990
|
+
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
11851
11991
|
}).from(crawlerEventsHourly).where(
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11992
|
+
and10(
|
|
11993
|
+
eq15(crawlerEventsHourly.projectId, projectId),
|
|
11994
|
+
eq15(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
11855
11995
|
gte2(crawlerEventsHourly.tsHour, priorStart),
|
|
11856
11996
|
lt(crawlerEventsHourly.tsHour, headlineStart)
|
|
11857
11997
|
)
|
|
11858
11998
|
).groupBy(crawlerEventsHourly.operator).all();
|
|
11859
11999
|
const referralByOperatorRows = db.select({
|
|
11860
12000
|
operator: aiReferralEventsHourly.operator,
|
|
11861
|
-
hits:
|
|
12001
|
+
hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
11862
12002
|
}).from(aiReferralEventsHourly).where(
|
|
11863
|
-
|
|
11864
|
-
|
|
12003
|
+
and10(
|
|
12004
|
+
eq15(aiReferralEventsHourly.projectId, projectId),
|
|
11865
12005
|
nonSubresourceReferralPathCondition(),
|
|
11866
12006
|
gte2(aiReferralEventsHourly.tsHour, headlineStart),
|
|
11867
12007
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
@@ -11869,10 +12009,10 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11869
12009
|
).groupBy(aiReferralEventsHourly.operator).all();
|
|
11870
12010
|
const userFetchByOperatorRows = db.select({
|
|
11871
12011
|
operator: aiUserFetchEventsHourly.operator,
|
|
11872
|
-
hits:
|
|
12012
|
+
hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
|
|
11873
12013
|
}).from(aiUserFetchEventsHourly).where(
|
|
11874
|
-
|
|
11875
|
-
|
|
12014
|
+
and10(
|
|
12015
|
+
eq15(aiUserFetchEventsHourly.projectId, projectId),
|
|
11876
12016
|
gte2(aiUserFetchEventsHourly.tsHour, headlineStart),
|
|
11877
12017
|
lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
|
|
11878
12018
|
)
|
|
@@ -11912,16 +12052,16 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11912
12052
|
);
|
|
11913
12053
|
const topPathsRows = db.select({
|
|
11914
12054
|
path: crawlerEventsHourly.pathNormalized,
|
|
11915
|
-
hits:
|
|
11916
|
-
operators:
|
|
12055
|
+
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
|
|
12056
|
+
operators: sql7`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
|
|
11917
12057
|
}).from(crawlerEventsHourly).where(
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
12058
|
+
and10(
|
|
12059
|
+
eq15(crawlerEventsHourly.projectId, projectId),
|
|
12060
|
+
eq15(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
11921
12061
|
gte2(crawlerEventsHourly.tsHour, headlineStart),
|
|
11922
12062
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
11923
12063
|
)
|
|
11924
|
-
).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc7(
|
|
12064
|
+
).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc7(sql7`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
|
|
11925
12065
|
const topCrawledPaths = topPathsRows.map((r) => ({
|
|
11926
12066
|
path: r.path,
|
|
11927
12067
|
verifiedHits: Number(r.hits),
|
|
@@ -11929,16 +12069,16 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11929
12069
|
}));
|
|
11930
12070
|
const referralProductsRows = db.select({
|
|
11931
12071
|
product: aiReferralEventsHourly.product,
|
|
11932
|
-
arrivals:
|
|
11933
|
-
landingPaths:
|
|
12072
|
+
arrivals: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
12073
|
+
landingPaths: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
|
|
11934
12074
|
}).from(aiReferralEventsHourly).where(
|
|
11935
|
-
|
|
11936
|
-
|
|
12075
|
+
and10(
|
|
12076
|
+
eq15(aiReferralEventsHourly.projectId, projectId),
|
|
11937
12077
|
nonSubresourceReferralPathCondition(),
|
|
11938
12078
|
gte2(aiReferralEventsHourly.tsHour, headlineStart),
|
|
11939
12079
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
11940
12080
|
)
|
|
11941
|
-
).groupBy(aiReferralEventsHourly.product).orderBy(desc7(
|
|
12081
|
+
).groupBy(aiReferralEventsHourly.product).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
|
|
11942
12082
|
const referralProducts = referralProductsRows.map((r) => ({
|
|
11943
12083
|
product: r.product,
|
|
11944
12084
|
arrivals: Number(r.arrivals),
|
|
@@ -11946,53 +12086,53 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11946
12086
|
}));
|
|
11947
12087
|
const topReferralRows = db.select({
|
|
11948
12088
|
path: aiReferralEventsHourly.landingPathNormalized,
|
|
11949
|
-
arrivals:
|
|
11950
|
-
products:
|
|
12089
|
+
arrivals: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
12090
|
+
products: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
|
|
11951
12091
|
}).from(aiReferralEventsHourly).where(
|
|
11952
|
-
|
|
11953
|
-
|
|
12092
|
+
and10(
|
|
12093
|
+
eq15(aiReferralEventsHourly.projectId, projectId),
|
|
11954
12094
|
nonSubresourceReferralPathCondition(),
|
|
11955
12095
|
gte2(aiReferralEventsHourly.tsHour, headlineStart),
|
|
11956
12096
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
11957
12097
|
)
|
|
11958
|
-
).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc7(
|
|
12098
|
+
).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
|
|
11959
12099
|
const topReferralLandingPaths = topReferralRows.map((r) => ({
|
|
11960
12100
|
path: r.path,
|
|
11961
12101
|
arrivals: Number(r.arrivals),
|
|
11962
12102
|
distinctProducts: Number(r.products)
|
|
11963
12103
|
}));
|
|
11964
12104
|
const crawlerTrendRows = db.select({
|
|
11965
|
-
date:
|
|
11966
|
-
hits:
|
|
12105
|
+
date: sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
|
|
12106
|
+
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
11967
12107
|
}).from(crawlerEventsHourly).where(
|
|
11968
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
12108
|
+
and10(
|
|
12109
|
+
eq15(crawlerEventsHourly.projectId, projectId),
|
|
12110
|
+
eq15(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
11971
12111
|
gte2(crawlerEventsHourly.tsHour, trendStart),
|
|
11972
12112
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
11973
12113
|
)
|
|
11974
|
-
).groupBy(
|
|
12114
|
+
).groupBy(sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
|
|
11975
12115
|
const referralTrendRows = db.select({
|
|
11976
|
-
date:
|
|
11977
|
-
hits:
|
|
12116
|
+
date: sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
|
|
12117
|
+
hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
11978
12118
|
}).from(aiReferralEventsHourly).where(
|
|
11979
|
-
|
|
11980
|
-
|
|
12119
|
+
and10(
|
|
12120
|
+
eq15(aiReferralEventsHourly.projectId, projectId),
|
|
11981
12121
|
nonSubresourceReferralPathCondition(),
|
|
11982
12122
|
gte2(aiReferralEventsHourly.tsHour, trendStart),
|
|
11983
12123
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
11984
12124
|
)
|
|
11985
|
-
).groupBy(
|
|
12125
|
+
).groupBy(sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
|
|
11986
12126
|
const userFetchTrendRows = db.select({
|
|
11987
|
-
date:
|
|
11988
|
-
hits:
|
|
12127
|
+
date: sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`,
|
|
12128
|
+
hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
|
|
11989
12129
|
}).from(aiUserFetchEventsHourly).where(
|
|
11990
|
-
|
|
11991
|
-
|
|
12130
|
+
and10(
|
|
12131
|
+
eq15(aiUserFetchEventsHourly.projectId, projectId),
|
|
11992
12132
|
gte2(aiUserFetchEventsHourly.tsHour, trendStart),
|
|
11993
12133
|
lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
|
|
11994
12134
|
)
|
|
11995
|
-
).groupBy(
|
|
12135
|
+
).groupBy(sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`).all();
|
|
11996
12136
|
const emptyTrendEntry = () => ({ verifiedCrawlerHits: 0, userFetchHits: 0, referralArrivals: 0 });
|
|
11997
12137
|
const dailyTrendMap = /* @__PURE__ */ new Map();
|
|
11998
12138
|
for (const r of crawlerTrendRows) {
|
|
@@ -12043,7 +12183,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12043
12183
|
};
|
|
12044
12184
|
}
|
|
12045
12185
|
function buildIndexingHealth(db, projectId) {
|
|
12046
|
-
const gsc = db.select().from(gscCoverageSnapshots).where(
|
|
12186
|
+
const gsc = db.select().from(gscCoverageSnapshots).where(eq15(gscCoverageSnapshots.projectId, projectId)).orderBy(desc7(gscCoverageSnapshots.date)).limit(1).get();
|
|
12047
12187
|
if (gsc) {
|
|
12048
12188
|
const total = gsc.indexed + gsc.notIndexed;
|
|
12049
12189
|
return {
|
|
@@ -12056,7 +12196,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
12056
12196
|
indexedPct: total > 0 ? Math.round(gsc.indexed / total * 100) : 0
|
|
12057
12197
|
};
|
|
12058
12198
|
}
|
|
12059
|
-
const bing = db.select().from(bingCoverageSnapshots).where(
|
|
12199
|
+
const bing = db.select().from(bingCoverageSnapshots).where(eq15(bingCoverageSnapshots.projectId, projectId)).orderBy(desc7(bingCoverageSnapshots.date)).limit(1).get();
|
|
12060
12200
|
if (bing) {
|
|
12061
12201
|
const total = bing.indexed + bing.notIndexed + bing.unknown;
|
|
12062
12202
|
return {
|
|
@@ -12072,7 +12212,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
12072
12212
|
return null;
|
|
12073
12213
|
}
|
|
12074
12214
|
function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
12075
|
-
const visibilityRuns = db.select().from(runs).where(
|
|
12215
|
+
const visibilityRuns = db.select().from(runs).where(and10(eq15(runs.projectId, projectId), eq15(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
|
|
12076
12216
|
const totalQueries = queryLookup.byId.size;
|
|
12077
12217
|
const points = [];
|
|
12078
12218
|
for (const run of visibilityRuns) {
|
|
@@ -12120,15 +12260,15 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
|
12120
12260
|
}
|
|
12121
12261
|
function buildInsightList(db, projectId, locationFilter) {
|
|
12122
12262
|
const recentRunIds = db.select({ id: runs.id, location: runs.location }).from(runs).where(
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
or3(
|
|
12263
|
+
and10(
|
|
12264
|
+
eq15(runs.projectId, projectId),
|
|
12265
|
+
eq15(runs.kind, RunKinds["answer-visibility"]),
|
|
12266
|
+
or3(eq15(runs.status, RunStatuses.completed), eq15(runs.status, RunStatuses.partial)),
|
|
12127
12267
|
notProbeRun()
|
|
12128
12268
|
)
|
|
12129
12269
|
).orderBy(desc7(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
|
|
12130
12270
|
if (recentRunIds.length === 0) return [];
|
|
12131
|
-
const rows = db.select().from(insights).where(
|
|
12271
|
+
const rows = db.select().from(insights).where(and10(eq15(insights.projectId, projectId), inArray6(insights.runId, recentRunIds))).orderBy(desc7(insights.createdAt)).all();
|
|
12132
12272
|
const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
12133
12273
|
const flat = rows.filter((r) => !r.dismissed).map((r) => {
|
|
12134
12274
|
const recommendation = r.recommendation;
|
|
@@ -12680,7 +12820,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
12680
12820
|
const project = resolveProject(db, projectName);
|
|
12681
12821
|
const queryLookup = loadQueryLookup(db, project.id);
|
|
12682
12822
|
const comparisonWindowDays = reportComparisonWindowDays(periodDays);
|
|
12683
|
-
const allRuns = db.select().from(runs).where(
|
|
12823
|
+
const allRuns = db.select().from(runs).where(and10(eq15(runs.projectId, project.id), notProbeRun())).orderBy(desc7(runs.createdAt), desc7(runs.id)).all();
|
|
12684
12824
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
12685
12825
|
const completedVisRunGroups = groupRunsByCreatedAt(
|
|
12686
12826
|
visibilityRuns.filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial)
|
|
@@ -12689,7 +12829,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
12689
12829
|
const representativeLatestRun = pickGroupRepresentative(latestVisRunGroup) ?? visibilityRuns[0] ?? null;
|
|
12690
12830
|
const latestSnapshots = loadSnapshotsForRunIds(db, latestVisRunGroup.map((r) => r.id));
|
|
12691
12831
|
const latestRunLocation = representativeLatestRun?.location ?? null;
|
|
12692
|
-
const competitorRows = db.select().from(competitors).where(
|
|
12832
|
+
const competitorRows = db.select().from(competitors).where(eq15(competitors.projectId, project.id)).all();
|
|
12693
12833
|
const competitorDomains = competitorRows.map((c) => c.domain);
|
|
12694
12834
|
const ownedDomains = project.ownedDomains;
|
|
12695
12835
|
const projectDomains = [project.canonicalDomain, ...ownedDomains];
|
|
@@ -12916,16 +13056,16 @@ async function reportRoutes(app) {
|
|
|
12916
13056
|
}
|
|
12917
13057
|
|
|
12918
13058
|
// ../api-routes/src/citations.ts
|
|
12919
|
-
import { and as
|
|
13059
|
+
import { and as and11, eq as eq16, inArray as inArray7 } from "drizzle-orm";
|
|
12920
13060
|
async function citationRoutes(app) {
|
|
12921
13061
|
app.get("/projects/:name/citations/visibility", async (request, reply) => {
|
|
12922
13062
|
const project = resolveProject(app.db, request.params.name);
|
|
12923
13063
|
const configuredProviders = project.providers;
|
|
12924
|
-
const projectQueries = app.db.select().from(queries).where(
|
|
13064
|
+
const projectQueries = app.db.select().from(queries).where(eq16(queries.projectId, project.id)).all();
|
|
12925
13065
|
if (projectQueries.length === 0) {
|
|
12926
13066
|
return reply.send(emptyCitationVisibility("no-queries"));
|
|
12927
13067
|
}
|
|
12928
|
-
const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
13068
|
+
const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and11(eq16(runs.projectId, project.id), notProbeRun())).all();
|
|
12929
13069
|
if (projectRuns.length === 0) {
|
|
12930
13070
|
return reply.send(emptyCitationVisibility("no-runs-yet"));
|
|
12931
13071
|
}
|
|
@@ -12949,7 +13089,7 @@ async function citationRoutes(app) {
|
|
|
12949
13089
|
queryId: s.queryId,
|
|
12950
13090
|
runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
|
|
12951
13091
|
}));
|
|
12952
|
-
const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
13092
|
+
const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq16(competitors.projectId, project.id)).all().map((c) => normalizeDomain2(c.domain)).filter((d) => d.length > 0);
|
|
12953
13093
|
const response = computeCitationVisibility({
|
|
12954
13094
|
queries: projectQueries.map((q) => ({ id: q.id, query: q.query })),
|
|
12955
13095
|
snapshots,
|
|
@@ -13081,7 +13221,7 @@ function normalizeDomain2(domain) {
|
|
|
13081
13221
|
}
|
|
13082
13222
|
|
|
13083
13223
|
// ../api-routes/src/visibility-stats.ts
|
|
13084
|
-
import { and as
|
|
13224
|
+
import { and as and12, desc as desc8, eq as eq17, inArray as inArray8 } from "drizzle-orm";
|
|
13085
13225
|
function round42(value) {
|
|
13086
13226
|
return Math.round(value * 1e4) / 1e4;
|
|
13087
13227
|
}
|
|
@@ -13201,8 +13341,8 @@ async function visibilityStatsRoutes(app) {
|
|
|
13201
13341
|
if (!Number.isInteger(n) || n <= 0) throw validationError('"lastRuns" must be a positive integer');
|
|
13202
13342
|
lastRuns = n;
|
|
13203
13343
|
}
|
|
13204
|
-
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
13205
|
-
let projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(
|
|
13344
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq17(queries.projectId, project.id)).all();
|
|
13345
|
+
let projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and12(eq17(runs.projectId, project.id), eq17(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc8(runs.createdAt)).all().filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial);
|
|
13206
13346
|
if (sinceMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) >= sinceMs);
|
|
13207
13347
|
if (untilMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) <= untilMs);
|
|
13208
13348
|
if (lastRuns !== null) projectRuns = projectRuns.slice(0, lastRuns);
|
|
@@ -13238,7 +13378,7 @@ async function visibilityStatsRoutes(app) {
|
|
|
13238
13378
|
}
|
|
13239
13379
|
|
|
13240
13380
|
// ../api-routes/src/composites.ts
|
|
13241
|
-
import { eq as
|
|
13381
|
+
import { eq as eq18, and as and13, desc as desc9, sql as sql8, like as like2, or as or4, inArray as inArray9 } from "drizzle-orm";
|
|
13242
13382
|
var TOP_INSIGHT_LIMIT = 5;
|
|
13243
13383
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
13244
13384
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -13256,7 +13396,7 @@ async function compositeRoutes(app) {
|
|
|
13256
13396
|
const project = resolveProject(app.db, request.params.name);
|
|
13257
13397
|
const filterLocation = (request.query.location ?? "").trim() || null;
|
|
13258
13398
|
const sinceIso = parseSinceFilter(request.query.since);
|
|
13259
|
-
const allRunsRaw = app.db.select().from(runs).where(
|
|
13399
|
+
const allRunsRaw = app.db.select().from(runs).where(and13(eq18(runs.projectId, project.id), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).all();
|
|
13260
13400
|
const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
|
|
13261
13401
|
const totalRuns = allRuns.length;
|
|
13262
13402
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
@@ -13269,15 +13409,15 @@ async function compositeRoutes(app) {
|
|
|
13269
13409
|
const previousVisibilityRun = pickGroupRepresentative(previousVisRunGroup);
|
|
13270
13410
|
const latestRunRow = allRuns[0] ?? null;
|
|
13271
13411
|
const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
|
|
13272
|
-
const healthRow = app.db.select().from(healthSnapshots).where(
|
|
13412
|
+
const healthRow = app.db.select().from(healthSnapshots).where(eq18(healthSnapshots.projectId, project.id)).orderBy(desc9(healthSnapshots.createdAt)).limit(1).get();
|
|
13273
13413
|
const health = healthRow ? mapHealthRow2(healthRow) : null;
|
|
13274
|
-
const insightRows = app.db.select().from(insights).where(
|
|
13414
|
+
const insightRows = app.db.select().from(insights).where(eq18(insights.projectId, project.id)).orderBy(desc9(insights.createdAt)).all();
|
|
13275
13415
|
const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
|
|
13276
13416
|
const sparklineRunIds = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => r.id);
|
|
13277
13417
|
const snapshotRunIds = new Set(sparklineRunIds);
|
|
13278
13418
|
for (const run of latestVisRunGroup) snapshotRunIds.add(run.id);
|
|
13279
13419
|
for (const run of previousVisRunGroup) snapshotRunIds.add(run.id);
|
|
13280
|
-
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
13420
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq18(queries.projectId, project.id)).all();
|
|
13281
13421
|
const queryIdByText = new Map(projectQueries.map((q) => [normalizeQueryText(q.query), q.id]));
|
|
13282
13422
|
const snapshotsByRun = loadSnapshotsByRunIds(app, [...snapshotRunIds], queryIdByText);
|
|
13283
13423
|
const latestSnapshots = latestVisRunGroup.flatMap((r) => snapshotsByRun.get(r.id) ?? []);
|
|
@@ -13293,7 +13433,7 @@ async function compositeRoutes(app) {
|
|
|
13293
13433
|
trackedPrevious,
|
|
13294
13434
|
previousVisibilityRun?.createdAt ?? null
|
|
13295
13435
|
);
|
|
13296
|
-
const competitorRows = app.db.select().from(competitors).where(
|
|
13436
|
+
const competitorRows = app.db.select().from(competitors).where(eq18(competitors.projectId, project.id)).all();
|
|
13297
13437
|
const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
|
|
13298
13438
|
for (const snapshots of snapshotsByRun.values()) {
|
|
13299
13439
|
for (const snapshot of snapshots) {
|
|
@@ -13411,25 +13551,25 @@ async function compositeRoutes(app) {
|
|
|
13411
13551
|
citedDomains: querySnapshots.citedDomains,
|
|
13412
13552
|
rawResponse: querySnapshots.rawResponse,
|
|
13413
13553
|
createdAt: querySnapshots.createdAt
|
|
13414
|
-
}).from(querySnapshots).innerJoin(queries,
|
|
13415
|
-
|
|
13416
|
-
|
|
13554
|
+
}).from(querySnapshots).innerJoin(queries, eq18(querySnapshots.queryId, queries.id)).where(
|
|
13555
|
+
and13(
|
|
13556
|
+
eq18(queries.projectId, project.id),
|
|
13417
13557
|
or4(
|
|
13418
|
-
|
|
13419
|
-
|
|
13420
|
-
|
|
13558
|
+
sql8`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
13559
|
+
sql8`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
13560
|
+
sql8`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
|
|
13421
13561
|
like2(queries.query, pattern)
|
|
13422
13562
|
)
|
|
13423
13563
|
)
|
|
13424
13564
|
).orderBy(desc9(querySnapshots.createdAt)).limit(limit + 1).all());
|
|
13425
13565
|
const insightMatches = app.db.select().from(insights).where(
|
|
13426
|
-
|
|
13427
|
-
|
|
13566
|
+
and13(
|
|
13567
|
+
eq18(insights.projectId, project.id),
|
|
13428
13568
|
or4(
|
|
13429
13569
|
like2(insights.title, pattern),
|
|
13430
13570
|
like2(insights.query, pattern),
|
|
13431
|
-
|
|
13432
|
-
|
|
13571
|
+
sql8`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
|
|
13572
|
+
sql8`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
|
|
13433
13573
|
)
|
|
13434
13574
|
)
|
|
13435
13575
|
).orderBy(desc9(insights.createdAt)).limit(limit + 1).all();
|
|
@@ -13634,17 +13774,17 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
|
|
|
13634
13774
|
const cutoff = new Date(Date.now() - 28 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
|
|
13635
13775
|
const rows = app.db.select({
|
|
13636
13776
|
query: gscSearchData.query,
|
|
13637
|
-
impressions:
|
|
13638
|
-
clicks:
|
|
13777
|
+
impressions: sql8`COALESCE(SUM(${gscSearchData.impressions}), 0)`,
|
|
13778
|
+
clicks: sql8`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
|
|
13639
13779
|
// Weighted average: SUM(position * impressions) / SUM(impressions).
|
|
13640
13780
|
// NULLIF guards the degenerate impressions=0 case (SQLite returns NULL,
|
|
13641
13781
|
// which the JS coerces to 0 — caught by the impression floor anyway).
|
|
13642
|
-
avgPosition:
|
|
13643
|
-
}).from(gscSearchData).where(
|
|
13644
|
-
|
|
13645
|
-
|
|
13646
|
-
|
|
13647
|
-
)).groupBy(gscSearchData.query).orderBy(
|
|
13782
|
+
avgPosition: sql8`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
|
|
13783
|
+
}).from(gscSearchData).where(and13(
|
|
13784
|
+
eq18(gscSearchData.projectId, projectId),
|
|
13785
|
+
sql8`${gscSearchData.date} >= ${cutoff}`,
|
|
13786
|
+
sql8`${gscSearchData.impressions} > 0`
|
|
13787
|
+
)).groupBy(gscSearchData.query).orderBy(sql8`SUM(${gscSearchData.impressions}) DESC`).limit(100).all();
|
|
13648
13788
|
const gscRows = rows.map((r) => ({
|
|
13649
13789
|
query: r.query,
|
|
13650
13790
|
impressions: Number(r.impressions),
|
|
@@ -13664,8 +13804,8 @@ function buildIndexCoverageScore(app, projectId) {
|
|
|
13664
13804
|
tooltip,
|
|
13665
13805
|
trend: []
|
|
13666
13806
|
};
|
|
13667
|
-
const gscRow = app.db.select().from(gscCoverageSnapshots).where(
|
|
13668
|
-
const bingRow = app.db.select().from(bingCoverageSnapshots).where(
|
|
13807
|
+
const gscRow = app.db.select().from(gscCoverageSnapshots).where(eq18(gscCoverageSnapshots.projectId, projectId)).orderBy(desc9(gscCoverageSnapshots.date)).limit(1).get();
|
|
13808
|
+
const bingRow = app.db.select().from(bingCoverageSnapshots).where(eq18(bingCoverageSnapshots.projectId, projectId)).orderBy(desc9(bingCoverageSnapshots.date)).limit(1).get();
|
|
13669
13809
|
const chosen = pickIndexCoverageRow(gscRow, bingRow);
|
|
13670
13810
|
if (!chosen) return empty;
|
|
13671
13811
|
const total = chosen.indexed + chosen.notIndexed;
|
|
@@ -13691,7 +13831,7 @@ function countGoogleDeindexedUrls(app, projectId) {
|
|
|
13691
13831
|
url: gscUrlInspections.url,
|
|
13692
13832
|
indexingState: gscUrlInspections.indexingState,
|
|
13693
13833
|
inspectedAt: gscUrlInspections.inspectedAt
|
|
13694
|
-
}).from(gscUrlInspections).where(
|
|
13834
|
+
}).from(gscUrlInspections).where(eq18(gscUrlInspections.projectId, projectId)).orderBy(desc9(gscUrlInspections.inspectedAt)).all();
|
|
13695
13835
|
if (rows.length === 0) return 0;
|
|
13696
13836
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
13697
13837
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -18338,7 +18478,7 @@ async function settingsRoutes(app, opts) {
|
|
|
18338
18478
|
|
|
18339
18479
|
// ../api-routes/src/keys.ts
|
|
18340
18480
|
import crypto12 from "crypto";
|
|
18341
|
-
import { desc as desc10, eq as
|
|
18481
|
+
import { desc as desc10, eq as eq19 } from "drizzle-orm";
|
|
18342
18482
|
var KEYS_WRITE_SCOPE = "keys.write";
|
|
18343
18483
|
function toApiKeyDto(row) {
|
|
18344
18484
|
const scopes = Array.isArray(row.scopes) ? row.scopes : [];
|
|
@@ -18347,6 +18487,7 @@ function toApiKeyDto(row) {
|
|
|
18347
18487
|
name: row.name,
|
|
18348
18488
|
keyPrefix: row.keyPrefix,
|
|
18349
18489
|
scopes,
|
|
18490
|
+
projectId: row.projectId ?? null,
|
|
18350
18491
|
readOnly: isReadOnlyKey(scopes),
|
|
18351
18492
|
createdAt: row.createdAt,
|
|
18352
18493
|
lastUsedAt: row.lastUsedAt ?? null,
|
|
@@ -18354,8 +18495,9 @@ function toApiKeyDto(row) {
|
|
|
18354
18495
|
};
|
|
18355
18496
|
}
|
|
18356
18497
|
async function keysRoutes(app) {
|
|
18357
|
-
app.get("/keys", async () => {
|
|
18358
|
-
const
|
|
18498
|
+
app.get("/keys", async (request) => {
|
|
18499
|
+
const scopedProjectId = request.apiKey?.projectId;
|
|
18500
|
+
const rows = scopedProjectId ? app.db.select().from(apiKeys).where(eq19(apiKeys.projectId, scopedProjectId)).orderBy(desc10(apiKeys.createdAt)).all() : app.db.select().from(apiKeys).orderBy(desc10(apiKeys.createdAt)).all();
|
|
18359
18501
|
return { keys: rows.map(toApiKeyDto) };
|
|
18360
18502
|
});
|
|
18361
18503
|
app.get("/keys/self", async (request) => {
|
|
@@ -18363,7 +18505,7 @@ async function keysRoutes(app) {
|
|
|
18363
18505
|
if (!id) {
|
|
18364
18506
|
throw notFound("API key", "self");
|
|
18365
18507
|
}
|
|
18366
|
-
const row = app.db.select().from(apiKeys).where(
|
|
18508
|
+
const row = app.db.select().from(apiKeys).where(eq19(apiKeys.id, id)).get();
|
|
18367
18509
|
if (!row) {
|
|
18368
18510
|
throw notFound("API key", id);
|
|
18369
18511
|
}
|
|
@@ -18375,7 +18517,17 @@ async function keysRoutes(app) {
|
|
|
18375
18517
|
if (!parsed.success) {
|
|
18376
18518
|
throw validationError("Invalid API key request", { issues: parsed.error.issues });
|
|
18377
18519
|
}
|
|
18378
|
-
const { name, scopes } = parsed.data;
|
|
18520
|
+
const { name, scopes, projectId } = parsed.data;
|
|
18521
|
+
const requesterProjectId = request.apiKey?.projectId;
|
|
18522
|
+
if (requesterProjectId && projectId !== requesterProjectId) {
|
|
18523
|
+
throw forbidden("A project-scoped key can only mint keys scoped to its own project.");
|
|
18524
|
+
}
|
|
18525
|
+
if (projectId) {
|
|
18526
|
+
const proj = app.db.select({ id: projects.id }).from(projects).where(eq19(projects.id, projectId)).get();
|
|
18527
|
+
if (!proj) {
|
|
18528
|
+
throw notFound("Project", projectId);
|
|
18529
|
+
}
|
|
18530
|
+
}
|
|
18379
18531
|
const raw = `cnry_${crypto12.randomBytes(16).toString("hex")}`;
|
|
18380
18532
|
const keyHash = hashApiKey(raw);
|
|
18381
18533
|
const keyPrefix = raw.slice(0, 9);
|
|
@@ -18389,6 +18541,7 @@ async function keysRoutes(app) {
|
|
|
18389
18541
|
keyHash,
|
|
18390
18542
|
keyPrefix,
|
|
18391
18543
|
scopes: effectiveScopes,
|
|
18544
|
+
projectId: projectId ?? null,
|
|
18392
18545
|
createdAt: now
|
|
18393
18546
|
}).run();
|
|
18394
18547
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
@@ -18396,7 +18549,7 @@ async function keysRoutes(app) {
|
|
|
18396
18549
|
action: "api-key.created",
|
|
18397
18550
|
entityType: "api-key",
|
|
18398
18551
|
entityId: id,
|
|
18399
|
-
diff: { name, keyPrefix, scopes: effectiveScopes }
|
|
18552
|
+
diff: { name, keyPrefix, scopes: effectiveScopes, projectId: projectId ?? null }
|
|
18400
18553
|
}));
|
|
18401
18554
|
});
|
|
18402
18555
|
const dto = {
|
|
@@ -18404,6 +18557,7 @@ async function keysRoutes(app) {
|
|
|
18404
18557
|
name,
|
|
18405
18558
|
keyPrefix,
|
|
18406
18559
|
scopes: effectiveScopes,
|
|
18560
|
+
projectId: projectId ?? null,
|
|
18407
18561
|
readOnly: isReadOnlyKey(effectiveScopes),
|
|
18408
18562
|
createdAt: now,
|
|
18409
18563
|
lastUsedAt: null,
|
|
@@ -18415,19 +18569,23 @@ async function keysRoutes(app) {
|
|
|
18415
18569
|
app.post("/keys/:id/revoke", async (request) => {
|
|
18416
18570
|
requireScope(request, KEYS_WRITE_SCOPE);
|
|
18417
18571
|
const { id } = request.params;
|
|
18418
|
-
const row = app.db.select().from(apiKeys).where(
|
|
18572
|
+
const row = app.db.select().from(apiKeys).where(eq19(apiKeys.id, id)).get();
|
|
18419
18573
|
if (!row) {
|
|
18420
18574
|
throw notFound("API key", id);
|
|
18421
18575
|
}
|
|
18422
18576
|
if (request.apiKey?.id === id) {
|
|
18423
18577
|
throw validationError("Cannot revoke the API key you are currently authenticating with");
|
|
18424
18578
|
}
|
|
18579
|
+
const requesterProjectId = request.apiKey?.projectId;
|
|
18580
|
+
if (requesterProjectId && row.projectId !== requesterProjectId) {
|
|
18581
|
+
throw forbidden("A project-scoped key can only revoke keys scoped to its own project.");
|
|
18582
|
+
}
|
|
18425
18583
|
if (row.revokedAt) {
|
|
18426
18584
|
return toApiKeyDto(row);
|
|
18427
18585
|
}
|
|
18428
18586
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18429
18587
|
app.db.transaction((tx) => {
|
|
18430
|
-
tx.update(apiKeys).set({ revokedAt: now }).where(
|
|
18588
|
+
tx.update(apiKeys).set({ revokedAt: now }).where(eq19(apiKeys.id, id)).run();
|
|
18431
18589
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
18432
18590
|
actor: "api",
|
|
18433
18591
|
action: "api-key.revoked",
|
|
@@ -18500,7 +18658,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
18500
18658
|
|
|
18501
18659
|
// ../api-routes/src/schedules.ts
|
|
18502
18660
|
import crypto13 from "crypto";
|
|
18503
|
-
import { and as
|
|
18661
|
+
import { and as and14, eq as eq20 } from "drizzle-orm";
|
|
18504
18662
|
function parseKindParam(raw) {
|
|
18505
18663
|
if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
|
|
18506
18664
|
const parsed = schedulableRunKindSchema.safeParse(raw);
|
|
@@ -18527,7 +18685,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
18527
18685
|
if (!sourceId) {
|
|
18528
18686
|
throw validationError('"sourceId" is required when kind is "traffic-sync"');
|
|
18529
18687
|
}
|
|
18530
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
18688
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq20(trafficSources.id, sourceId)).get();
|
|
18531
18689
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
18532
18690
|
throw notFound("Traffic source", sourceId);
|
|
18533
18691
|
}
|
|
@@ -18572,7 +18730,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
18572
18730
|
}
|
|
18573
18731
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18574
18732
|
const enabledBool = enabled !== false;
|
|
18575
|
-
const existing = app.db.select().from(schedules).where(
|
|
18733
|
+
const existing = app.db.select().from(schedules).where(and14(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
|
|
18576
18734
|
if (existing) {
|
|
18577
18735
|
app.db.update(schedules).set({
|
|
18578
18736
|
cronExpr,
|
|
@@ -18582,7 +18740,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
18582
18740
|
sourceId: sourceId ?? null,
|
|
18583
18741
|
enabled: enabledBool,
|
|
18584
18742
|
updatedAt: now
|
|
18585
|
-
}).where(
|
|
18743
|
+
}).where(eq20(schedules.id, existing.id)).run();
|
|
18586
18744
|
} else {
|
|
18587
18745
|
app.db.insert(schedules).values({
|
|
18588
18746
|
id: crypto13.randomUUID(),
|
|
@@ -18606,13 +18764,13 @@ async function scheduleRoutes(app, opts) {
|
|
|
18606
18764
|
diff: { kind, cronExpr, preset, timezone, providers, sourceId }
|
|
18607
18765
|
});
|
|
18608
18766
|
opts.onScheduleUpdated?.("upsert", project.id, kind);
|
|
18609
|
-
const schedule = app.db.select().from(schedules).where(
|
|
18767
|
+
const schedule = app.db.select().from(schedules).where(and14(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
|
|
18610
18768
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
18611
18769
|
});
|
|
18612
18770
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
18613
18771
|
const project = resolveProject(app.db, request.params.name);
|
|
18614
18772
|
const kind = parseKindParam(request.query?.kind);
|
|
18615
|
-
const schedule = app.db.select().from(schedules).where(
|
|
18773
|
+
const schedule = app.db.select().from(schedules).where(and14(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
|
|
18616
18774
|
if (!schedule) {
|
|
18617
18775
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
18618
18776
|
}
|
|
@@ -18621,11 +18779,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
18621
18779
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
18622
18780
|
const project = resolveProject(app.db, request.params.name);
|
|
18623
18781
|
const kind = parseKindParam(request.query?.kind);
|
|
18624
|
-
const schedule = app.db.select().from(schedules).where(
|
|
18782
|
+
const schedule = app.db.select().from(schedules).where(and14(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
|
|
18625
18783
|
if (!schedule) {
|
|
18626
18784
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
18627
18785
|
}
|
|
18628
|
-
app.db.delete(schedules).where(
|
|
18786
|
+
app.db.delete(schedules).where(eq20(schedules.id, schedule.id)).run();
|
|
18629
18787
|
writeAuditLog(app.db, {
|
|
18630
18788
|
projectId: project.id,
|
|
18631
18789
|
actor: "api",
|
|
@@ -18658,7 +18816,7 @@ function formatSchedule(row) {
|
|
|
18658
18816
|
|
|
18659
18817
|
// ../api-routes/src/notifications.ts
|
|
18660
18818
|
import crypto14 from "crypto";
|
|
18661
|
-
import { eq as
|
|
18819
|
+
import { eq as eq21 } from "drizzle-orm";
|
|
18662
18820
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
18663
18821
|
async function notificationRoutes(app, opts = {}) {
|
|
18664
18822
|
const allowLoopback = opts.allowLoopbackWebhooks === true;
|
|
@@ -18698,22 +18856,22 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
18698
18856
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
18699
18857
|
});
|
|
18700
18858
|
return reply.status(201).send({
|
|
18701
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
18859
|
+
...formatNotification(app.db.select().from(notifications).where(eq21(notifications.id, id)).get()),
|
|
18702
18860
|
webhookSecret
|
|
18703
18861
|
});
|
|
18704
18862
|
});
|
|
18705
18863
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
18706
18864
|
const project = resolveProject(app.db, request.params.name);
|
|
18707
|
-
const rows = app.db.select().from(notifications).where(
|
|
18865
|
+
const rows = app.db.select().from(notifications).where(eq21(notifications.projectId, project.id)).all();
|
|
18708
18866
|
return reply.send(rows.map(formatNotification));
|
|
18709
18867
|
});
|
|
18710
18868
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
18711
18869
|
const project = resolveProject(app.db, request.params.name);
|
|
18712
|
-
const notification = app.db.select().from(notifications).where(
|
|
18870
|
+
const notification = app.db.select().from(notifications).where(eq21(notifications.id, request.params.id)).get();
|
|
18713
18871
|
if (!notification || notification.projectId !== project.id) {
|
|
18714
18872
|
throw notFound("Notification", request.params.id);
|
|
18715
18873
|
}
|
|
18716
|
-
app.db.delete(notifications).where(
|
|
18874
|
+
app.db.delete(notifications).where(eq21(notifications.id, notification.id)).run();
|
|
18717
18875
|
writeAuditLog(app.db, {
|
|
18718
18876
|
projectId: project.id,
|
|
18719
18877
|
actor: "api",
|
|
@@ -18725,7 +18883,7 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
18725
18883
|
});
|
|
18726
18884
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
18727
18885
|
const project = resolveProject(app.db, request.params.name);
|
|
18728
|
-
const notification = app.db.select().from(notifications).where(
|
|
18886
|
+
const notification = app.db.select().from(notifications).where(eq21(notifications.id, request.params.id)).get();
|
|
18729
18887
|
if (!notification || notification.projectId !== project.id) {
|
|
18730
18888
|
throw notFound("Notification", request.params.id);
|
|
18731
18889
|
}
|
|
@@ -18778,7 +18936,7 @@ function formatNotification(row) {
|
|
|
18778
18936
|
|
|
18779
18937
|
// ../api-routes/src/google.ts
|
|
18780
18938
|
import crypto18 from "crypto";
|
|
18781
|
-
import { eq as
|
|
18939
|
+
import { eq as eq22, and as and15, desc as desc11, sql as sql9, inArray as inArray10 } from "drizzle-orm";
|
|
18782
18940
|
|
|
18783
18941
|
// ../api-routes/src/gbp-summary.ts
|
|
18784
18942
|
function computeMetricTotals(rows) {
|
|
@@ -20611,7 +20769,7 @@ async function googleRoutes(app, opts) {
|
|
|
20611
20769
|
if (!projectId) {
|
|
20612
20770
|
return reply.status(400).send("Stale OAuth state \u2014 restart the connect flow.");
|
|
20613
20771
|
}
|
|
20614
|
-
const project = app.db.select().from(projects).where(
|
|
20772
|
+
const project = app.db.select().from(projects).where(eq22(projects.id, projectId)).get();
|
|
20615
20773
|
if (!project) {
|
|
20616
20774
|
return reply.status(400).send("Project no longer exists. Restart the connect flow.");
|
|
20617
20775
|
}
|
|
@@ -20743,22 +20901,22 @@ async function googleRoutes(app, opts) {
|
|
|
20743
20901
|
if (opts.onGscSyncRequested) {
|
|
20744
20902
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
20745
20903
|
}
|
|
20746
|
-
const run = app.db.select().from(runs).where(
|
|
20904
|
+
const run = app.db.select().from(runs).where(eq22(runs.id, runId)).get();
|
|
20747
20905
|
return run;
|
|
20748
20906
|
});
|
|
20749
20907
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
20750
20908
|
const project = resolveProject(app.db, request.params.name);
|
|
20751
20909
|
const { startDate, endDate, query, page, limit, offset } = request.query;
|
|
20752
20910
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
20753
|
-
const conditions = [
|
|
20754
|
-
if (startDate) conditions.push(
|
|
20755
|
-
else if (cutoffDate) conditions.push(
|
|
20756
|
-
if (endDate) conditions.push(
|
|
20757
|
-
if (query) conditions.push(
|
|
20758
|
-
if (page) conditions.push(
|
|
20911
|
+
const conditions = [eq22(gscSearchData.projectId, project.id)];
|
|
20912
|
+
if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
|
|
20913
|
+
else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
|
|
20914
|
+
if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
|
|
20915
|
+
if (query) conditions.push(sql9`${gscSearchData.query} LIKE ${"%" + escapeLikePattern(query) + "%"} ESCAPE '\\'`);
|
|
20916
|
+
if (page) conditions.push(sql9`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
|
|
20759
20917
|
const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
|
|
20760
20918
|
const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
|
|
20761
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
20919
|
+
const rows = app.db.select().from(gscSearchData).where(and15(...conditions)).orderBy(desc11(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
|
|
20762
20920
|
return rows.map((r) => ({
|
|
20763
20921
|
date: r.date,
|
|
20764
20922
|
query: r.query,
|
|
@@ -20775,20 +20933,31 @@ async function googleRoutes(app, opts) {
|
|
|
20775
20933
|
const project = resolveProject(app.db, request.params.name);
|
|
20776
20934
|
const { startDate, endDate } = request.query;
|
|
20777
20935
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
20778
|
-
const
|
|
20779
|
-
|
|
20780
|
-
|
|
20781
|
-
|
|
20782
|
-
|
|
20936
|
+
const windowStart = startDate ?? cutoffDate ?? "";
|
|
20937
|
+
const windowEnd = endDate ?? "9999-12-31";
|
|
20938
|
+
const dailyTotals = readGscDailyTotals(app.db, project.id, windowStart, windowEnd);
|
|
20939
|
+
const conditions = [eq22(gscSearchData.projectId, project.id)];
|
|
20940
|
+
if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
|
|
20941
|
+
else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
|
|
20942
|
+
if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
|
|
20943
|
+
const dimensionedRows = app.db.select({
|
|
20783
20944
|
date: gscSearchData.date,
|
|
20784
|
-
clicks:
|
|
20785
|
-
impressions:
|
|
20786
|
-
}).from(gscSearchData).where(
|
|
20787
|
-
const daily =
|
|
20788
|
-
|
|
20789
|
-
|
|
20790
|
-
|
|
20791
|
-
|
|
20945
|
+
clicks: sql9`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
|
|
20946
|
+
impressions: sql9`COALESCE(SUM(${gscSearchData.impressions}), 0)`
|
|
20947
|
+
}).from(gscSearchData).where(and15(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
|
|
20948
|
+
const daily = mergeGscDailyTotalsWithFallback(
|
|
20949
|
+
dailyTotals,
|
|
20950
|
+
dimensionedRows.map((r) => ({
|
|
20951
|
+
date: r.date,
|
|
20952
|
+
clicks: r.clicks,
|
|
20953
|
+
impressions: r.impressions,
|
|
20954
|
+
position: 0
|
|
20955
|
+
}))
|
|
20956
|
+
).map((d) => ({
|
|
20957
|
+
date: d.date,
|
|
20958
|
+
clicks: d.clicks,
|
|
20959
|
+
impressions: d.impressions,
|
|
20960
|
+
ctr: d.impressions > 0 ? d.clicks / d.impressions : 0
|
|
20792
20961
|
}));
|
|
20793
20962
|
const totalClicks = daily.reduce((sum, d) => sum + d.clicks, 0);
|
|
20794
20963
|
const totalImpressions = daily.reduce((sum, d) => sum + d.impressions, 0);
|
|
@@ -20866,9 +21035,9 @@ async function googleRoutes(app, opts) {
|
|
|
20866
21035
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
20867
21036
|
const project = resolveProject(app.db, request.params.name);
|
|
20868
21037
|
const { url, limit } = request.query;
|
|
20869
|
-
const conditions = [
|
|
20870
|
-
if (url) conditions.push(
|
|
20871
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
21038
|
+
const conditions = [eq22(gscUrlInspections.projectId, project.id)];
|
|
21039
|
+
if (url) conditions.push(eq22(gscUrlInspections.url, url));
|
|
21040
|
+
const rows = app.db.select().from(gscUrlInspections).where(and15(...conditions)).orderBy(desc11(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
20872
21041
|
return rows.map((r) => ({
|
|
20873
21042
|
id: r.id,
|
|
20874
21043
|
url: r.url,
|
|
@@ -20887,7 +21056,7 @@ async function googleRoutes(app, opts) {
|
|
|
20887
21056
|
});
|
|
20888
21057
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
20889
21058
|
const project = resolveProject(app.db, request.params.name);
|
|
20890
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
21059
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
20891
21060
|
const byUrl = /* @__PURE__ */ new Map();
|
|
20892
21061
|
for (const row of allInspections) {
|
|
20893
21062
|
const existing = byUrl.get(row.url);
|
|
@@ -20915,7 +21084,7 @@ async function googleRoutes(app, opts) {
|
|
|
20915
21084
|
});
|
|
20916
21085
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
20917
21086
|
const project = resolveProject(app.db, request.params.name);
|
|
20918
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
21087
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
20919
21088
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
20920
21089
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
20921
21090
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -20964,7 +21133,7 @@ async function googleRoutes(app, opts) {
|
|
|
20964
21133
|
const total = latestByUrl.size;
|
|
20965
21134
|
const indexed = indexedUrls.length;
|
|
20966
21135
|
const notIndexed = notIndexedUrls.length;
|
|
20967
|
-
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(
|
|
21136
|
+
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(eq22(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.createdAt)).limit(1).get();
|
|
20968
21137
|
const lastSyncedAt = latestSnapshot?.createdAt ?? null;
|
|
20969
21138
|
const formatRow = (r) => ({
|
|
20970
21139
|
id: r.id,
|
|
@@ -21015,7 +21184,7 @@ async function googleRoutes(app, opts) {
|
|
|
21015
21184
|
const project = resolveProject(app.db, request.params.name);
|
|
21016
21185
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
21017
21186
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
21018
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
21187
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq22(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.date)).limit(limit).all();
|
|
21019
21188
|
return rows.map((r) => ({
|
|
21020
21189
|
date: r.date,
|
|
21021
21190
|
indexed: r.indexed,
|
|
@@ -21084,7 +21253,7 @@ async function googleRoutes(app, opts) {
|
|
|
21084
21253
|
if (opts.onInspectSitemapRequested) {
|
|
21085
21254
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
21086
21255
|
}
|
|
21087
|
-
const run = app.db.select().from(runs).where(
|
|
21256
|
+
const run = app.db.select().from(runs).where(eq22(runs.id, runId)).get();
|
|
21088
21257
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
21089
21258
|
});
|
|
21090
21259
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
|
|
@@ -21111,7 +21280,7 @@ async function googleRoutes(app, opts) {
|
|
|
21111
21280
|
if (opts.onInspectSitemapRequested) {
|
|
21112
21281
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
21113
21282
|
}
|
|
21114
|
-
const run = app.db.select().from(runs).where(
|
|
21283
|
+
const run = app.db.select().from(runs).where(eq22(runs.id, runId)).get();
|
|
21115
21284
|
return run;
|
|
21116
21285
|
});
|
|
21117
21286
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
|
|
@@ -21158,7 +21327,7 @@ async function googleRoutes(app, opts) {
|
|
|
21158
21327
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
21159
21328
|
let urlsToNotify = request.body?.urls ?? [];
|
|
21160
21329
|
if (request.body?.allUnindexed) {
|
|
21161
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
21330
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
21162
21331
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
21163
21332
|
for (const row of allInspections) {
|
|
21164
21333
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -21276,7 +21445,7 @@ async function googleRoutes(app, opts) {
|
|
|
21276
21445
|
};
|
|
21277
21446
|
}
|
|
21278
21447
|
function listSelectionResponse(projectId) {
|
|
21279
|
-
const rows = app.db.select().from(gbpLocations).where(
|
|
21448
|
+
const rows = app.db.select().from(gbpLocations).where(eq22(gbpLocations.projectId, projectId)).all();
|
|
21280
21449
|
const dtos = rows.map(rowToDto2);
|
|
21281
21450
|
return {
|
|
21282
21451
|
locations: dtos,
|
|
@@ -21285,15 +21454,15 @@ async function googleRoutes(app, opts) {
|
|
|
21285
21454
|
};
|
|
21286
21455
|
}
|
|
21287
21456
|
function clearGbpProjectData(tx, projectId) {
|
|
21288
|
-
tx.delete(gbpDailyMetrics).where(
|
|
21289
|
-
tx.delete(gbpKeywordImpressions).where(
|
|
21290
|
-
tx.delete(gbpKeywordMonthly).where(
|
|
21291
|
-
tx.delete(gbpPlaceActions).where(
|
|
21292
|
-
tx.delete(gbpLodgingSnapshots).where(
|
|
21293
|
-
tx.delete(gbpLocations).where(
|
|
21457
|
+
tx.delete(gbpDailyMetrics).where(eq22(gbpDailyMetrics.projectId, projectId)).run();
|
|
21458
|
+
tx.delete(gbpKeywordImpressions).where(eq22(gbpKeywordImpressions.projectId, projectId)).run();
|
|
21459
|
+
tx.delete(gbpKeywordMonthly).where(eq22(gbpKeywordMonthly.projectId, projectId)).run();
|
|
21460
|
+
tx.delete(gbpPlaceActions).where(eq22(gbpPlaceActions.projectId, projectId)).run();
|
|
21461
|
+
tx.delete(gbpLodgingSnapshots).where(eq22(gbpLodgingSnapshots.projectId, projectId)).run();
|
|
21462
|
+
tx.delete(gbpLocations).where(eq22(gbpLocations.projectId, projectId)).run();
|
|
21294
21463
|
}
|
|
21295
21464
|
function currentProjectAccount(projectId) {
|
|
21296
|
-
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(
|
|
21465
|
+
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(eq22(gbpLocations.projectId, projectId)).limit(1).get();
|
|
21297
21466
|
return row?.accountName ?? null;
|
|
21298
21467
|
}
|
|
21299
21468
|
app.post("/projects/:name/gbp/locations/discover", async (request) => {
|
|
@@ -21363,7 +21532,7 @@ async function googleRoutes(app, opts) {
|
|
|
21363
21532
|
app.db.transaction((tx) => {
|
|
21364
21533
|
if (switching) clearGbpProjectData(tx, project.id);
|
|
21365
21534
|
for (const remote of remoteLocations) {
|
|
21366
|
-
const existing = tx.select().from(gbpLocations).where(
|
|
21535
|
+
const existing = tx.select().from(gbpLocations).where(and15(eq22(gbpLocations.projectId, project.id), eq22(gbpLocations.locationName, remote.name))).get();
|
|
21367
21536
|
const profile = buildLocationProfileFields(remote);
|
|
21368
21537
|
if (existing) {
|
|
21369
21538
|
tx.update(gbpLocations).set({
|
|
@@ -21376,7 +21545,7 @@ async function googleRoutes(app, opts) {
|
|
|
21376
21545
|
mapsUri: remote.metadata?.mapsUri ?? null,
|
|
21377
21546
|
...profile,
|
|
21378
21547
|
updatedAt: now
|
|
21379
|
-
}).where(
|
|
21548
|
+
}).where(eq22(gbpLocations.id, existing.id)).run();
|
|
21380
21549
|
} else {
|
|
21381
21550
|
tx.insert(gbpLocations).values({
|
|
21382
21551
|
id: crypto18.randomUUID(),
|
|
@@ -21460,11 +21629,11 @@ async function googleRoutes(app, opts) {
|
|
|
21460
21629
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid selection request");
|
|
21461
21630
|
}
|
|
21462
21631
|
const { selected } = parsed.data;
|
|
21463
|
-
const existing = app.db.select().from(gbpLocations).where(
|
|
21632
|
+
const existing = app.db.select().from(gbpLocations).where(and15(eq22(gbpLocations.projectId, project.id), eq22(gbpLocations.locationName, locationName))).get();
|
|
21464
21633
|
if (!existing) throw notFound("GBP location", locationName);
|
|
21465
21634
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21466
21635
|
app.db.transaction((tx) => {
|
|
21467
|
-
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(
|
|
21636
|
+
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(eq22(gbpLocations.id, existing.id)).run();
|
|
21468
21637
|
writeAuditLog(tx, {
|
|
21469
21638
|
projectId: project.id,
|
|
21470
21639
|
actor: "api",
|
|
@@ -21473,7 +21642,7 @@ async function googleRoutes(app, opts) {
|
|
|
21473
21642
|
entityId: locationName
|
|
21474
21643
|
});
|
|
21475
21644
|
});
|
|
21476
|
-
const refreshed = app.db.select().from(gbpLocations).where(
|
|
21645
|
+
const refreshed = app.db.select().from(gbpLocations).where(eq22(gbpLocations.id, existing.id)).get();
|
|
21477
21646
|
return rowToDto2(refreshed);
|
|
21478
21647
|
});
|
|
21479
21648
|
app.delete("/projects/:name/gbp/connection", async (request, reply) => {
|
|
@@ -21517,10 +21686,10 @@ async function googleRoutes(app, opts) {
|
|
|
21517
21686
|
});
|
|
21518
21687
|
app.get("/projects/:name/gbp/metrics", async (request) => {
|
|
21519
21688
|
const project = resolveProject(app.db, request.params.name);
|
|
21520
|
-
const conditions = [
|
|
21521
|
-
if (request.query.locationName) conditions.push(
|
|
21522
|
-
if (request.query.metric) conditions.push(
|
|
21523
|
-
const rows = app.db.select().from(gbpDailyMetrics).where(
|
|
21689
|
+
const conditions = [eq22(gbpDailyMetrics.projectId, project.id)];
|
|
21690
|
+
if (request.query.locationName) conditions.push(eq22(gbpDailyMetrics.locationName, request.query.locationName));
|
|
21691
|
+
if (request.query.metric) conditions.push(eq22(gbpDailyMetrics.metric, request.query.metric));
|
|
21692
|
+
const rows = app.db.select().from(gbpDailyMetrics).where(and15(...conditions)).orderBy(desc11(gbpDailyMetrics.date)).all();
|
|
21524
21693
|
return {
|
|
21525
21694
|
metrics: rows.map((r) => ({ locationName: r.locationName, date: r.date, metric: r.metric, value: r.value })),
|
|
21526
21695
|
total: rows.length
|
|
@@ -21528,9 +21697,9 @@ async function googleRoutes(app, opts) {
|
|
|
21528
21697
|
});
|
|
21529
21698
|
app.get("/projects/:name/gbp/keywords", async (request) => {
|
|
21530
21699
|
const project = resolveProject(app.db, request.params.name);
|
|
21531
|
-
const conditions = [
|
|
21532
|
-
if (request.query.locationName) conditions.push(
|
|
21533
|
-
const rows = app.db.select().from(gbpKeywordImpressions).where(
|
|
21700
|
+
const conditions = [eq22(gbpKeywordImpressions.projectId, project.id)];
|
|
21701
|
+
if (request.query.locationName) conditions.push(eq22(gbpKeywordImpressions.locationName, request.query.locationName));
|
|
21702
|
+
const rows = app.db.select().from(gbpKeywordImpressions).where(and15(...conditions)).all();
|
|
21534
21703
|
rows.sort((a, b) => (b.valueCount ?? -1) - (a.valueCount ?? -1));
|
|
21535
21704
|
const thresholded = rows.filter((r) => r.valueThreshold !== null).length;
|
|
21536
21705
|
return {
|
|
@@ -21548,9 +21717,9 @@ async function googleRoutes(app, opts) {
|
|
|
21548
21717
|
});
|
|
21549
21718
|
app.get("/projects/:name/gbp/place-actions", async (request) => {
|
|
21550
21719
|
const project = resolveProject(app.db, request.params.name);
|
|
21551
|
-
const conditions = [
|
|
21552
|
-
if (request.query.locationName) conditions.push(
|
|
21553
|
-
const rows = app.db.select().from(gbpPlaceActions).where(
|
|
21720
|
+
const conditions = [eq22(gbpPlaceActions.projectId, project.id)];
|
|
21721
|
+
if (request.query.locationName) conditions.push(eq22(gbpPlaceActions.locationName, request.query.locationName));
|
|
21722
|
+
const rows = app.db.select().from(gbpPlaceActions).where(and15(...conditions)).all();
|
|
21554
21723
|
return {
|
|
21555
21724
|
placeActions: rows.map((r) => ({
|
|
21556
21725
|
locationName: r.locationName,
|
|
@@ -21565,9 +21734,9 @@ async function googleRoutes(app, opts) {
|
|
|
21565
21734
|
});
|
|
21566
21735
|
app.get("/projects/:name/gbp/lodging", async (request) => {
|
|
21567
21736
|
const project = resolveProject(app.db, request.params.name);
|
|
21568
|
-
const conditions = [
|
|
21569
|
-
if (request.query.locationName) conditions.push(
|
|
21570
|
-
const rows = app.db.select().from(gbpLodgingSnapshots).where(
|
|
21737
|
+
const conditions = [eq22(gbpLodgingSnapshots.projectId, project.id)];
|
|
21738
|
+
if (request.query.locationName) conditions.push(eq22(gbpLodgingSnapshots.locationName, request.query.locationName));
|
|
21739
|
+
const rows = app.db.select().from(gbpLodgingSnapshots).where(and15(...conditions)).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
21571
21740
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
21572
21741
|
for (const row of rows) {
|
|
21573
21742
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -21582,9 +21751,9 @@ async function googleRoutes(app, opts) {
|
|
|
21582
21751
|
});
|
|
21583
21752
|
app.get("/projects/:name/gbp/attributes", async (request) => {
|
|
21584
21753
|
const project = resolveProject(app.db, request.params.name);
|
|
21585
|
-
const conditions = [
|
|
21586
|
-
if (request.query.locationName) conditions.push(
|
|
21587
|
-
const rows = app.db.select().from(gbpAttributesSnapshots).where(
|
|
21754
|
+
const conditions = [eq22(gbpAttributesSnapshots.projectId, project.id)];
|
|
21755
|
+
if (request.query.locationName) conditions.push(eq22(gbpAttributesSnapshots.locationName, request.query.locationName));
|
|
21756
|
+
const rows = app.db.select().from(gbpAttributesSnapshots).where(and15(...conditions)).orderBy(desc11(gbpAttributesSnapshots.syncedAt)).all();
|
|
21588
21757
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
21589
21758
|
for (const row of rows) {
|
|
21590
21759
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -21602,9 +21771,9 @@ async function googleRoutes(app, opts) {
|
|
|
21602
21771
|
});
|
|
21603
21772
|
app.get("/projects/:name/gbp/places", async (request) => {
|
|
21604
21773
|
const project = resolveProject(app.db, request.params.name);
|
|
21605
|
-
const conditions = [
|
|
21606
|
-
if (request.query.locationName) conditions.push(
|
|
21607
|
-
const rows = app.db.select().from(gbpPlaceDetails).where(
|
|
21774
|
+
const conditions = [eq22(gbpPlaceDetails.projectId, project.id)];
|
|
21775
|
+
if (request.query.locationName) conditions.push(eq22(gbpPlaceDetails.locationName, request.query.locationName));
|
|
21776
|
+
const rows = app.db.select().from(gbpPlaceDetails).where(and15(...conditions)).orderBy(desc11(gbpPlaceDetails.syncedAt)).all();
|
|
21608
21777
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
21609
21778
|
for (const row of rows) {
|
|
21610
21779
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -21623,7 +21792,7 @@ async function googleRoutes(app, opts) {
|
|
|
21623
21792
|
app.get("/projects/:name/gbp/summary", async (request) => {
|
|
21624
21793
|
const project = resolveProject(app.db, request.params.name);
|
|
21625
21794
|
const locationName = request.query.locationName ?? null;
|
|
21626
|
-
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(
|
|
21795
|
+
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and15(eq22(gbpLocations.projectId, project.id), eq22(gbpLocations.selected, true))).all().map((r) => r.n);
|
|
21627
21796
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
21628
21797
|
if (locationNames.length === 0) {
|
|
21629
21798
|
return buildGbpSummary({
|
|
@@ -21637,10 +21806,10 @@ async function googleRoutes(app, opts) {
|
|
|
21637
21806
|
locationProfiles: []
|
|
21638
21807
|
});
|
|
21639
21808
|
}
|
|
21640
|
-
const metricRows = app.db.select().from(gbpDailyMetrics).where(
|
|
21641
|
-
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(
|
|
21642
|
-
const placeActionRows = app.db.select().from(gbpPlaceActions).where(
|
|
21643
|
-
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(
|
|
21809
|
+
const metricRows = app.db.select().from(gbpDailyMetrics).where(and15(eq22(gbpDailyMetrics.projectId, project.id), inArray10(gbpDailyMetrics.locationName, locationNames))).all();
|
|
21810
|
+
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and15(eq22(gbpKeywordImpressions.projectId, project.id), inArray10(gbpKeywordImpressions.locationName, locationNames))).all();
|
|
21811
|
+
const placeActionRows = app.db.select().from(gbpPlaceActions).where(and15(eq22(gbpPlaceActions.projectId, project.id), inArray10(gbpPlaceActions.locationName, locationNames))).all();
|
|
21812
|
+
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and15(eq22(gbpLodgingSnapshots.projectId, project.id), inArray10(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
21644
21813
|
const latestLodgingByLocation = /* @__PURE__ */ new Map();
|
|
21645
21814
|
for (const row of lodgingRows) {
|
|
21646
21815
|
if (!latestLodgingByLocation.has(row.locationName)) {
|
|
@@ -21654,7 +21823,7 @@ async function googleRoutes(app, opts) {
|
|
|
21654
21823
|
regularHours: gbpLocations.regularHours,
|
|
21655
21824
|
primaryPhone: gbpLocations.primaryPhone,
|
|
21656
21825
|
openStatus: gbpLocations.openStatus
|
|
21657
|
-
}).from(gbpLocations).where(
|
|
21826
|
+
}).from(gbpLocations).where(and15(eq22(gbpLocations.projectId, project.id), inArray10(gbpLocations.locationName, locationNames))).all();
|
|
21658
21827
|
return buildGbpSummary({
|
|
21659
21828
|
locationName,
|
|
21660
21829
|
locationCount: locationNames.length,
|
|
@@ -21670,7 +21839,7 @@ async function googleRoutes(app, opts) {
|
|
|
21670
21839
|
|
|
21671
21840
|
// ../api-routes/src/ads.ts
|
|
21672
21841
|
import crypto19 from "crypto";
|
|
21673
|
-
import { eq as
|
|
21842
|
+
import { eq as eq23, and as and16, asc as asc3, gte as gte3, lte as lte2, inArray as inArray11 } from "drizzle-orm";
|
|
21674
21843
|
function statusDto(row) {
|
|
21675
21844
|
if (!row) return { connected: false };
|
|
21676
21845
|
return {
|
|
@@ -21720,7 +21889,7 @@ async function adsRoutes(app, opts) {
|
|
|
21720
21889
|
createdAt: existingCfg?.createdAt ?? now,
|
|
21721
21890
|
updatedAt: now
|
|
21722
21891
|
});
|
|
21723
|
-
const existingRow = app.db.select().from(adsConnections).where(
|
|
21892
|
+
const existingRow = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
21724
21893
|
app.db.transaction((tx) => {
|
|
21725
21894
|
if (existingRow) {
|
|
21726
21895
|
tx.update(adsConnections).set({
|
|
@@ -21730,7 +21899,7 @@ async function adsRoutes(app, opts) {
|
|
|
21730
21899
|
timezone: account.timezone,
|
|
21731
21900
|
status: account.status,
|
|
21732
21901
|
updatedAt: now
|
|
21733
|
-
}).where(
|
|
21902
|
+
}).where(eq23(adsConnections.id, existingRow.id)).run();
|
|
21734
21903
|
} else {
|
|
21735
21904
|
tx.insert(adsConnections).values({
|
|
21736
21905
|
id: crypto19.randomUUID(),
|
|
@@ -21752,16 +21921,16 @@ async function adsRoutes(app, opts) {
|
|
|
21752
21921
|
entityId: account.id
|
|
21753
21922
|
}));
|
|
21754
21923
|
});
|
|
21755
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21924
|
+
const row = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
21756
21925
|
return statusDto(row);
|
|
21757
21926
|
}
|
|
21758
21927
|
);
|
|
21759
21928
|
app.delete("/projects/:name/ads/connection", async (request) => {
|
|
21760
21929
|
const project = resolveProject(app.db, request.params.name);
|
|
21761
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21930
|
+
const row = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
21762
21931
|
if (row) {
|
|
21763
21932
|
app.db.transaction((tx) => {
|
|
21764
|
-
tx.delete(adsConnections).where(
|
|
21933
|
+
tx.delete(adsConnections).where(eq23(adsConnections.id, row.id)).run();
|
|
21765
21934
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
21766
21935
|
projectId: project.id,
|
|
21767
21936
|
actor: "api",
|
|
@@ -21777,18 +21946,18 @@ async function adsRoutes(app, opts) {
|
|
|
21777
21946
|
});
|
|
21778
21947
|
app.get("/projects/:name/ads/status", async (request) => {
|
|
21779
21948
|
const project = resolveProject(app.db, request.params.name);
|
|
21780
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21949
|
+
const row = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
21781
21950
|
return statusDto(row);
|
|
21782
21951
|
});
|
|
21783
21952
|
app.post("/projects/:name/ads/sync", async (request) => {
|
|
21784
21953
|
const project = resolveProject(app.db, request.params.name);
|
|
21785
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21954
|
+
const row = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
21786
21955
|
if (!row) {
|
|
21787
21956
|
throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
|
|
21788
21957
|
}
|
|
21789
|
-
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
21790
|
-
|
|
21791
|
-
|
|
21958
|
+
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and16(
|
|
21959
|
+
eq23(runs.projectId, project.id),
|
|
21960
|
+
eq23(runs.kind, RunKinds["ads-sync"]),
|
|
21792
21961
|
inArray11(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
21793
21962
|
)).get();
|
|
21794
21963
|
if (inFlight) {
|
|
@@ -21810,9 +21979,9 @@ async function adsRoutes(app, opts) {
|
|
|
21810
21979
|
});
|
|
21811
21980
|
app.get("/projects/:name/ads/campaigns", async (request) => {
|
|
21812
21981
|
const project = resolveProject(app.db, request.params.name);
|
|
21813
|
-
const campaignRows = app.db.select().from(adsCampaigns).where(
|
|
21814
|
-
const groupRows = app.db.select().from(adsAdGroups).where(
|
|
21815
|
-
const adRows = app.db.select().from(adsAds).where(
|
|
21982
|
+
const campaignRows = app.db.select().from(adsCampaigns).where(eq23(adsCampaigns.projectId, project.id)).all();
|
|
21983
|
+
const groupRows = app.db.select().from(adsAdGroups).where(eq23(adsAdGroups.projectId, project.id)).all();
|
|
21984
|
+
const adRows = app.db.select().from(adsAds).where(eq23(adsAds.projectId, project.id)).all();
|
|
21816
21985
|
const adsByGroup = /* @__PURE__ */ new Map();
|
|
21817
21986
|
for (const ad of adRows) {
|
|
21818
21987
|
const dto = {
|
|
@@ -21866,12 +22035,12 @@ async function adsRoutes(app, opts) {
|
|
|
21866
22035
|
}
|
|
21867
22036
|
parsedLevel = result.data;
|
|
21868
22037
|
}
|
|
21869
|
-
const conditions = [
|
|
21870
|
-
if (parsedLevel) conditions.push(
|
|
21871
|
-
if (entityId) conditions.push(
|
|
22038
|
+
const conditions = [eq23(adsInsightsDaily.projectId, project.id)];
|
|
22039
|
+
if (parsedLevel) conditions.push(eq23(adsInsightsDaily.level, parsedLevel));
|
|
22040
|
+
if (entityId) conditions.push(eq23(adsInsightsDaily.entityId, entityId));
|
|
21872
22041
|
if (from) conditions.push(gte3(adsInsightsDaily.date, from));
|
|
21873
22042
|
if (to) conditions.push(lte2(adsInsightsDaily.date, to));
|
|
21874
|
-
const rows = app.db.select().from(adsInsightsDaily).where(
|
|
22043
|
+
const rows = app.db.select().from(adsInsightsDaily).where(and16(...conditions)).orderBy(asc3(adsInsightsDaily.date)).all();
|
|
21875
22044
|
const dtoRows = rows.map((row) => ({
|
|
21876
22045
|
level: row.level,
|
|
21877
22046
|
entityId: row.entityId,
|
|
@@ -21883,19 +22052,19 @@ async function adsRoutes(app, opts) {
|
|
|
21883
22052
|
ctr: adsCtr(row.clicks, row.impressions),
|
|
21884
22053
|
cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
|
|
21885
22054
|
}));
|
|
21886
|
-
const conn = app.db.select().from(adsConnections).where(
|
|
22055
|
+
const conn = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
21887
22056
|
const response = { rows: dtoRows, currencyCode: conn?.currencyCode ?? null };
|
|
21888
22057
|
return response;
|
|
21889
22058
|
});
|
|
21890
22059
|
app.get("/projects/:name/ads/summary", async (request) => {
|
|
21891
22060
|
const project = resolveProject(app.db, request.params.name);
|
|
21892
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21893
|
-
const campaignCount = app.db.select().from(adsCampaigns).where(
|
|
21894
|
-
const adGroupCount = app.db.select().from(adsAdGroups).where(
|
|
21895
|
-
const adCount = app.db.select().from(adsAds).where(
|
|
21896
|
-
const campaignInsights = app.db.select().from(adsInsightsDaily).where(
|
|
21897
|
-
|
|
21898
|
-
|
|
22061
|
+
const row = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
22062
|
+
const campaignCount = app.db.select().from(adsCampaigns).where(eq23(adsCampaigns.projectId, project.id)).all().length;
|
|
22063
|
+
const adGroupCount = app.db.select().from(adsAdGroups).where(eq23(adsAdGroups.projectId, project.id)).all().length;
|
|
22064
|
+
const adCount = app.db.select().from(adsAds).where(eq23(adsAds.projectId, project.id)).all().length;
|
|
22065
|
+
const campaignInsights = app.db.select().from(adsInsightsDaily).where(and16(
|
|
22066
|
+
eq23(adsInsightsDaily.projectId, project.id),
|
|
22067
|
+
eq23(adsInsightsDaily.level, "campaign")
|
|
21899
22068
|
)).all();
|
|
21900
22069
|
let impressions = 0;
|
|
21901
22070
|
let clicks = 0;
|
|
@@ -21935,7 +22104,7 @@ async function adsRoutes(app, opts) {
|
|
|
21935
22104
|
|
|
21936
22105
|
// ../api-routes/src/bing.ts
|
|
21937
22106
|
import crypto20 from "crypto";
|
|
21938
|
-
import { eq as
|
|
22107
|
+
import { eq as eq24, and as and17, desc as desc12 } from "drizzle-orm";
|
|
21939
22108
|
|
|
21940
22109
|
// ../integration-bing/src/constants.ts
|
|
21941
22110
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -22309,7 +22478,7 @@ async function bingRoutes(app, opts) {
|
|
|
22309
22478
|
const store = requireConnectionStore();
|
|
22310
22479
|
const project = resolveProject(app.db, request.params.name);
|
|
22311
22480
|
requireConnection(store, project.canonicalDomain);
|
|
22312
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
22481
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
22313
22482
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
22314
22483
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
22315
22484
|
for (const row of allInspections) {
|
|
@@ -22398,7 +22567,7 @@ async function bingRoutes(app, opts) {
|
|
|
22398
22567
|
const project = resolveProject(app.db, request.params.name);
|
|
22399
22568
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
22400
22569
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
22401
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
22570
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq24(bingCoverageSnapshots.projectId, project.id)).orderBy(desc12(bingCoverageSnapshots.date)).limit(limit).all();
|
|
22402
22571
|
return rows.map((r) => ({
|
|
22403
22572
|
date: r.date,
|
|
22404
22573
|
indexed: r.indexed,
|
|
@@ -22410,7 +22579,7 @@ async function bingRoutes(app, opts) {
|
|
|
22410
22579
|
requireConnectionStore();
|
|
22411
22580
|
const project = resolveProject(app.db, request.params.name);
|
|
22412
22581
|
const { url, limit } = request.query;
|
|
22413
|
-
const whereClause = url ?
|
|
22582
|
+
const whereClause = url ? and17(eq24(bingUrlInspections.projectId, project.id), eq24(bingUrlInspections.url, url)) : eq24(bingUrlInspections.projectId, project.id);
|
|
22414
22583
|
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc12(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
22415
22584
|
return filtered.map((r) => ({
|
|
22416
22585
|
id: r.id,
|
|
@@ -22500,7 +22669,7 @@ async function bingRoutes(app, opts) {
|
|
|
22500
22669
|
anchorCount: result.AnchorCount ?? null,
|
|
22501
22670
|
discoveryDate
|
|
22502
22671
|
}).run();
|
|
22503
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
22672
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq24(runs.id, runId)).run();
|
|
22504
22673
|
return {
|
|
22505
22674
|
id,
|
|
22506
22675
|
url,
|
|
@@ -22516,7 +22685,7 @@ async function bingRoutes(app, opts) {
|
|
|
22516
22685
|
} catch (e) {
|
|
22517
22686
|
const msg = e instanceof Error ? e.message : String(e);
|
|
22518
22687
|
bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
|
|
22519
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
22688
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
|
|
22520
22689
|
throw e;
|
|
22521
22690
|
}
|
|
22522
22691
|
});
|
|
@@ -22543,7 +22712,7 @@ async function bingRoutes(app, opts) {
|
|
|
22543
22712
|
} else {
|
|
22544
22713
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
22545
22714
|
}
|
|
22546
|
-
const run = app.db.select().from(runs).where(
|
|
22715
|
+
const run = app.db.select().from(runs).where(eq24(runs.id, runId)).get();
|
|
22547
22716
|
return run;
|
|
22548
22717
|
});
|
|
22549
22718
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -22555,7 +22724,7 @@ async function bingRoutes(app, opts) {
|
|
|
22555
22724
|
}
|
|
22556
22725
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
22557
22726
|
if (request.body?.allUnindexed) {
|
|
22558
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
22727
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
22559
22728
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
22560
22729
|
for (const row of allInspections) {
|
|
22561
22730
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -22642,18 +22811,19 @@ async function bingRoutes(app, opts) {
|
|
|
22642
22811
|
import fs from "fs";
|
|
22643
22812
|
import path from "path";
|
|
22644
22813
|
import os from "os";
|
|
22645
|
-
import { eq as
|
|
22814
|
+
import { eq as eq25, and as and18 } from "drizzle-orm";
|
|
22646
22815
|
function getScreenshotDir() {
|
|
22647
22816
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
22648
22817
|
}
|
|
22649
22818
|
async function cdpRoutes(app, opts) {
|
|
22650
22819
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
22651
22820
|
const { snapshotId } = request.params;
|
|
22652
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(
|
|
22821
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath, projectId: runs.projectId }).from(querySnapshots).innerJoin(runs, eq25(querySnapshots.runId, runs.id)).where(eq25(querySnapshots.id, snapshotId)).get();
|
|
22653
22822
|
if (!snapshot?.screenshotPath) {
|
|
22654
22823
|
const err = notFound("Screenshot", snapshotId);
|
|
22655
22824
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
22656
22825
|
}
|
|
22826
|
+
assertProjectScope(request, snapshot.projectId);
|
|
22657
22827
|
const base = path.resolve(getScreenshotDir());
|
|
22658
22828
|
const fullPath = path.resolve(path.join(base, snapshot.screenshotPath));
|
|
22659
22829
|
if (!fullPath.startsWith(base + path.sep) && fullPath !== base) {
|
|
@@ -22716,7 +22886,7 @@ async function cdpRoutes(app, opts) {
|
|
|
22716
22886
|
async (request, reply) => {
|
|
22717
22887
|
const project = resolveProject(app.db, request.params.name);
|
|
22718
22888
|
const { runId } = request.params;
|
|
22719
|
-
const run = app.db.select().from(runs).where(
|
|
22889
|
+
const run = app.db.select().from(runs).where(and18(eq25(runs.id, runId), eq25(runs.projectId, project.id))).get();
|
|
22720
22890
|
if (!run) {
|
|
22721
22891
|
const err = notFound("Run", runId);
|
|
22722
22892
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -22729,8 +22899,8 @@ async function cdpRoutes(app, opts) {
|
|
|
22729
22899
|
citedDomains: querySnapshots.citedDomains,
|
|
22730
22900
|
screenshotPath: querySnapshots.screenshotPath,
|
|
22731
22901
|
rawResponse: querySnapshots.rawResponse
|
|
22732
|
-
}).from(querySnapshots).where(
|
|
22733
|
-
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
22902
|
+
}).from(querySnapshots).where(eq25(querySnapshots.runId, runId)).all());
|
|
22903
|
+
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq25(queries.projectId, project.id)).all();
|
|
22734
22904
|
const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
|
|
22735
22905
|
const byQuery = /* @__PURE__ */ new Map();
|
|
22736
22906
|
for (const snap of snapshots) {
|
|
@@ -22813,7 +22983,7 @@ async function cdpRoutes(app, opts) {
|
|
|
22813
22983
|
|
|
22814
22984
|
// ../api-routes/src/ga.ts
|
|
22815
22985
|
import crypto21 from "crypto";
|
|
22816
|
-
import { eq as
|
|
22986
|
+
import { eq as eq26, desc as desc13, and as and19, sql as sql10 } from "drizzle-orm";
|
|
22817
22987
|
function gaLog(level, action, ctx) {
|
|
22818
22988
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
22819
22989
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -23020,10 +23190,10 @@ async function ga4Routes(app, opts) {
|
|
|
23020
23190
|
if (!saConn && !oauthConn) {
|
|
23021
23191
|
throw notFound("GA4 connection", project.name);
|
|
23022
23192
|
}
|
|
23023
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
23024
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
23025
|
-
app.db.delete(gaAiReferrals).where(
|
|
23026
|
-
app.db.delete(gaSocialReferrals).where(
|
|
23193
|
+
app.db.delete(gaTrafficSnapshots).where(eq26(gaTrafficSnapshots.projectId, project.id)).run();
|
|
23194
|
+
app.db.delete(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).run();
|
|
23195
|
+
app.db.delete(gaAiReferrals).where(eq26(gaAiReferrals.projectId, project.id)).run();
|
|
23196
|
+
app.db.delete(gaSocialReferrals).where(eq26(gaSocialReferrals.projectId, project.id)).run();
|
|
23027
23197
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
23028
23198
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
23029
23199
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -23044,7 +23214,7 @@ async function ga4Routes(app, opts) {
|
|
|
23044
23214
|
if (!connected) {
|
|
23045
23215
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
23046
23216
|
}
|
|
23047
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
23217
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
23048
23218
|
return {
|
|
23049
23219
|
connected: true,
|
|
23050
23220
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -23108,10 +23278,10 @@ async function ga4Routes(app, opts) {
|
|
|
23108
23278
|
app.db.transaction((tx) => {
|
|
23109
23279
|
if (syncTraffic) {
|
|
23110
23280
|
tx.delete(gaTrafficSnapshots).where(
|
|
23111
|
-
|
|
23112
|
-
|
|
23113
|
-
|
|
23114
|
-
|
|
23281
|
+
and19(
|
|
23282
|
+
eq26(gaTrafficSnapshots.projectId, project.id),
|
|
23283
|
+
sql10`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
23284
|
+
sql10`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
23115
23285
|
)
|
|
23116
23286
|
).run();
|
|
23117
23287
|
for (const row of rows) {
|
|
@@ -23132,10 +23302,10 @@ async function ga4Routes(app, opts) {
|
|
|
23132
23302
|
}
|
|
23133
23303
|
if (syncAi) {
|
|
23134
23304
|
tx.delete(gaAiReferrals).where(
|
|
23135
|
-
|
|
23136
|
-
|
|
23137
|
-
|
|
23138
|
-
|
|
23305
|
+
and19(
|
|
23306
|
+
eq26(gaAiReferrals.projectId, project.id),
|
|
23307
|
+
sql10`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
23308
|
+
sql10`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
23139
23309
|
)
|
|
23140
23310
|
).run();
|
|
23141
23311
|
for (const row of aiReferrals) {
|
|
@@ -23158,10 +23328,10 @@ async function ga4Routes(app, opts) {
|
|
|
23158
23328
|
}
|
|
23159
23329
|
if (syncSocial) {
|
|
23160
23330
|
tx.delete(gaSocialReferrals).where(
|
|
23161
|
-
|
|
23162
|
-
|
|
23163
|
-
|
|
23164
|
-
|
|
23331
|
+
and19(
|
|
23332
|
+
eq26(gaSocialReferrals.projectId, project.id),
|
|
23333
|
+
sql10`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
23334
|
+
sql10`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
23165
23335
|
)
|
|
23166
23336
|
).run();
|
|
23167
23337
|
for (const row of socialReferrals) {
|
|
@@ -23180,7 +23350,7 @@ async function ga4Routes(app, opts) {
|
|
|
23180
23350
|
}
|
|
23181
23351
|
}
|
|
23182
23352
|
if (syncSummary) {
|
|
23183
|
-
tx.delete(gaTrafficSummaries).where(
|
|
23353
|
+
tx.delete(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).run();
|
|
23184
23354
|
tx.insert(gaTrafficSummaries).values({
|
|
23185
23355
|
id: crypto21.randomUUID(),
|
|
23186
23356
|
projectId: project.id,
|
|
@@ -23192,7 +23362,7 @@ async function ga4Routes(app, opts) {
|
|
|
23192
23362
|
syncedAt: now,
|
|
23193
23363
|
syncRunId: runId
|
|
23194
23364
|
}).run();
|
|
23195
|
-
tx.delete(gaTrafficWindowSummaries).where(
|
|
23365
|
+
tx.delete(gaTrafficWindowSummaries).where(eq26(gaTrafficWindowSummaries.projectId, project.id)).run();
|
|
23196
23366
|
for (const ws of windowSummaries) {
|
|
23197
23367
|
tx.insert(gaTrafficWindowSummaries).values({
|
|
23198
23368
|
id: crypto21.randomUUID(),
|
|
@@ -23210,7 +23380,7 @@ async function ga4Routes(app, opts) {
|
|
|
23210
23380
|
}
|
|
23211
23381
|
}
|
|
23212
23382
|
});
|
|
23213
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
23383
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq26(runs.id, runId)).run();
|
|
23214
23384
|
const syncedComponents = only ? [
|
|
23215
23385
|
...syncTraffic ? ["traffic"] : [],
|
|
23216
23386
|
...syncSummary ? ["summary"] : [],
|
|
@@ -23239,7 +23409,7 @@ async function ga4Routes(app, opts) {
|
|
|
23239
23409
|
} catch (e) {
|
|
23240
23410
|
const msg = e instanceof Error ? e.message : String(e);
|
|
23241
23411
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
|
|
23242
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
23412
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(runs.id, runId)).run();
|
|
23243
23413
|
throw e;
|
|
23244
23414
|
}
|
|
23245
23415
|
});
|
|
@@ -23250,66 +23420,66 @@ async function ga4Routes(app, opts) {
|
|
|
23250
23420
|
const window = parseWindow(request.query.window);
|
|
23251
23421
|
const cutoff = windowCutoff(window);
|
|
23252
23422
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
23253
|
-
const snapshotConditions = [
|
|
23254
|
-
if (cutoffDate) snapshotConditions.push(
|
|
23255
|
-
const aiConditions = [
|
|
23256
|
-
if (cutoffDate) aiConditions.push(
|
|
23257
|
-
const socialConditions = [
|
|
23258
|
-
if (cutoffDate) socialConditions.push(
|
|
23423
|
+
const snapshotConditions = [eq26(gaTrafficSnapshots.projectId, project.id)];
|
|
23424
|
+
if (cutoffDate) snapshotConditions.push(sql10`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
23425
|
+
const aiConditions = [eq26(gaAiReferrals.projectId, project.id)];
|
|
23426
|
+
if (cutoffDate) aiConditions.push(sql10`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
23427
|
+
const socialConditions = [eq26(gaSocialReferrals.projectId, project.id)];
|
|
23428
|
+
if (cutoffDate) socialConditions.push(sql10`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
23259
23429
|
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
23260
23430
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
23261
23431
|
totalOrganicSessions: gaTrafficWindowSummaries.totalOrganicSessions,
|
|
23262
23432
|
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
23263
23433
|
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
23264
23434
|
}).from(gaTrafficWindowSummaries).where(
|
|
23265
|
-
|
|
23266
|
-
|
|
23267
|
-
|
|
23435
|
+
and19(
|
|
23436
|
+
eq26(gaTrafficWindowSummaries.projectId, project.id),
|
|
23437
|
+
eq26(gaTrafficWindowSummaries.windowKey, window)
|
|
23268
23438
|
)
|
|
23269
23439
|
).get() : null;
|
|
23270
23440
|
const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
|
|
23271
|
-
totalSessions:
|
|
23272
|
-
totalOrganicSessions:
|
|
23273
|
-
totalUsers:
|
|
23274
|
-
}).from(gaTrafficSnapshots).where(
|
|
23441
|
+
totalSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
23442
|
+
totalOrganicSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
23443
|
+
totalUsers: sql10`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
23444
|
+
}).from(gaTrafficSnapshots).where(and19(...snapshotConditions)).get() : null;
|
|
23275
23445
|
const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
23276
23446
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
23277
23447
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
23278
23448
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
23279
|
-
}).from(gaTrafficSummaries).where(
|
|
23449
|
+
}).from(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).get();
|
|
23280
23450
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
23281
|
-
totalDirectSessions:
|
|
23282
|
-
}).from(gaTrafficSnapshots).where(
|
|
23451
|
+
totalDirectSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
23452
|
+
}).from(gaTrafficSnapshots).where(and19(...snapshotConditions)).get();
|
|
23283
23453
|
const summaryMeta = app.db.select({
|
|
23284
23454
|
periodStart: gaTrafficSummaries.periodStart,
|
|
23285
23455
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
23286
|
-
}).from(gaTrafficSummaries).where(
|
|
23456
|
+
}).from(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).get();
|
|
23287
23457
|
const rows = app.db.select({
|
|
23288
|
-
landingPage:
|
|
23289
|
-
sessions:
|
|
23290
|
-
organicSessions:
|
|
23291
|
-
directSessions:
|
|
23292
|
-
users:
|
|
23293
|
-
}).from(gaTrafficSnapshots).where(
|
|
23458
|
+
landingPage: sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
23459
|
+
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
23460
|
+
organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
23461
|
+
directSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
|
|
23462
|
+
users: sql10`SUM(${gaTrafficSnapshots.users})`
|
|
23463
|
+
}).from(gaTrafficSnapshots).where(and19(...snapshotConditions)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
23294
23464
|
const aiReferralRows = app.db.select({
|
|
23295
23465
|
source: gaAiReferrals.source,
|
|
23296
23466
|
medium: gaAiReferrals.medium,
|
|
23297
23467
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
23298
|
-
sessions:
|
|
23299
|
-
users:
|
|
23300
|
-
}).from(gaAiReferrals).where(
|
|
23468
|
+
sessions: sql10`SUM(${gaAiReferrals.sessions})`,
|
|
23469
|
+
users: sql10`SUM(${gaAiReferrals.users})`
|
|
23470
|
+
}).from(gaAiReferrals).where(and19(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
|
|
23301
23471
|
const aiReferralLandingPageRows = app.db.select({
|
|
23302
23472
|
source: gaAiReferrals.source,
|
|
23303
23473
|
medium: gaAiReferrals.medium,
|
|
23304
23474
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
23305
|
-
landingPage:
|
|
23306
|
-
sessions:
|
|
23307
|
-
users:
|
|
23308
|
-
}).from(gaAiReferrals).where(
|
|
23475
|
+
landingPage: sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
23476
|
+
sessions: sql10`SUM(${gaAiReferrals.sessions})`,
|
|
23477
|
+
users: sql10`SUM(${gaAiReferrals.users})`
|
|
23478
|
+
}).from(gaAiReferrals).where(and19(...aiConditions)).groupBy(
|
|
23309
23479
|
gaAiReferrals.source,
|
|
23310
23480
|
gaAiReferrals.medium,
|
|
23311
23481
|
gaAiReferrals.sourceDimension,
|
|
23312
|
-
|
|
23482
|
+
sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
|
|
23313
23483
|
).all();
|
|
23314
23484
|
const aiReferrals = pickWinningDimension(
|
|
23315
23485
|
aiReferralRows,
|
|
@@ -23320,10 +23490,10 @@ async function ga4Routes(app, opts) {
|
|
|
23320
23490
|
(r) => `${r.source}\0${r.medium}\0${r.landingPage}`
|
|
23321
23491
|
);
|
|
23322
23492
|
const aiDeduped = app.db.select({
|
|
23323
|
-
sessions:
|
|
23324
|
-
users:
|
|
23493
|
+
sessions: sql10`COALESCE(SUM(max_sessions), 0)`,
|
|
23494
|
+
users: sql10`COALESCE(SUM(max_users), 0)`
|
|
23325
23495
|
}).from(
|
|
23326
|
-
|
|
23496
|
+
sql10`(
|
|
23327
23497
|
SELECT date, source, medium,
|
|
23328
23498
|
MAX(dimension_sessions) AS max_sessions,
|
|
23329
23499
|
MAX(dimension_users) AS max_users
|
|
@@ -23332,7 +23502,7 @@ async function ga4Routes(app, opts) {
|
|
|
23332
23502
|
SUM(sessions) AS dimension_sessions,
|
|
23333
23503
|
SUM(users) AS dimension_users
|
|
23334
23504
|
FROM ga_ai_referrals
|
|
23335
|
-
WHERE project_id = ${project.id}${cutoffDate ?
|
|
23505
|
+
WHERE project_id = ${project.id}${cutoffDate ? sql10` AND date >= ${cutoffDate}` : sql10``}
|
|
23336
23506
|
GROUP BY date, source, medium, source_dimension
|
|
23337
23507
|
)
|
|
23338
23508
|
GROUP BY date, source, medium
|
|
@@ -23340,9 +23510,9 @@ async function ga4Routes(app, opts) {
|
|
|
23340
23510
|
).get();
|
|
23341
23511
|
const aiBySessionRows = app.db.select({
|
|
23342
23512
|
channelGroup: gaAiReferrals.channelGroup,
|
|
23343
|
-
sessions:
|
|
23344
|
-
users:
|
|
23345
|
-
}).from(gaAiReferrals).where(
|
|
23513
|
+
sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
|
|
23514
|
+
users: sql10`COALESCE(SUM(${gaAiReferrals.users}), 0)`
|
|
23515
|
+
}).from(gaAiReferrals).where(and19(...aiConditions, eq26(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
|
|
23346
23516
|
const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
|
|
23347
23517
|
let aiBySessionUsers = 0;
|
|
23348
23518
|
for (const row of aiBySessionRows) {
|
|
@@ -23354,14 +23524,14 @@ async function ga4Routes(app, opts) {
|
|
|
23354
23524
|
source: gaSocialReferrals.source,
|
|
23355
23525
|
medium: gaSocialReferrals.medium,
|
|
23356
23526
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
23357
|
-
sessions:
|
|
23358
|
-
users:
|
|
23359
|
-
}).from(gaSocialReferrals).where(
|
|
23527
|
+
sessions: sql10`SUM(${gaSocialReferrals.sessions})`,
|
|
23528
|
+
users: sql10`SUM(${gaSocialReferrals.users})`
|
|
23529
|
+
}).from(gaSocialReferrals).where(and19(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql10`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
23360
23530
|
const socialTotals = app.db.select({
|
|
23361
|
-
sessions:
|
|
23362
|
-
users:
|
|
23363
|
-
}).from(gaSocialReferrals).where(
|
|
23364
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
23531
|
+
sessions: sql10`SUM(${gaSocialReferrals.sessions})`,
|
|
23532
|
+
users: sql10`SUM(${gaSocialReferrals.users})`
|
|
23533
|
+
}).from(gaSocialReferrals).where(and19(...socialConditions)).get();
|
|
23534
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
23365
23535
|
const total = summaryRow?.totalSessions ?? 0;
|
|
23366
23536
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
23367
23537
|
const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
|
|
@@ -23441,22 +23611,22 @@ async function ga4Routes(app, opts) {
|
|
|
23441
23611
|
const project = resolveProject(app.db, request.params.name);
|
|
23442
23612
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
23443
23613
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
23444
|
-
const conditions = [
|
|
23445
|
-
if (cutoffDate) conditions.push(
|
|
23614
|
+
const conditions = [eq26(gaAiReferrals.projectId, project.id)];
|
|
23615
|
+
if (cutoffDate) conditions.push(sql10`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
23446
23616
|
const rows = app.db.select({
|
|
23447
23617
|
date: gaAiReferrals.date,
|
|
23448
23618
|
source: gaAiReferrals.source,
|
|
23449
23619
|
medium: gaAiReferrals.medium,
|
|
23450
|
-
landingPage:
|
|
23620
|
+
landingPage: sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
23451
23621
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
23452
|
-
sessions:
|
|
23453
|
-
users:
|
|
23454
|
-
}).from(gaAiReferrals).where(
|
|
23622
|
+
sessions: sql10`SUM(${gaAiReferrals.sessions})`,
|
|
23623
|
+
users: sql10`SUM(${gaAiReferrals.users})`
|
|
23624
|
+
}).from(gaAiReferrals).where(and19(...conditions)).groupBy(
|
|
23455
23625
|
gaAiReferrals.date,
|
|
23456
23626
|
gaAiReferrals.source,
|
|
23457
23627
|
gaAiReferrals.medium,
|
|
23458
23628
|
gaAiReferrals.sourceDimension,
|
|
23459
|
-
|
|
23629
|
+
sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
|
|
23460
23630
|
).orderBy(gaAiReferrals.date).all();
|
|
23461
23631
|
return rows;
|
|
23462
23632
|
});
|
|
@@ -23464,8 +23634,8 @@ async function ga4Routes(app, opts) {
|
|
|
23464
23634
|
const project = resolveProject(app.db, request.params.name);
|
|
23465
23635
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
23466
23636
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
23467
|
-
const conditions = [
|
|
23468
|
-
if (cutoffDate) conditions.push(
|
|
23637
|
+
const conditions = [eq26(gaSocialReferrals.projectId, project.id)];
|
|
23638
|
+
if (cutoffDate) conditions.push(sql10`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
23469
23639
|
const rows = app.db.select({
|
|
23470
23640
|
date: gaSocialReferrals.date,
|
|
23471
23641
|
source: gaSocialReferrals.source,
|
|
@@ -23473,7 +23643,7 @@ async function ga4Routes(app, opts) {
|
|
|
23473
23643
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
23474
23644
|
sessions: gaSocialReferrals.sessions,
|
|
23475
23645
|
users: gaSocialReferrals.users
|
|
23476
|
-
}).from(gaSocialReferrals).where(
|
|
23646
|
+
}).from(gaSocialReferrals).where(and19(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
23477
23647
|
return rows;
|
|
23478
23648
|
});
|
|
23479
23649
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -23486,10 +23656,10 @@ async function ga4Routes(app, opts) {
|
|
|
23486
23656
|
d.setDate(d.getDate() - n);
|
|
23487
23657
|
return fmt(d);
|
|
23488
23658
|
};
|
|
23489
|
-
const sumSocial = (from, to) => app.db.select({ sessions:
|
|
23490
|
-
|
|
23491
|
-
|
|
23492
|
-
|
|
23659
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and19(
|
|
23660
|
+
eq26(gaSocialReferrals.projectId, project.id),
|
|
23661
|
+
sql10`${gaSocialReferrals.date} >= ${from}`,
|
|
23662
|
+
sql10`${gaSocialReferrals.date} < ${to}`
|
|
23493
23663
|
)).get();
|
|
23494
23664
|
const current7d = sumSocial(daysAgo(7), fmt(today));
|
|
23495
23665
|
const prev7d = sumSocial(daysAgo(14), daysAgo(7));
|
|
@@ -23498,19 +23668,19 @@ async function ga4Routes(app, opts) {
|
|
|
23498
23668
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
23499
23669
|
const sourceCurrent = app.db.select({
|
|
23500
23670
|
source: gaSocialReferrals.source,
|
|
23501
|
-
sessions:
|
|
23502
|
-
}).from(gaSocialReferrals).where(
|
|
23503
|
-
|
|
23504
|
-
|
|
23505
|
-
|
|
23671
|
+
sessions: sql10`SUM(${gaSocialReferrals.sessions})`
|
|
23672
|
+
}).from(gaSocialReferrals).where(and19(
|
|
23673
|
+
eq26(gaSocialReferrals.projectId, project.id),
|
|
23674
|
+
sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
|
|
23675
|
+
sql10`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
23506
23676
|
)).groupBy(gaSocialReferrals.source).all();
|
|
23507
23677
|
const sourcePrev = app.db.select({
|
|
23508
23678
|
source: gaSocialReferrals.source,
|
|
23509
|
-
sessions:
|
|
23510
|
-
}).from(gaSocialReferrals).where(
|
|
23511
|
-
|
|
23512
|
-
|
|
23513
|
-
|
|
23679
|
+
sessions: sql10`SUM(${gaSocialReferrals.sessions})`
|
|
23680
|
+
}).from(gaSocialReferrals).where(and19(
|
|
23681
|
+
eq26(gaSocialReferrals.projectId, project.id),
|
|
23682
|
+
sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
|
|
23683
|
+
sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`
|
|
23514
23684
|
)).groupBy(gaSocialReferrals.source).all();
|
|
23515
23685
|
const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
|
|
23516
23686
|
let biggestMover = null;
|
|
@@ -23549,16 +23719,16 @@ async function ga4Routes(app, opts) {
|
|
|
23549
23719
|
return fmt(d);
|
|
23550
23720
|
};
|
|
23551
23721
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
23552
|
-
const sumTotal = (from, to) => app.db.select({ sessions:
|
|
23553
|
-
const sumOrganic = (from, to) => app.db.select({ sessions:
|
|
23554
|
-
const sumDirect = (from, to) => app.db.select({ sessions:
|
|
23555
|
-
const sumAi = (from, to) => app.db.select({ sessions:
|
|
23556
|
-
|
|
23557
|
-
|
|
23558
|
-
|
|
23559
|
-
|
|
23722
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and19(eq26(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
23723
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and19(eq26(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
23724
|
+
const sumDirect = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and19(eq26(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
23725
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and19(
|
|
23726
|
+
eq26(gaAiReferrals.projectId, project.id),
|
|
23727
|
+
sql10`${gaAiReferrals.date} >= ${from}`,
|
|
23728
|
+
sql10`${gaAiReferrals.date} < ${to}`,
|
|
23729
|
+
eq26(gaAiReferrals.sourceDimension, "session")
|
|
23560
23730
|
)).get();
|
|
23561
|
-
const sumSocial = (from, to) => app.db.select({ sessions:
|
|
23731
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and19(eq26(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${from}`, sql10`${gaSocialReferrals.date} < ${to}`)).get();
|
|
23562
23732
|
const todayStr = fmt(today);
|
|
23563
23733
|
const buildTrend = (sum) => {
|
|
23564
23734
|
const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
|
|
@@ -23567,17 +23737,17 @@ async function ga4Routes(app, opts) {
|
|
|
23567
23737
|
const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
|
|
23568
23738
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
23569
23739
|
};
|
|
23570
|
-
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions:
|
|
23571
|
-
|
|
23572
|
-
|
|
23573
|
-
|
|
23574
|
-
|
|
23740
|
+
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and19(
|
|
23741
|
+
eq26(gaAiReferrals.projectId, project.id),
|
|
23742
|
+
sql10`${gaAiReferrals.date} >= ${daysAgo(7)}`,
|
|
23743
|
+
sql10`${gaAiReferrals.date} < ${todayStr}`,
|
|
23744
|
+
eq26(gaAiReferrals.sourceDimension, "session")
|
|
23575
23745
|
)).groupBy(gaAiReferrals.source).all();
|
|
23576
|
-
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions:
|
|
23577
|
-
|
|
23578
|
-
|
|
23579
|
-
|
|
23580
|
-
|
|
23746
|
+
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and19(
|
|
23747
|
+
eq26(gaAiReferrals.projectId, project.id),
|
|
23748
|
+
sql10`${gaAiReferrals.date} >= ${daysAgo(14)}`,
|
|
23749
|
+
sql10`${gaAiReferrals.date} < ${daysAgo(7)}`,
|
|
23750
|
+
eq26(gaAiReferrals.sourceDimension, "session")
|
|
23581
23751
|
)).groupBy(gaAiReferrals.source).all();
|
|
23582
23752
|
const findBiggestMover = (current, prev) => {
|
|
23583
23753
|
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
@@ -23593,8 +23763,8 @@ async function ga4Routes(app, opts) {
|
|
|
23593
23763
|
}
|
|
23594
23764
|
return mover;
|
|
23595
23765
|
};
|
|
23596
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions:
|
|
23597
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions:
|
|
23766
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and19(eq26(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql10`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
23767
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and19(eq26(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
23598
23768
|
return {
|
|
23599
23769
|
total: buildTrend(sumTotal),
|
|
23600
23770
|
organic: buildTrend(sumOrganic),
|
|
@@ -23609,14 +23779,14 @@ async function ga4Routes(app, opts) {
|
|
|
23609
23779
|
const project = resolveProject(app.db, request.params.name);
|
|
23610
23780
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
23611
23781
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
23612
|
-
const conditions = [
|
|
23613
|
-
if (cutoffDate) conditions.push(
|
|
23782
|
+
const conditions = [eq26(gaTrafficSnapshots.projectId, project.id)];
|
|
23783
|
+
if (cutoffDate) conditions.push(sql10`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
23614
23784
|
const rows = app.db.select({
|
|
23615
23785
|
date: gaTrafficSnapshots.date,
|
|
23616
|
-
sessions:
|
|
23617
|
-
organicSessions:
|
|
23618
|
-
users:
|
|
23619
|
-
}).from(gaTrafficSnapshots).where(
|
|
23786
|
+
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
23787
|
+
organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
23788
|
+
users: sql10`SUM(${gaTrafficSnapshots.users})`
|
|
23789
|
+
}).from(gaTrafficSnapshots).where(and19(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
23620
23790
|
return rows.map((r) => ({
|
|
23621
23791
|
date: r.date,
|
|
23622
23792
|
sessions: r.sessions ?? 0,
|
|
@@ -23628,11 +23798,11 @@ async function ga4Routes(app, opts) {
|
|
|
23628
23798
|
const project = resolveProject(app.db, request.params.name);
|
|
23629
23799
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
23630
23800
|
const trafficPages = app.db.select({
|
|
23631
|
-
landingPage:
|
|
23632
|
-
sessions:
|
|
23633
|
-
organicSessions:
|
|
23634
|
-
users:
|
|
23635
|
-
}).from(gaTrafficSnapshots).where(
|
|
23801
|
+
landingPage: sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
23802
|
+
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
23803
|
+
organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
23804
|
+
users: sql10`SUM(${gaTrafficSnapshots.users})`
|
|
23805
|
+
}).from(gaTrafficSnapshots).where(eq26(gaTrafficSnapshots.projectId, project.id)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
23636
23806
|
return {
|
|
23637
23807
|
pages: trafficPages.map((r) => ({
|
|
23638
23808
|
landingPage: r.landingPage,
|
|
@@ -25280,7 +25450,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
25280
25450
|
|
|
25281
25451
|
// ../api-routes/src/backlinks.ts
|
|
25282
25452
|
import crypto23 from "crypto";
|
|
25283
|
-
import { and as
|
|
25453
|
+
import { and as and21, asc as asc4, desc as desc14, eq as eq27, sql as sql11 } from "drizzle-orm";
|
|
25284
25454
|
|
|
25285
25455
|
// ../integration-commoncrawl/src/constants.ts
|
|
25286
25456
|
import os2 from "os";
|
|
@@ -25570,7 +25740,7 @@ async function queryBacklinks(opts) {
|
|
|
25570
25740
|
const reversed = opts.targets.map(reverseDomain);
|
|
25571
25741
|
const targetList = reversed.map(quote).join(", ");
|
|
25572
25742
|
const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
|
|
25573
|
-
const
|
|
25743
|
+
const sql14 = `
|
|
25574
25744
|
WITH vertices AS (
|
|
25575
25745
|
SELECT * FROM read_csv(
|
|
25576
25746
|
${quote(opts.vertexPath)},
|
|
@@ -25606,7 +25776,7 @@ async function queryBacklinks(opts) {
|
|
|
25606
25776
|
const conn = await instance.connect();
|
|
25607
25777
|
let rows;
|
|
25608
25778
|
try {
|
|
25609
|
-
const reader = await conn.runAndReadAll(
|
|
25779
|
+
const reader = await conn.runAndReadAll(sql14);
|
|
25610
25780
|
rows = reader.getRowObjects();
|
|
25611
25781
|
} finally {
|
|
25612
25782
|
conn.disconnectSync?.();
|
|
@@ -25683,7 +25853,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
25683
25853
|
}
|
|
25684
25854
|
|
|
25685
25855
|
// ../api-routes/src/backlinks-filter.ts
|
|
25686
|
-
import { and as
|
|
25856
|
+
import { and as and20, ne as ne3, notLike } from "drizzle-orm";
|
|
25687
25857
|
var BACKLINK_FILTER_PATTERNS = [
|
|
25688
25858
|
"*.google.com",
|
|
25689
25859
|
"*.googleusercontent.com",
|
|
@@ -25706,7 +25876,7 @@ function backlinkCrawlerExclusionClause() {
|
|
|
25706
25876
|
conditions.push(ne3(backlinkDomains.linkingDomain, pattern));
|
|
25707
25877
|
}
|
|
25708
25878
|
}
|
|
25709
|
-
const combined =
|
|
25879
|
+
const combined = and20(...conditions);
|
|
25710
25880
|
if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
|
|
25711
25881
|
return combined;
|
|
25712
25882
|
}
|
|
@@ -25777,11 +25947,11 @@ function mapRunRow(row) {
|
|
|
25777
25947
|
}
|
|
25778
25948
|
function latestSummaryForProject(db, projectId, source, release) {
|
|
25779
25949
|
const conditions = [
|
|
25780
|
-
|
|
25781
|
-
|
|
25950
|
+
eq27(backlinkSummaries.projectId, projectId),
|
|
25951
|
+
eq27(backlinkSummaries.source, source)
|
|
25782
25952
|
];
|
|
25783
|
-
if (release) conditions.push(
|
|
25784
|
-
return db.select().from(backlinkSummaries).where(
|
|
25953
|
+
if (release) conditions.push(eq27(backlinkSummaries.release, release));
|
|
25954
|
+
return db.select().from(backlinkSummaries).where(and21(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
25785
25955
|
}
|
|
25786
25956
|
function parseExcludeCrawlers(value) {
|
|
25787
25957
|
if (!value) return false;
|
|
@@ -25789,19 +25959,19 @@ function parseExcludeCrawlers(value) {
|
|
|
25789
25959
|
return lower === "1" || lower === "true" || lower === "yes";
|
|
25790
25960
|
}
|
|
25791
25961
|
function computeFilteredSummary(db, base) {
|
|
25792
|
-
const baseDomainCondition =
|
|
25793
|
-
|
|
25794
|
-
|
|
25795
|
-
|
|
25962
|
+
const baseDomainCondition = and21(
|
|
25963
|
+
eq27(backlinkDomains.projectId, base.projectId),
|
|
25964
|
+
eq27(backlinkDomains.source, base.source),
|
|
25965
|
+
eq27(backlinkDomains.release, base.release)
|
|
25796
25966
|
);
|
|
25797
|
-
const filteredCondition =
|
|
25967
|
+
const filteredCondition = and21(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
25798
25968
|
const unfilteredAgg = db.select({
|
|
25799
|
-
count:
|
|
25800
|
-
total:
|
|
25969
|
+
count: sql11`count(*)`,
|
|
25970
|
+
total: sql11`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
25801
25971
|
}).from(backlinkDomains).where(baseDomainCondition).get();
|
|
25802
25972
|
const filteredAgg = db.select({
|
|
25803
|
-
count:
|
|
25804
|
-
total:
|
|
25973
|
+
count: sql11`count(*)`,
|
|
25974
|
+
total: sql11`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
25805
25975
|
}).from(backlinkDomains).where(filteredCondition).get();
|
|
25806
25976
|
const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc14(backlinkDomains.numHosts)).limit(10).all();
|
|
25807
25977
|
const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
|
|
@@ -25824,13 +25994,13 @@ function computeFilteredSummary(db, base) {
|
|
|
25824
25994
|
};
|
|
25825
25995
|
}
|
|
25826
25996
|
function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
|
|
25827
|
-
const summary = db.select().from(backlinkSummaries).where(
|
|
25997
|
+
const summary = db.select().from(backlinkSummaries).where(and21(eq27(backlinkSummaries.projectId, projectId), eq27(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
25828
25998
|
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
25829
25999
|
if (summary && excludeCrawlers) {
|
|
25830
|
-
const filtered = db.select({ count:
|
|
25831
|
-
|
|
25832
|
-
|
|
25833
|
-
|
|
26000
|
+
const filtered = db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(and21(
|
|
26001
|
+
eq27(backlinkDomains.projectId, projectId),
|
|
26002
|
+
eq27(backlinkDomains.source, source),
|
|
26003
|
+
eq27(backlinkDomains.release, summary.release),
|
|
25834
26004
|
backlinkCrawlerExclusionClause()
|
|
25835
26005
|
)).get();
|
|
25836
26006
|
totalLinkingDomains = Number(filtered?.count ?? 0);
|
|
@@ -25845,7 +26015,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
|
|
|
25845
26015
|
};
|
|
25846
26016
|
}
|
|
25847
26017
|
function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
|
|
25848
|
-
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
26018
|
+
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
25849
26019
|
const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
|
|
25850
26020
|
const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
|
|
25851
26021
|
const sources = [
|
|
@@ -25901,7 +26071,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25901
26071
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
25902
26072
|
);
|
|
25903
26073
|
}
|
|
25904
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
26074
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.release, release)).get();
|
|
25905
26075
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
25906
26076
|
if (existing) {
|
|
25907
26077
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -25912,9 +26082,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
25912
26082
|
phaseDetail: null,
|
|
25913
26083
|
error: null,
|
|
25914
26084
|
updatedAt: now
|
|
25915
|
-
}).where(
|
|
26085
|
+
}).where(eq27(ccReleaseSyncs.id, existing.id)).run();
|
|
25916
26086
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
25917
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
26087
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.id, existing.id)).get();
|
|
25918
26088
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
25919
26089
|
}
|
|
25920
26090
|
const id = crypto23.randomUUID();
|
|
@@ -25926,7 +26096,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25926
26096
|
updatedAt: now
|
|
25927
26097
|
}).run();
|
|
25928
26098
|
opts.onReleaseSyncRequested(id, release);
|
|
25929
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
26099
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.id, id)).get();
|
|
25930
26100
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
25931
26101
|
});
|
|
25932
26102
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
@@ -25984,7 +26154,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25984
26154
|
createdAt: now
|
|
25985
26155
|
}).run();
|
|
25986
26156
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
25987
|
-
const run = app.db.select().from(runs).where(
|
|
26157
|
+
const run = app.db.select().from(runs).where(eq27(runs.id, runId)).get();
|
|
25988
26158
|
return reply.status(201).send(mapRunRow(run));
|
|
25989
26159
|
});
|
|
25990
26160
|
app.get(
|
|
@@ -26010,13 +26180,13 @@ async function backlinksRoutes(app, opts) {
|
|
|
26010
26180
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
26011
26181
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
26012
26182
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
26013
|
-
const baseDomainCondition =
|
|
26014
|
-
|
|
26015
|
-
|
|
26016
|
-
|
|
26183
|
+
const baseDomainCondition = and21(
|
|
26184
|
+
eq27(backlinkDomains.projectId, project.id),
|
|
26185
|
+
eq27(backlinkDomains.source, source),
|
|
26186
|
+
eq27(backlinkDomains.release, targetRelease)
|
|
26017
26187
|
);
|
|
26018
|
-
const domainCondition = excludeCrawlers ?
|
|
26019
|
-
const totalRow = app.db.select({ count:
|
|
26188
|
+
const domainCondition = excludeCrawlers ? and21(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
26189
|
+
const totalRow = app.db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
26020
26190
|
const rows = app.db.select({
|
|
26021
26191
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
26022
26192
|
numHosts: backlinkDomains.numHosts,
|
|
@@ -26039,7 +26209,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
26039
26209
|
async (request, reply) => {
|
|
26040
26210
|
const project = resolveProject(app.db, request.params.name);
|
|
26041
26211
|
const source = parseSourceParam(request.query.source);
|
|
26042
|
-
const rows = app.db.select().from(backlinkSummaries).where(
|
|
26212
|
+
const rows = app.db.select().from(backlinkSummaries).where(and21(eq27(backlinkSummaries.projectId, project.id), eq27(backlinkSummaries.source, source))).orderBy(asc4(backlinkSummaries.queriedAt)).all();
|
|
26043
26213
|
const response = rows.map((r) => ({
|
|
26044
26214
|
release: r.release,
|
|
26045
26215
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -26086,7 +26256,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
26086
26256
|
createdAt: now
|
|
26087
26257
|
}).run();
|
|
26088
26258
|
opts.onBingBacklinkSyncRequested(runId, project.id);
|
|
26089
|
-
const run = app.db.select().from(runs).where(
|
|
26259
|
+
const run = app.db.select().from(runs).where(eq27(runs.id, runId)).get();
|
|
26090
26260
|
return reply.status(201).send(mapRunRow(run));
|
|
26091
26261
|
}
|
|
26092
26262
|
);
|
|
@@ -26095,7 +26265,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
26095
26265
|
// ../api-routes/src/traffic.ts
|
|
26096
26266
|
import crypto25 from "crypto";
|
|
26097
26267
|
import { Agent as UndiciAgent } from "undici";
|
|
26098
|
-
import { and as
|
|
26268
|
+
import { and as and22, desc as desc15, eq as eq28, gte as gte4, lte as lte3, sql as sql12 } from "drizzle-orm";
|
|
26099
26269
|
|
|
26100
26270
|
// ../integration-cloud-run/src/auth.ts
|
|
26101
26271
|
import crypto24 from "crypto";
|
|
@@ -29297,14 +29467,18 @@ function trimOrNull(value) {
|
|
|
29297
29467
|
function buildEventId2(event) {
|
|
29298
29468
|
return `wordpress:${event.observed_at}:${event.id}`;
|
|
29299
29469
|
}
|
|
29300
|
-
function normalizeWordpressTrafficEvent(event) {
|
|
29470
|
+
function normalizeWordpressTrafficEvent(event, site) {
|
|
29301
29471
|
if (!event.observed_at) return null;
|
|
29302
29472
|
if (typeof event.id !== "number" || !Number.isFinite(event.id)) return null;
|
|
29303
|
-
|
|
29473
|
+
if (!event.path) return null;
|
|
29474
|
+
const path8 = event.path.trim();
|
|
29304
29475
|
if (!path8) return null;
|
|
29305
29476
|
const queryString = trimOrNull(event.query_string);
|
|
29306
29477
|
const host = trimOrNull(event.host);
|
|
29307
29478
|
const requestUrl = host ? `https://${host}${path8}${queryString ? `?${queryString}` : ""}` : `${path8}${queryString ? `?${queryString}` : ""}`;
|
|
29479
|
+
const labels = {};
|
|
29480
|
+
if (host) labels.host = host;
|
|
29481
|
+
if (site?.anonymous_id) labels.anonymousId = site.anonymous_id;
|
|
29308
29482
|
return {
|
|
29309
29483
|
sourceType: TrafficSourceTypes.wordpress,
|
|
29310
29484
|
evidenceKind: TrafficEvidenceKinds["raw-request"],
|
|
@@ -29325,7 +29499,7 @@ function normalizeWordpressTrafficEvent(event) {
|
|
|
29325
29499
|
responseSizeBytes: null,
|
|
29326
29500
|
providerResource: {
|
|
29327
29501
|
type: "wordpress_site",
|
|
29328
|
-
labels
|
|
29502
|
+
labels
|
|
29329
29503
|
},
|
|
29330
29504
|
providerLabels: {}
|
|
29331
29505
|
};
|
|
@@ -29430,7 +29604,7 @@ async function listWordpressTrafficEvents(options) {
|
|
|
29430
29604
|
const entries = body.events ?? [];
|
|
29431
29605
|
rawEntryCount += entries.length;
|
|
29432
29606
|
for (const entry of entries) {
|
|
29433
|
-
const normalized = normalizeWordpressTrafficEvent(entry);
|
|
29607
|
+
const normalized = normalizeWordpressTrafficEvent(entry, body.site);
|
|
29434
29608
|
if (normalized) {
|
|
29435
29609
|
events.push(normalized);
|
|
29436
29610
|
} else {
|
|
@@ -29912,8 +30086,8 @@ async function runBackfillTask(options) {
|
|
|
29912
30086
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
29913
30087
|
try {
|
|
29914
30088
|
app.db.transaction((tx) => {
|
|
29915
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
29916
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
30089
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq28(runs.id, runId)).run();
|
|
30090
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq28(trafficSources.id, sourceRow.id)).run();
|
|
29917
30091
|
});
|
|
29918
30092
|
} catch {
|
|
29919
30093
|
}
|
|
@@ -29928,7 +30102,7 @@ async function runBackfillTask(options) {
|
|
|
29928
30102
|
if (allEvents.length === 0) {
|
|
29929
30103
|
const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
29930
30104
|
try {
|
|
29931
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(
|
|
30105
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq28(runs.id, runId)).run();
|
|
29932
30106
|
} catch {
|
|
29933
30107
|
}
|
|
29934
30108
|
return;
|
|
@@ -29950,29 +30124,29 @@ async function runBackfillTask(options) {
|
|
|
29950
30124
|
try {
|
|
29951
30125
|
app.db.transaction((tx) => {
|
|
29952
30126
|
tx.delete(crawlerEventsHourly).where(
|
|
29953
|
-
|
|
29954
|
-
|
|
30127
|
+
and22(
|
|
30128
|
+
eq28(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
29955
30129
|
gte4(crawlerEventsHourly.tsHour, windowStartIso),
|
|
29956
30130
|
lte3(crawlerEventsHourly.tsHour, windowEndIso)
|
|
29957
30131
|
)
|
|
29958
30132
|
).run();
|
|
29959
30133
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
29960
|
-
|
|
29961
|
-
|
|
30134
|
+
and22(
|
|
30135
|
+
eq28(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
29962
30136
|
gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
29963
30137
|
lte3(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
29964
30138
|
)
|
|
29965
30139
|
).run();
|
|
29966
30140
|
tx.delete(aiReferralEventsHourly).where(
|
|
29967
|
-
|
|
29968
|
-
|
|
30141
|
+
and22(
|
|
30142
|
+
eq28(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
29969
30143
|
gte4(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
29970
30144
|
lte3(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
29971
30145
|
)
|
|
29972
30146
|
).run();
|
|
29973
30147
|
tx.delete(rawEventSamples).where(
|
|
29974
|
-
|
|
29975
|
-
|
|
30148
|
+
and22(
|
|
30149
|
+
eq28(rawEventSamples.sourceId, sourceRow.id),
|
|
29976
30150
|
gte4(rawEventSamples.ts, windowStartIso),
|
|
29977
30151
|
lte3(rawEventSamples.ts, windowEndIso)
|
|
29978
30152
|
)
|
|
@@ -30061,8 +30235,8 @@ async function runBackfillTask(options) {
|
|
|
30061
30235
|
lastError: null,
|
|
30062
30236
|
lastEventIds: newRingBuffer,
|
|
30063
30237
|
updatedAt: finishedAt
|
|
30064
|
-
}).where(
|
|
30065
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
30238
|
+
}).where(eq28(trafficSources.id, sourceRow.id)).run();
|
|
30239
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq28(runs.id, runId)).run();
|
|
30066
30240
|
});
|
|
30067
30241
|
} catch (e) {
|
|
30068
30242
|
markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -30144,7 +30318,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30144
30318
|
createdAt: existing?.createdAt ?? now,
|
|
30145
30319
|
updatedAt: now
|
|
30146
30320
|
});
|
|
30147
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
30321
|
+
const activeSource = app.db.select().from(trafficSources).where(eq28(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
|
|
30148
30322
|
const config = {
|
|
30149
30323
|
gcpProjectId,
|
|
30150
30324
|
serviceName: serviceName ?? null,
|
|
@@ -30160,8 +30334,8 @@ async function trafficRoutes(app, opts) {
|
|
|
30160
30334
|
lastError: null,
|
|
30161
30335
|
configJson: config,
|
|
30162
30336
|
updatedAt: now
|
|
30163
|
-
}).where(
|
|
30164
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
30337
|
+
}).where(eq28(trafficSources.id, activeSource.id)).run();
|
|
30338
|
+
sourceRow = app.db.select().from(trafficSources).where(eq28(trafficSources.id, activeSource.id)).get();
|
|
30165
30339
|
} else {
|
|
30166
30340
|
const newId = crypto25.randomUUID();
|
|
30167
30341
|
app.db.insert(trafficSources).values({
|
|
@@ -30178,7 +30352,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30178
30352
|
createdAt: now,
|
|
30179
30353
|
updatedAt: now
|
|
30180
30354
|
}).run();
|
|
30181
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
30355
|
+
sourceRow = app.db.select().from(trafficSources).where(eq28(trafficSources.id, newId)).get();
|
|
30182
30356
|
}
|
|
30183
30357
|
writeAuditLog(app.db, {
|
|
30184
30358
|
projectId: project.id,
|
|
@@ -30230,7 +30404,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30230
30404
|
createdAt: existing?.createdAt ?? now,
|
|
30231
30405
|
updatedAt: now
|
|
30232
30406
|
});
|
|
30233
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
30407
|
+
const activeSource = app.db.select().from(trafficSources).where(eq28(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
|
|
30234
30408
|
const config = { baseUrl, username };
|
|
30235
30409
|
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
30236
30410
|
let sourceRow;
|
|
@@ -30241,8 +30415,8 @@ async function trafficRoutes(app, opts) {
|
|
|
30241
30415
|
lastError: null,
|
|
30242
30416
|
configJson: config,
|
|
30243
30417
|
updatedAt: now
|
|
30244
|
-
}).where(
|
|
30245
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
30418
|
+
}).where(eq28(trafficSources.id, activeSource.id)).run();
|
|
30419
|
+
sourceRow = app.db.select().from(trafficSources).where(eq28(trafficSources.id, activeSource.id)).get();
|
|
30246
30420
|
} else {
|
|
30247
30421
|
const newId = crypto25.randomUUID();
|
|
30248
30422
|
app.db.insert(trafficSources).values({
|
|
@@ -30259,7 +30433,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30259
30433
|
createdAt: now,
|
|
30260
30434
|
updatedAt: now
|
|
30261
30435
|
}).run();
|
|
30262
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
30436
|
+
sourceRow = app.db.select().from(trafficSources).where(eq28(trafficSources.id, newId)).get();
|
|
30263
30437
|
}
|
|
30264
30438
|
writeAuditLog(app.db, {
|
|
30265
30439
|
projectId: project.id,
|
|
@@ -30313,7 +30487,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30313
30487
|
createdAt: existing?.createdAt ?? now,
|
|
30314
30488
|
updatedAt: now
|
|
30315
30489
|
});
|
|
30316
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
30490
|
+
const activeSource = app.db.select().from(trafficSources).where(eq28(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
|
|
30317
30491
|
const config = { projectId, teamId, environment };
|
|
30318
30492
|
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
30319
30493
|
const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
|
|
@@ -30325,8 +30499,8 @@ async function trafficRoutes(app, opts) {
|
|
|
30325
30499
|
lastError: null,
|
|
30326
30500
|
configJson: config,
|
|
30327
30501
|
updatedAt: now
|
|
30328
|
-
}).where(
|
|
30329
|
-
row = tx.select().from(trafficSources).where(
|
|
30502
|
+
}).where(eq28(trafficSources.id, activeSource.id)).run();
|
|
30503
|
+
row = tx.select().from(trafficSources).where(eq28(trafficSources.id, activeSource.id)).get();
|
|
30330
30504
|
} else {
|
|
30331
30505
|
const newId = crypto25.randomUUID();
|
|
30332
30506
|
tx.insert(trafficSources).values({
|
|
@@ -30351,12 +30525,12 @@ async function trafficRoutes(app, opts) {
|
|
|
30351
30525
|
createdAt: now,
|
|
30352
30526
|
updatedAt: now
|
|
30353
30527
|
}).run();
|
|
30354
|
-
row = tx.select().from(trafficSources).where(
|
|
30528
|
+
row = tx.select().from(trafficSources).where(eq28(trafficSources.id, newId)).get();
|
|
30355
30529
|
}
|
|
30356
30530
|
const existingSchedule = tx.select().from(schedules).where(
|
|
30357
|
-
|
|
30358
|
-
|
|
30359
|
-
|
|
30531
|
+
and22(
|
|
30532
|
+
eq28(schedules.projectId, project.id),
|
|
30533
|
+
eq28(schedules.kind, SchedulableRunKinds["traffic-sync"])
|
|
30360
30534
|
)
|
|
30361
30535
|
).get();
|
|
30362
30536
|
let created = false;
|
|
@@ -30405,7 +30579,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30405
30579
|
});
|
|
30406
30580
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
30407
30581
|
const project = resolveProject(app.db, request.params.name);
|
|
30408
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
30582
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq28(trafficSources.id, request.params.id)).get();
|
|
30409
30583
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
30410
30584
|
throw notFound("Traffic source", request.params.id);
|
|
30411
30585
|
}
|
|
@@ -30431,8 +30605,8 @@ async function trafficRoutes(app, opts) {
|
|
|
30431
30605
|
const markFailed = (msg, errorCode) => {
|
|
30432
30606
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
30433
30607
|
app.db.transaction((tx) => {
|
|
30434
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
30435
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
30608
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq28(runs.id, runId)).run();
|
|
30609
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq28(trafficSources.id, sourceRow.id)).run();
|
|
30436
30610
|
});
|
|
30437
30611
|
try {
|
|
30438
30612
|
opts.onTrafficSynced?.({
|
|
@@ -30513,7 +30687,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30513
30687
|
}
|
|
30514
30688
|
const credential = credentialStore.getConnection(project.name);
|
|
30515
30689
|
if (!credential) {
|
|
30516
|
-
app.db.delete(runs).where(
|
|
30690
|
+
app.db.delete(runs).where(eq28(runs.id, runId)).run();
|
|
30517
30691
|
throw validationError(
|
|
30518
30692
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
30519
30693
|
);
|
|
@@ -30562,12 +30736,12 @@ async function trafficRoutes(app, opts) {
|
|
|
30562
30736
|
auditAction = "traffic.vercel.synced";
|
|
30563
30737
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
30564
30738
|
if (!credentialStore) {
|
|
30565
|
-
app.db.delete(runs).where(
|
|
30739
|
+
app.db.delete(runs).where(eq28(runs.id, runId)).run();
|
|
30566
30740
|
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
30567
30741
|
}
|
|
30568
30742
|
const credential = credentialStore.getConnection(project.name);
|
|
30569
30743
|
if (!credential) {
|
|
30570
|
-
app.db.delete(runs).where(
|
|
30744
|
+
app.db.delete(runs).where(eq28(runs.id, runId)).run();
|
|
30571
30745
|
throw validationError(
|
|
30572
30746
|
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
30573
30747
|
);
|
|
@@ -30667,7 +30841,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30667
30841
|
let aiReferralHitsCount = 0;
|
|
30668
30842
|
let unknownHitsCount = 0;
|
|
30669
30843
|
app.db.transaction((tx) => {
|
|
30670
|
-
const latestRow = tx.select().from(trafficSources).where(
|
|
30844
|
+
const latestRow = tx.select().from(trafficSources).where(eq28(trafficSources.id, sourceRow.id)).get();
|
|
30671
30845
|
const previousIds = latestRow.lastEventIds ?? [];
|
|
30672
30846
|
const seenEventIds = new Set(previousIds);
|
|
30673
30847
|
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
@@ -30715,7 +30889,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30715
30889
|
crawlerEventsHourly.status
|
|
30716
30890
|
],
|
|
30717
30891
|
set: {
|
|
30718
|
-
hits:
|
|
30892
|
+
hits: sql12`${crawlerEventsHourly.hits} + ${bucket.hits}`,
|
|
30719
30893
|
sampledUserAgent: bucket.sampledUserAgent,
|
|
30720
30894
|
updatedAt: finishedAt
|
|
30721
30895
|
}
|
|
@@ -30748,7 +30922,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30748
30922
|
aiUserFetchEventsHourly.status
|
|
30749
30923
|
],
|
|
30750
30924
|
set: {
|
|
30751
|
-
hits:
|
|
30925
|
+
hits: sql12`${aiUserFetchEventsHourly.hits} + ${bucket.hits}`,
|
|
30752
30926
|
sampledUserAgent: bucket.sampledUserAgent,
|
|
30753
30927
|
updatedAt: finishedAt
|
|
30754
30928
|
}
|
|
@@ -30783,7 +30957,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30783
30957
|
aiReferralEventsHourly.status
|
|
30784
30958
|
],
|
|
30785
30959
|
set: {
|
|
30786
|
-
sessionsOrHits:
|
|
30960
|
+
sessionsOrHits: sql12`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
|
|
30787
30961
|
updatedAt: finishedAt
|
|
30788
30962
|
}
|
|
30789
30963
|
}).run();
|
|
@@ -30835,8 +31009,8 @@ async function trafficRoutes(app, opts) {
|
|
|
30835
31009
|
if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
30836
31010
|
sourceUpdate.lastCursor = nextCursor ?? null;
|
|
30837
31011
|
}
|
|
30838
|
-
tx.update(trafficSources).set(sourceUpdate).where(
|
|
30839
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
31012
|
+
tx.update(trafficSources).set(sourceUpdate).where(eq28(trafficSources.id, sourceRow.id)).run();
|
|
31013
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq28(runs.id, runId)).run();
|
|
30840
31014
|
});
|
|
30841
31015
|
writeAuditLog(app.db, {
|
|
30842
31016
|
projectId: project.id,
|
|
@@ -30888,7 +31062,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30888
31062
|
});
|
|
30889
31063
|
app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
|
|
30890
31064
|
const project = resolveProject(app.db, request.params.name);
|
|
30891
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
31065
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq28(trafficSources.id, request.params.id)).get();
|
|
30892
31066
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
30893
31067
|
throw notFound("Traffic source", request.params.id);
|
|
30894
31068
|
}
|
|
@@ -31074,10 +31248,10 @@ async function trafficRoutes(app, opts) {
|
|
|
31074
31248
|
function buildSourceDetail(projectId, row, since) {
|
|
31075
31249
|
const crawlerPathRows = app.db.select({
|
|
31076
31250
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
31077
|
-
hits:
|
|
31251
|
+
hits: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
31078
31252
|
}).from(crawlerEventsHourly).where(
|
|
31079
|
-
|
|
31080
|
-
|
|
31253
|
+
and22(
|
|
31254
|
+
eq28(crawlerEventsHourly.sourceId, row.id),
|
|
31081
31255
|
gte4(crawlerEventsHourly.tsHour, since)
|
|
31082
31256
|
)
|
|
31083
31257
|
).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
@@ -31085,29 +31259,29 @@ async function trafficRoutes(app, opts) {
|
|
|
31085
31259
|
crawlerPathRows.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
|
|
31086
31260
|
);
|
|
31087
31261
|
const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
|
|
31088
|
-
const aiUserFetchTotals = app.db.select({ total:
|
|
31089
|
-
|
|
31090
|
-
|
|
31262
|
+
const aiUserFetchTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
31263
|
+
and22(
|
|
31264
|
+
eq28(aiUserFetchEventsHourly.sourceId, row.id),
|
|
31091
31265
|
gte4(aiUserFetchEventsHourly.tsHour, since)
|
|
31092
31266
|
)
|
|
31093
31267
|
).get();
|
|
31094
|
-
const aiTotals = app.db.select({ total:
|
|
31095
|
-
|
|
31096
|
-
|
|
31268
|
+
const aiTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31269
|
+
and22(
|
|
31270
|
+
eq28(aiReferralEventsHourly.sourceId, row.id),
|
|
31097
31271
|
gte4(aiReferralEventsHourly.tsHour, since)
|
|
31098
31272
|
)
|
|
31099
31273
|
).get();
|
|
31100
|
-
const sampleTotals = app.db.select({ total:
|
|
31101
|
-
|
|
31102
|
-
|
|
31274
|
+
const sampleTotals = app.db.select({ total: sql12`COUNT(*)` }).from(rawEventSamples).where(
|
|
31275
|
+
and22(
|
|
31276
|
+
eq28(rawEventSamples.sourceId, row.id),
|
|
31103
31277
|
gte4(rawEventSamples.ts, since)
|
|
31104
31278
|
)
|
|
31105
31279
|
).get();
|
|
31106
31280
|
const latestRun = app.db.select().from(runs).where(
|
|
31107
|
-
|
|
31108
|
-
|
|
31109
|
-
|
|
31110
|
-
|
|
31281
|
+
and22(
|
|
31282
|
+
eq28(runs.projectId, projectId),
|
|
31283
|
+
eq28(runs.kind, RunKinds["traffic-sync"]),
|
|
31284
|
+
eq28(runs.sourceId, row.id)
|
|
31111
31285
|
)
|
|
31112
31286
|
).orderBy(desc15(runs.startedAt)).limit(1).get();
|
|
31113
31287
|
return {
|
|
@@ -31138,7 +31312,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31138
31312
|
"`advanceToNow` must be `true`. There is no implicit reset."
|
|
31139
31313
|
);
|
|
31140
31314
|
}
|
|
31141
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
31315
|
+
const sourceRow = app.db.select().from(trafficSources).where(and22(eq28(trafficSources.projectId, project.id), eq28(trafficSources.id, request.params.id))).get();
|
|
31142
31316
|
if (!sourceRow) {
|
|
31143
31317
|
throw notFound("traffic source", request.params.id);
|
|
31144
31318
|
}
|
|
@@ -31155,7 +31329,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31155
31329
|
status: TrafficSourceStatuses.connected,
|
|
31156
31330
|
lastError: null,
|
|
31157
31331
|
updatedAt: now
|
|
31158
|
-
}).where(
|
|
31332
|
+
}).where(eq28(trafficSources.id, sourceRow.id)).run();
|
|
31159
31333
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
31160
31334
|
projectId: project.id,
|
|
31161
31335
|
actor: "api",
|
|
@@ -31163,20 +31337,20 @@ async function trafficRoutes(app, opts) {
|
|
|
31163
31337
|
entityType: "traffic_source",
|
|
31164
31338
|
entityId: sourceRow.id
|
|
31165
31339
|
}));
|
|
31166
|
-
updatedRow = tx.select().from(trafficSources).where(
|
|
31340
|
+
updatedRow = tx.select().from(trafficSources).where(eq28(trafficSources.id, sourceRow.id)).get();
|
|
31167
31341
|
});
|
|
31168
31342
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
31169
31343
|
});
|
|
31170
31344
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
31171
31345
|
const project = resolveProject(app.db, request.params.name);
|
|
31172
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
31346
|
+
const rows = app.db.select().from(trafficSources).where(eq28(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
31173
31347
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
31174
31348
|
const response = { sources };
|
|
31175
31349
|
return response;
|
|
31176
31350
|
});
|
|
31177
31351
|
app.get("/projects/:name/traffic/status", async (request) => {
|
|
31178
31352
|
const project = resolveProject(app.db, request.params.name);
|
|
31179
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
31353
|
+
const rows = app.db.select().from(trafficSources).where(eq28(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
31180
31354
|
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
31181
31355
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
31182
31356
|
const response = { sources };
|
|
@@ -31186,7 +31360,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31186
31360
|
"/projects/:name/traffic/sources/:id",
|
|
31187
31361
|
async (request) => {
|
|
31188
31362
|
const project = resolveProject(app.db, request.params.name);
|
|
31189
|
-
const row = app.db.select().from(trafficSources).where(
|
|
31363
|
+
const row = app.db.select().from(trafficSources).where(eq28(trafficSources.id, request.params.id)).get();
|
|
31190
31364
|
if (!row || row.projectId !== project.id) {
|
|
31191
31365
|
throw notFound("Traffic source", request.params.id);
|
|
31192
31366
|
}
|
|
@@ -31238,15 +31412,15 @@ async function trafficRoutes(app, opts) {
|
|
|
31238
31412
|
let aiReferralTotal = 0;
|
|
31239
31413
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
31240
31414
|
const crawlerFilters = [
|
|
31241
|
-
|
|
31415
|
+
eq28(crawlerEventsHourly.projectId, project.id),
|
|
31242
31416
|
gte4(crawlerEventsHourly.tsHour, sinceIso),
|
|
31243
31417
|
lte3(crawlerEventsHourly.tsHour, untilIso)
|
|
31244
31418
|
];
|
|
31245
|
-
if (sourceIdParam) crawlerFilters.push(
|
|
31246
|
-
const crawlerWhere =
|
|
31419
|
+
if (sourceIdParam) crawlerFilters.push(eq28(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
31420
|
+
const crawlerWhere = and22(...crawlerFilters);
|
|
31247
31421
|
const pathTotals = app.db.select({
|
|
31248
31422
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
31249
|
-
hits:
|
|
31423
|
+
hits: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
31250
31424
|
}).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
31251
31425
|
crawlerSegments = segmentCrawlerHits(
|
|
31252
31426
|
pathTotals.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
|
|
@@ -31270,13 +31444,13 @@ async function trafficRoutes(app, opts) {
|
|
|
31270
31444
|
}
|
|
31271
31445
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
31272
31446
|
const userFetchFilters = [
|
|
31273
|
-
|
|
31447
|
+
eq28(aiUserFetchEventsHourly.projectId, project.id),
|
|
31274
31448
|
gte4(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
31275
31449
|
lte3(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
31276
31450
|
];
|
|
31277
|
-
if (sourceIdParam) userFetchFilters.push(
|
|
31278
|
-
const userFetchWhere =
|
|
31279
|
-
const total = app.db.select({ total:
|
|
31451
|
+
if (sourceIdParam) userFetchFilters.push(eq28(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
31452
|
+
const userFetchWhere = and22(...userFetchFilters);
|
|
31453
|
+
const total = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
31280
31454
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
31281
31455
|
const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(desc15(aiUserFetchEventsHourly.tsHour)).limit(limit).all();
|
|
31282
31456
|
for (const r of rows) {
|
|
@@ -31295,13 +31469,13 @@ async function trafficRoutes(app, opts) {
|
|
|
31295
31469
|
}
|
|
31296
31470
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
31297
31471
|
const aiFilters = [
|
|
31298
|
-
|
|
31472
|
+
eq28(aiReferralEventsHourly.projectId, project.id),
|
|
31299
31473
|
gte4(aiReferralEventsHourly.tsHour, sinceIso),
|
|
31300
31474
|
lte3(aiReferralEventsHourly.tsHour, untilIso)
|
|
31301
31475
|
];
|
|
31302
|
-
if (sourceIdParam) aiFilters.push(
|
|
31303
|
-
const aiWhere =
|
|
31304
|
-
const total = app.db.select({ total:
|
|
31476
|
+
if (sourceIdParam) aiFilters.push(eq28(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
31477
|
+
const aiWhere = and22(...aiFilters);
|
|
31478
|
+
const total = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
31305
31479
|
aiReferralTotal = Number(total?.total ?? 0);
|
|
31306
31480
|
const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc15(aiReferralEventsHourly.tsHour)).limit(limit).all();
|
|
31307
31481
|
for (const r of rows) {
|
|
@@ -31507,7 +31681,7 @@ function readInstalledManifest(skillDir) {
|
|
|
31507
31681
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
31508
31682
|
|
|
31509
31683
|
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
31510
|
-
import { and as
|
|
31684
|
+
import { and as and23, eq as eq29 } from "drizzle-orm";
|
|
31511
31685
|
function skippedNoProject() {
|
|
31512
31686
|
return {
|
|
31513
31687
|
status: CheckStatuses.skipped,
|
|
@@ -31523,8 +31697,8 @@ var BACKLINKS_CHECKS = [
|
|
|
31523
31697
|
title: "Backlinks source connected",
|
|
31524
31698
|
run: (ctx) => {
|
|
31525
31699
|
if (!ctx.project) return skippedNoProject();
|
|
31526
|
-
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(
|
|
31527
|
-
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
31700
|
+
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq29(projects.id, ctx.project.id)).get();
|
|
31701
|
+
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
31528
31702
|
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
31529
31703
|
const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
|
|
31530
31704
|
const connected = [];
|
|
@@ -31539,9 +31713,9 @@ var BACKLINKS_CHECKS = [
|
|
|
31539
31713
|
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
|
|
31540
31714
|
};
|
|
31541
31715
|
}
|
|
31542
|
-
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(
|
|
31543
|
-
|
|
31544
|
-
|
|
31716
|
+
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and23(
|
|
31717
|
+
eq29(backlinkSummaries.projectId, ctx.project.id),
|
|
31718
|
+
eq29(backlinkSummaries.source, BacklinkSources.commoncrawl)
|
|
31545
31719
|
)).limit(1).get() : false;
|
|
31546
31720
|
return {
|
|
31547
31721
|
status: CheckStatuses.ok,
|
|
@@ -31701,7 +31875,7 @@ var BING_AUTH_CHECKS = [
|
|
|
31701
31875
|
];
|
|
31702
31876
|
|
|
31703
31877
|
// ../api-routes/src/doctor/checks/content.ts
|
|
31704
|
-
import { eq as
|
|
31878
|
+
import { eq as eq30 } from "drizzle-orm";
|
|
31705
31879
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
31706
31880
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
31707
31881
|
function skippedNoProject2() {
|
|
@@ -31714,7 +31888,7 @@ function skippedNoProject2() {
|
|
|
31714
31888
|
}
|
|
31715
31889
|
function loadProject(ctx) {
|
|
31716
31890
|
if (!ctx.project) return null;
|
|
31717
|
-
return ctx.db.select().from(projects).where(
|
|
31891
|
+
return ctx.db.select().from(projects).where(eq30(projects.id, ctx.project.id)).get() ?? null;
|
|
31718
31892
|
}
|
|
31719
31893
|
function percent(value) {
|
|
31720
31894
|
return Math.round(value * 100);
|
|
@@ -31806,7 +31980,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
31806
31980
|
);
|
|
31807
31981
|
|
|
31808
31982
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
31809
|
-
import { eq as
|
|
31983
|
+
import { eq as eq31 } from "drizzle-orm";
|
|
31810
31984
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
31811
31985
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
31812
31986
|
var adsConnectionCheck = {
|
|
@@ -31823,7 +31997,7 @@ var adsConnectionCheck = {
|
|
|
31823
31997
|
remediation: null
|
|
31824
31998
|
};
|
|
31825
31999
|
}
|
|
31826
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
32000
|
+
const row = ctx.db.select().from(adsConnections).where(eq31(adsConnections.projectId, ctx.project.id)).get();
|
|
31827
32001
|
if (!row) {
|
|
31828
32002
|
return {
|
|
31829
32003
|
status: CheckStatuses.skipped,
|
|
@@ -31873,7 +32047,7 @@ var adsRecentSyncCheck = {
|
|
|
31873
32047
|
remediation: null
|
|
31874
32048
|
};
|
|
31875
32049
|
}
|
|
31876
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
32050
|
+
const row = ctx.db.select().from(adsConnections).where(eq31(adsConnections.projectId, ctx.project.id)).get();
|
|
31877
32051
|
if (!row) {
|
|
31878
32052
|
return {
|
|
31879
32053
|
status: CheckStatuses.skipped,
|
|
@@ -32064,7 +32238,7 @@ var ga4ConnectionCheck = {
|
|
|
32064
32238
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
32065
32239
|
|
|
32066
32240
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
32067
|
-
import { and as
|
|
32241
|
+
import { and as and24, eq as eq32 } from "drizzle-orm";
|
|
32068
32242
|
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
32069
32243
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
32070
32244
|
function skippedNoProject3() {
|
|
@@ -32297,7 +32471,7 @@ var recentSyncCheck = {
|
|
|
32297
32471
|
title: "GBP recent sync",
|
|
32298
32472
|
run: (ctx) => {
|
|
32299
32473
|
if (!ctx.project) return skippedNoProject3();
|
|
32300
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(
|
|
32474
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and24(eq32(gbpLocations.projectId, ctx.project.id), eq32(gbpLocations.selected, true))).all();
|
|
32301
32475
|
if (selected.length === 0) {
|
|
32302
32476
|
return {
|
|
32303
32477
|
status: CheckStatuses.skipped,
|
|
@@ -32357,7 +32531,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
32357
32531
|
);
|
|
32358
32532
|
|
|
32359
32533
|
// ../api-routes/src/doctor/checks/places.ts
|
|
32360
|
-
import { eq as
|
|
32534
|
+
import { eq as eq33 } from "drizzle-orm";
|
|
32361
32535
|
var apiKeyCheck = {
|
|
32362
32536
|
id: "gbp.places.api-key",
|
|
32363
32537
|
category: CheckCategories.auth,
|
|
@@ -32402,7 +32576,7 @@ var apiKeyCheck = {
|
|
|
32402
32576
|
details: { tier: cfg.tier }
|
|
32403
32577
|
};
|
|
32404
32578
|
}
|
|
32405
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
32579
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq33(gbpLocations.projectId, ctx.project.id)).all();
|
|
32406
32580
|
const selected = rows.filter((r) => r.selected);
|
|
32407
32581
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
32408
32582
|
const details = {
|
|
@@ -32899,7 +33073,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
32899
33073
|
];
|
|
32900
33074
|
|
|
32901
33075
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
32902
|
-
import { and as
|
|
33076
|
+
import { and as and25, eq as eq34, gte as gte5, ne as ne4, sql as sql13 } from "drizzle-orm";
|
|
32903
33077
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
32904
33078
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
32905
33079
|
function skippedNoProject5() {
|
|
@@ -32913,8 +33087,8 @@ function skippedNoProject5() {
|
|
|
32913
33087
|
function loadProbes(ctx) {
|
|
32914
33088
|
if (!ctx.project) return [];
|
|
32915
33089
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
32916
|
-
|
|
32917
|
-
|
|
33090
|
+
and25(
|
|
33091
|
+
eq34(trafficSources.projectId, ctx.project.id),
|
|
32918
33092
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
32919
33093
|
)
|
|
32920
33094
|
).all();
|
|
@@ -32993,17 +33167,17 @@ var recentDataCheck = {
|
|
|
32993
33167
|
const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
|
|
32994
33168
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
32995
33169
|
const recentCrawlers = Number(
|
|
32996
|
-
ctx.db.select({ total:
|
|
32997
|
-
|
|
32998
|
-
|
|
33170
|
+
ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
33171
|
+
and25(
|
|
33172
|
+
eq34(crawlerEventsHourly.projectId, ctx.project.id),
|
|
32999
33173
|
gte5(crawlerEventsHourly.tsHour, warnCutoff)
|
|
33000
33174
|
)
|
|
33001
33175
|
).get()?.total ?? 0
|
|
33002
33176
|
);
|
|
33003
33177
|
const recentReferrals = Number(
|
|
33004
|
-
ctx.db.select({ total:
|
|
33005
|
-
|
|
33006
|
-
|
|
33178
|
+
ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
33179
|
+
and25(
|
|
33180
|
+
eq34(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
33007
33181
|
gte5(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
33008
33182
|
)
|
|
33009
33183
|
).get()?.total ?? 0
|
|
@@ -33017,17 +33191,17 @@ var recentDataCheck = {
|
|
|
33017
33191
|
};
|
|
33018
33192
|
}
|
|
33019
33193
|
const olderCrawlers = Number(
|
|
33020
|
-
ctx.db.select({ total:
|
|
33021
|
-
|
|
33022
|
-
|
|
33194
|
+
ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
33195
|
+
and25(
|
|
33196
|
+
eq34(crawlerEventsHourly.projectId, ctx.project.id),
|
|
33023
33197
|
gte5(crawlerEventsHourly.tsHour, failCutoff)
|
|
33024
33198
|
)
|
|
33025
33199
|
).get()?.total ?? 0
|
|
33026
33200
|
);
|
|
33027
33201
|
const olderReferrals = Number(
|
|
33028
|
-
ctx.db.select({ total:
|
|
33029
|
-
|
|
33030
|
-
|
|
33202
|
+
ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
33203
|
+
and25(
|
|
33204
|
+
eq34(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
33031
33205
|
gte5(aiReferralEventsHourly.tsHour, failCutoff)
|
|
33032
33206
|
)
|
|
33033
33207
|
).get()?.total ?? 0
|
|
@@ -33443,7 +33617,7 @@ async function doctorRoutes(app, opts) {
|
|
|
33443
33617
|
|
|
33444
33618
|
// ../api-routes/src/discovery/routes.ts
|
|
33445
33619
|
import crypto27 from "crypto";
|
|
33446
|
-
import { and as
|
|
33620
|
+
import { and as and26, desc as desc16, eq as eq35, gte as gte6, inArray as inArray12 } from "drizzle-orm";
|
|
33447
33621
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
33448
33622
|
async function discoveryRoutes(app, opts) {
|
|
33449
33623
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -33475,9 +33649,9 @@ async function discoveryRoutes(app, opts) {
|
|
|
33475
33649
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33476
33650
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
33477
33651
|
const decision = app.db.transaction((tx) => {
|
|
33478
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
33479
|
-
|
|
33480
|
-
|
|
33652
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and26(
|
|
33653
|
+
eq35(discoverySessions.projectId, project.id),
|
|
33654
|
+
eq35(discoverySessions.icpDescription, icpDescription),
|
|
33481
33655
|
inArray12(discoverySessions.status, [
|
|
33482
33656
|
DiscoverySessionStatuses.queued,
|
|
33483
33657
|
DiscoverySessionStatuses.seeding,
|
|
@@ -33547,7 +33721,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33547
33721
|
const project = resolveProject(app.db, request.params.name);
|
|
33548
33722
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
33549
33723
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
33550
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
33724
|
+
const rows = app.db.select().from(discoverySessions).where(eq35(discoverySessions.projectId, project.id)).orderBy(desc16(discoverySessions.createdAt)).limit(limit).all();
|
|
33551
33725
|
return reply.send(rows.map(serializeSession));
|
|
33552
33726
|
}
|
|
33553
33727
|
);
|
|
@@ -33555,11 +33729,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
33555
33729
|
"/projects/:name/discover/sessions/:id",
|
|
33556
33730
|
async (request, reply) => {
|
|
33557
33731
|
const project = resolveProject(app.db, request.params.name);
|
|
33558
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
33732
|
+
const session = app.db.select().from(discoverySessions).where(eq35(discoverySessions.id, request.params.id)).get();
|
|
33559
33733
|
if (!session || session.projectId !== project.id) {
|
|
33560
33734
|
throw notFound("Discovery session", request.params.id);
|
|
33561
33735
|
}
|
|
33562
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
33736
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq35(discoveryProbes.sessionId, session.id)).all();
|
|
33563
33737
|
const detail = {
|
|
33564
33738
|
...serializeSession(session),
|
|
33565
33739
|
probes: probeRows.map(serializeProbe)
|
|
@@ -33571,7 +33745,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33571
33745
|
"/projects/:name/discover/sessions/:id/harvest",
|
|
33572
33746
|
async (request, reply) => {
|
|
33573
33747
|
const project = resolveProject(app.db, request.params.name);
|
|
33574
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
33748
|
+
const session = app.db.select().from(discoverySessions).where(eq35(discoverySessions.id, request.params.id)).get();
|
|
33575
33749
|
if (!session || session.projectId !== project.id) {
|
|
33576
33750
|
throw notFound("Discovery session", request.params.id);
|
|
33577
33751
|
}
|
|
@@ -33579,7 +33753,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33579
33753
|
const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
|
|
33580
33754
|
const applyAnchor = request.query.anchor !== "false";
|
|
33581
33755
|
const provider = session.seedProvider ?? "gemini";
|
|
33582
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
33756
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq35(discoveryProbes.sessionId, session.id)).all();
|
|
33583
33757
|
const extract = opts.harvestSearchQueries;
|
|
33584
33758
|
const probesWithQueries = probeRows.map((row) => {
|
|
33585
33759
|
if (!extract || !row.rawResponse) return { searchQueries: [] };
|
|
@@ -33590,7 +33764,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33590
33764
|
return { searchQueries: [] };
|
|
33591
33765
|
}
|
|
33592
33766
|
});
|
|
33593
|
-
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(
|
|
33767
|
+
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq35(queries.projectId, project.id)).all().map((r) => r.query);
|
|
33594
33768
|
const anchorTerms = buildHarvestAnchorTerms(
|
|
33595
33769
|
[session.icpDescription ?? "", ...trackedQueries],
|
|
33596
33770
|
effectiveDomains(project)
|
|
@@ -33637,12 +33811,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
33637
33811
|
"/projects/:name/discover/sessions/:id/promote",
|
|
33638
33812
|
async (request, reply) => {
|
|
33639
33813
|
const project = resolveProject(app.db, request.params.name);
|
|
33640
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
33814
|
+
const session = app.db.select().from(discoverySessions).where(eq35(discoverySessions.id, request.params.id)).get();
|
|
33641
33815
|
if (!session || session.projectId !== project.id) {
|
|
33642
33816
|
throw notFound("Discovery session", request.params.id);
|
|
33643
33817
|
}
|
|
33644
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
33645
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
33818
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq35(discoveryProbes.sessionId, session.id)).all();
|
|
33819
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq35(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
33646
33820
|
const seenCompetitors = new Set(existingCompetitors);
|
|
33647
33821
|
const cited = /* @__PURE__ */ new Set();
|
|
33648
33822
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -33671,7 +33845,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33671
33845
|
);
|
|
33672
33846
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
33673
33847
|
const project = resolveProject(app.db, request.params.name);
|
|
33674
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
33848
|
+
const session = app.db.select().from(discoverySessions).where(eq35(discoverySessions.id, request.params.id)).get();
|
|
33675
33849
|
if (!session || session.projectId !== project.id) {
|
|
33676
33850
|
throw notFound("Discovery session", request.params.id);
|
|
33677
33851
|
}
|
|
@@ -33694,7 +33868,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33694
33868
|
const bucketSet = new Set(buckets);
|
|
33695
33869
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
33696
33870
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
33697
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
33871
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq35(discoveryProbes.sessionId, session.id)).all();
|
|
33698
33872
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
33699
33873
|
for (const probe of probeRows) {
|
|
33700
33874
|
if (!probe.bucket) continue;
|
|
@@ -33702,7 +33876,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33702
33876
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
33703
33877
|
}
|
|
33704
33878
|
const existingQueries = new Set(
|
|
33705
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
33879
|
+
app.db.select({ query: queries.query }).from(queries).where(eq35(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
33706
33880
|
);
|
|
33707
33881
|
const promotedQueries = [];
|
|
33708
33882
|
const skippedQueries = [];
|
|
@@ -33718,7 +33892,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33718
33892
|
const skippedCompetitors = [];
|
|
33719
33893
|
if (includeCompetitors) {
|
|
33720
33894
|
const existingCompetitors = new Set(
|
|
33721
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
33895
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq35(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
33722
33896
|
);
|
|
33723
33897
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
33724
33898
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -33825,7 +33999,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
33825
33999
|
|
|
33826
34000
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
33827
34001
|
import crypto28 from "crypto";
|
|
33828
|
-
import { eq as
|
|
34002
|
+
import { eq as eq36 } from "drizzle-orm";
|
|
33829
34003
|
var DEFAULT_MAX_PROBES = 100;
|
|
33830
34004
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
33831
34005
|
function classifyProbeBucket(input) {
|
|
@@ -33879,7 +34053,7 @@ async function executeDiscovery(opts) {
|
|
|
33879
34053
|
status: DiscoverySessionStatuses.seeding,
|
|
33880
34054
|
dedupThreshold,
|
|
33881
34055
|
startedAt
|
|
33882
|
-
}).where(
|
|
34056
|
+
}).where(eq36(discoverySessions.id, opts.sessionId)).run();
|
|
33883
34057
|
const seedResult = await opts.deps.seed({
|
|
33884
34058
|
project: opts.project,
|
|
33885
34059
|
icpDescription: opts.icpDescription,
|
|
@@ -33905,7 +34079,7 @@ async function executeDiscovery(opts) {
|
|
|
33905
34079
|
seedCountRaw,
|
|
33906
34080
|
seedCount,
|
|
33907
34081
|
warning
|
|
33908
|
-
}).where(
|
|
34082
|
+
}).where(eq36(discoverySessions.id, opts.sessionId)).run();
|
|
33909
34083
|
const probeRows = [];
|
|
33910
34084
|
const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
|
|
33911
34085
|
for (const query of probedCanonicals) {
|
|
@@ -33946,7 +34120,7 @@ async function executeDiscovery(opts) {
|
|
|
33946
34120
|
wastedCount: buckets["wasted-surface"],
|
|
33947
34121
|
competitorMap,
|
|
33948
34122
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33949
|
-
}).where(
|
|
34123
|
+
}).where(eq36(discoverySessions.id, opts.sessionId)).run();
|
|
33950
34124
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
33951
34125
|
return {
|
|
33952
34126
|
buckets,
|
|
@@ -33986,7 +34160,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
33986
34160
|
status: DiscoverySessionStatuses.failed,
|
|
33987
34161
|
error,
|
|
33988
34162
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33989
|
-
}).where(
|
|
34163
|
+
}).where(eq36(discoverySessions.id, sessionId)).run();
|
|
33990
34164
|
}
|
|
33991
34165
|
function dedupeStrings(input) {
|
|
33992
34166
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -34004,7 +34178,7 @@ function dedupeStrings(input) {
|
|
|
34004
34178
|
|
|
34005
34179
|
// ../api-routes/src/technical-aeo.ts
|
|
34006
34180
|
import crypto29 from "crypto";
|
|
34007
|
-
import { and as
|
|
34181
|
+
import { and as and27, asc as asc5, count, desc as desc17, eq as eq37, inArray as inArray13 } from "drizzle-orm";
|
|
34008
34182
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
34009
34183
|
function emptyScore(projectName) {
|
|
34010
34184
|
return {
|
|
@@ -34036,9 +34210,9 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
34036
34210
|
async function technicalAeoRoutes(app, opts) {
|
|
34037
34211
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
34038
34212
|
const project = resolveProject(app.db, request.params.name);
|
|
34039
|
-
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs,
|
|
34040
|
-
|
|
34041
|
-
|
|
34213
|
+
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq37(siteAuditSnapshots.runId, runs.id)).where(and27(
|
|
34214
|
+
eq37(siteAuditSnapshots.projectId, project.id),
|
|
34215
|
+
eq37(runs.kind, RunKinds["site-audit"]),
|
|
34042
34216
|
inArray13(runs.status, SURFACEABLE_STATUSES),
|
|
34043
34217
|
notProbeRun()
|
|
34044
34218
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
@@ -34071,9 +34245,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
34071
34245
|
});
|
|
34072
34246
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
34073
34247
|
const project = resolveProject(app.db, request.params.name);
|
|
34074
|
-
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs,
|
|
34075
|
-
|
|
34076
|
-
|
|
34248
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq37(siteAuditSnapshots.runId, runs.id)).where(and27(
|
|
34249
|
+
eq37(siteAuditSnapshots.projectId, project.id),
|
|
34250
|
+
eq37(runs.kind, RunKinds["site-audit"]),
|
|
34077
34251
|
inArray13(runs.status, SURFACEABLE_STATUSES),
|
|
34078
34252
|
notProbeRun()
|
|
34079
34253
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
@@ -34081,14 +34255,14 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
34081
34255
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
34082
34256
|
}
|
|
34083
34257
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
34084
|
-
const conds = [
|
|
34085
|
-
if (statusFilter) conds.push(
|
|
34086
|
-
const where =
|
|
34258
|
+
const conds = [eq37(siteAuditPages.runId, latest.runId)];
|
|
34259
|
+
if (statusFilter) conds.push(eq37(siteAuditPages.status, statusFilter));
|
|
34260
|
+
const where = and27(...conds);
|
|
34087
34261
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
34088
34262
|
const total = totalRow?.value ?? 0;
|
|
34089
34263
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
34090
34264
|
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
34091
|
-
const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ?
|
|
34265
|
+
const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ? asc5(siteAuditPages.url) : asc5(siteAuditPages.overallScore);
|
|
34092
34266
|
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
34093
34267
|
const pages = rows.map((row) => ({
|
|
34094
34268
|
url: row.url,
|
|
@@ -34107,9 +34281,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
34107
34281
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
34108
34282
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
34109
34283
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
34110
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
34111
|
-
|
|
34112
|
-
|
|
34284
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq37(siteAuditSnapshots.runId, runs.id)).where(and27(
|
|
34285
|
+
eq37(siteAuditSnapshots.projectId, project.id),
|
|
34286
|
+
eq37(runs.kind, RunKinds["site-audit"]),
|
|
34113
34287
|
inArray13(runs.status, SURFACEABLE_STATUSES),
|
|
34114
34288
|
notProbeRun()
|
|
34115
34289
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
@@ -34121,9 +34295,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
34121
34295
|
if (!parsed.success) {
|
|
34122
34296
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
34123
34297
|
}
|
|
34124
|
-
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
34125
|
-
|
|
34126
|
-
|
|
34298
|
+
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and27(
|
|
34299
|
+
eq37(runs.projectId, project.id),
|
|
34300
|
+
eq37(runs.kind, RunKinds["site-audit"]),
|
|
34127
34301
|
inArray13(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
34128
34302
|
)).get();
|
|
34129
34303
|
if (existing) {
|
|
@@ -34714,9 +34888,9 @@ var IntelligenceService = class {
|
|
|
34714
34888
|
*/
|
|
34715
34889
|
analyzeAndPersist(runId, projectId) {
|
|
34716
34890
|
const recentRuns = this.db.select().from(runs).where(
|
|
34717
|
-
|
|
34718
|
-
|
|
34719
|
-
or5(
|
|
34891
|
+
and28(
|
|
34892
|
+
eq38(runs.projectId, projectId),
|
|
34893
|
+
or5(eq38(runs.status, "completed"), eq38(runs.status, "partial")),
|
|
34720
34894
|
// Defensive: RunCoordinator already skips probes before this is
|
|
34721
34895
|
// called, but if a future call site invokes analyzeAndPersist
|
|
34722
34896
|
// directly for a probe, probes still must not pollute the
|
|
@@ -34798,7 +34972,7 @@ var IntelligenceService = class {
|
|
|
34798
34972
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
34799
34973
|
*/
|
|
34800
34974
|
analyzeAndPersistGbp(runId, projectId) {
|
|
34801
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
34975
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq38(runs.id, runId)).get();
|
|
34802
34976
|
if (!runRow) {
|
|
34803
34977
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
34804
34978
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -34806,9 +34980,9 @@ var IntelligenceService = class {
|
|
|
34806
34980
|
}
|
|
34807
34981
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
34808
34982
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
34809
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
34810
|
-
|
|
34811
|
-
|
|
34983
|
+
const selected = this.db.select().from(gbpLocations).where(and28(
|
|
34984
|
+
eq38(gbpLocations.projectId, projectId),
|
|
34985
|
+
eq38(gbpLocations.selected, true),
|
|
34812
34986
|
gte7(gbpLocations.syncedAt, windowStart),
|
|
34813
34987
|
lte4(gbpLocations.syncedAt, windowEnd)
|
|
34814
34988
|
)).all();
|
|
@@ -34843,12 +35017,12 @@ var IntelligenceService = class {
|
|
|
34843
35017
|
}
|
|
34844
35018
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
34845
35019
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
34846
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
34847
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
34848
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
34849
|
-
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(
|
|
35020
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and28(eq38(gbpDailyMetrics.projectId, projectId), eq38(gbpDailyMetrics.locationName, locationName))).all();
|
|
35021
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and28(eq38(gbpPlaceActions.projectId, projectId), eq38(gbpPlaceActions.locationName, locationName))).all();
|
|
35022
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and28(eq38(gbpLodgingSnapshots.projectId, projectId), eq38(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
35023
|
+
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and28(eq38(gbpLocations.projectId, projectId), eq38(gbpLocations.locationName, locationName))).get();
|
|
34850
35024
|
const descriptionMissing = !(ownerRow?.description ?? "").trim();
|
|
34851
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(
|
|
35025
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and28(eq38(gbpPlaceDetails.projectId, projectId), eq38(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
34852
35026
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
34853
35027
|
const summary = buildGbpSummary({
|
|
34854
35028
|
locationName,
|
|
@@ -34884,7 +35058,7 @@ var IntelligenceService = class {
|
|
|
34884
35058
|
/** Build the month-over-month keyword series for a location from the
|
|
34885
35059
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
34886
35060
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
34887
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
35061
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and28(eq38(gbpKeywordMonthly.projectId, projectId), eq38(gbpKeywordMonthly.locationName, locationName))).all();
|
|
34888
35062
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
34889
35063
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
34890
35064
|
const recentMonth = months[0] ?? null;
|
|
@@ -34915,7 +35089,7 @@ var IntelligenceService = class {
|
|
|
34915
35089
|
*/
|
|
34916
35090
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
34917
35091
|
const covered = new Set(coveredLocationNames);
|
|
34918
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
35092
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and28(eq38(insights.projectId, projectId), eq38(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
34919
35093
|
const staleIds = [];
|
|
34920
35094
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
34921
35095
|
for (const row of existing) {
|
|
@@ -34926,7 +35100,7 @@ var IntelligenceService = class {
|
|
|
34926
35100
|
}
|
|
34927
35101
|
this.db.transaction((tx) => {
|
|
34928
35102
|
for (const id of staleIds) {
|
|
34929
|
-
tx.delete(insights).where(
|
|
35103
|
+
tx.delete(insights).where(eq38(insights.id, id)).run();
|
|
34930
35104
|
}
|
|
34931
35105
|
for (const insight of gbpInsights) {
|
|
34932
35106
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -35004,7 +35178,7 @@ var IntelligenceService = class {
|
|
|
35004
35178
|
* create per run + aggregate). DB is left untouched.
|
|
35005
35179
|
*/
|
|
35006
35180
|
backfill(projectName, opts, onProgress) {
|
|
35007
|
-
const project = this.db.select().from(projects).where(
|
|
35181
|
+
const project = this.db.select().from(projects).where(eq38(projects.name, projectName)).get();
|
|
35008
35182
|
if (!project) {
|
|
35009
35183
|
throw new Error(`Project "${projectName}" not found`);
|
|
35010
35184
|
}
|
|
@@ -35017,13 +35191,13 @@ var IntelligenceService = class {
|
|
|
35017
35191
|
sinceTimestamp = parsed;
|
|
35018
35192
|
}
|
|
35019
35193
|
const allRuns = this.db.select().from(runs).where(
|
|
35020
|
-
|
|
35021
|
-
|
|
35022
|
-
or5(
|
|
35194
|
+
and28(
|
|
35195
|
+
eq38(runs.projectId, project.id),
|
|
35196
|
+
or5(eq38(runs.status, "completed"), eq38(runs.status, "partial")),
|
|
35023
35197
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
35024
35198
|
ne5(runs.trigger, RunTriggers.probe)
|
|
35025
35199
|
)
|
|
35026
|
-
).orderBy(
|
|
35200
|
+
).orderBy(asc6(runs.finishedAt)).all();
|
|
35027
35201
|
let startIdx = 0;
|
|
35028
35202
|
let endIdx = allRuns.length;
|
|
35029
35203
|
if (opts?.fromRunId) {
|
|
@@ -35098,7 +35272,7 @@ var IntelligenceService = class {
|
|
|
35098
35272
|
return { processed, skipped, totalInsights };
|
|
35099
35273
|
}
|
|
35100
35274
|
loadTrackedCompetitors(projectId) {
|
|
35101
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
35275
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq38(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
35102
35276
|
}
|
|
35103
35277
|
/**
|
|
35104
35278
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -35119,15 +35293,15 @@ var IntelligenceService = class {
|
|
|
35119
35293
|
}
|
|
35120
35294
|
persistResult(result, runId, projectId) {
|
|
35121
35295
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
35122
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
35296
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq38(insights.runId, runId)).all();
|
|
35123
35297
|
for (const row of existingInsights) {
|
|
35124
35298
|
if (row.dismissed) {
|
|
35125
35299
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
35126
35300
|
}
|
|
35127
35301
|
}
|
|
35128
35302
|
this.db.transaction((tx) => {
|
|
35129
|
-
tx.delete(insights).where(
|
|
35130
|
-
tx.delete(healthSnapshots).where(
|
|
35303
|
+
tx.delete(insights).where(eq38(insights.runId, runId)).run();
|
|
35304
|
+
tx.delete(healthSnapshots).where(eq38(healthSnapshots.runId, runId)).run();
|
|
35131
35305
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
35132
35306
|
for (const insight of result.insights) {
|
|
35133
35307
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -35180,24 +35354,24 @@ var IntelligenceService = class {
|
|
|
35180
35354
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
35181
35355
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
35182
35356
|
if (regressions.length === 0) return rawInsights;
|
|
35183
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
35357
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq38(gscSearchData.projectId, projectId)).all();
|
|
35184
35358
|
const gscConnected = gscRows.length > 0;
|
|
35185
35359
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
35186
35360
|
for (const row of gscRows) {
|
|
35187
35361
|
const key = row.query.toLowerCase();
|
|
35188
35362
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
35189
35363
|
}
|
|
35190
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
35364
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq38(projects.id, projectId)).get();
|
|
35191
35365
|
const locationCount = Math.max(
|
|
35192
35366
|
1,
|
|
35193
35367
|
(projectRow?.locations ?? []).length
|
|
35194
35368
|
);
|
|
35195
35369
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
35196
35370
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
35197
|
-
|
|
35198
|
-
|
|
35199
|
-
|
|
35200
|
-
or5(
|
|
35371
|
+
and28(
|
|
35372
|
+
eq38(runs.projectId, projectId),
|
|
35373
|
+
eq38(runs.kind, RunKinds["answer-visibility"]),
|
|
35374
|
+
or5(eq38(runs.status, "completed"), eq38(runs.status, "partial")),
|
|
35201
35375
|
// Defensive — see top of file.
|
|
35202
35376
|
ne5(runs.trigger, RunTriggers.probe)
|
|
35203
35377
|
)
|
|
@@ -35217,7 +35391,7 @@ var IntelligenceService = class {
|
|
|
35217
35391
|
const haveHistory = recentRunIds.length > 0;
|
|
35218
35392
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
35219
35393
|
if (haveHistory) {
|
|
35220
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
35394
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and28(eq38(insights.type, "regression"), inArray14(insights.runId, recentRunIds))).all();
|
|
35221
35395
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
35222
35396
|
for (const row of priorRows) {
|
|
35223
35397
|
if (!row.runId) continue;
|
|
@@ -35246,7 +35420,7 @@ var IntelligenceService = class {
|
|
|
35246
35420
|
});
|
|
35247
35421
|
}
|
|
35248
35422
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
35249
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
35423
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq38(projects.id, projectId)).get();
|
|
35250
35424
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
35251
35425
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
35252
35426
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -35263,7 +35437,7 @@ var IntelligenceService = class {
|
|
|
35263
35437
|
citedDomains: querySnapshots.citedDomains,
|
|
35264
35438
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
35265
35439
|
snapshotLocation: querySnapshots.location
|
|
35266
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
35440
|
+
}).from(querySnapshots).leftJoin(queries, eq38(querySnapshots.queryId, queries.id)).where(eq38(querySnapshots.runId, runId)).all();
|
|
35267
35441
|
const snapshots = [];
|
|
35268
35442
|
let orphanCount = 0;
|
|
35269
35443
|
for (const r of rows) {
|
|
@@ -35316,6 +35490,7 @@ export {
|
|
|
35316
35490
|
schedules,
|
|
35317
35491
|
notifications,
|
|
35318
35492
|
gscSearchData,
|
|
35493
|
+
gscDailyTotals,
|
|
35319
35494
|
gscUrlInspections,
|
|
35320
35495
|
gscCoverageSnapshots,
|
|
35321
35496
|
siteAuditSnapshots,
|