@ainyc/canonry 4.33.1 → 4.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/assets/assets/index-Cfv0_lwq.css +1 -0
- package/assets/assets/index-u7ZXZ5mA.js +302 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-5EBN7736.js → chunk-B3FBOECD.js} +1 -1
- package/dist/{chunk-XW3F5EEW.js → chunk-EM5GVF3C.js} +73 -20
- package/dist/{chunk-BJXHETQW.js → chunk-MLS5KJWK.js} +243 -39
- package/dist/{chunk-DZENHID5.js → chunk-NLV4MZZF.js} +1105 -294
- package/dist/cli.js +100 -563
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-XKOUBRCE.js → intelligence-service-WAJOEOJV.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +8 -8
- package/assets/assets/index-47V0U52s.js +0 -302
- package/assets/assets/index-CNKAwZMB.css +0 -1
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
loadConfig,
|
|
6
6
|
loadConfigRaw,
|
|
7
7
|
saveConfigPatch
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-B3FBOECD.js";
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_RUN_HISTORY_LIMIT,
|
|
11
11
|
IntelligenceService,
|
|
@@ -39,11 +39,13 @@ import {
|
|
|
39
39
|
ccReleaseSyncs,
|
|
40
40
|
competitors,
|
|
41
41
|
crawlerEventsHourly,
|
|
42
|
+
createClient,
|
|
42
43
|
createLogger,
|
|
43
44
|
discoveryProbes,
|
|
44
45
|
discoverySessions,
|
|
45
46
|
dropLegacyCredentialColumns,
|
|
46
47
|
extractLegacyCredentials,
|
|
48
|
+
filterTrackedSnapshots,
|
|
47
49
|
gaAiReferrals,
|
|
48
50
|
gaSocialReferrals,
|
|
49
51
|
gaTrafficSnapshots,
|
|
@@ -59,6 +61,7 @@ import {
|
|
|
59
61
|
isBlogShapedQuery,
|
|
60
62
|
isTrendBaseline,
|
|
61
63
|
mapOpportunitiesToNextSteps,
|
|
64
|
+
migrate,
|
|
62
65
|
notifications,
|
|
63
66
|
parseJsonColumn,
|
|
64
67
|
pickGroupRepresentative,
|
|
@@ -70,7 +73,7 @@ import {
|
|
|
70
73
|
schedules,
|
|
71
74
|
trafficSources,
|
|
72
75
|
usageCounters
|
|
73
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-MLS5KJWK.js";
|
|
74
77
|
import {
|
|
75
78
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
76
79
|
AGENT_PROVIDER_IDS,
|
|
@@ -89,6 +92,7 @@ import {
|
|
|
89
92
|
DiscoveryCompetitorTypes,
|
|
90
93
|
DiscoverySessionStatuses,
|
|
91
94
|
MemorySources,
|
|
95
|
+
ProviderNames,
|
|
92
96
|
RunKinds,
|
|
93
97
|
RunStatuses,
|
|
94
98
|
RunTriggers,
|
|
@@ -126,6 +130,7 @@ import {
|
|
|
126
130
|
discoveryBucketSchema,
|
|
127
131
|
discoveryPromoteRequestSchema,
|
|
128
132
|
discoveryRunRequestSchema,
|
|
133
|
+
effectiveBrandNames,
|
|
129
134
|
effectiveDomains,
|
|
130
135
|
emptyCitationVisibility,
|
|
131
136
|
extractAnswerMentions,
|
|
@@ -143,7 +148,9 @@ import {
|
|
|
143
148
|
isBrowserProvider,
|
|
144
149
|
keywordGenerateRequestSchema,
|
|
145
150
|
locationContextSchema,
|
|
151
|
+
mentionStateFromAnswerMentioned,
|
|
146
152
|
missingDependency,
|
|
153
|
+
normalizeProjectAliases,
|
|
147
154
|
normalizeProjectDomain,
|
|
148
155
|
normalizeUrlPath,
|
|
149
156
|
notFound,
|
|
@@ -179,7 +186,7 @@ import {
|
|
|
179
186
|
visibilityStateFromAnswerMentioned,
|
|
180
187
|
windowCutoff,
|
|
181
188
|
wordpressEnvSchema
|
|
182
|
-
} from "./chunk-
|
|
189
|
+
} from "./chunk-EM5GVF3C.js";
|
|
183
190
|
|
|
184
191
|
// src/telemetry.ts
|
|
185
192
|
import crypto from "crypto";
|
|
@@ -332,13 +339,146 @@ function trackEvent(event, properties, options) {
|
|
|
332
339
|
}).finally(() => clearTimeout(timeout));
|
|
333
340
|
}
|
|
334
341
|
|
|
342
|
+
// src/update-check.ts
|
|
343
|
+
import { createRequire as createRequire2 } from "module";
|
|
344
|
+
var _require2 = createRequire2(import.meta.url);
|
|
345
|
+
var { version: PKG_VERSION } = _require2("../package.json");
|
|
346
|
+
var PKG_NAME = "@ainyc/canonry";
|
|
347
|
+
var NPM_DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PKG_NAME}/dist-tags`;
|
|
348
|
+
var NPM_PACKAGE_URL = `https://www.npmjs.com/package/${PKG_NAME}`;
|
|
349
|
+
var FETCH_TIMEOUT_MS = 1500;
|
|
350
|
+
function isUpdateCheckEnabled() {
|
|
351
|
+
if (process.env.CANONRY_DISABLE_UPDATE_CHECK === "1") return false;
|
|
352
|
+
if (process.env.DO_NOT_TRACK === "1") return false;
|
|
353
|
+
if (process.env.CI) return false;
|
|
354
|
+
if (!configExists()) return true;
|
|
355
|
+
try {
|
|
356
|
+
const raw = loadConfigRaw();
|
|
357
|
+
return raw?.updateCheck !== false;
|
|
358
|
+
} catch {
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function compareSemver(a, b) {
|
|
363
|
+
const parse = (v) => {
|
|
364
|
+
const core = v.split(/[-+]/)[0];
|
|
365
|
+
if (!core) return null;
|
|
366
|
+
const parts = core.split(".");
|
|
367
|
+
if (parts.length < 3) return null;
|
|
368
|
+
const nums = [];
|
|
369
|
+
for (let i = 0; i < 3; i++) {
|
|
370
|
+
const n = Number(parts[i]);
|
|
371
|
+
if (!Number.isInteger(n) || n < 0) return null;
|
|
372
|
+
nums.push(n);
|
|
373
|
+
}
|
|
374
|
+
return [nums[0], nums[1], nums[2]];
|
|
375
|
+
};
|
|
376
|
+
const pa = parse(a);
|
|
377
|
+
const pb = parse(b);
|
|
378
|
+
if (!pa || !pb) return 0;
|
|
379
|
+
for (let i = 0; i < 3; i++) {
|
|
380
|
+
if (pa[i] > pb[i]) return 1;
|
|
381
|
+
if (pa[i] < pb[i]) return -1;
|
|
382
|
+
}
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
async function fetchLatestVersion(opts) {
|
|
386
|
+
const controller = new AbortController();
|
|
387
|
+
const timeout = setTimeout(() => controller.abort(), opts?.timeoutMs ?? FETCH_TIMEOUT_MS);
|
|
388
|
+
timeout.unref();
|
|
389
|
+
try {
|
|
390
|
+
const res = await fetch(NPM_DIST_TAGS_URL, {
|
|
391
|
+
signal: controller.signal,
|
|
392
|
+
headers: { accept: "application/json" }
|
|
393
|
+
});
|
|
394
|
+
if (!res.ok) return null;
|
|
395
|
+
const data = await res.json();
|
|
396
|
+
if (typeof data.latest !== "string") return null;
|
|
397
|
+
return data.latest;
|
|
398
|
+
} catch {
|
|
399
|
+
return null;
|
|
400
|
+
} finally {
|
|
401
|
+
clearTimeout(timeout);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function buildUpdateAvailable(current, latest) {
|
|
405
|
+
if (compareSemver(latest, current) <= 0) return null;
|
|
406
|
+
return {
|
|
407
|
+
current,
|
|
408
|
+
latest,
|
|
409
|
+
url: NPM_PACKAGE_URL,
|
|
410
|
+
upgradeCommand: `npm install -g ${PKG_NAME}`
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
async function checkLatestVersionForCli(opts) {
|
|
414
|
+
if (!isUpdateCheckEnabled()) return null;
|
|
415
|
+
if (!configExists()) return null;
|
|
416
|
+
const now = opts?.now ? opts.now() : /* @__PURE__ */ new Date();
|
|
417
|
+
const ttlMs = (opts?.ttlHours ?? 24) * 60 * 60 * 1e3;
|
|
418
|
+
let raw;
|
|
419
|
+
try {
|
|
420
|
+
raw = loadConfigRaw();
|
|
421
|
+
} catch {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
if (!raw) return null;
|
|
425
|
+
const lastCheckedAt = raw.lastUpdateCheckAt ? Date.parse(raw.lastUpdateCheckAt) : NaN;
|
|
426
|
+
const cachedLatest = typeof raw.lastKnownLatestVersion === "string" ? raw.lastKnownLatestVersion : void 0;
|
|
427
|
+
if (Number.isFinite(lastCheckedAt) && now.getTime() - lastCheckedAt < ttlMs) {
|
|
428
|
+
if (!cachedLatest) return null;
|
|
429
|
+
return buildUpdateAvailable(PKG_VERSION, cachedLatest);
|
|
430
|
+
}
|
|
431
|
+
const latest = await fetchLatestVersion();
|
|
432
|
+
if (!latest) {
|
|
433
|
+
try {
|
|
434
|
+
saveConfigPatch({ lastUpdateCheckAt: now.toISOString() });
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
saveConfigPatch({
|
|
441
|
+
lastUpdateCheckAt: now.toISOString(),
|
|
442
|
+
lastKnownLatestVersion: latest
|
|
443
|
+
});
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
return buildUpdateAvailable(PKG_VERSION, latest);
|
|
447
|
+
}
|
|
448
|
+
var memoryCache = null;
|
|
449
|
+
var inFlight = null;
|
|
450
|
+
function startBackgroundRefresh(getNow) {
|
|
451
|
+
if (inFlight) return;
|
|
452
|
+
inFlight = (async () => {
|
|
453
|
+
try {
|
|
454
|
+
const latest = await fetchLatestVersion();
|
|
455
|
+
memoryCache = { fetchedAt: getNow(), latest };
|
|
456
|
+
} catch {
|
|
457
|
+
memoryCache = { fetchedAt: getNow(), latest: null };
|
|
458
|
+
} finally {
|
|
459
|
+
inFlight = null;
|
|
460
|
+
}
|
|
461
|
+
})();
|
|
462
|
+
}
|
|
463
|
+
function checkLatestVersionForServer(opts) {
|
|
464
|
+
if (!isUpdateCheckEnabled()) return null;
|
|
465
|
+
const getNow = opts?.now ?? Date.now;
|
|
466
|
+
const ttl = opts?.ttlMs ?? 60 * 60 * 1e3;
|
|
467
|
+
const now = getNow();
|
|
468
|
+
if (!memoryCache || now - memoryCache.fetchedAt >= ttl) {
|
|
469
|
+
startBackgroundRefresh(getNow);
|
|
470
|
+
}
|
|
471
|
+
if (!memoryCache || !memoryCache.latest) return null;
|
|
472
|
+
return buildUpdateAvailable(PKG_VERSION, memoryCache.latest);
|
|
473
|
+
}
|
|
474
|
+
|
|
335
475
|
// src/server.ts
|
|
336
|
-
import { createRequire as
|
|
476
|
+
import { createRequire as createRequire4 } from "module";
|
|
337
477
|
import crypto34 from "crypto";
|
|
338
478
|
import fs12 from "fs";
|
|
339
479
|
import path14 from "path";
|
|
340
480
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
341
|
-
import { eq as
|
|
481
|
+
import { eq as eq41 } from "drizzle-orm";
|
|
342
482
|
import Fastify from "fastify";
|
|
343
483
|
|
|
344
484
|
// ../api-routes/src/auth.ts
|
|
@@ -439,7 +579,11 @@ function resolveSnapshotMentionResult(snapshot, project) {
|
|
|
439
579
|
canonicalDomain: project.canonicalDomain,
|
|
440
580
|
ownedDomains: normalizeOwnedDomains(project.ownedDomains)
|
|
441
581
|
});
|
|
442
|
-
|
|
582
|
+
const brandNames = effectiveBrandNames({
|
|
583
|
+
displayName: project.displayName,
|
|
584
|
+
aliases: normalizeOwnedDomains(project.aliases)
|
|
585
|
+
});
|
|
586
|
+
return extractAnswerMentions(snapshot.answerText, brandNames, domains);
|
|
443
587
|
}
|
|
444
588
|
if (typeof snapshot.answerMentioned === "boolean") {
|
|
445
589
|
return { mentioned: snapshot.answerMentioned, matchedTerms: [] };
|
|
@@ -452,6 +596,9 @@ function resolveSnapshotAnswerMentioned(snapshot, project) {
|
|
|
452
596
|
function resolveSnapshotVisibilityState(snapshot, project) {
|
|
453
597
|
return visibilityStateFromAnswerMentioned(resolveSnapshotMentionResult(snapshot, project).mentioned);
|
|
454
598
|
}
|
|
599
|
+
function resolveSnapshotMentionState(snapshot, project) {
|
|
600
|
+
return mentionStateFromAnswerMentioned(resolveSnapshotMentionResult(snapshot, project).mentioned);
|
|
601
|
+
}
|
|
455
602
|
function resolveSnapshotMatchedTerms(snapshot, project) {
|
|
456
603
|
return resolveSnapshotMentionResult(snapshot, project).matchedTerms;
|
|
457
604
|
}
|
|
@@ -502,12 +649,16 @@ async function projectRoutes(app, opts) {
|
|
|
502
649
|
});
|
|
503
650
|
}
|
|
504
651
|
const nextAutoExtractBacklinks = body.autoExtractBacklinks !== void 0 ? body.autoExtractBacklinks ? 1 : 0 : existing?.autoExtractBacklinks ?? 0;
|
|
652
|
+
const nextAliases = normalizeProjectAliases(body.displayName, body.aliases ?? []);
|
|
505
653
|
if (existing) {
|
|
654
|
+
const prevAliases = parseJsonColumn(existing.aliases, []);
|
|
655
|
+
const aliasesChanged = !aliasArraysEqual(prevAliases, nextAliases);
|
|
506
656
|
app.db.transaction((tx) => {
|
|
507
657
|
tx.update(projects).set({
|
|
508
658
|
displayName: body.displayName,
|
|
509
659
|
canonicalDomain: body.canonicalDomain,
|
|
510
660
|
ownedDomains: JSON.stringify(body.ownedDomains ?? []),
|
|
661
|
+
aliases: JSON.stringify(nextAliases),
|
|
511
662
|
country: body.country,
|
|
512
663
|
language: body.language,
|
|
513
664
|
tags: JSON.stringify(body.tags ?? []),
|
|
@@ -529,6 +680,7 @@ async function projectRoutes(app, opts) {
|
|
|
529
680
|
});
|
|
530
681
|
});
|
|
531
682
|
opts.onProjectUpserted?.(existing.id, name);
|
|
683
|
+
if (aliasesChanged) opts.onAliasesChanged?.(existing.id, name);
|
|
532
684
|
const updated = app.db.select().from(projects).where(eq3(projects.id, existing.id)).get();
|
|
533
685
|
return reply.status(200).send(formatProject(updated));
|
|
534
686
|
}
|
|
@@ -540,6 +692,7 @@ async function projectRoutes(app, opts) {
|
|
|
540
692
|
displayName: body.displayName,
|
|
541
693
|
canonicalDomain: body.canonicalDomain,
|
|
542
694
|
ownedDomains: JSON.stringify(body.ownedDomains ?? []),
|
|
695
|
+
aliases: JSON.stringify(nextAliases),
|
|
543
696
|
country: body.country,
|
|
544
697
|
language: body.language,
|
|
545
698
|
tags: JSON.stringify(body.tags ?? []),
|
|
@@ -687,6 +840,7 @@ async function projectRoutes(app, opts) {
|
|
|
687
840
|
displayName: project.displayName,
|
|
688
841
|
canonicalDomain: project.canonicalDomain,
|
|
689
842
|
ownedDomains: parseJsonColumn(project.ownedDomains, []),
|
|
843
|
+
aliases: parseJsonColumn(project.aliases, []),
|
|
690
844
|
country: project.country,
|
|
691
845
|
language: project.language,
|
|
692
846
|
queries: qs.map((q) => q.query),
|
|
@@ -722,6 +876,7 @@ function formatProject(row) {
|
|
|
722
876
|
displayName: row.displayName,
|
|
723
877
|
canonicalDomain: row.canonicalDomain,
|
|
724
878
|
ownedDomains: parseJsonColumn(row.ownedDomains, []),
|
|
879
|
+
aliases: parseJsonColumn(row.aliases, []),
|
|
725
880
|
country: row.country,
|
|
726
881
|
language: row.language,
|
|
727
882
|
tags: parseJsonColumn(row.tags, []),
|
|
@@ -736,6 +891,13 @@ function formatProject(row) {
|
|
|
736
891
|
updatedAt: row.updatedAt
|
|
737
892
|
};
|
|
738
893
|
}
|
|
894
|
+
function aliasArraysEqual(a, b) {
|
|
895
|
+
if (a.length !== b.length) return false;
|
|
896
|
+
for (let i = 0; i < a.length; i++) {
|
|
897
|
+
if (a[i].toLowerCase() !== b[i].toLowerCase()) return false;
|
|
898
|
+
}
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
739
901
|
|
|
740
902
|
// ../api-routes/src/queries.ts
|
|
741
903
|
import crypto5 from "crypto";
|
|
@@ -1138,7 +1300,7 @@ function parseCompetitorBatch(value) {
|
|
|
1138
1300
|
|
|
1139
1301
|
// ../api-routes/src/runs.ts
|
|
1140
1302
|
import crypto8 from "crypto";
|
|
1141
|
-
import { eq as eq7, asc, desc, sql as sql2 } from "drizzle-orm";
|
|
1303
|
+
import { and as and2, eq as eq7, asc, desc, or as or2, sql as sql2 } from "drizzle-orm";
|
|
1142
1304
|
|
|
1143
1305
|
// ../api-routes/src/run-queue.ts
|
|
1144
1306
|
import crypto7 from "crypto";
|
|
@@ -1232,23 +1394,36 @@ async function runRoutes(app, opts) {
|
|
|
1232
1394
|
if (projectLocations.length === 0) {
|
|
1233
1395
|
throw validationError("No locations configured for this project");
|
|
1234
1396
|
}
|
|
1235
|
-
const
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1397
|
+
const result = app.db.transaction((tx) => {
|
|
1398
|
+
const activeRun = tx.select({ id: runs.id }).from(runs).where(and2(
|
|
1399
|
+
eq7(runs.projectId, project.id),
|
|
1400
|
+
or2(eq7(runs.status, "queued"), eq7(runs.status, "running"))
|
|
1401
|
+
)).get();
|
|
1402
|
+
if (activeRun) {
|
|
1403
|
+
return { conflict: true };
|
|
1404
|
+
}
|
|
1405
|
+
const inserted = [];
|
|
1406
|
+
for (const loc of projectLocations) {
|
|
1407
|
+
const runId2 = crypto8.randomUUID();
|
|
1408
|
+
tx.insert(runs).values({
|
|
1409
|
+
id: runId2,
|
|
1410
|
+
projectId: project.id,
|
|
1411
|
+
kind,
|
|
1412
|
+
status: "queued",
|
|
1413
|
+
trigger,
|
|
1414
|
+
location: loc.label,
|
|
1415
|
+
queries: queriesColumn,
|
|
1416
|
+
createdAt: now
|
|
1417
|
+
}).run();
|
|
1418
|
+
inserted.push({ runId: runId2, loc });
|
|
1419
|
+
}
|
|
1420
|
+
return { conflict: false, inserted };
|
|
1421
|
+
});
|
|
1422
|
+
if (result.conflict) {
|
|
1423
|
+
throw runInProgress(project.name);
|
|
1249
1424
|
}
|
|
1250
1425
|
const results = [];
|
|
1251
|
-
for (const { runId: runId2, loc } of
|
|
1426
|
+
for (const { runId: runId2, loc } of result.inserted) {
|
|
1252
1427
|
writeAuditLog(app.db, {
|
|
1253
1428
|
projectId: project.id,
|
|
1254
1429
|
actor: "api",
|
|
@@ -1436,7 +1611,8 @@ function loadRunDetail(app, run) {
|
|
|
1436
1611
|
const project = app.db.select({
|
|
1437
1612
|
displayName: projects.displayName,
|
|
1438
1613
|
canonicalDomain: projects.canonicalDomain,
|
|
1439
|
-
ownedDomains: projects.ownedDomains
|
|
1614
|
+
ownedDomains: projects.ownedDomains,
|
|
1615
|
+
aliases: projects.aliases
|
|
1440
1616
|
}).from(projects).where(eq7(projects.id, run.projectId)).get();
|
|
1441
1617
|
const snapshots = app.db.select({
|
|
1442
1618
|
id: querySnapshots.id,
|
|
@@ -1468,7 +1644,10 @@ function loadRunDetail(app, run) {
|
|
|
1468
1644
|
provider: s.provider,
|
|
1469
1645
|
citationState: s.citationState,
|
|
1470
1646
|
answerMentioned,
|
|
1647
|
+
// Legacy alias of `mentionState`, retained for backwards compatibility.
|
|
1471
1648
|
visibilityState: project ? resolveSnapshotVisibilityState(s, project) : answerMentioned ? "visible" : "not-visible",
|
|
1649
|
+
// Canonical vocabulary for answer-text presence; new consumers prefer this.
|
|
1650
|
+
mentionState: project ? resolveSnapshotMentionState(s, project) : answerMentioned ? "mentioned" : "not-mentioned",
|
|
1472
1651
|
answerText: s.answerText,
|
|
1473
1652
|
citedDomains: parseJsonColumn(s.citedDomains, []),
|
|
1474
1653
|
competitorOverlap: parseJsonColumn(s.competitorOverlap, []),
|
|
@@ -1486,7 +1665,7 @@ function loadRunDetail(app, run) {
|
|
|
1486
1665
|
|
|
1487
1666
|
// ../api-routes/src/apply.ts
|
|
1488
1667
|
import crypto10 from "crypto";
|
|
1489
|
-
import { and as
|
|
1668
|
+
import { and as and3, eq as eq8 } from "drizzle-orm";
|
|
1490
1669
|
|
|
1491
1670
|
// ../api-routes/src/schedule-utils.ts
|
|
1492
1671
|
var DAY_MAP = {
|
|
@@ -1583,7 +1762,7 @@ import http from "http";
|
|
|
1583
1762
|
import https from "https";
|
|
1584
1763
|
import net from "net";
|
|
1585
1764
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
1586
|
-
async function resolveWebhookTarget(raw) {
|
|
1765
|
+
async function resolveWebhookTarget(raw, options = {}) {
|
|
1587
1766
|
let parsed;
|
|
1588
1767
|
try {
|
|
1589
1768
|
parsed = new URL(raw);
|
|
@@ -1604,7 +1783,7 @@ async function resolveWebhookTarget(raw) {
|
|
|
1604
1783
|
if (addresses.length === 0) {
|
|
1605
1784
|
return { ok: false, message: '"url" hostname could not be resolved' };
|
|
1606
1785
|
}
|
|
1607
|
-
const blocked = addresses.find((entry) => isBlockedAddress(entry.address));
|
|
1786
|
+
const blocked = addresses.find((entry) => isBlockedAddress(entry.address, options));
|
|
1608
1787
|
if (blocked) {
|
|
1609
1788
|
return { ok: false, message: '"url" must not resolve to a private or loopback address' };
|
|
1610
1789
|
}
|
|
@@ -1682,34 +1861,40 @@ async function resolveHostAddresses(hostname) {
|
|
|
1682
1861
|
return [];
|
|
1683
1862
|
}
|
|
1684
1863
|
}
|
|
1685
|
-
function isBlockedAddress(address) {
|
|
1864
|
+
function isBlockedAddress(address, options) {
|
|
1686
1865
|
const normalized = stripIpv6Brackets(address).toLowerCase();
|
|
1687
1866
|
const family = net.isIP(normalized);
|
|
1688
1867
|
if (family === 4) {
|
|
1689
|
-
return isBlockedIpv4(normalized);
|
|
1868
|
+
return isBlockedIpv4(normalized, options);
|
|
1690
1869
|
}
|
|
1691
1870
|
if (family === 6) {
|
|
1692
1871
|
const mappedIpv4 = extractMappedIpv4(normalized);
|
|
1693
1872
|
if (mappedIpv4) {
|
|
1694
|
-
return isBlockedIpv4(mappedIpv4);
|
|
1873
|
+
return isBlockedIpv4(mappedIpv4, options);
|
|
1695
1874
|
}
|
|
1696
|
-
return isBlockedIpv6(normalized);
|
|
1875
|
+
return isBlockedIpv6(normalized, options);
|
|
1697
1876
|
}
|
|
1698
1877
|
return true;
|
|
1699
1878
|
}
|
|
1700
|
-
function isBlockedIpv4(address) {
|
|
1879
|
+
function isBlockedIpv4(address, options) {
|
|
1701
1880
|
const octets = address.split(".").map((part) => Number.parseInt(part, 10));
|
|
1702
1881
|
if (octets.length !== 4 || octets.some(Number.isNaN)) {
|
|
1703
1882
|
return true;
|
|
1704
1883
|
}
|
|
1705
1884
|
const [first, second] = octets;
|
|
1885
|
+
if (first === 127 && !options.allowLoopback) {
|
|
1886
|
+
return true;
|
|
1887
|
+
}
|
|
1706
1888
|
return first === 0 || first === 10 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 198 && (second === 18 || second === 19);
|
|
1707
1889
|
}
|
|
1708
|
-
function isBlockedIpv6(address) {
|
|
1890
|
+
function isBlockedIpv6(address, options) {
|
|
1709
1891
|
const normalized = address.split("%")[0].toLowerCase();
|
|
1710
1892
|
if (normalized === "::") {
|
|
1711
1893
|
return true;
|
|
1712
1894
|
}
|
|
1895
|
+
if (normalized === "::1") {
|
|
1896
|
+
return !options.allowLoopback;
|
|
1897
|
+
}
|
|
1713
1898
|
const firstHextetText = normalized.split(":")[0] ?? "";
|
|
1714
1899
|
const firstHextet = firstHextetText === "" ? 0 : Number.parseInt(firstHextetText, 16);
|
|
1715
1900
|
if (Number.isNaN(firstHextet)) {
|
|
@@ -1740,6 +1925,7 @@ function stripIpv6Brackets(value) {
|
|
|
1740
1925
|
|
|
1741
1926
|
// ../api-routes/src/apply.ts
|
|
1742
1927
|
async function applyRoutes(app, opts) {
|
|
1928
|
+
const allowLoopback = opts?.allowLoopbackWebhooks === true;
|
|
1743
1929
|
app.post("/apply", async (request, reply) => {
|
|
1744
1930
|
const parsed = projectConfigSchema.safeParse(request.body);
|
|
1745
1931
|
if (!parsed.success) {
|
|
@@ -1794,7 +1980,7 @@ async function applyRoutes(app, opts) {
|
|
|
1794
1980
|
const hasNotifications = "notifications" in rawSpec;
|
|
1795
1981
|
if (hasNotifications) {
|
|
1796
1982
|
for (const notif of config.spec.notifications) {
|
|
1797
|
-
const urlCheck = await resolveWebhookTarget(notif.url ?? "");
|
|
1983
|
+
const urlCheck = await resolveWebhookTarget(notif.url ?? "", { allowLoopback });
|
|
1798
1984
|
if (!urlCheck.ok) throw validationError(`Notification URL invalid: ${urlCheck.message}`);
|
|
1799
1985
|
}
|
|
1800
1986
|
}
|
|
@@ -1803,14 +1989,21 @@ async function applyRoutes(app, opts) {
|
|
|
1803
1989
|
const configQueries = resolveConfigSpecQueries(config.spec);
|
|
1804
1990
|
let projectId;
|
|
1805
1991
|
let scheduleAction = null;
|
|
1992
|
+
let aliasesChanged = false;
|
|
1806
1993
|
app.db.transaction((tx) => {
|
|
1807
1994
|
const existing = tx.select().from(projects).where(eq8(projects.name, name)).get();
|
|
1995
|
+
const nextAliases = normalizeProjectAliases(config.spec.displayName, config.spec.aliases ?? []);
|
|
1996
|
+
if (existing) {
|
|
1997
|
+
const prevAliases = parseJsonColumn(existing.aliases, []);
|
|
1998
|
+
aliasesChanged = !aliasArraysEqual2(prevAliases, nextAliases);
|
|
1999
|
+
}
|
|
1808
2000
|
if (existing) {
|
|
1809
2001
|
projectId = existing.id;
|
|
1810
2002
|
tx.update(projects).set({
|
|
1811
2003
|
displayName: config.spec.displayName,
|
|
1812
2004
|
canonicalDomain: config.spec.canonicalDomain,
|
|
1813
2005
|
ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
|
|
2006
|
+
aliases: JSON.stringify(nextAliases),
|
|
1814
2007
|
country: config.spec.country,
|
|
1815
2008
|
language: config.spec.language,
|
|
1816
2009
|
labels: JSON.stringify(config.metadata.labels),
|
|
@@ -1837,6 +2030,7 @@ async function applyRoutes(app, opts) {
|
|
|
1837
2030
|
displayName: config.spec.displayName,
|
|
1838
2031
|
canonicalDomain: config.spec.canonicalDomain,
|
|
1839
2032
|
ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
|
|
2033
|
+
aliases: JSON.stringify(nextAliases),
|
|
1840
2034
|
country: config.spec.country,
|
|
1841
2035
|
language: config.spec.language,
|
|
1842
2036
|
tags: "[]",
|
|
@@ -1895,7 +2089,7 @@ async function applyRoutes(app, opts) {
|
|
|
1895
2089
|
});
|
|
1896
2090
|
const AV_KIND = SchedulableRunKinds["answer-visibility"];
|
|
1897
2091
|
if (resolvedSchedule) {
|
|
1898
|
-
const existingSched = tx.select().from(schedules).where(
|
|
2092
|
+
const existingSched = tx.select().from(schedules).where(and3(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
|
|
1899
2093
|
if (existingSched) {
|
|
1900
2094
|
tx.update(schedules).set({
|
|
1901
2095
|
cronExpr: resolvedSchedule.cronExpr,
|
|
@@ -1921,9 +2115,9 @@ async function applyRoutes(app, opts) {
|
|
|
1921
2115
|
}
|
|
1922
2116
|
scheduleAction = "upsert";
|
|
1923
2117
|
} else if (deleteSchedule) {
|
|
1924
|
-
const existingSched = tx.select().from(schedules).where(
|
|
2118
|
+
const existingSched = tx.select().from(schedules).where(and3(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
|
|
1925
2119
|
if (existingSched) {
|
|
1926
|
-
tx.delete(schedules).where(
|
|
2120
|
+
tx.delete(schedules).where(and3(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).run();
|
|
1927
2121
|
scheduleAction = "delete";
|
|
1928
2122
|
}
|
|
1929
2123
|
}
|
|
@@ -1956,6 +2150,9 @@ async function applyRoutes(app, opts) {
|
|
|
1956
2150
|
if (!hasNotifications) {
|
|
1957
2151
|
opts?.onProjectUpserted?.(projectId, config.metadata.name);
|
|
1958
2152
|
}
|
|
2153
|
+
if (aliasesChanged) {
|
|
2154
|
+
opts?.onAliasesChanged?.(projectId, config.metadata.name);
|
|
2155
|
+
}
|
|
1959
2156
|
if ("google" in rawSpec && config.spec.google?.gsc?.propertyUrl) {
|
|
1960
2157
|
opts?.onGoogleConnectionPropertyUpdated?.(config.spec.canonicalDomain, "gsc", config.spec.google.gsc.propertyUrl);
|
|
1961
2158
|
}
|
|
@@ -1966,6 +2163,7 @@ async function applyRoutes(app, opts) {
|
|
|
1966
2163
|
displayName: project.displayName,
|
|
1967
2164
|
canonicalDomain: project.canonicalDomain,
|
|
1968
2165
|
ownedDomains: parseJsonColumn(project.ownedDomains, []),
|
|
2166
|
+
aliases: parseJsonColumn(project.aliases, []),
|
|
1969
2167
|
country: project.country,
|
|
1970
2168
|
language: project.language,
|
|
1971
2169
|
tags: parseJsonColumn(project.tags, []),
|
|
@@ -1981,6 +2179,13 @@ async function applyRoutes(app, opts) {
|
|
|
1981
2179
|
});
|
|
1982
2180
|
});
|
|
1983
2181
|
}
|
|
2182
|
+
function aliasArraysEqual2(a, b) {
|
|
2183
|
+
if (a.length !== b.length) return false;
|
|
2184
|
+
for (let i = 0; i < a.length; i++) {
|
|
2185
|
+
if (a[i].toLowerCase() !== b[i].toLowerCase()) return false;
|
|
2186
|
+
}
|
|
2187
|
+
return true;
|
|
2188
|
+
}
|
|
1984
2189
|
function normalizeCompetitorList2(domains) {
|
|
1985
2190
|
const seen = /* @__PURE__ */ new Set();
|
|
1986
2191
|
const result = [];
|
|
@@ -2088,6 +2293,7 @@ async function historyRoutes(app) {
|
|
|
2088
2293
|
citationState: s.citationState,
|
|
2089
2294
|
answerMentioned: resolveSnapshotAnswerMentioned(s, project),
|
|
2090
2295
|
visibilityState: resolveSnapshotVisibilityState(s, project),
|
|
2296
|
+
mentionState: resolveSnapshotMentionState(s, project),
|
|
2091
2297
|
answerText: s.answerText,
|
|
2092
2298
|
citedDomains: parseJsonColumn(s.citedDomains, []),
|
|
2093
2299
|
competitorOverlap: parseJsonColumn(s.competitorOverlap, []),
|
|
@@ -2141,9 +2347,11 @@ async function historyRoutes(app) {
|
|
|
2141
2347
|
const run = projectRuns.find((r) => r.id === snap.runId);
|
|
2142
2348
|
let transition = snap.citationState === CitationStates.cited ? "cited" : "not-cited";
|
|
2143
2349
|
let visibilityTransition = snap.answerMentioned ? "visible" : "not-visible";
|
|
2350
|
+
let mentionTransition = snap.answerMentioned ? "mentioned" : "not-mentioned";
|
|
2144
2351
|
if (idx === 0) {
|
|
2145
2352
|
transition = "new";
|
|
2146
2353
|
visibilityTransition = "new";
|
|
2354
|
+
mentionTransition = "new";
|
|
2147
2355
|
} else {
|
|
2148
2356
|
const prev = snaps[idx - 1];
|
|
2149
2357
|
if (prev.citationState === CitationStates["not-cited"] && snap.citationState === CitationStates.cited) {
|
|
@@ -2153,8 +2361,10 @@ async function historyRoutes(app) {
|
|
|
2153
2361
|
}
|
|
2154
2362
|
if (!prev.answerMentioned && snap.answerMentioned) {
|
|
2155
2363
|
visibilityTransition = "emerging";
|
|
2364
|
+
mentionTransition = "emerging";
|
|
2156
2365
|
} else if (prev.answerMentioned && !snap.answerMentioned) {
|
|
2157
2366
|
visibilityTransition = "lost";
|
|
2367
|
+
mentionTransition = "lost";
|
|
2158
2368
|
}
|
|
2159
2369
|
}
|
|
2160
2370
|
return {
|
|
@@ -2163,8 +2373,12 @@ async function historyRoutes(app) {
|
|
|
2163
2373
|
citationState: snap.citationState,
|
|
2164
2374
|
transition,
|
|
2165
2375
|
answerMentioned: snap.answerMentioned,
|
|
2376
|
+
// Legacy aliases of `mentionState` / `mentionTransition`.
|
|
2166
2377
|
visibilityState: snap.answerMentioned ? "visible" : "not-visible",
|
|
2167
2378
|
visibilityTransition,
|
|
2379
|
+
// Canonical-vocabulary fields — new consumers prefer these.
|
|
2380
|
+
mentionState: snap.answerMentioned ? "mentioned" : "not-mentioned",
|
|
2381
|
+
mentionTransition,
|
|
2168
2382
|
location: snap.location
|
|
2169
2383
|
};
|
|
2170
2384
|
});
|
|
@@ -2220,7 +2434,8 @@ async function historyRoutes(app) {
|
|
|
2220
2434
|
const resolved = {
|
|
2221
2435
|
...s,
|
|
2222
2436
|
resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
|
|
2223
|
-
resolvedVisibilityState: resolveSnapshotVisibilityState(s, project)
|
|
2437
|
+
resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
|
|
2438
|
+
resolvedMentionState: resolveSnapshotMentionState(s, project)
|
|
2224
2439
|
};
|
|
2225
2440
|
const existing = map1.get(s.queryId);
|
|
2226
2441
|
if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === CitationStates.cited) {
|
|
@@ -2232,7 +2447,8 @@ async function historyRoutes(app) {
|
|
|
2232
2447
|
const resolved = {
|
|
2233
2448
|
...s,
|
|
2234
2449
|
resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
|
|
2235
|
-
resolvedVisibilityState: resolveSnapshotVisibilityState(s, project)
|
|
2450
|
+
resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
|
|
2451
|
+
resolvedMentionState: resolveSnapshotMentionState(s, project)
|
|
2236
2452
|
};
|
|
2237
2453
|
const existing = map2.get(s.queryId);
|
|
2238
2454
|
if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === CitationStates.cited) {
|
|
@@ -2250,8 +2466,11 @@ async function historyRoutes(app) {
|
|
|
2250
2466
|
run2State: s2?.citationState ?? null,
|
|
2251
2467
|
run1AnswerMentioned: s1?.resolvedAnswerMentioned ?? null,
|
|
2252
2468
|
run2AnswerMentioned: s2?.resolvedAnswerMentioned ?? null,
|
|
2469
|
+
// Legacy aliases — same data as run{1,2}MentionState below.
|
|
2253
2470
|
run1VisibilityState: s1?.resolvedVisibilityState ?? null,
|
|
2254
2471
|
run2VisibilityState: s2?.resolvedVisibilityState ?? null,
|
|
2472
|
+
run1MentionState: s1?.resolvedMentionState ?? null,
|
|
2473
|
+
run2MentionState: s2?.resolvedMentionState ?? null,
|
|
2255
2474
|
changed: (s1?.citationState ?? null) !== (s2?.citationState ?? null),
|
|
2256
2475
|
visibilityChanged: (s1?.resolvedAnswerMentioned ?? null) !== (s2?.resolvedAnswerMentioned ?? null)
|
|
2257
2476
|
};
|
|
@@ -2292,7 +2511,7 @@ async function analyticsRoutes(app) {
|
|
|
2292
2511
|
});
|
|
2293
2512
|
}
|
|
2294
2513
|
const runIds = projectRuns.map((r) => r.id);
|
|
2295
|
-
const rawSnapshots = app.db.select({
|
|
2514
|
+
const rawSnapshots = filterTrackedSnapshots(app.db.select({
|
|
2296
2515
|
runId: querySnapshots.runId,
|
|
2297
2516
|
queryId: querySnapshots.queryId,
|
|
2298
2517
|
provider: querySnapshots.provider,
|
|
@@ -2300,7 +2519,7 @@ async function analyticsRoutes(app) {
|
|
|
2300
2519
|
answerMentioned: querySnapshots.answerMentioned,
|
|
2301
2520
|
answerText: querySnapshots.answerText,
|
|
2302
2521
|
createdAt: querySnapshots.createdAt
|
|
2303
|
-
}).from(querySnapshots).where(inArray2(querySnapshots.runId, runIds)).all();
|
|
2522
|
+
}).from(querySnapshots).where(inArray2(querySnapshots.runId, runIds)).all());
|
|
2304
2523
|
const allSnapshots = rawSnapshots.map((s) => ({
|
|
2305
2524
|
...s,
|
|
2306
2525
|
resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
|
|
@@ -2339,13 +2558,13 @@ async function analyticsRoutes(app) {
|
|
|
2339
2558
|
const runIdToCreatedAt = new Map(windowRuns.map((r) => [r.id, r.createdAt]));
|
|
2340
2559
|
const consistencyMap = /* @__PURE__ */ new Map();
|
|
2341
2560
|
if (windowRunIds.length > 0) {
|
|
2342
|
-
const allWindowSnaps = app.db.select({
|
|
2561
|
+
const allWindowSnaps = filterTrackedSnapshots(app.db.select({
|
|
2343
2562
|
queryId: querySnapshots.queryId,
|
|
2344
2563
|
runId: querySnapshots.runId,
|
|
2345
2564
|
citationState: querySnapshots.citationState,
|
|
2346
2565
|
answerMentioned: querySnapshots.answerMentioned,
|
|
2347
2566
|
answerText: querySnapshots.answerText
|
|
2348
|
-
}).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all();
|
|
2567
|
+
}).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all());
|
|
2349
2568
|
for (const s of allWindowSnaps) {
|
|
2350
2569
|
const timePoint = runIdToCreatedAt.get(s.runId) ?? s.runId;
|
|
2351
2570
|
let entry = consistencyMap.get(s.queryId);
|
|
@@ -2358,7 +2577,7 @@ async function analyticsRoutes(app) {
|
|
|
2358
2577
|
if (resolveSnapshotAnswerMentioned(s, project)) entry.mentionedRuns.add(timePoint);
|
|
2359
2578
|
}
|
|
2360
2579
|
}
|
|
2361
|
-
const rawSnapshots = app.db.select({
|
|
2580
|
+
const rawSnapshots = filterTrackedSnapshots(app.db.select({
|
|
2362
2581
|
queryId: querySnapshots.queryId,
|
|
2363
2582
|
query: queries.query,
|
|
2364
2583
|
provider: querySnapshots.provider,
|
|
@@ -2366,7 +2585,7 @@ async function analyticsRoutes(app) {
|
|
|
2366
2585
|
answerMentioned: querySnapshots.answerMentioned,
|
|
2367
2586
|
answerText: querySnapshots.answerText,
|
|
2368
2587
|
competitorOverlap: querySnapshots.competitorOverlap
|
|
2369
|
-
}).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, latestGroupRunIds)).all();
|
|
2588
|
+
}).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, latestGroupRunIds)).all());
|
|
2370
2589
|
const snapshots = rawSnapshots.map((s) => ({
|
|
2371
2590
|
...s,
|
|
2372
2591
|
resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
|
|
@@ -2620,7 +2839,7 @@ function buildCategoryCounts(counts) {
|
|
|
2620
2839
|
}
|
|
2621
2840
|
|
|
2622
2841
|
// ../api-routes/src/intelligence.ts
|
|
2623
|
-
import { eq as eq11, desc as desc4, and as
|
|
2842
|
+
import { eq as eq11, desc as desc4, and as and4, inArray as inArray3 } from "drizzle-orm";
|
|
2624
2843
|
function emptyHealthSnapshot(projectId) {
|
|
2625
2844
|
return {
|
|
2626
2845
|
id: `no-data:${projectId}`,
|
|
@@ -2709,7 +2928,7 @@ async function intelligenceRoutes(app) {
|
|
|
2709
2928
|
if (request.query.runId) {
|
|
2710
2929
|
conditions.push(eq11(insights.runId, request.query.runId));
|
|
2711
2930
|
}
|
|
2712
|
-
const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] :
|
|
2931
|
+
const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and4(...conditions)).orderBy(desc4(insights.createdAt)).all();
|
|
2713
2932
|
const showDismissed = request.query.dismissed === "true";
|
|
2714
2933
|
const result = rows.filter((r) => showDismissed || !r.dismissed).map(mapInsightRow);
|
|
2715
2934
|
return reply.send(result);
|
|
@@ -2733,7 +2952,7 @@ async function intelligenceRoutes(app) {
|
|
|
2733
2952
|
});
|
|
2734
2953
|
app.get("/projects/:name/health/latest", async (request, reply) => {
|
|
2735
2954
|
const project = resolveProject(app.db, request.params.name);
|
|
2736
|
-
const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
2955
|
+
const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and4(
|
|
2737
2956
|
eq11(runs.projectId, project.id),
|
|
2738
2957
|
eq11(runs.kind, RunKinds["answer-visibility"]),
|
|
2739
2958
|
inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
|
|
@@ -2741,7 +2960,7 @@ async function intelligenceRoutes(app) {
|
|
|
2741
2960
|
const latestGroup = groupRunsByCreatedAt(projectVisRuns)[0] ?? [];
|
|
2742
2961
|
const latestGroupRunIds = latestGroup.map((r) => r.id);
|
|
2743
2962
|
if (latestGroupRunIds.length > 0) {
|
|
2744
|
-
const groupRows = app.db.select().from(healthSnapshots).where(
|
|
2963
|
+
const groupRows = app.db.select().from(healthSnapshots).where(and4(
|
|
2745
2964
|
eq11(healthSnapshots.projectId, project.id),
|
|
2746
2965
|
inArray3(healthSnapshots.runId, latestGroupRunIds)
|
|
2747
2966
|
)).all();
|
|
@@ -2765,7 +2984,7 @@ async function intelligenceRoutes(app) {
|
|
|
2765
2984
|
}
|
|
2766
2985
|
|
|
2767
2986
|
// ../api-routes/src/report.ts
|
|
2768
|
-
import { and as
|
|
2987
|
+
import { and as and6, desc as desc6, eq as eq13, gte, inArray as inArray5, lt, lte, ne, or as or3, sql as sql3 } from "drizzle-orm";
|
|
2769
2988
|
|
|
2770
2989
|
// ../api-routes/src/report-renderer.ts
|
|
2771
2990
|
var COLORS = {
|
|
@@ -5001,7 +5220,7 @@ function renderReportHtml(report, opts = {}) {
|
|
|
5001
5220
|
}
|
|
5002
5221
|
|
|
5003
5222
|
// ../api-routes/src/content-data.ts
|
|
5004
|
-
import { and as
|
|
5223
|
+
import { and as and5, eq as eq12, desc as desc5, inArray as inArray4 } from "drizzle-orm";
|
|
5005
5224
|
var RECENT_RUNS_WINDOW = 5;
|
|
5006
5225
|
function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
5007
5226
|
const projectId = project.id;
|
|
@@ -5125,7 +5344,7 @@ function listCompetitorDomains(db, projectId) {
|
|
|
5125
5344
|
}
|
|
5126
5345
|
function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter) {
|
|
5127
5346
|
const rows = db.select({ id: runs.id, location: runs.location }).from(runs).where(
|
|
5128
|
-
|
|
5347
|
+
and5(
|
|
5129
5348
|
eq12(runs.projectId, projectId),
|
|
5130
5349
|
eq12(runs.kind, RunKinds["answer-visibility"]),
|
|
5131
5350
|
// Queued/running/failed/cancelled runs may have partial or no
|
|
@@ -5173,7 +5392,7 @@ function buildCandidateQueries(opts) {
|
|
|
5173
5392
|
const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(eq12(queries.projectId, opts.projectId)).all();
|
|
5174
5393
|
const queryIdByText = new Map(queryRows.map((r) => [r.text, r.id]));
|
|
5175
5394
|
const candidateQueryIds = opts.candidateQueryStrings.map((q) => queryIdByText.get(q)).filter((id) => Boolean(id));
|
|
5176
|
-
const snapshotRows = opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateQueryIds.includes(r.queryId));
|
|
5395
|
+
const snapshotRows = filterTrackedSnapshots(opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all()).filter((r) => candidateQueryIds.includes(r.queryId));
|
|
5177
5396
|
const snapshotsByQuery = /* @__PURE__ */ new Map();
|
|
5178
5397
|
for (const row of snapshotRows) {
|
|
5179
5398
|
const list = snapshotsByQuery.get(row.queryId) ?? [];
|
|
@@ -5383,8 +5602,8 @@ function safeNum(value) {
|
|
|
5383
5602
|
}
|
|
5384
5603
|
return 0;
|
|
5385
5604
|
}
|
|
5386
|
-
function categorizeQuery(query,
|
|
5387
|
-
return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain,
|
|
5605
|
+
function categorizeQuery(query, projectBrandNames, canonicalDomain) {
|
|
5606
|
+
return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain, projectBrandNames));
|
|
5388
5607
|
}
|
|
5389
5608
|
function loadSnapshotsForRun(db, runId) {
|
|
5390
5609
|
return loadSnapshotsForRunIds(db, [runId]);
|
|
@@ -5392,7 +5611,7 @@ function loadSnapshotsForRun(db, runId) {
|
|
|
5392
5611
|
function loadSnapshotsForRunIds(db, runIds) {
|
|
5393
5612
|
if (runIds.length === 0) return [];
|
|
5394
5613
|
const rows = db.select().from(querySnapshots).where(inArray5(querySnapshots.runId, [...runIds])).all();
|
|
5395
|
-
return rows.map((r) => ({
|
|
5614
|
+
return rows.filter((r) => r.queryId !== null).map((r) => ({
|
|
5396
5615
|
id: r.id,
|
|
5397
5616
|
runId: r.runId,
|
|
5398
5617
|
queryId: r.queryId,
|
|
@@ -5413,7 +5632,7 @@ function loadQueryLookup(db, projectId) {
|
|
|
5413
5632
|
for (const row of rows) byId.set(row.id, row.query);
|
|
5414
5633
|
return { byId };
|
|
5415
5634
|
}
|
|
5416
|
-
function buildGscSection(db, projectId,
|
|
5635
|
+
function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries) {
|
|
5417
5636
|
const allRows = db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
|
|
5418
5637
|
if (allRows.length === 0) return null;
|
|
5419
5638
|
let maxDate = "";
|
|
@@ -5448,11 +5667,11 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
|
|
|
5448
5667
|
impressions: agg.impressions,
|
|
5449
5668
|
ctr: agg.impressions > 0 ? agg.clicks / agg.impressions : 0,
|
|
5450
5669
|
avgPosition: agg.impressions > 0 ? agg.weightedPositionSum / agg.impressions : 0,
|
|
5451
|
-
category: categorizeQuery(query,
|
|
5670
|
+
category: categorizeQuery(query, projectBrandNames, canonicalDomain)
|
|
5452
5671
|
})).sort((a, b) => b.clicks - a.clicks).slice(0, TOP_QUERIES_LIMIT);
|
|
5453
5672
|
const categoryAgg = /* @__PURE__ */ new Map();
|
|
5454
5673
|
for (const [query, agg] of queryAgg) {
|
|
5455
|
-
const cat = categorizeQuery(query,
|
|
5674
|
+
const cat = categorizeQuery(query, projectBrandNames, canonicalDomain);
|
|
5456
5675
|
const bucket = categoryAgg.get(cat) ?? { clicks: 0, impressions: 0 };
|
|
5457
5676
|
bucket.clicks += agg.clicks;
|
|
5458
5677
|
bucket.impressions += agg.impressions;
|
|
@@ -5470,7 +5689,7 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
|
|
|
5470
5689
|
const trackedSet = new Set(trackedQueries.map((q) => q.toLowerCase()));
|
|
5471
5690
|
const gscQuerySet = new Set([...queryAgg.keys()].map((q) => q.toLowerCase()));
|
|
5472
5691
|
const trackedButNoGsc = trackedQueries.filter((q) => !gscQuerySet.has(q.toLowerCase())).sort();
|
|
5473
|
-
const gscButNotTracked = [...queryAgg.entries()].filter(([q]) => !trackedSet.has(q.toLowerCase())).filter(([q]) => categorizeQuery(q,
|
|
5692
|
+
const gscButNotTracked = [...queryAgg.entries()].filter(([q]) => !trackedSet.has(q.toLowerCase())).filter(([q]) => categorizeQuery(q, projectBrandNames, canonicalDomain) !== "brand").sort((a, b) => b[1].impressions - a[1].impressions).map(([q]) => q).slice(0, TOP_QUERIES_LIMIT);
|
|
5474
5693
|
return {
|
|
5475
5694
|
periodStart,
|
|
5476
5695
|
periodEnd,
|
|
@@ -5487,7 +5706,7 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
|
|
|
5487
5706
|
}
|
|
5488
5707
|
function buildGaSection(db, projectId) {
|
|
5489
5708
|
const windowSummary = db.select().from(gaTrafficWindowSummaries).where(
|
|
5490
|
-
|
|
5709
|
+
and6(
|
|
5491
5710
|
eq13(gaTrafficWindowSummaries.projectId, projectId),
|
|
5492
5711
|
eq13(gaTrafficWindowSummaries.windowKey, "30d")
|
|
5493
5712
|
)
|
|
@@ -5665,7 +5884,7 @@ function nonSubresourceReferralPathCondition() {
|
|
|
5665
5884
|
}
|
|
5666
5885
|
function buildServerActivity(db, projectId) {
|
|
5667
5886
|
const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
|
|
5668
|
-
|
|
5887
|
+
and6(
|
|
5669
5888
|
eq13(trafficSources.projectId, projectId),
|
|
5670
5889
|
ne(trafficSources.status, TrafficSourceStatuses.archived)
|
|
5671
5890
|
)
|
|
@@ -5681,7 +5900,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5681
5900
|
const trendStart = new Date(trendStartMs).toISOString();
|
|
5682
5901
|
const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
5683
5902
|
db.select({ total: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
5684
|
-
|
|
5903
|
+
and6(
|
|
5685
5904
|
eq13(crawlerEventsHourly.projectId, projectId),
|
|
5686
5905
|
eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
5687
5906
|
gte(crawlerEventsHourly.tsHour, windowStartIso),
|
|
@@ -5691,7 +5910,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5691
5910
|
);
|
|
5692
5911
|
const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
5693
5912
|
db.select({ total: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
5694
|
-
|
|
5913
|
+
and6(
|
|
5695
5914
|
eq13(crawlerEventsHourly.projectId, projectId),
|
|
5696
5915
|
ne(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
5697
5916
|
gte(crawlerEventsHourly.tsHour, windowStartIso),
|
|
@@ -5701,7 +5920,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5701
5920
|
);
|
|
5702
5921
|
const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
5703
5922
|
db.select({ total: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
5704
|
-
|
|
5923
|
+
and6(
|
|
5705
5924
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5706
5925
|
nonSubresourceReferralPathCondition(),
|
|
5707
5926
|
gte(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
@@ -5720,7 +5939,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5720
5939
|
verificationStatus: crawlerEventsHourly.verificationStatus,
|
|
5721
5940
|
hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
5722
5941
|
}).from(crawlerEventsHourly).where(
|
|
5723
|
-
|
|
5942
|
+
and6(
|
|
5724
5943
|
eq13(crawlerEventsHourly.projectId, projectId),
|
|
5725
5944
|
gte(crawlerEventsHourly.tsHour, headlineStart),
|
|
5726
5945
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
@@ -5730,7 +5949,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5730
5949
|
operator: crawlerEventsHourly.operator,
|
|
5731
5950
|
hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
5732
5951
|
}).from(crawlerEventsHourly).where(
|
|
5733
|
-
|
|
5952
|
+
and6(
|
|
5734
5953
|
eq13(crawlerEventsHourly.projectId, projectId),
|
|
5735
5954
|
eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
5736
5955
|
gte(crawlerEventsHourly.tsHour, priorStart),
|
|
@@ -5741,7 +5960,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5741
5960
|
operator: aiReferralEventsHourly.operator,
|
|
5742
5961
|
hits: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
5743
5962
|
}).from(aiReferralEventsHourly).where(
|
|
5744
|
-
|
|
5963
|
+
and6(
|
|
5745
5964
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5746
5965
|
nonSubresourceReferralPathCondition(),
|
|
5747
5966
|
gte(aiReferralEventsHourly.tsHour, headlineStart),
|
|
@@ -5782,7 +6001,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5782
6001
|
hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
|
|
5783
6002
|
operators: sql3`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
|
|
5784
6003
|
}).from(crawlerEventsHourly).where(
|
|
5785
|
-
|
|
6004
|
+
and6(
|
|
5786
6005
|
eq13(crawlerEventsHourly.projectId, projectId),
|
|
5787
6006
|
eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
5788
6007
|
gte(crawlerEventsHourly.tsHour, headlineStart),
|
|
@@ -5799,7 +6018,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5799
6018
|
arrivals: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
5800
6019
|
landingPaths: sql3`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
|
|
5801
6020
|
}).from(aiReferralEventsHourly).where(
|
|
5802
|
-
|
|
6021
|
+
and6(
|
|
5803
6022
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5804
6023
|
nonSubresourceReferralPathCondition(),
|
|
5805
6024
|
gte(aiReferralEventsHourly.tsHour, headlineStart),
|
|
@@ -5816,7 +6035,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5816
6035
|
arrivals: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
5817
6036
|
products: sql3`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
|
|
5818
6037
|
}).from(aiReferralEventsHourly).where(
|
|
5819
|
-
|
|
6038
|
+
and6(
|
|
5820
6039
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5821
6040
|
nonSubresourceReferralPathCondition(),
|
|
5822
6041
|
gte(aiReferralEventsHourly.tsHour, headlineStart),
|
|
@@ -5832,7 +6051,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5832
6051
|
date: sql3`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
|
|
5833
6052
|
hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
5834
6053
|
}).from(crawlerEventsHourly).where(
|
|
5835
|
-
|
|
6054
|
+
and6(
|
|
5836
6055
|
eq13(crawlerEventsHourly.projectId, projectId),
|
|
5837
6056
|
eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
5838
6057
|
gte(crawlerEventsHourly.tsHour, trendStart),
|
|
@@ -5843,7 +6062,7 @@ function buildServerActivity(db, projectId) {
|
|
|
5843
6062
|
date: sql3`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
|
|
5844
6063
|
hits: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
5845
6064
|
}).from(aiReferralEventsHourly).where(
|
|
5846
|
-
|
|
6065
|
+
and6(
|
|
5847
6066
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5848
6067
|
nonSubresourceReferralPathCondition(),
|
|
5849
6068
|
gte(aiReferralEventsHourly.tsHour, trendStart),
|
|
@@ -5918,7 +6137,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
5918
6137
|
return null;
|
|
5919
6138
|
}
|
|
5920
6139
|
function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
5921
|
-
const visibilityRuns = db.select().from(runs).where(
|
|
6140
|
+
const visibilityRuns = db.select().from(runs).where(and6(eq13(runs.projectId, projectId), eq13(runs.kind, RunKinds["answer-visibility"]))).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
|
|
5922
6141
|
const totalQueries = queryLookup.byId.size;
|
|
5923
6142
|
const points = [];
|
|
5924
6143
|
for (const run of visibilityRuns) {
|
|
@@ -5964,14 +6183,14 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
|
5964
6183
|
}
|
|
5965
6184
|
function buildInsightList(db, projectId, locationFilter) {
|
|
5966
6185
|
const recentRunIds = db.select({ id: runs.id, location: runs.location }).from(runs).where(
|
|
5967
|
-
|
|
6186
|
+
and6(
|
|
5968
6187
|
eq13(runs.projectId, projectId),
|
|
5969
6188
|
eq13(runs.kind, RunKinds["answer-visibility"]),
|
|
5970
|
-
|
|
6189
|
+
or3(eq13(runs.status, RunStatuses.completed), eq13(runs.status, RunStatuses.partial))
|
|
5971
6190
|
)
|
|
5972
6191
|
).orderBy(desc6(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
|
|
5973
6192
|
if (recentRunIds.length === 0) return [];
|
|
5974
|
-
const rows = db.select().from(insights).where(
|
|
6193
|
+
const rows = db.select().from(insights).where(and6(eq13(insights.projectId, projectId), inArray5(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
|
|
5975
6194
|
const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
5976
6195
|
const flat = rows.filter((r) => !r.dismissed).map((r) => {
|
|
5977
6196
|
const recommendation = parseJsonColumn(r.recommendation, null);
|
|
@@ -6159,7 +6378,8 @@ function buildReportActionPlan(input) {
|
|
|
6159
6378
|
}
|
|
6160
6379
|
for (const [index, opportunity] of input.contentOpportunities.slice(0, 2).entries()) {
|
|
6161
6380
|
const verb = contentActionVerb(opportunity.action);
|
|
6162
|
-
const
|
|
6381
|
+
const bestPageUrl = opportunity.ourBestPage?.url;
|
|
6382
|
+
const hasUsablePage = !!bestPageUrl && bestPageUrl !== "/" && bestPageUrl.trim() !== "";
|
|
6163
6383
|
const evidence = [
|
|
6164
6384
|
`Opportunity score ${Math.round(opportunity.score)} with ${opportunity.actionConfidence} confidence`,
|
|
6165
6385
|
`Demand source: ${opportunity.demandSource}`
|
|
@@ -6168,17 +6388,22 @@ function buildReportActionPlan(input) {
|
|
|
6168
6388
|
evidence.push(`${opportunity.winningCompetitor.domain} is the current winning cited source`);
|
|
6169
6389
|
}
|
|
6170
6390
|
if (opportunity.ourBestPage) {
|
|
6171
|
-
|
|
6391
|
+
if (bestPageUrl === "/") {
|
|
6392
|
+
evidence.push("No topical page yet \u2014 homepage is the closest slug match");
|
|
6393
|
+
} else {
|
|
6394
|
+
evidence.push(`Best matching owned page: ${bestPageUrl}`);
|
|
6395
|
+
}
|
|
6172
6396
|
} else {
|
|
6173
6397
|
evidence.push("No matching owned page was found");
|
|
6174
6398
|
}
|
|
6399
|
+
const targetIsExistingPage = opportunity.action !== "create" && hasUsablePage;
|
|
6175
6400
|
actions.push({
|
|
6176
6401
|
audience: "both",
|
|
6177
6402
|
priority: 20 + index,
|
|
6178
6403
|
horizon: opportunity.actionConfidence === "high" ? "short-term" : "medium-term",
|
|
6179
6404
|
category: "content",
|
|
6180
6405
|
title: `${verb} content for "${opportunity.query}"`,
|
|
6181
|
-
action:
|
|
6406
|
+
action: targetIsExistingPage ? `${verb} the existing page so it directly answers the tracked query and cites the strongest supporting evidence.` : `${verb} a new page for "${opportunity.query}" that directly answers the query and earns citations from AI answer engines.`,
|
|
6182
6407
|
why: opportunity.drivers.length > 0 ? opportunity.drivers : ["Canonry ranked this as a content opportunity from search-demand and citation evidence."],
|
|
6183
6408
|
evidence,
|
|
6184
6409
|
successMetric: `A future check cites ${input.canonicalDomain} for "${opportunity.query}" and the matching GSC query/page improves.`,
|
|
@@ -6523,6 +6748,11 @@ function buildProjectReport(db, projectName) {
|
|
|
6523
6748
|
const competitorDomains = competitorRows.map((c) => c.domain);
|
|
6524
6749
|
const ownedDomains = parseJsonColumn(project.ownedDomains, []);
|
|
6525
6750
|
const projectDomains = [project.canonicalDomain, ...ownedDomains];
|
|
6751
|
+
const projectAliases = parseJsonColumn(project.aliases, []);
|
|
6752
|
+
const projectBrandNames = effectiveBrandNames({
|
|
6753
|
+
displayName: project.displayName,
|
|
6754
|
+
aliases: projectAliases
|
|
6755
|
+
});
|
|
6526
6756
|
const citationScorecard = buildCitationScorecard(latestSnapshots, queryLookup);
|
|
6527
6757
|
const competitorLandscape = buildCompetitorLandscape(
|
|
6528
6758
|
latestSnapshots,
|
|
@@ -6533,7 +6763,7 @@ function buildProjectReport(db, projectName) {
|
|
|
6533
6763
|
const mentionLandscape = buildMentionLandscape(
|
|
6534
6764
|
latestSnapshots,
|
|
6535
6765
|
competitorDomains,
|
|
6536
|
-
|
|
6766
|
+
projectBrandNames,
|
|
6537
6767
|
projectDomains,
|
|
6538
6768
|
queryLookup
|
|
6539
6769
|
);
|
|
@@ -6542,7 +6772,7 @@ function buildProjectReport(db, projectName) {
|
|
|
6542
6772
|
const gscSection = buildGscSection(
|
|
6543
6773
|
db,
|
|
6544
6774
|
project.id,
|
|
6545
|
-
|
|
6775
|
+
projectBrandNames,
|
|
6546
6776
|
project.canonicalDomain,
|
|
6547
6777
|
trackedQueries
|
|
6548
6778
|
);
|
|
@@ -6761,8 +6991,9 @@ async function citationRoutes(app) {
|
|
|
6761
6991
|
if (rawSnapshots.length === 0) {
|
|
6762
6992
|
return reply.send(emptyCitationVisibility("no-runs-yet"));
|
|
6763
6993
|
}
|
|
6764
|
-
const snapshots = rawSnapshots.map((s) => ({
|
|
6994
|
+
const snapshots = rawSnapshots.filter((s) => s.queryId !== null).map((s) => ({
|
|
6765
6995
|
...s,
|
|
6996
|
+
queryId: s.queryId,
|
|
6766
6997
|
runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
|
|
6767
6998
|
}));
|
|
6768
6999
|
const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, project.id)).all().map((c) => normalizeDomain2(c.domain)).filter((d) => d.length > 0);
|
|
@@ -6897,7 +7128,7 @@ function normalizeDomain2(domain) {
|
|
|
6897
7128
|
}
|
|
6898
7129
|
|
|
6899
7130
|
// ../api-routes/src/composites.ts
|
|
6900
|
-
import { eq as eq15, and as
|
|
7131
|
+
import { eq as eq15, and as and7, desc as desc7, sql as sql4, like, or as or4, inArray as inArray7 } from "drizzle-orm";
|
|
6901
7132
|
var TOP_INSIGHT_LIMIT = 5;
|
|
6902
7133
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
6903
7134
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -6998,7 +7229,7 @@ async function compositeRoutes(app) {
|
|
|
6998
7229
|
const limit = clampSearchLimit(request.query.limit);
|
|
6999
7230
|
const escaped = escapeLikePattern(rawQuery);
|
|
7000
7231
|
const pattern = `%${escaped}%`;
|
|
7001
|
-
const snapshotMatches = app.db.select({
|
|
7232
|
+
const snapshotMatches = filterTrackedSnapshots(app.db.select({
|
|
7002
7233
|
id: querySnapshots.id,
|
|
7003
7234
|
runId: querySnapshots.runId,
|
|
7004
7235
|
queryId: querySnapshots.queryId,
|
|
@@ -7011,20 +7242,20 @@ async function compositeRoutes(app) {
|
|
|
7011
7242
|
rawResponse: querySnapshots.rawResponse,
|
|
7012
7243
|
createdAt: querySnapshots.createdAt
|
|
7013
7244
|
}).from(querySnapshots).innerJoin(queries, eq15(querySnapshots.queryId, queries.id)).where(
|
|
7014
|
-
|
|
7245
|
+
and7(
|
|
7015
7246
|
eq15(queries.projectId, project.id),
|
|
7016
|
-
|
|
7247
|
+
or4(
|
|
7017
7248
|
sql4`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
7018
7249
|
sql4`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
7019
7250
|
sql4`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
|
|
7020
7251
|
like(queries.query, pattern)
|
|
7021
7252
|
)
|
|
7022
7253
|
)
|
|
7023
|
-
).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all();
|
|
7254
|
+
).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all());
|
|
7024
7255
|
const insightMatches = app.db.select().from(insights).where(
|
|
7025
|
-
|
|
7256
|
+
and7(
|
|
7026
7257
|
eq15(insights.projectId, project.id),
|
|
7027
|
-
|
|
7258
|
+
or4(
|
|
7028
7259
|
like(insights.title, pattern),
|
|
7029
7260
|
like(insights.query, pattern),
|
|
7030
7261
|
sql4`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
|
|
@@ -7094,7 +7325,7 @@ function summarizeRun(run) {
|
|
|
7094
7325
|
function loadSnapshotsByRunIds(app, runIds) {
|
|
7095
7326
|
const result = /* @__PURE__ */ new Map();
|
|
7096
7327
|
if (runIds.length === 0) return result;
|
|
7097
|
-
const rows = app.db.select({
|
|
7328
|
+
const rows = filterTrackedSnapshots(app.db.select({
|
|
7098
7329
|
runId: querySnapshots.runId,
|
|
7099
7330
|
queryId: querySnapshots.queryId,
|
|
7100
7331
|
provider: querySnapshots.provider,
|
|
@@ -7102,7 +7333,7 @@ function loadSnapshotsByRunIds(app, runIds) {
|
|
|
7102
7333
|
citationState: querySnapshots.citationState,
|
|
7103
7334
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
7104
7335
|
citedDomains: querySnapshots.citedDomains
|
|
7105
|
-
}).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all();
|
|
7336
|
+
}).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all());
|
|
7106
7337
|
for (const row of rows) {
|
|
7107
7338
|
const list = result.get(row.runId) ?? [];
|
|
7108
7339
|
list.push({
|
|
@@ -7352,6 +7583,7 @@ function formatProject2(row) {
|
|
|
7352
7583
|
displayName: row.displayName,
|
|
7353
7584
|
canonicalDomain: row.canonicalDomain,
|
|
7354
7585
|
ownedDomains: parseJsonColumn(row.ownedDomains, []),
|
|
7586
|
+
aliases: parseJsonColumn(row.aliases, []),
|
|
7355
7587
|
country: row.country,
|
|
7356
7588
|
language: row.language,
|
|
7357
7589
|
tags: parseJsonColumn(row.tags, []),
|
|
@@ -7654,6 +7886,7 @@ var routeCatalog = [
|
|
|
7654
7886
|
displayName: stringSchema,
|
|
7655
7887
|
canonicalDomain: stringSchema,
|
|
7656
7888
|
ownedDomains: stringArraySchema,
|
|
7889
|
+
aliases: stringArraySchema,
|
|
7657
7890
|
country: stringSchema,
|
|
7658
7891
|
language: stringSchema,
|
|
7659
7892
|
tags: stringArraySchema,
|
|
@@ -10873,7 +11106,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
10873
11106
|
|
|
10874
11107
|
// ../api-routes/src/schedules.ts
|
|
10875
11108
|
import crypto11 from "crypto";
|
|
10876
|
-
import { and as
|
|
11109
|
+
import { and as and8, eq as eq16 } from "drizzle-orm";
|
|
10877
11110
|
function parseKindParam(raw) {
|
|
10878
11111
|
if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
|
|
10879
11112
|
const parsed = schedulableRunKindSchema.safeParse(raw);
|
|
@@ -10939,7 +11172,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
10939
11172
|
}
|
|
10940
11173
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10941
11174
|
const enabledInt = enabled === false ? 0 : 1;
|
|
10942
|
-
const existing = app.db.select().from(schedules).where(
|
|
11175
|
+
const existing = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
|
|
10943
11176
|
if (existing) {
|
|
10944
11177
|
app.db.update(schedules).set({
|
|
10945
11178
|
cronExpr,
|
|
@@ -10973,13 +11206,13 @@ async function scheduleRoutes(app, opts) {
|
|
|
10973
11206
|
diff: { kind, cronExpr, preset, timezone, providers, sourceId }
|
|
10974
11207
|
});
|
|
10975
11208
|
opts.onScheduleUpdated?.("upsert", project.id, kind);
|
|
10976
|
-
const schedule = app.db.select().from(schedules).where(
|
|
11209
|
+
const schedule = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
|
|
10977
11210
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
10978
11211
|
});
|
|
10979
11212
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
10980
11213
|
const project = resolveProject(app.db, request.params.name);
|
|
10981
11214
|
const kind = parseKindParam(request.query?.kind);
|
|
10982
|
-
const schedule = app.db.select().from(schedules).where(
|
|
11215
|
+
const schedule = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
|
|
10983
11216
|
if (!schedule) {
|
|
10984
11217
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
10985
11218
|
}
|
|
@@ -10988,7 +11221,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
10988
11221
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
10989
11222
|
const project = resolveProject(app.db, request.params.name);
|
|
10990
11223
|
const kind = parseKindParam(request.query?.kind);
|
|
10991
|
-
const schedule = app.db.select().from(schedules).where(
|
|
11224
|
+
const schedule = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
|
|
10992
11225
|
if (!schedule) {
|
|
10993
11226
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
10994
11227
|
}
|
|
@@ -11027,7 +11260,8 @@ function formatSchedule(row) {
|
|
|
11027
11260
|
import crypto12 from "crypto";
|
|
11028
11261
|
import { eq as eq17 } from "drizzle-orm";
|
|
11029
11262
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
11030
|
-
async function notificationRoutes(app) {
|
|
11263
|
+
async function notificationRoutes(app, opts = {}) {
|
|
11264
|
+
const allowLoopback = opts.allowLoopbackWebhooks === true;
|
|
11031
11265
|
app.get("/notifications/events", async (_request, reply) => {
|
|
11032
11266
|
return reply.send(VALID_EVENTS);
|
|
11033
11267
|
});
|
|
@@ -11035,7 +11269,7 @@ async function notificationRoutes(app) {
|
|
|
11035
11269
|
const project = resolveProject(app.db, request.params.name);
|
|
11036
11270
|
const { channel, url, events, source } = request.body ?? {};
|
|
11037
11271
|
if (channel !== "webhook") throw validationError('Only "webhook" channel is supported');
|
|
11038
|
-
const urlCheck = await resolveWebhookTarget(url ?? "");
|
|
11272
|
+
const urlCheck = await resolveWebhookTarget(url ?? "", { allowLoopback });
|
|
11039
11273
|
if (!urlCheck.ok) throw validationError(urlCheck.message);
|
|
11040
11274
|
if (!events?.length) throw validationError('"events" must be a non-empty array');
|
|
11041
11275
|
const invalid = events.filter((e) => !VALID_EVENTS.includes(e));
|
|
@@ -11096,7 +11330,7 @@ async function notificationRoutes(app) {
|
|
|
11096
11330
|
throw notFound("Notification", request.params.id);
|
|
11097
11331
|
}
|
|
11098
11332
|
const config = parseJsonColumn(notification.config, { url: "", events: [] });
|
|
11099
|
-
const urlCheck = await resolveWebhookTarget(config.url);
|
|
11333
|
+
const urlCheck = await resolveWebhookTarget(config.url, { allowLoopback });
|
|
11100
11334
|
if (!urlCheck.ok) throw validationError(`Stored webhook URL is invalid: ${urlCheck.message}`);
|
|
11101
11335
|
const payload = {
|
|
11102
11336
|
source: "canonry",
|
|
@@ -11144,7 +11378,7 @@ function formatNotification(row) {
|
|
|
11144
11378
|
|
|
11145
11379
|
// ../api-routes/src/google.ts
|
|
11146
11380
|
import crypto14 from "crypto";
|
|
11147
|
-
import { eq as eq18, and as
|
|
11381
|
+
import { eq as eq18, and as and9, desc as desc8, sql as sql5 } from "drizzle-orm";
|
|
11148
11382
|
|
|
11149
11383
|
// ../integration-google/src/constants.ts
|
|
11150
11384
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -12154,7 +12388,23 @@ async function getValidToken(store, domain, connectionType, clientId, clientSecr
|
|
|
12154
12388
|
};
|
|
12155
12389
|
}
|
|
12156
12390
|
async function googleRoutes(app, opts) {
|
|
12157
|
-
|
|
12391
|
+
if (opts.googleStateSecret === void 0) {
|
|
12392
|
+
app.log.warn(
|
|
12393
|
+
"googleStateSecret is not configured \u2014 Google OAuth routes will not be registered. Set GOOGLE_STATE_SECRET to enable Google integrations."
|
|
12394
|
+
);
|
|
12395
|
+
return;
|
|
12396
|
+
}
|
|
12397
|
+
if (opts.googleStateSecret === "") {
|
|
12398
|
+
throw new Error(
|
|
12399
|
+
"googleStateSecret is empty. Set a non-empty secret (e.g. `openssl rand -hex 32`) via the GOOGLE_STATE_SECRET environment variable."
|
|
12400
|
+
);
|
|
12401
|
+
}
|
|
12402
|
+
if (opts.googleStateSecret === "insecure-default-secret") {
|
|
12403
|
+
throw new Error(
|
|
12404
|
+
"googleStateSecret is set to the legacy insecure default. Generate a real secret (e.g. `openssl rand -hex 32`) and set GOOGLE_STATE_SECRET."
|
|
12405
|
+
);
|
|
12406
|
+
}
|
|
12407
|
+
const stateSecret = opts.googleStateSecret;
|
|
12158
12408
|
function getAuthConfig() {
|
|
12159
12409
|
return opts.getGoogleAuthConfig?.() ?? {};
|
|
12160
12410
|
}
|
|
@@ -12361,7 +12611,7 @@ async function googleRoutes(app, opts) {
|
|
|
12361
12611
|
if (page) conditions.push(sql5`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
12362
12612
|
const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
|
|
12363
12613
|
const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
|
|
12364
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
12614
|
+
const rows = app.db.select().from(gscSearchData).where(and9(...conditions)).orderBy(desc8(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
|
|
12365
12615
|
return rows.map((r) => ({
|
|
12366
12616
|
date: r.date,
|
|
12367
12617
|
query: r.query,
|
|
@@ -12435,7 +12685,7 @@ async function googleRoutes(app, opts) {
|
|
|
12435
12685
|
const { url, limit } = request.query;
|
|
12436
12686
|
const conditions = [eq18(gscUrlInspections.projectId, project.id)];
|
|
12437
12687
|
if (url) conditions.push(eq18(gscUrlInspections.url, url));
|
|
12438
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
12688
|
+
const rows = app.db.select().from(gscUrlInspections).where(and9(...conditions)).orderBy(desc8(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
12439
12689
|
return rows.map((r) => ({
|
|
12440
12690
|
id: r.id,
|
|
12441
12691
|
url: r.url,
|
|
@@ -12787,7 +13037,7 @@ async function googleRoutes(app, opts) {
|
|
|
12787
13037
|
|
|
12788
13038
|
// ../api-routes/src/bing.ts
|
|
12789
13039
|
import crypto15 from "crypto";
|
|
12790
|
-
import { eq as eq19, and as
|
|
13040
|
+
import { eq as eq19, and as and10, desc as desc9 } from "drizzle-orm";
|
|
12791
13041
|
|
|
12792
13042
|
// ../integration-bing/src/constants.ts
|
|
12793
13043
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -13201,7 +13451,7 @@ async function bingRoutes(app, opts) {
|
|
|
13201
13451
|
requireConnectionStore();
|
|
13202
13452
|
const project = resolveProject(app.db, request.params.name);
|
|
13203
13453
|
const { url, limit } = request.query;
|
|
13204
|
-
const whereClause = url ?
|
|
13454
|
+
const whereClause = url ? and10(eq19(bingUrlInspections.projectId, project.id), eq19(bingUrlInspections.url, url)) : eq19(bingUrlInspections.projectId, project.id);
|
|
13205
13455
|
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc9(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
13206
13456
|
return filtered.map((r) => ({
|
|
13207
13457
|
id: r.id,
|
|
@@ -13433,7 +13683,7 @@ async function bingRoutes(app, opts) {
|
|
|
13433
13683
|
import fs from "fs";
|
|
13434
13684
|
import path from "path";
|
|
13435
13685
|
import os2 from "os";
|
|
13436
|
-
import { eq as eq20, and as
|
|
13686
|
+
import { eq as eq20, and as and11 } from "drizzle-orm";
|
|
13437
13687
|
function getScreenshotDir() {
|
|
13438
13688
|
return path.join(os2.homedir(), ".canonry", "screenshots");
|
|
13439
13689
|
}
|
|
@@ -13506,12 +13756,12 @@ async function cdpRoutes(app, opts) {
|
|
|
13506
13756
|
async (request, reply) => {
|
|
13507
13757
|
const project = resolveProject(app.db, request.params.name);
|
|
13508
13758
|
const { runId } = request.params;
|
|
13509
|
-
const run = app.db.select().from(runs).where(
|
|
13759
|
+
const run = app.db.select().from(runs).where(and11(eq20(runs.id, runId), eq20(runs.projectId, project.id))).get();
|
|
13510
13760
|
if (!run) {
|
|
13511
13761
|
const err = notFound("Run", runId);
|
|
13512
13762
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
13513
13763
|
}
|
|
13514
|
-
const snapshots = app.db.select({
|
|
13764
|
+
const snapshots = filterTrackedSnapshots(app.db.select({
|
|
13515
13765
|
id: querySnapshots.id,
|
|
13516
13766
|
queryId: querySnapshots.queryId,
|
|
13517
13767
|
provider: querySnapshots.provider,
|
|
@@ -13519,7 +13769,7 @@ async function cdpRoutes(app, opts) {
|
|
|
13519
13769
|
citedDomains: querySnapshots.citedDomains,
|
|
13520
13770
|
screenshotPath: querySnapshots.screenshotPath,
|
|
13521
13771
|
rawResponse: querySnapshots.rawResponse
|
|
13522
|
-
}).from(querySnapshots).where(eq20(querySnapshots.runId, runId)).all();
|
|
13772
|
+
}).from(querySnapshots).where(eq20(querySnapshots.runId, runId)).all());
|
|
13523
13773
|
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq20(queries.projectId, project.id)).all();
|
|
13524
13774
|
const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
|
|
13525
13775
|
const byQuery = /* @__PURE__ */ new Map();
|
|
@@ -13603,7 +13853,7 @@ async function cdpRoutes(app, opts) {
|
|
|
13603
13853
|
|
|
13604
13854
|
// ../api-routes/src/ga.ts
|
|
13605
13855
|
import crypto16 from "crypto";
|
|
13606
|
-
import { eq as eq21, desc as desc10, and as
|
|
13856
|
+
import { eq as eq21, desc as desc10, and as and12, sql as sql6 } from "drizzle-orm";
|
|
13607
13857
|
function gaLog(level, action, ctx) {
|
|
13608
13858
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
13609
13859
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -13898,7 +14148,7 @@ async function ga4Routes(app, opts) {
|
|
|
13898
14148
|
app.db.transaction((tx) => {
|
|
13899
14149
|
if (syncTraffic) {
|
|
13900
14150
|
tx.delete(gaTrafficSnapshots).where(
|
|
13901
|
-
|
|
14151
|
+
and12(
|
|
13902
14152
|
eq21(gaTrafficSnapshots.projectId, project.id),
|
|
13903
14153
|
sql6`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
13904
14154
|
sql6`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
@@ -13922,7 +14172,7 @@ async function ga4Routes(app, opts) {
|
|
|
13922
14172
|
}
|
|
13923
14173
|
if (syncAi) {
|
|
13924
14174
|
tx.delete(gaAiReferrals).where(
|
|
13925
|
-
|
|
14175
|
+
and12(
|
|
13926
14176
|
eq21(gaAiReferrals.projectId, project.id),
|
|
13927
14177
|
sql6`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
13928
14178
|
sql6`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
@@ -13948,7 +14198,7 @@ async function ga4Routes(app, opts) {
|
|
|
13948
14198
|
}
|
|
13949
14199
|
if (syncSocial) {
|
|
13950
14200
|
tx.delete(gaSocialReferrals).where(
|
|
13951
|
-
|
|
14201
|
+
and12(
|
|
13952
14202
|
eq21(gaSocialReferrals.projectId, project.id),
|
|
13953
14203
|
sql6`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
13954
14204
|
sql6`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
@@ -14052,7 +14302,7 @@ async function ga4Routes(app, opts) {
|
|
|
14052
14302
|
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
14053
14303
|
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
14054
14304
|
}).from(gaTrafficWindowSummaries).where(
|
|
14055
|
-
|
|
14305
|
+
and12(
|
|
14056
14306
|
eq21(gaTrafficWindowSummaries.projectId, project.id),
|
|
14057
14307
|
eq21(gaTrafficWindowSummaries.windowKey, window)
|
|
14058
14308
|
)
|
|
@@ -14061,7 +14311,7 @@ async function ga4Routes(app, opts) {
|
|
|
14061
14311
|
totalSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
14062
14312
|
totalOrganicSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
14063
14313
|
totalUsers: sql6`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
14064
|
-
}).from(gaTrafficSnapshots).where(
|
|
14314
|
+
}).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).get() : null;
|
|
14065
14315
|
const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
14066
14316
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
14067
14317
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
@@ -14069,7 +14319,7 @@ async function ga4Routes(app, opts) {
|
|
|
14069
14319
|
}).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
|
|
14070
14320
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
14071
14321
|
totalDirectSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
14072
|
-
}).from(gaTrafficSnapshots).where(
|
|
14322
|
+
}).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).get();
|
|
14073
14323
|
const summaryMeta = app.db.select({
|
|
14074
14324
|
periodStart: gaTrafficSummaries.periodStart,
|
|
14075
14325
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
@@ -14080,14 +14330,14 @@ async function ga4Routes(app, opts) {
|
|
|
14080
14330
|
organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
14081
14331
|
directSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
|
|
14082
14332
|
users: sql6`SUM(${gaTrafficSnapshots.users})`
|
|
14083
|
-
}).from(gaTrafficSnapshots).where(
|
|
14333
|
+
}).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).groupBy(sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql6`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
14084
14334
|
const aiReferralRows = app.db.select({
|
|
14085
14335
|
source: gaAiReferrals.source,
|
|
14086
14336
|
medium: gaAiReferrals.medium,
|
|
14087
14337
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
14088
14338
|
sessions: sql6`SUM(${gaAiReferrals.sessions})`,
|
|
14089
14339
|
users: sql6`SUM(${gaAiReferrals.users})`
|
|
14090
|
-
}).from(gaAiReferrals).where(
|
|
14340
|
+
}).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
|
|
14091
14341
|
const aiReferralLandingPageRows = app.db.select({
|
|
14092
14342
|
source: gaAiReferrals.source,
|
|
14093
14343
|
medium: gaAiReferrals.medium,
|
|
@@ -14095,7 +14345,7 @@ async function ga4Routes(app, opts) {
|
|
|
14095
14345
|
landingPage: sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
14096
14346
|
sessions: sql6`SUM(${gaAiReferrals.sessions})`,
|
|
14097
14347
|
users: sql6`SUM(${gaAiReferrals.users})`
|
|
14098
|
-
}).from(gaAiReferrals).where(
|
|
14348
|
+
}).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(
|
|
14099
14349
|
gaAiReferrals.source,
|
|
14100
14350
|
gaAiReferrals.medium,
|
|
14101
14351
|
gaAiReferrals.sourceDimension,
|
|
@@ -14132,7 +14382,7 @@ async function ga4Routes(app, opts) {
|
|
|
14132
14382
|
channelGroup: gaAiReferrals.channelGroup,
|
|
14133
14383
|
sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
|
|
14134
14384
|
users: sql6`COALESCE(SUM(${gaAiReferrals.users}), 0)`
|
|
14135
|
-
}).from(gaAiReferrals).where(
|
|
14385
|
+
}).from(gaAiReferrals).where(and12(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
|
|
14136
14386
|
const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
|
|
14137
14387
|
let aiBySessionUsers = 0;
|
|
14138
14388
|
for (const row of aiBySessionRows) {
|
|
@@ -14146,11 +14396,11 @@ async function ga4Routes(app, opts) {
|
|
|
14146
14396
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
14147
14397
|
sessions: sql6`SUM(${gaSocialReferrals.sessions})`,
|
|
14148
14398
|
users: sql6`SUM(${gaSocialReferrals.users})`
|
|
14149
|
-
}).from(gaSocialReferrals).where(
|
|
14399
|
+
}).from(gaSocialReferrals).where(and12(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql6`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
14150
14400
|
const socialTotals = app.db.select({
|
|
14151
14401
|
sessions: sql6`SUM(${gaSocialReferrals.sessions})`,
|
|
14152
14402
|
users: sql6`SUM(${gaSocialReferrals.users})`
|
|
14153
|
-
}).from(gaSocialReferrals).where(
|
|
14403
|
+
}).from(gaSocialReferrals).where(and12(...socialConditions)).get();
|
|
14154
14404
|
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
14155
14405
|
const total = summaryRow?.totalSessions ?? 0;
|
|
14156
14406
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
@@ -14241,7 +14491,7 @@ async function ga4Routes(app, opts) {
|
|
|
14241
14491
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
14242
14492
|
sessions: sql6`SUM(${gaAiReferrals.sessions})`,
|
|
14243
14493
|
users: sql6`SUM(${gaAiReferrals.users})`
|
|
14244
|
-
}).from(gaAiReferrals).where(
|
|
14494
|
+
}).from(gaAiReferrals).where(and12(...conditions)).groupBy(
|
|
14245
14495
|
gaAiReferrals.date,
|
|
14246
14496
|
gaAiReferrals.source,
|
|
14247
14497
|
gaAiReferrals.medium,
|
|
@@ -14263,7 +14513,7 @@ async function ga4Routes(app, opts) {
|
|
|
14263
14513
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
14264
14514
|
sessions: gaSocialReferrals.sessions,
|
|
14265
14515
|
users: gaSocialReferrals.users
|
|
14266
|
-
}).from(gaSocialReferrals).where(
|
|
14516
|
+
}).from(gaSocialReferrals).where(and12(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
14267
14517
|
return rows;
|
|
14268
14518
|
});
|
|
14269
14519
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -14276,7 +14526,7 @@ async function ga4Routes(app, opts) {
|
|
|
14276
14526
|
d.setDate(d.getDate() - n);
|
|
14277
14527
|
return fmt(d);
|
|
14278
14528
|
};
|
|
14279
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
14529
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and12(
|
|
14280
14530
|
eq21(gaSocialReferrals.projectId, project.id),
|
|
14281
14531
|
sql6`${gaSocialReferrals.date} >= ${from}`,
|
|
14282
14532
|
sql6`${gaSocialReferrals.date} < ${to}`
|
|
@@ -14289,7 +14539,7 @@ async function ga4Routes(app, opts) {
|
|
|
14289
14539
|
const sourceCurrent = app.db.select({
|
|
14290
14540
|
source: gaSocialReferrals.source,
|
|
14291
14541
|
sessions: sql6`SUM(${gaSocialReferrals.sessions})`
|
|
14292
|
-
}).from(gaSocialReferrals).where(
|
|
14542
|
+
}).from(gaSocialReferrals).where(and12(
|
|
14293
14543
|
eq21(gaSocialReferrals.projectId, project.id),
|
|
14294
14544
|
sql6`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
|
|
14295
14545
|
sql6`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
@@ -14297,7 +14547,7 @@ async function ga4Routes(app, opts) {
|
|
|
14297
14547
|
const sourcePrev = app.db.select({
|
|
14298
14548
|
source: gaSocialReferrals.source,
|
|
14299
14549
|
sessions: sql6`SUM(${gaSocialReferrals.sessions})`
|
|
14300
|
-
}).from(gaSocialReferrals).where(
|
|
14550
|
+
}).from(gaSocialReferrals).where(and12(
|
|
14301
14551
|
eq21(gaSocialReferrals.projectId, project.id),
|
|
14302
14552
|
sql6`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
|
|
14303
14553
|
sql6`${gaSocialReferrals.date} < ${daysAgo2(7)}`
|
|
@@ -14339,16 +14589,16 @@ async function ga4Routes(app, opts) {
|
|
|
14339
14589
|
return fmt(d);
|
|
14340
14590
|
};
|
|
14341
14591
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
14342
|
-
const sumTotal = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
14343
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
14344
|
-
const sumDirect = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
14345
|
-
const sumAi = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
14592
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
14593
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
14594
|
+
const sumDirect = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
14595
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
|
|
14346
14596
|
eq21(gaAiReferrals.projectId, project.id),
|
|
14347
14597
|
sql6`${gaAiReferrals.date} >= ${from}`,
|
|
14348
14598
|
sql6`${gaAiReferrals.date} < ${to}`,
|
|
14349
14599
|
eq21(gaAiReferrals.sourceDimension, "session")
|
|
14350
14600
|
)).get();
|
|
14351
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
14601
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${from}`, sql6`${gaSocialReferrals.date} < ${to}`)).get();
|
|
14352
14602
|
const todayStr = fmt(today);
|
|
14353
14603
|
const buildTrend = (sum) => {
|
|
14354
14604
|
const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
|
|
@@ -14357,13 +14607,13 @@ async function ga4Routes(app, opts) {
|
|
|
14357
14607
|
const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
|
|
14358
14608
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
14359
14609
|
};
|
|
14360
|
-
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
14610
|
+
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
|
|
14361
14611
|
eq21(gaAiReferrals.projectId, project.id),
|
|
14362
14612
|
sql6`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
|
|
14363
14613
|
sql6`${gaAiReferrals.date} < ${todayStr}`,
|
|
14364
14614
|
eq21(gaAiReferrals.sourceDimension, "session")
|
|
14365
14615
|
)).groupBy(gaAiReferrals.source).all();
|
|
14366
|
-
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
14616
|
+
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
|
|
14367
14617
|
eq21(gaAiReferrals.projectId, project.id),
|
|
14368
14618
|
sql6`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
|
|
14369
14619
|
sql6`${gaAiReferrals.date} < ${daysAgo2(7)}`,
|
|
@@ -14383,8 +14633,8 @@ async function ga4Routes(app, opts) {
|
|
|
14383
14633
|
}
|
|
14384
14634
|
return mover;
|
|
14385
14635
|
};
|
|
14386
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
14387
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
14636
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql6`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
14637
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql6`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
14388
14638
|
return {
|
|
14389
14639
|
total: buildTrend(sumTotal),
|
|
14390
14640
|
organic: buildTrend(sumOrganic),
|
|
@@ -14406,7 +14656,7 @@ async function ga4Routes(app, opts) {
|
|
|
14406
14656
|
sessions: sql6`SUM(${gaTrafficSnapshots.sessions})`,
|
|
14407
14657
|
organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
14408
14658
|
users: sql6`SUM(${gaTrafficSnapshots.users})`
|
|
14409
|
-
}).from(gaTrafficSnapshots).where(
|
|
14659
|
+
}).from(gaTrafficSnapshots).where(and12(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
14410
14660
|
return rows.map((r) => ({
|
|
14411
14661
|
date: r.date,
|
|
14412
14662
|
sessions: r.sessions ?? 0,
|
|
@@ -16059,7 +16309,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
16059
16309
|
|
|
16060
16310
|
// ../api-routes/src/backlinks.ts
|
|
16061
16311
|
import crypto18 from "crypto";
|
|
16062
|
-
import { and as
|
|
16312
|
+
import { and as and14, asc as asc2, desc as desc11, eq as eq22, sql as sql7 } from "drizzle-orm";
|
|
16063
16313
|
|
|
16064
16314
|
// ../integration-commoncrawl/src/constants.ts
|
|
16065
16315
|
import os3 from "os";
|
|
@@ -16234,7 +16484,7 @@ function parseContentLength2(value) {
|
|
|
16234
16484
|
|
|
16235
16485
|
// ../integration-commoncrawl/src/plugin-resolver.ts
|
|
16236
16486
|
import fs3 from "fs";
|
|
16237
|
-
import { createRequire as
|
|
16487
|
+
import { createRequire as createRequire3 } from "module";
|
|
16238
16488
|
import path4 from "path";
|
|
16239
16489
|
function pluginDirFor(pkgJson) {
|
|
16240
16490
|
return path4.dirname(pkgJson);
|
|
@@ -16253,7 +16503,7 @@ function loadDuckdb(opts = {}) {
|
|
|
16253
16503
|
);
|
|
16254
16504
|
}
|
|
16255
16505
|
try {
|
|
16256
|
-
const pluginRequire =
|
|
16506
|
+
const pluginRequire = createRequire3(duckdbPkg);
|
|
16257
16507
|
return pluginRequire("@duckdb/node-api");
|
|
16258
16508
|
} catch {
|
|
16259
16509
|
throw missingDependency(
|
|
@@ -16456,7 +16706,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
16456
16706
|
}
|
|
16457
16707
|
|
|
16458
16708
|
// ../api-routes/src/backlinks-filter.ts
|
|
16459
|
-
import { and as
|
|
16709
|
+
import { and as and13, ne as ne2, notLike } from "drizzle-orm";
|
|
16460
16710
|
var BACKLINK_FILTER_PATTERNS = [
|
|
16461
16711
|
"*.google.com",
|
|
16462
16712
|
"*.googleusercontent.com",
|
|
@@ -16479,7 +16729,7 @@ function backlinkCrawlerExclusionClause() {
|
|
|
16479
16729
|
conditions.push(ne2(backlinkDomains.linkingDomain, pattern));
|
|
16480
16730
|
}
|
|
16481
16731
|
}
|
|
16482
|
-
const combined =
|
|
16732
|
+
const combined = and13(...conditions);
|
|
16483
16733
|
if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
|
|
16484
16734
|
return combined;
|
|
16485
16735
|
}
|
|
@@ -16540,7 +16790,7 @@ function mapRunRow(row) {
|
|
|
16540
16790
|
};
|
|
16541
16791
|
}
|
|
16542
16792
|
function latestSummaryForProject(db, projectId, release) {
|
|
16543
|
-
const condition = release ?
|
|
16793
|
+
const condition = release ? and14(eq22(backlinkSummaries.projectId, projectId), eq22(backlinkSummaries.release, release)) : eq22(backlinkSummaries.projectId, projectId);
|
|
16544
16794
|
return db.select().from(backlinkSummaries).where(condition).orderBy(desc11(backlinkSummaries.queriedAt)).limit(1).get();
|
|
16545
16795
|
}
|
|
16546
16796
|
function parseExcludeCrawlers(value) {
|
|
@@ -16549,11 +16799,11 @@ function parseExcludeCrawlers(value) {
|
|
|
16549
16799
|
return lower === "1" || lower === "true" || lower === "yes";
|
|
16550
16800
|
}
|
|
16551
16801
|
function computeFilteredSummary(db, base) {
|
|
16552
|
-
const baseDomainCondition =
|
|
16802
|
+
const baseDomainCondition = and14(
|
|
16553
16803
|
eq22(backlinkDomains.projectId, base.projectId),
|
|
16554
16804
|
eq22(backlinkDomains.release, base.release)
|
|
16555
16805
|
);
|
|
16556
|
-
const filteredCondition =
|
|
16806
|
+
const filteredCondition = and14(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
16557
16807
|
const unfilteredAgg = db.select({
|
|
16558
16808
|
count: sql7`count(*)`,
|
|
16559
16809
|
total: sql7`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
@@ -16729,11 +16979,11 @@ async function backlinksRoutes(app, opts) {
|
|
|
16729
16979
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
16730
16980
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
16731
16981
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
16732
|
-
const baseDomainCondition =
|
|
16982
|
+
const baseDomainCondition = and14(
|
|
16733
16983
|
eq22(backlinkDomains.projectId, project.id),
|
|
16734
16984
|
eq22(backlinkDomains.release, targetRelease)
|
|
16735
16985
|
);
|
|
16736
|
-
const domainCondition = excludeCrawlers ?
|
|
16986
|
+
const domainCondition = excludeCrawlers ? and14(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
16737
16987
|
const totalRow = app.db.select({ count: sql7`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
16738
16988
|
const rows = app.db.select({
|
|
16739
16989
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
@@ -16769,7 +17019,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
16769
17019
|
|
|
16770
17020
|
// ../api-routes/src/traffic.ts
|
|
16771
17021
|
import crypto20 from "crypto";
|
|
16772
|
-
import { and as
|
|
17022
|
+
import { and as and15, desc as desc12, eq as eq23, gte as gte2, lte as lte2, sql as sql8 } from "drizzle-orm";
|
|
16773
17023
|
|
|
16774
17024
|
// ../integration-cloud-run/src/auth.ts
|
|
16775
17025
|
import crypto19 from "crypto";
|
|
@@ -17837,7 +18087,7 @@ var DEFAULT_WP_MAX_PAGES = 20;
|
|
|
17837
18087
|
var DEFAULT_VERCEL_MAX_PAGES = 50;
|
|
17838
18088
|
var MAX_TRACKED_EVENT_IDS = 1e3;
|
|
17839
18089
|
var DEFAULT_BACKFILL_DAYS = 30;
|
|
17840
|
-
var MAX_BACKFILL_DAYS =
|
|
18090
|
+
var MAX_BACKFILL_DAYS = 90;
|
|
17841
18091
|
var BACKFILL_MAX_PAGES = 1e3;
|
|
17842
18092
|
var BACKFILL_SAMPLE_LIMIT = 500;
|
|
17843
18093
|
function parseSourceConfig(row) {
|
|
@@ -17917,21 +18167,21 @@ async function runBackfillTask(options) {
|
|
|
17917
18167
|
try {
|
|
17918
18168
|
app.db.transaction((tx) => {
|
|
17919
18169
|
tx.delete(crawlerEventsHourly).where(
|
|
17920
|
-
|
|
18170
|
+
and15(
|
|
17921
18171
|
eq23(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
17922
18172
|
gte2(crawlerEventsHourly.tsHour, windowStartIso),
|
|
17923
18173
|
lte2(crawlerEventsHourly.tsHour, windowEndIso)
|
|
17924
18174
|
)
|
|
17925
18175
|
).run();
|
|
17926
18176
|
tx.delete(aiReferralEventsHourly).where(
|
|
17927
|
-
|
|
18177
|
+
and15(
|
|
17928
18178
|
eq23(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
17929
18179
|
gte2(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
17930
18180
|
lte2(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
17931
18181
|
)
|
|
17932
18182
|
).run();
|
|
17933
18183
|
tx.delete(rawEventSamples).where(
|
|
17934
|
-
|
|
18184
|
+
and15(
|
|
17935
18185
|
eq23(rawEventSamples.sourceId, sourceRow.id),
|
|
17936
18186
|
gte2(rawEventSamples.ts, windowStartIso),
|
|
17937
18187
|
lte2(rawEventSamples.ts, windowEndIso)
|
|
@@ -18354,7 +18604,8 @@ async function trafficRoutes(app, opts) {
|
|
|
18354
18604
|
endTime: windowEnd.toISOString(),
|
|
18355
18605
|
pageSize,
|
|
18356
18606
|
maxPages,
|
|
18357
|
-
firstSync: isFirstSync
|
|
18607
|
+
firstSync: isFirstSync,
|
|
18608
|
+
requestUrlSubstrings: [project.canonicalDomain]
|
|
18358
18609
|
});
|
|
18359
18610
|
allEvents = page.events;
|
|
18360
18611
|
} catch (e) {
|
|
@@ -18680,7 +18931,8 @@ async function trafficRoutes(app, opts) {
|
|
|
18680
18931
|
// ring-buffer reseed at the end takes the most-recent IDs from the
|
|
18681
18932
|
// dedupedEvents anyway.
|
|
18682
18933
|
firstSync: false,
|
|
18683
|
-
orderBy: "timestamp asc"
|
|
18934
|
+
orderBy: "timestamp asc",
|
|
18935
|
+
requestUrlSubstrings: [project.canonicalDomain]
|
|
18684
18936
|
});
|
|
18685
18937
|
return page.events;
|
|
18686
18938
|
};
|
|
@@ -18793,25 +19045,25 @@ async function trafficRoutes(app, opts) {
|
|
|
18793
19045
|
});
|
|
18794
19046
|
function buildSourceDetail(projectId, row, since) {
|
|
18795
19047
|
const crawlerTotals = app.db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
18796
|
-
|
|
19048
|
+
and15(
|
|
18797
19049
|
eq23(crawlerEventsHourly.sourceId, row.id),
|
|
18798
19050
|
gte2(crawlerEventsHourly.tsHour, since)
|
|
18799
19051
|
)
|
|
18800
19052
|
).get();
|
|
18801
19053
|
const aiTotals = app.db.select({ total: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
18802
|
-
|
|
19054
|
+
and15(
|
|
18803
19055
|
eq23(aiReferralEventsHourly.sourceId, row.id),
|
|
18804
19056
|
gte2(aiReferralEventsHourly.tsHour, since)
|
|
18805
19057
|
)
|
|
18806
19058
|
).get();
|
|
18807
19059
|
const sampleTotals = app.db.select({ total: sql8`COUNT(*)` }).from(rawEventSamples).where(
|
|
18808
|
-
|
|
19060
|
+
and15(
|
|
18809
19061
|
eq23(rawEventSamples.sourceId, row.id),
|
|
18810
19062
|
gte2(rawEventSamples.ts, since)
|
|
18811
19063
|
)
|
|
18812
19064
|
).get();
|
|
18813
19065
|
const latestRun = app.db.select().from(runs).where(
|
|
18814
|
-
|
|
19066
|
+
and15(
|
|
18815
19067
|
eq23(runs.projectId, projectId),
|
|
18816
19068
|
eq23(runs.kind, RunKinds["traffic-sync"]),
|
|
18817
19069
|
eq23(runs.sourceId, row.id)
|
|
@@ -18905,7 +19157,7 @@ async function trafficRoutes(app, opts) {
|
|
|
18905
19157
|
lte2(crawlerEventsHourly.tsHour, untilIso)
|
|
18906
19158
|
];
|
|
18907
19159
|
if (sourceIdParam) crawlerFilters.push(eq23(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
18908
|
-
const crawlerWhere =
|
|
19160
|
+
const crawlerWhere = and15(...crawlerFilters);
|
|
18909
19161
|
const total = app.db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
|
|
18910
19162
|
crawlerTotal = Number(total?.total ?? 0);
|
|
18911
19163
|
const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc12(crawlerEventsHourly.tsHour)).limit(limit).all();
|
|
@@ -18930,7 +19182,7 @@ async function trafficRoutes(app, opts) {
|
|
|
18930
19182
|
lte2(aiReferralEventsHourly.tsHour, untilIso)
|
|
18931
19183
|
];
|
|
18932
19184
|
if (sourceIdParam) aiFilters.push(eq23(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
18933
|
-
const aiWhere =
|
|
19185
|
+
const aiWhere = and15(...aiFilters);
|
|
18934
19186
|
const total = app.db.select({ total: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
18935
19187
|
aiReferralTotal = Number(total?.total ?? 0);
|
|
18936
19188
|
const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc12(aiReferralEventsHourly.tsHour)).limit(limit).all();
|
|
@@ -19585,7 +19837,7 @@ var providersConfiguredCheck = {
|
|
|
19585
19837
|
var PROVIDERS_CHECKS = [providersConfiguredCheck];
|
|
19586
19838
|
|
|
19587
19839
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
19588
|
-
import { and as
|
|
19840
|
+
import { and as and16, eq as eq24, gte as gte3, ne as ne3, sql as sql9 } from "drizzle-orm";
|
|
19589
19841
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
19590
19842
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
19591
19843
|
function skippedNoProject2() {
|
|
@@ -19599,7 +19851,7 @@ function skippedNoProject2() {
|
|
|
19599
19851
|
function loadProbes(ctx) {
|
|
19600
19852
|
if (!ctx.project) return [];
|
|
19601
19853
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
19602
|
-
|
|
19854
|
+
and16(
|
|
19603
19855
|
eq24(trafficSources.projectId, ctx.project.id),
|
|
19604
19856
|
ne3(trafficSources.status, TrafficSourceStatuses.archived)
|
|
19605
19857
|
)
|
|
@@ -19680,7 +19932,7 @@ var recentDataCheck = {
|
|
|
19680
19932
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
19681
19933
|
const recentCrawlers = Number(
|
|
19682
19934
|
ctx.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
19683
|
-
|
|
19935
|
+
and16(
|
|
19684
19936
|
eq24(crawlerEventsHourly.projectId, ctx.project.id),
|
|
19685
19937
|
gte3(crawlerEventsHourly.tsHour, warnCutoff)
|
|
19686
19938
|
)
|
|
@@ -19688,7 +19940,7 @@ var recentDataCheck = {
|
|
|
19688
19940
|
);
|
|
19689
19941
|
const recentReferrals = Number(
|
|
19690
19942
|
ctx.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
19691
|
-
|
|
19943
|
+
and16(
|
|
19692
19944
|
eq24(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
19693
19945
|
gte3(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
19694
19946
|
)
|
|
@@ -19704,7 +19956,7 @@ var recentDataCheck = {
|
|
|
19704
19956
|
}
|
|
19705
19957
|
const olderCrawlers = Number(
|
|
19706
19958
|
ctx.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
19707
|
-
|
|
19959
|
+
and16(
|
|
19708
19960
|
eq24(crawlerEventsHourly.projectId, ctx.project.id),
|
|
19709
19961
|
gte3(crawlerEventsHourly.tsHour, failCutoff)
|
|
19710
19962
|
)
|
|
@@ -19712,7 +19964,7 @@ var recentDataCheck = {
|
|
|
19712
19964
|
);
|
|
19713
19965
|
const olderReferrals = Number(
|
|
19714
19966
|
ctx.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
19715
|
-
|
|
19967
|
+
and16(
|
|
19716
19968
|
eq24(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
19717
19969
|
gte3(aiReferralEventsHourly.tsHour, failCutoff)
|
|
19718
19970
|
)
|
|
@@ -20010,7 +20262,7 @@ async function doctorRoutes(app, opts) {
|
|
|
20010
20262
|
|
|
20011
20263
|
// ../api-routes/src/discovery/routes.ts
|
|
20012
20264
|
import crypto21 from "crypto";
|
|
20013
|
-
import { and as
|
|
20265
|
+
import { and as and17, desc as desc13, eq as eq25, gte as gte4, inArray as inArray8 } from "drizzle-orm";
|
|
20014
20266
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
20015
20267
|
async function discoveryRoutes(app, opts) {
|
|
20016
20268
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -20042,7 +20294,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
20042
20294
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20043
20295
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
20044
20296
|
const decision = app.db.transaction((tx) => {
|
|
20045
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
20297
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and17(
|
|
20046
20298
|
eq25(discoverySessions.projectId, project.id),
|
|
20047
20299
|
eq25(discoverySessions.icpDescription, icpDescription),
|
|
20048
20300
|
inArray8(discoverySessions.status, [
|
|
@@ -20506,6 +20758,7 @@ async function apiRoutes(app, opts) {
|
|
|
20506
20758
|
await api.register(projectRoutes, {
|
|
20507
20759
|
onProjectDeleted: opts.onProjectDeleted,
|
|
20508
20760
|
onProjectUpserted: opts.onProjectUpserted,
|
|
20761
|
+
onAliasesChanged: opts.onAliasesChanged,
|
|
20509
20762
|
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
20510
20763
|
});
|
|
20511
20764
|
await api.register(queryRoutes, {
|
|
@@ -20520,7 +20773,9 @@ async function apiRoutes(app, opts) {
|
|
|
20520
20773
|
await api.register(applyRoutes, {
|
|
20521
20774
|
onScheduleUpdated: opts.onScheduleUpdated,
|
|
20522
20775
|
onProjectUpserted: opts.onProjectUpserted,
|
|
20776
|
+
onAliasesChanged: opts.onAliasesChanged,
|
|
20523
20777
|
validProviderNames: opts.providerAdapters?.map((a) => a.name),
|
|
20778
|
+
allowLoopbackWebhooks: opts.allowLoopbackWebhooks,
|
|
20524
20779
|
onGoogleConnectionPropertyUpdated: (domain, connectionType, propertyId) => {
|
|
20525
20780
|
opts.googleConnectionStore?.updateConnection(domain, connectionType, {
|
|
20526
20781
|
propertyId,
|
|
@@ -20551,7 +20806,9 @@ async function apiRoutes(app, opts) {
|
|
|
20551
20806
|
onScheduleUpdated: opts.onScheduleUpdated,
|
|
20552
20807
|
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
20553
20808
|
});
|
|
20554
|
-
await api.register(notificationRoutes
|
|
20809
|
+
await api.register(notificationRoutes, {
|
|
20810
|
+
allowLoopbackWebhooks: opts.allowLoopbackWebhooks
|
|
20811
|
+
});
|
|
20555
20812
|
await api.register(telemetryRoutes, {
|
|
20556
20813
|
getTelemetryStatus: opts.getTelemetryStatus,
|
|
20557
20814
|
setTelemetryEnabled: opts.setTelemetryEnabled
|
|
@@ -20806,7 +21063,7 @@ function isVertexConfig(config) {
|
|
|
20806
21063
|
function resolveModel(config) {
|
|
20807
21064
|
return config.model || DEFAULT_MODEL;
|
|
20808
21065
|
}
|
|
20809
|
-
function
|
|
21066
|
+
function createClient2(config) {
|
|
20810
21067
|
if (isVertexConfig(config)) {
|
|
20811
21068
|
return new GoogleGenAI({
|
|
20812
21069
|
vertexai: true,
|
|
@@ -20846,7 +21103,7 @@ async function healthcheck(config) {
|
|
|
20846
21103
|
if (!validation.ok) return validation;
|
|
20847
21104
|
try {
|
|
20848
21105
|
const model = resolveModel(config);
|
|
20849
|
-
const client =
|
|
21106
|
+
const client = createClient2(config);
|
|
20850
21107
|
const result = await withRetry(
|
|
20851
21108
|
() => client.models.generateContent({
|
|
20852
21109
|
model,
|
|
@@ -20873,7 +21130,7 @@ async function healthcheck(config) {
|
|
|
20873
21130
|
async function executeTrackedQuery(input) {
|
|
20874
21131
|
const model = resolveModel(input.config);
|
|
20875
21132
|
const prompt = buildPrompt(input.query, input.location);
|
|
20876
|
-
const client =
|
|
21133
|
+
const client = createClient2(input.config);
|
|
20877
21134
|
try {
|
|
20878
21135
|
const result = await withRetry(
|
|
20879
21136
|
() => client.models.generateContent({
|
|
@@ -21038,7 +21295,7 @@ function extractDomainFromUri(uri) {
|
|
|
21038
21295
|
}
|
|
21039
21296
|
async function generateText(prompt, config) {
|
|
21040
21297
|
const model = resolveModel(config);
|
|
21041
|
-
const client =
|
|
21298
|
+
const client = createClient2(config);
|
|
21042
21299
|
const result = await withRetry(
|
|
21043
21300
|
() => client.models.generateContent({
|
|
21044
21301
|
model,
|
|
@@ -23358,7 +23615,7 @@ import crypto24 from "crypto";
|
|
|
23358
23615
|
import fs7 from "fs";
|
|
23359
23616
|
import path9 from "path";
|
|
23360
23617
|
import os5 from "os";
|
|
23361
|
-
import { and as
|
|
23618
|
+
import { and as and18, eq as eq27, inArray as inArray9, sql as sql10 } from "drizzle-orm";
|
|
23362
23619
|
|
|
23363
23620
|
// src/run-telemetry.ts
|
|
23364
23621
|
import crypto23 from "crypto";
|
|
@@ -23465,11 +23722,15 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
|
23465
23722
|
function escapeRegExp5(value) {
|
|
23466
23723
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23467
23724
|
}
|
|
23468
|
-
function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains) {
|
|
23725
|
+
function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains, ownBrandNames = []) {
|
|
23469
23726
|
if (!answerText || answerText.length < 20) return [];
|
|
23470
23727
|
const ownBrandKeys = new Set(
|
|
23471
23728
|
ownDomains.flatMap((domain) => collectBrandKeysFromDomain(domain))
|
|
23472
23729
|
);
|
|
23730
|
+
for (const name of ownBrandNames) {
|
|
23731
|
+
const key = brandKeyFromText(name);
|
|
23732
|
+
if (key.length >= 4) ownBrandKeys.add(key);
|
|
23733
|
+
}
|
|
23473
23734
|
const knownCompetitorKeys = new Set(
|
|
23474
23735
|
[...citedDomains, ...competitorDomains].flatMap((domain) => collectBrandKeysFromDomain(domain)).filter((key) => !ownBrandKeys.has(key))
|
|
23475
23736
|
);
|
|
@@ -23737,7 +23998,7 @@ var JobRunner = class {
|
|
|
23737
23998
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
23738
23999
|
}
|
|
23739
24000
|
if (existingRun.status === "queued") {
|
|
23740
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
24001
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and18(eq27(runs.id, runId), eq27(runs.status, "queued"))).run();
|
|
23741
24002
|
}
|
|
23742
24003
|
this.throwIfRunCancelled(runId);
|
|
23743
24004
|
const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
|
|
@@ -23762,13 +24023,17 @@ var JobRunner = class {
|
|
|
23762
24023
|
}
|
|
23763
24024
|
log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
23764
24025
|
const scopedQueryNames = parseJsonColumn(existingRun.queries, null);
|
|
23765
|
-
projectQueries = scopedQueryNames ? this.db.select().from(queries).where(
|
|
24026
|
+
projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and18(eq27(queries.projectId, projectId), inArray9(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
|
|
23766
24027
|
const projectCompetitors = this.db.select().from(competitors).where(eq27(competitors.projectId, projectId)).all();
|
|
23767
24028
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
23768
24029
|
const allDomains = effectiveDomains({
|
|
23769
24030
|
canonicalDomain: project.canonicalDomain,
|
|
23770
24031
|
ownedDomains: parseJsonColumn(project.ownedDomains, [])
|
|
23771
24032
|
});
|
|
24033
|
+
const allBrandNames = effectiveBrandNames({
|
|
24034
|
+
displayName: project.displayName,
|
|
24035
|
+
aliases: parseJsonColumn(project.aliases, [])
|
|
24036
|
+
});
|
|
23772
24037
|
const executionContext = {
|
|
23773
24038
|
providerCount: activeProviders.length,
|
|
23774
24039
|
providers: activeProviders.map((provider) => provider.adapter.name),
|
|
@@ -23829,7 +24094,7 @@ var JobRunner = class {
|
|
|
23829
24094
|
const citationState = determineCitationState(normalized, allDomains);
|
|
23830
24095
|
const answerMentioned = determineAnswerMentioned(
|
|
23831
24096
|
normalized.answerText,
|
|
23832
|
-
|
|
24097
|
+
allBrandNames,
|
|
23833
24098
|
allDomains
|
|
23834
24099
|
);
|
|
23835
24100
|
const overlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
@@ -23837,7 +24102,8 @@ var JobRunner = class {
|
|
|
23837
24102
|
normalized.answerText,
|
|
23838
24103
|
allDomains,
|
|
23839
24104
|
normalized.citedDomains,
|
|
23840
|
-
competitorDomains
|
|
24105
|
+
competitorDomains,
|
|
24106
|
+
allBrandNames
|
|
23841
24107
|
);
|
|
23842
24108
|
let screenshotRelPath = null;
|
|
23843
24109
|
if (raw.screenshotPath && fs7.existsSync(raw.screenshotPath)) {
|
|
@@ -23851,6 +24117,7 @@ var JobRunner = class {
|
|
|
23851
24117
|
id: snapshotId,
|
|
23852
24118
|
runId,
|
|
23853
24119
|
queryId: q.id,
|
|
24120
|
+
queryText: q.query,
|
|
23854
24121
|
provider: providerName,
|
|
23855
24122
|
model: raw.model,
|
|
23856
24123
|
citationState,
|
|
@@ -23874,6 +24141,7 @@ var JobRunner = class {
|
|
|
23874
24141
|
id: crypto24.randomUUID(),
|
|
23875
24142
|
runId,
|
|
23876
24143
|
queryId: q.id,
|
|
24144
|
+
queryText: q.query,
|
|
23877
24145
|
provider: providerName,
|
|
23878
24146
|
model: raw.model,
|
|
23879
24147
|
citationState,
|
|
@@ -24098,7 +24366,7 @@ function buildPhases(input) {
|
|
|
24098
24366
|
|
|
24099
24367
|
// src/gsc-sync.ts
|
|
24100
24368
|
import crypto25 from "crypto";
|
|
24101
|
-
import { eq as eq28, and as
|
|
24369
|
+
import { eq as eq28, and as and19, sql as sql11 } from "drizzle-orm";
|
|
24102
24370
|
var log2 = createLogger("GscSync");
|
|
24103
24371
|
function formatDate3(d) {
|
|
24104
24372
|
return d.toISOString().split("T")[0];
|
|
@@ -24150,7 +24418,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
24150
24418
|
});
|
|
24151
24419
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
24152
24420
|
db.delete(gscSearchData).where(
|
|
24153
|
-
|
|
24421
|
+
and19(
|
|
24154
24422
|
eq28(gscSearchData.projectId, projectId),
|
|
24155
24423
|
sql11`${gscSearchData.date} >= ${startDate}`,
|
|
24156
24424
|
sql11`${gscSearchData.date} <= ${endDate}`
|
|
@@ -24239,7 +24507,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
24239
24507
|
}
|
|
24240
24508
|
}
|
|
24241
24509
|
const snapshotDate = formatDate3(/* @__PURE__ */ new Date());
|
|
24242
|
-
db.delete(gscCoverageSnapshots).where(
|
|
24510
|
+
db.delete(gscCoverageSnapshots).where(and19(eq28(gscCoverageSnapshots.projectId, projectId), eq28(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
24243
24511
|
db.insert(gscCoverageSnapshots).values({
|
|
24244
24512
|
id: crypto25.randomUUID(),
|
|
24245
24513
|
projectId,
|
|
@@ -24262,7 +24530,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
24262
24530
|
|
|
24263
24531
|
// src/gsc-inspect-sitemap.ts
|
|
24264
24532
|
import crypto26 from "crypto";
|
|
24265
|
-
import { eq as eq29, and as
|
|
24533
|
+
import { eq as eq29, and as and20 } from "drizzle-orm";
|
|
24266
24534
|
|
|
24267
24535
|
// src/sitemap-parser.ts
|
|
24268
24536
|
var log3 = createLogger("SitemapParser");
|
|
@@ -24478,7 +24746,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
24478
24746
|
}
|
|
24479
24747
|
}
|
|
24480
24748
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
24481
|
-
db.delete(gscCoverageSnapshots).where(
|
|
24749
|
+
db.delete(gscCoverageSnapshots).where(and20(eq29(gscCoverageSnapshots.projectId, projectId), eq29(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
24482
24750
|
db.insert(gscCoverageSnapshots).values({
|
|
24483
24751
|
id: crypto26.randomUUID(),
|
|
24484
24752
|
projectId,
|
|
@@ -24689,7 +24957,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
24689
24957
|
// src/commoncrawl-sync.ts
|
|
24690
24958
|
import crypto28 from "crypto";
|
|
24691
24959
|
import path10 from "path";
|
|
24692
|
-
import { and as
|
|
24960
|
+
import { and as and21, eq as eq31, sql as sql12 } from "drizzle-orm";
|
|
24693
24961
|
var log6 = createLogger("CommonCrawlSync");
|
|
24694
24962
|
var INSERT_CHUNK_SIZE = 1e4;
|
|
24695
24963
|
function defaultDeps() {
|
|
@@ -24880,7 +25148,7 @@ function computeSummary(rows) {
|
|
|
24880
25148
|
// src/backlink-extract.ts
|
|
24881
25149
|
import crypto29 from "crypto";
|
|
24882
25150
|
import fs8 from "fs";
|
|
24883
|
-
import { and as
|
|
25151
|
+
import { and as and22, desc as desc15, eq as eq32 } from "drizzle-orm";
|
|
24884
25152
|
var log7 = createLogger("BacklinkExtract");
|
|
24885
25153
|
function defaultDeps2() {
|
|
24886
25154
|
return {
|
|
@@ -24926,7 +25194,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
24926
25194
|
const targetDomain = project.canonicalDomain;
|
|
24927
25195
|
db.transaction((tx) => {
|
|
24928
25196
|
tx.delete(backlinkDomains).where(
|
|
24929
|
-
|
|
25197
|
+
and22(eq32(backlinkDomains.projectId, projectId), eq32(backlinkDomains.release, release))
|
|
24930
25198
|
).run();
|
|
24931
25199
|
if (rows.length > 0) {
|
|
24932
25200
|
const values = rows.map((r) => ({
|
|
@@ -24997,9 +25265,10 @@ function computeSummary2(rows) {
|
|
|
24997
25265
|
|
|
24998
25266
|
// src/discovery-run.ts
|
|
24999
25267
|
import crypto30 from "crypto";
|
|
25000
|
-
import { eq as eq33 } from "drizzle-orm";
|
|
25268
|
+
import { and as and23, eq as eq33 } from "drizzle-orm";
|
|
25001
25269
|
var log8 = createLogger("DiscoveryRun");
|
|
25002
25270
|
var DEFAULT_SEED_COUNT = 30;
|
|
25271
|
+
var QUERIES_PER_INTENT_BUCKET = 6;
|
|
25003
25272
|
async function executeDiscoveryRun(opts) {
|
|
25004
25273
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
25005
25274
|
opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq33(runs.id, opts.runId)).run();
|
|
@@ -25204,6 +25473,7 @@ function buildLocationConstraint(locations) {
|
|
|
25204
25473
|
}
|
|
25205
25474
|
function buildSeedPrompt(input) {
|
|
25206
25475
|
const locationConstraint = buildLocationConstraint(input.locations ?? []);
|
|
25476
|
+
const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
|
|
25207
25477
|
return [
|
|
25208
25478
|
"You are an AEO (Answer Engine Optimization) analyst expanding a tracked-query basket for a customer.",
|
|
25209
25479
|
"",
|
|
@@ -25211,14 +25481,15 @@ function buildSeedPrompt(input) {
|
|
|
25211
25481
|
`ICP: ${input.icpDescription}`,
|
|
25212
25482
|
...locationConstraint.length > 0 ? ["", ...locationConstraint] : [],
|
|
25213
25483
|
"",
|
|
25214
|
-
"Brainstorm
|
|
25215
|
-
|
|
25216
|
-
"
|
|
25217
|
-
"
|
|
25218
|
-
"
|
|
25219
|
-
|
|
25484
|
+
"Brainstorm queries a member of this ICP would type into an AI answer engine (Gemini, ChatGPT, Perplexity). Generate candidates across the five intent buckets below \u2014 these are SEMANTICALLY DISTINCT search intents, not stylistic variants. A query should fit one bucket cleanly. Diversity across buckets is the point: it keeps the list from collapsing into near-synonyms of a single intent.",
|
|
25485
|
+
"",
|
|
25486
|
+
' 1. Informational \u2014 the searcher wants to understand a concept, market, or problem. Templates: "what is X", "how does X work", "X explained", "why X matters".',
|
|
25487
|
+
` 2. Commercial \u2014 the searcher is researching category leaders before a purchase. Templates: "best X for Y", "top X ${currentYear}", "leading X providers", "X for [use case]".`,
|
|
25488
|
+
' 3. Navigational \u2014 the searcher is looking for a specific brand, place, or directory. Templates: "X near me", "X reviews", "X website", "X directory".',
|
|
25489
|
+
' 4. Comparative \u2014 the searcher is weighing named alternatives head-to-head. Templates: "X vs Y", "X or Y for Z", "alternatives to X".',
|
|
25490
|
+
' 5. Transactional \u2014 the searcher is ready to act on a purchase or booking. Templates: "book X", "X pricing", "X discount code", "buy X online".',
|
|
25220
25491
|
"",
|
|
25221
|
-
|
|
25492
|
+
`Generate EXACTLY ${QUERIES_PER_INTENT_BUCKET} queries per bucket \u2014 ${QUERIES_PER_INTENT_BUCKET * 5} total. Return ONE query per line. Plain text only \u2014 no numbering, bullets, quotes, bucket labels, or commentary.`
|
|
25222
25493
|
].join("\n");
|
|
25223
25494
|
}
|
|
25224
25495
|
function parseQueryLines(text, max) {
|
|
@@ -25253,33 +25524,40 @@ function writeDiscoveryInsight(db, input) {
|
|
|
25253
25524
|
aspirational: buckets.aspirational,
|
|
25254
25525
|
totalProbes
|
|
25255
25526
|
});
|
|
25256
|
-
db.
|
|
25257
|
-
|
|
25258
|
-
|
|
25259
|
-
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
|
|
25263
|
-
|
|
25264
|
-
|
|
25265
|
-
|
|
25266
|
-
|
|
25267
|
-
|
|
25268
|
-
|
|
25269
|
-
|
|
25270
|
-
|
|
25271
|
-
|
|
25272
|
-
|
|
25273
|
-
|
|
25274
|
-
|
|
25275
|
-
|
|
25276
|
-
|
|
25277
|
-
|
|
25278
|
-
|
|
25279
|
-
|
|
25280
|
-
|
|
25281
|
-
|
|
25282
|
-
|
|
25527
|
+
db.transaction((tx) => {
|
|
25528
|
+
tx.update(insights).set({ dismissed: true }).where(and23(
|
|
25529
|
+
eq33(insights.projectId, input.projectId),
|
|
25530
|
+
eq33(insights.type, "discovery.basket-divergence"),
|
|
25531
|
+
eq33(insights.dismissed, false)
|
|
25532
|
+
)).run();
|
|
25533
|
+
tx.insert(insights).values({
|
|
25534
|
+
id: crypto30.randomUUID(),
|
|
25535
|
+
projectId: input.projectId,
|
|
25536
|
+
runId: input.runId,
|
|
25537
|
+
type: "discovery.basket-divergence",
|
|
25538
|
+
severity,
|
|
25539
|
+
title,
|
|
25540
|
+
// query/provider fields don't fit the visibility-snapshot model for
|
|
25541
|
+
// a session-level insight. Use the session marker so the
|
|
25542
|
+
// (query, provider) index stays distinct across sessions; PR 5 will
|
|
25543
|
+
// formalize a session-scoped insight subtype.
|
|
25544
|
+
query: `discovery:${input.sessionId}`,
|
|
25545
|
+
provider: input.seedProvider,
|
|
25546
|
+
recommendation: JSON.stringify({
|
|
25547
|
+
action: "review-discovered-basket",
|
|
25548
|
+
summary: `Run \`canonry discover show ${input.sessionId} --format json\` to inspect the per-query breakdown, then \`canonry discover promote <project> ${input.sessionId}\` to merge cited + aspirational findings into the project.`,
|
|
25549
|
+
bucketCounts: buckets,
|
|
25550
|
+
topCompetitors
|
|
25551
|
+
}),
|
|
25552
|
+
cause: JSON.stringify({
|
|
25553
|
+
sessionId: input.sessionId,
|
|
25554
|
+
totalProbes,
|
|
25555
|
+
seedProvider: input.seedProvider
|
|
25556
|
+
}),
|
|
25557
|
+
dismissed: false,
|
|
25558
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
25559
|
+
}).run();
|
|
25560
|
+
});
|
|
25283
25561
|
}
|
|
25284
25562
|
function buildDiscoveryInsightTitle(input) {
|
|
25285
25563
|
const parts = [];
|
|
@@ -25290,6 +25568,512 @@ function buildDiscoveryInsightTitle(input) {
|
|
|
25290
25568
|
return parts.join(" \u2022 ");
|
|
25291
25569
|
}
|
|
25292
25570
|
|
|
25571
|
+
// src/commands/backfill.ts
|
|
25572
|
+
import { and as and24, eq as eq34, inArray as inArray10 } from "drizzle-orm";
|
|
25573
|
+
var SNAPSHOT_BATCH_SIZE = 500;
|
|
25574
|
+
async function backfillAnswerVisibilityCommand(opts) {
|
|
25575
|
+
const config = loadConfig();
|
|
25576
|
+
const db = createClient(config.database);
|
|
25577
|
+
migrate(db);
|
|
25578
|
+
const projectFilter = opts?.project?.trim();
|
|
25579
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq34(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
25580
|
+
let examined = 0;
|
|
25581
|
+
let updated = 0;
|
|
25582
|
+
let mentioned = 0;
|
|
25583
|
+
let reparsed = 0;
|
|
25584
|
+
let providerErrors = 0;
|
|
25585
|
+
if (scopedProjects.length > 0) {
|
|
25586
|
+
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and24(
|
|
25587
|
+
eq34(runs.kind, RunKinds["answer-visibility"]),
|
|
25588
|
+
inArray10(runs.projectId, scopedProjects.map((project) => project.id))
|
|
25589
|
+
)).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq34(runs.kind, RunKinds["answer-visibility"])).all();
|
|
25590
|
+
const runIdsByProject = /* @__PURE__ */ new Map();
|
|
25591
|
+
for (const run of runRows) {
|
|
25592
|
+
const existing = runIdsByProject.get(run.projectId);
|
|
25593
|
+
if (existing) existing.push(run.id);
|
|
25594
|
+
else runIdsByProject.set(run.projectId, [run.id]);
|
|
25595
|
+
}
|
|
25596
|
+
for (const project of scopedProjects) {
|
|
25597
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, project.id)).all().map((row) => row.domain);
|
|
25598
|
+
const runIds = runIdsByProject.get(project.id) ?? [];
|
|
25599
|
+
if (runIds.length === 0) continue;
|
|
25600
|
+
const projectDomains = effectiveDomains({
|
|
25601
|
+
canonicalDomain: project.canonicalDomain,
|
|
25602
|
+
ownedDomains: parseJsonColumn(project.ownedDomains, [])
|
|
25603
|
+
});
|
|
25604
|
+
const projectBrandNames = effectiveBrandNames({
|
|
25605
|
+
displayName: project.displayName,
|
|
25606
|
+
aliases: parseJsonColumn(project.aliases, [])
|
|
25607
|
+
});
|
|
25608
|
+
for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
|
|
25609
|
+
const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
|
|
25610
|
+
const snapshotRows = db.select({
|
|
25611
|
+
id: querySnapshots.id,
|
|
25612
|
+
provider: querySnapshots.provider,
|
|
25613
|
+
citationState: querySnapshots.citationState,
|
|
25614
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
25615
|
+
answerText: querySnapshots.answerText,
|
|
25616
|
+
citedDomains: querySnapshots.citedDomains,
|
|
25617
|
+
competitorOverlap: querySnapshots.competitorOverlap,
|
|
25618
|
+
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
25619
|
+
rawResponse: querySnapshots.rawResponse
|
|
25620
|
+
}).from(querySnapshots).where(inArray10(querySnapshots.runId, batchRunIds)).all();
|
|
25621
|
+
const pendingUpdates = [];
|
|
25622
|
+
for (const snapshot of snapshotRows) {
|
|
25623
|
+
examined++;
|
|
25624
|
+
const reparsedResult = reparseProviderSnapshot(snapshot.provider, snapshot.rawResponse);
|
|
25625
|
+
if (reparsedResult) reparsed++;
|
|
25626
|
+
if (reparsedResult?.providerError) providerErrors++;
|
|
25627
|
+
const answerText = reparsedResult?.answerText ?? snapshot.answerText ?? "";
|
|
25628
|
+
const nextValue = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
|
|
25629
|
+
if (nextValue) mentioned++;
|
|
25630
|
+
const nextPatch = {};
|
|
25631
|
+
if (snapshot.answerMentioned !== nextValue) {
|
|
25632
|
+
nextPatch.answerMentioned = nextValue;
|
|
25633
|
+
}
|
|
25634
|
+
if ((snapshot.answerText ?? "") !== answerText) {
|
|
25635
|
+
nextPatch.answerText = answerText;
|
|
25636
|
+
}
|
|
25637
|
+
if (reparsedResult) {
|
|
25638
|
+
const normalized = {
|
|
25639
|
+
provider: snapshot.provider,
|
|
25640
|
+
answerText,
|
|
25641
|
+
citedDomains: reparsedResult.citedDomains,
|
|
25642
|
+
groundingSources: reparsedResult.groundingSources,
|
|
25643
|
+
searchQueries: reparsedResult.searchQueries
|
|
25644
|
+
};
|
|
25645
|
+
const nextCitationState = determineCitationState(normalized, projectDomains);
|
|
25646
|
+
const nextCitedDomains = JSON.stringify(reparsedResult.citedDomains);
|
|
25647
|
+
const nextCompetitorOverlap = JSON.stringify(
|
|
25648
|
+
computeCompetitorOverlap(normalized, competitorDomains)
|
|
25649
|
+
);
|
|
25650
|
+
const nextRecommendedCompetitors = JSON.stringify(
|
|
25651
|
+
extractRecommendedCompetitors(
|
|
25652
|
+
normalized.answerText,
|
|
25653
|
+
projectDomains,
|
|
25654
|
+
normalized.citedDomains,
|
|
25655
|
+
competitorDomains,
|
|
25656
|
+
projectBrandNames
|
|
25657
|
+
)
|
|
25658
|
+
);
|
|
25659
|
+
const nextRawResponse = stringifyStoredSnapshotEnvelope(
|
|
25660
|
+
snapshot.rawResponse,
|
|
25661
|
+
reparsedResult
|
|
25662
|
+
);
|
|
25663
|
+
if (snapshot.citationState !== nextCitationState) {
|
|
25664
|
+
nextPatch.citationState = nextCitationState;
|
|
25665
|
+
}
|
|
25666
|
+
if (snapshot.citedDomains !== nextCitedDomains) {
|
|
25667
|
+
nextPatch.citedDomains = nextCitedDomains;
|
|
25668
|
+
}
|
|
25669
|
+
if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
|
|
25670
|
+
nextPatch.competitorOverlap = nextCompetitorOverlap;
|
|
25671
|
+
}
|
|
25672
|
+
if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
|
|
25673
|
+
nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
|
|
25674
|
+
}
|
|
25675
|
+
if (snapshot.rawResponse !== nextRawResponse) {
|
|
25676
|
+
nextPatch.rawResponse = nextRawResponse;
|
|
25677
|
+
}
|
|
25678
|
+
}
|
|
25679
|
+
if (Object.keys(nextPatch).length > 0) {
|
|
25680
|
+
pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
|
|
25681
|
+
}
|
|
25682
|
+
}
|
|
25683
|
+
if (pendingUpdates.length > 0) {
|
|
25684
|
+
db.transaction((tx) => {
|
|
25685
|
+
for (const update of pendingUpdates) {
|
|
25686
|
+
tx.update(querySnapshots).set(update.patch).where(eq34(querySnapshots.id, update.id)).run();
|
|
25687
|
+
}
|
|
25688
|
+
});
|
|
25689
|
+
updated += pendingUpdates.length;
|
|
25690
|
+
}
|
|
25691
|
+
}
|
|
25692
|
+
}
|
|
25693
|
+
}
|
|
25694
|
+
const result = {
|
|
25695
|
+
project: projectFilter ?? null,
|
|
25696
|
+
projects: scopedProjects.length,
|
|
25697
|
+
examined,
|
|
25698
|
+
updated,
|
|
25699
|
+
mentioned,
|
|
25700
|
+
reparsed,
|
|
25701
|
+
providerErrors
|
|
25702
|
+
};
|
|
25703
|
+
if (opts?.format === "json") {
|
|
25704
|
+
console.log(JSON.stringify(result, null, 2));
|
|
25705
|
+
return;
|
|
25706
|
+
}
|
|
25707
|
+
console.log("Answer visibility backfill complete.\n");
|
|
25708
|
+
if (projectFilter) {
|
|
25709
|
+
console.log(` Project: ${projectFilter}`);
|
|
25710
|
+
}
|
|
25711
|
+
console.log(` Projects: ${scopedProjects.length}`);
|
|
25712
|
+
console.log(` Examined: ${examined}`);
|
|
25713
|
+
console.log(` Updated: ${updated}`);
|
|
25714
|
+
console.log(` Mentioned: ${mentioned}`);
|
|
25715
|
+
console.log(` Reparsed: ${reparsed}`);
|
|
25716
|
+
console.log(` Errors: ${providerErrors}`);
|
|
25717
|
+
}
|
|
25718
|
+
function backfillNormalizedPaths(db, opts) {
|
|
25719
|
+
const baseConditions = [];
|
|
25720
|
+
if (opts?.projectId) {
|
|
25721
|
+
baseConditions.push(eq34(gaTrafficSnapshots.projectId, opts.projectId));
|
|
25722
|
+
}
|
|
25723
|
+
const rows = db.select({
|
|
25724
|
+
id: gaTrafficSnapshots.id,
|
|
25725
|
+
landingPage: gaTrafficSnapshots.landingPage,
|
|
25726
|
+
landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
|
|
25727
|
+
}).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and24(...baseConditions) : void 0).all();
|
|
25728
|
+
let updated = 0;
|
|
25729
|
+
let unchanged = 0;
|
|
25730
|
+
if (rows.length > 0) {
|
|
25731
|
+
db.transaction((tx) => {
|
|
25732
|
+
for (const row of rows) {
|
|
25733
|
+
const next = normalizeUrlPath(row.landingPage);
|
|
25734
|
+
if (next === null) {
|
|
25735
|
+
unchanged++;
|
|
25736
|
+
continue;
|
|
25737
|
+
}
|
|
25738
|
+
if (row.landingPageNormalized === next) {
|
|
25739
|
+
unchanged++;
|
|
25740
|
+
continue;
|
|
25741
|
+
}
|
|
25742
|
+
tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq34(gaTrafficSnapshots.id, row.id)).run();
|
|
25743
|
+
updated++;
|
|
25744
|
+
}
|
|
25745
|
+
});
|
|
25746
|
+
}
|
|
25747
|
+
return { examined: rows.length, updated, unchanged };
|
|
25748
|
+
}
|
|
25749
|
+
async function backfillNormalizedPathsCommand(opts) {
|
|
25750
|
+
const config = loadConfig();
|
|
25751
|
+
const db = createClient(config.database);
|
|
25752
|
+
migrate(db);
|
|
25753
|
+
const projectFilter = opts?.project?.trim();
|
|
25754
|
+
let projectId;
|
|
25755
|
+
if (projectFilter) {
|
|
25756
|
+
const project = db.select({ id: projects.id }).from(projects).where(eq34(projects.name, projectFilter)).get();
|
|
25757
|
+
if (!project) {
|
|
25758
|
+
const result2 = {
|
|
25759
|
+
project: projectFilter,
|
|
25760
|
+
examined: 0,
|
|
25761
|
+
updated: 0,
|
|
25762
|
+
unchanged: 0
|
|
25763
|
+
};
|
|
25764
|
+
if (opts?.format === "json") {
|
|
25765
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
25766
|
+
return;
|
|
25767
|
+
}
|
|
25768
|
+
console.log(`Backfill normalized-paths: project "${projectFilter}" not found.`);
|
|
25769
|
+
return;
|
|
25770
|
+
}
|
|
25771
|
+
projectId = project.id;
|
|
25772
|
+
}
|
|
25773
|
+
const { examined, updated, unchanged } = backfillNormalizedPaths(db, { projectId });
|
|
25774
|
+
const result = {
|
|
25775
|
+
project: projectFilter ?? null,
|
|
25776
|
+
examined,
|
|
25777
|
+
updated,
|
|
25778
|
+
unchanged
|
|
25779
|
+
};
|
|
25780
|
+
if (opts?.format === "json") {
|
|
25781
|
+
console.log(JSON.stringify(result, null, 2));
|
|
25782
|
+
return;
|
|
25783
|
+
}
|
|
25784
|
+
console.log("Normalized-path backfill complete.\n");
|
|
25785
|
+
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
25786
|
+
console.log(` Examined: ${examined}`);
|
|
25787
|
+
console.log(` Updated: ${updated}`);
|
|
25788
|
+
console.log(` Unchanged: ${unchanged}`);
|
|
25789
|
+
}
|
|
25790
|
+
function backfillAiReferralPaths(db, opts) {
|
|
25791
|
+
const baseConditions = [];
|
|
25792
|
+
if (opts?.projectId) {
|
|
25793
|
+
baseConditions.push(eq34(gaAiReferrals.projectId, opts.projectId));
|
|
25794
|
+
}
|
|
25795
|
+
const rows = db.select({
|
|
25796
|
+
id: gaAiReferrals.id,
|
|
25797
|
+
landingPage: gaAiReferrals.landingPage,
|
|
25798
|
+
landingPageNormalized: gaAiReferrals.landingPageNormalized
|
|
25799
|
+
}).from(gaAiReferrals).where(baseConditions.length > 0 ? and24(...baseConditions) : void 0).all();
|
|
25800
|
+
let updated = 0;
|
|
25801
|
+
let unchanged = 0;
|
|
25802
|
+
if (rows.length > 0) {
|
|
25803
|
+
db.transaction((tx) => {
|
|
25804
|
+
for (const row of rows) {
|
|
25805
|
+
const next = normalizeUrlPath(row.landingPage);
|
|
25806
|
+
if (next === null) {
|
|
25807
|
+
unchanged++;
|
|
25808
|
+
continue;
|
|
25809
|
+
}
|
|
25810
|
+
if (row.landingPageNormalized === next) {
|
|
25811
|
+
unchanged++;
|
|
25812
|
+
continue;
|
|
25813
|
+
}
|
|
25814
|
+
tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq34(gaAiReferrals.id, row.id)).run();
|
|
25815
|
+
updated++;
|
|
25816
|
+
}
|
|
25817
|
+
});
|
|
25818
|
+
}
|
|
25819
|
+
return { examined: rows.length, updated, unchanged };
|
|
25820
|
+
}
|
|
25821
|
+
async function backfillAiReferralPathsCommand(opts) {
|
|
25822
|
+
const config = loadConfig();
|
|
25823
|
+
const db = createClient(config.database);
|
|
25824
|
+
migrate(db);
|
|
25825
|
+
const projectFilter = opts?.project?.trim();
|
|
25826
|
+
let projectId;
|
|
25827
|
+
if (projectFilter) {
|
|
25828
|
+
const project = db.select({ id: projects.id }).from(projects).where(eq34(projects.name, projectFilter)).get();
|
|
25829
|
+
if (!project) {
|
|
25830
|
+
const result2 = {
|
|
25831
|
+
project: projectFilter,
|
|
25832
|
+
examined: 0,
|
|
25833
|
+
updated: 0,
|
|
25834
|
+
unchanged: 0
|
|
25835
|
+
};
|
|
25836
|
+
if (opts?.format === "json") {
|
|
25837
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
25838
|
+
return;
|
|
25839
|
+
}
|
|
25840
|
+
console.log(`Backfill ai-referral-paths: project "${projectFilter}" not found.`);
|
|
25841
|
+
return;
|
|
25842
|
+
}
|
|
25843
|
+
projectId = project.id;
|
|
25844
|
+
}
|
|
25845
|
+
const { examined, updated, unchanged } = backfillAiReferralPaths(db, { projectId });
|
|
25846
|
+
const result = {
|
|
25847
|
+
project: projectFilter ?? null,
|
|
25848
|
+
examined,
|
|
25849
|
+
updated,
|
|
25850
|
+
unchanged
|
|
25851
|
+
};
|
|
25852
|
+
if (opts?.format === "json") {
|
|
25853
|
+
console.log(JSON.stringify(result, null, 2));
|
|
25854
|
+
return;
|
|
25855
|
+
}
|
|
25856
|
+
console.log("AI referral landing-page backfill complete.\n");
|
|
25857
|
+
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
25858
|
+
console.log(` Examined: ${examined}`);
|
|
25859
|
+
console.log(` Updated: ${updated}`);
|
|
25860
|
+
console.log(` Unchanged: ${unchanged}`);
|
|
25861
|
+
}
|
|
25862
|
+
function backfillProjectAnswerMentions(db, projectId) {
|
|
25863
|
+
const project = db.select().from(projects).where(eq34(projects.id, projectId)).get();
|
|
25864
|
+
if (!project) return { examined: 0, updated: 0, mentioned: 0 };
|
|
25865
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, projectId)).all().map((row) => row.domain);
|
|
25866
|
+
const runRows = db.select({ id: runs.id }).from(runs).where(and24(eq34(runs.kind, RunKinds["answer-visibility"]), eq34(runs.projectId, projectId))).all();
|
|
25867
|
+
const runIds = runRows.map((r) => r.id);
|
|
25868
|
+
let examined = 0;
|
|
25869
|
+
let updated = 0;
|
|
25870
|
+
let mentioned = 0;
|
|
25871
|
+
if (runIds.length === 0) return { examined, updated, mentioned };
|
|
25872
|
+
const projectDomains = effectiveDomains({
|
|
25873
|
+
canonicalDomain: project.canonicalDomain,
|
|
25874
|
+
ownedDomains: parseJsonColumn(project.ownedDomains, [])
|
|
25875
|
+
});
|
|
25876
|
+
const projectBrandNames = effectiveBrandNames({
|
|
25877
|
+
displayName: project.displayName,
|
|
25878
|
+
aliases: parseJsonColumn(project.aliases, [])
|
|
25879
|
+
});
|
|
25880
|
+
for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
|
|
25881
|
+
const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
|
|
25882
|
+
const snapshotRows = db.select({
|
|
25883
|
+
id: querySnapshots.id,
|
|
25884
|
+
provider: querySnapshots.provider,
|
|
25885
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
25886
|
+
answerText: querySnapshots.answerText,
|
|
25887
|
+
citedDomains: querySnapshots.citedDomains,
|
|
25888
|
+
competitorOverlap: querySnapshots.competitorOverlap,
|
|
25889
|
+
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
25890
|
+
rawResponse: querySnapshots.rawResponse
|
|
25891
|
+
}).from(querySnapshots).where(inArray10(querySnapshots.runId, batchRunIds)).all();
|
|
25892
|
+
const pendingUpdates = [];
|
|
25893
|
+
for (const snapshot of snapshotRows) {
|
|
25894
|
+
examined++;
|
|
25895
|
+
const answerText = snapshot.answerText ?? "";
|
|
25896
|
+
const nextAnswerMentioned = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
|
|
25897
|
+
if (nextAnswerMentioned) mentioned++;
|
|
25898
|
+
const citedDomains = parseJsonColumn(snapshot.citedDomains, []);
|
|
25899
|
+
const groundingSources = readStoredGroundingSources(snapshot.rawResponse);
|
|
25900
|
+
const normalized = {
|
|
25901
|
+
provider: snapshot.provider,
|
|
25902
|
+
answerText,
|
|
25903
|
+
citedDomains,
|
|
25904
|
+
groundingSources,
|
|
25905
|
+
searchQueries: []
|
|
25906
|
+
};
|
|
25907
|
+
const nextCompetitorOverlap = JSON.stringify(
|
|
25908
|
+
computeCompetitorOverlap(normalized, competitorDomains)
|
|
25909
|
+
);
|
|
25910
|
+
const nextRecommendedCompetitors = JSON.stringify(
|
|
25911
|
+
extractRecommendedCompetitors(
|
|
25912
|
+
answerText,
|
|
25913
|
+
projectDomains,
|
|
25914
|
+
citedDomains,
|
|
25915
|
+
competitorDomains,
|
|
25916
|
+
projectBrandNames
|
|
25917
|
+
)
|
|
25918
|
+
);
|
|
25919
|
+
const nextPatch = {};
|
|
25920
|
+
if (snapshot.answerMentioned !== nextAnswerMentioned) {
|
|
25921
|
+
nextPatch.answerMentioned = nextAnswerMentioned;
|
|
25922
|
+
}
|
|
25923
|
+
if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
|
|
25924
|
+
nextPatch.competitorOverlap = nextCompetitorOverlap;
|
|
25925
|
+
}
|
|
25926
|
+
if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
|
|
25927
|
+
nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
|
|
25928
|
+
}
|
|
25929
|
+
if (Object.keys(nextPatch).length > 0) {
|
|
25930
|
+
pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
|
|
25931
|
+
}
|
|
25932
|
+
}
|
|
25933
|
+
if (pendingUpdates.length > 0) {
|
|
25934
|
+
db.transaction((tx) => {
|
|
25935
|
+
for (const update of pendingUpdates) {
|
|
25936
|
+
tx.update(querySnapshots).set(update.patch).where(eq34(querySnapshots.id, update.id)).run();
|
|
25937
|
+
}
|
|
25938
|
+
});
|
|
25939
|
+
updated += pendingUpdates.length;
|
|
25940
|
+
}
|
|
25941
|
+
}
|
|
25942
|
+
return { examined, updated, mentioned };
|
|
25943
|
+
}
|
|
25944
|
+
async function backfillAnswerMentionsCommand(opts) {
|
|
25945
|
+
const config = loadConfig();
|
|
25946
|
+
const db = createClient(config.database);
|
|
25947
|
+
migrate(db);
|
|
25948
|
+
const projectFilter = opts?.project?.trim();
|
|
25949
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq34(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
25950
|
+
let examined = 0;
|
|
25951
|
+
let updated = 0;
|
|
25952
|
+
let mentioned = 0;
|
|
25953
|
+
for (const project of scopedProjects) {
|
|
25954
|
+
const result2 = backfillProjectAnswerMentions(db, project.id);
|
|
25955
|
+
examined += result2.examined;
|
|
25956
|
+
updated += result2.updated;
|
|
25957
|
+
mentioned += result2.mentioned;
|
|
25958
|
+
}
|
|
25959
|
+
const result = {
|
|
25960
|
+
project: projectFilter ?? null,
|
|
25961
|
+
projects: scopedProjects.length,
|
|
25962
|
+
examined,
|
|
25963
|
+
updated,
|
|
25964
|
+
mentioned
|
|
25965
|
+
};
|
|
25966
|
+
if (opts?.format === "json") {
|
|
25967
|
+
console.log(JSON.stringify(result, null, 2));
|
|
25968
|
+
return;
|
|
25969
|
+
}
|
|
25970
|
+
console.log("Answer mentions backfill complete.\n");
|
|
25971
|
+
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
25972
|
+
console.log(` Projects: ${scopedProjects.length}`);
|
|
25973
|
+
console.log(` Examined: ${examined}`);
|
|
25974
|
+
console.log(` Updated: ${updated}`);
|
|
25975
|
+
console.log(` Mentioned: ${mentioned}`);
|
|
25976
|
+
}
|
|
25977
|
+
function readStoredGroundingSources(rawResponse) {
|
|
25978
|
+
const envelope = parseJsonColumn(rawResponse, {});
|
|
25979
|
+
const sources = envelope.groundingSources;
|
|
25980
|
+
if (!Array.isArray(sources)) return [];
|
|
25981
|
+
const result = [];
|
|
25982
|
+
for (const source of sources) {
|
|
25983
|
+
if (source && typeof source === "object") {
|
|
25984
|
+
const uri = source.uri;
|
|
25985
|
+
const title = source.title;
|
|
25986
|
+
if (typeof uri === "string") {
|
|
25987
|
+
result.push({ uri, title: typeof title === "string" ? title : "" });
|
|
25988
|
+
}
|
|
25989
|
+
}
|
|
25990
|
+
}
|
|
25991
|
+
return result;
|
|
25992
|
+
}
|
|
25993
|
+
async function backfillInsightsCommand(project, opts) {
|
|
25994
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-WAJOEOJV.js");
|
|
25995
|
+
const config = loadConfig();
|
|
25996
|
+
const db = createClient(config.database);
|
|
25997
|
+
migrate(db);
|
|
25998
|
+
const service = new IntelligenceService2(db);
|
|
25999
|
+
const isJson = opts?.format === "json";
|
|
26000
|
+
if (!isJson) {
|
|
26001
|
+
process.stderr.write(`Backfilling insights for "${project}"...
|
|
26002
|
+
`);
|
|
26003
|
+
}
|
|
26004
|
+
const result = service.backfill(project, {
|
|
26005
|
+
fromRunId: opts?.fromRun,
|
|
26006
|
+
toRunId: opts?.toRun
|
|
26007
|
+
}, (info) => {
|
|
26008
|
+
if (!isJson) {
|
|
26009
|
+
process.stderr.write(` [${info.index}/${info.total}] ${info.runId} \u2014 ${info.insights} insights
|
|
26010
|
+
`);
|
|
26011
|
+
}
|
|
26012
|
+
});
|
|
26013
|
+
const output = {
|
|
26014
|
+
project,
|
|
26015
|
+
processed: result.processed,
|
|
26016
|
+
skipped: result.skipped,
|
|
26017
|
+
totalInsights: result.totalInsights
|
|
26018
|
+
};
|
|
26019
|
+
if (isJson) {
|
|
26020
|
+
console.log(JSON.stringify(output, null, 2));
|
|
26021
|
+
return;
|
|
26022
|
+
}
|
|
26023
|
+
console.log(`
|
|
26024
|
+
Backfill complete.`);
|
|
26025
|
+
console.log(` Processed: ${result.processed}`);
|
|
26026
|
+
console.log(` Skipped: ${result.skipped}`);
|
|
26027
|
+
console.log(` Insights: ${result.totalInsights}`);
|
|
26028
|
+
}
|
|
26029
|
+
function reparseProviderSnapshot(provider, rawResponse) {
|
|
26030
|
+
const envelope = parseJsonColumn(rawResponse, {});
|
|
26031
|
+
const apiResponse = resolveStoredApiResponse(envelope);
|
|
26032
|
+
if (!apiResponse) return null;
|
|
26033
|
+
switch (provider) {
|
|
26034
|
+
case ProviderNames.openai:
|
|
26035
|
+
return reparseStoredResult2(apiResponse);
|
|
26036
|
+
case ProviderNames.claude:
|
|
26037
|
+
return reparseStoredResult3(apiResponse);
|
|
26038
|
+
case ProviderNames.gemini:
|
|
26039
|
+
return reparseStoredResult(apiResponse);
|
|
26040
|
+
case ProviderNames.perplexity:
|
|
26041
|
+
return reparseStoredResult4(apiResponse);
|
|
26042
|
+
default:
|
|
26043
|
+
return null;
|
|
26044
|
+
}
|
|
26045
|
+
}
|
|
26046
|
+
function resolveStoredApiResponse(parsed) {
|
|
26047
|
+
const nested = parsed.apiResponse;
|
|
26048
|
+
if (nested !== null && typeof nested === "object" && !Array.isArray(nested)) {
|
|
26049
|
+
return nested;
|
|
26050
|
+
}
|
|
26051
|
+
if (looksLikeProviderApiResponse(parsed)) {
|
|
26052
|
+
return parsed;
|
|
26053
|
+
}
|
|
26054
|
+
return null;
|
|
26055
|
+
}
|
|
26056
|
+
function looksLikeProviderApiResponse(value) {
|
|
26057
|
+
return Array.isArray(value.output) || Array.isArray(value.content) || Array.isArray(value.candidates) || Array.isArray(value.choices);
|
|
26058
|
+
}
|
|
26059
|
+
function stringifyStoredSnapshotEnvelope(rawResponse, reparsed) {
|
|
26060
|
+
const parsed = parseJsonColumn(rawResponse, {});
|
|
26061
|
+
const apiResponse = resolveStoredApiResponse(parsed);
|
|
26062
|
+
const envelope = apiResponse === parsed ? {} : { ...parsed };
|
|
26063
|
+
delete envelope.answerText;
|
|
26064
|
+
delete envelope.citedDomains;
|
|
26065
|
+
delete envelope.competitorOverlap;
|
|
26066
|
+
delete envelope.recommendedCompetitors;
|
|
26067
|
+
delete envelope.providerError;
|
|
26068
|
+
return JSON.stringify({
|
|
26069
|
+
...envelope,
|
|
26070
|
+
groundingSources: reparsed.groundingSources,
|
|
26071
|
+
searchQueries: reparsed.searchQueries,
|
|
26072
|
+
...reparsed.providerError ? { providerError: reparsed.providerError } : {},
|
|
26073
|
+
...apiResponse ? { apiResponse } : {}
|
|
26074
|
+
});
|
|
26075
|
+
}
|
|
26076
|
+
|
|
25293
26077
|
// src/provider-registry.ts
|
|
25294
26078
|
var ProviderRegistry = class {
|
|
25295
26079
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -25343,7 +26127,7 @@ var ProviderRegistry = class {
|
|
|
25343
26127
|
|
|
25344
26128
|
// src/scheduler.ts
|
|
25345
26129
|
import cron from "node-cron";
|
|
25346
|
-
import { and as
|
|
26130
|
+
import { and as and25, eq as eq35 } from "drizzle-orm";
|
|
25347
26131
|
var log9 = createLogger("Scheduler");
|
|
25348
26132
|
function taskKey(projectId, kind) {
|
|
25349
26133
|
return `${projectId}::${kind}`;
|
|
@@ -25358,7 +26142,7 @@ var Scheduler = class {
|
|
|
25358
26142
|
}
|
|
25359
26143
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
25360
26144
|
start() {
|
|
25361
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
26145
|
+
const allSchedules = this.db.select().from(schedules).where(eq35(schedules.enabled, 1)).all();
|
|
25362
26146
|
for (const schedule of allSchedules) {
|
|
25363
26147
|
const missedRunAt = schedule.nextRunAt;
|
|
25364
26148
|
this.registerCronTask(schedule);
|
|
@@ -25388,7 +26172,7 @@ var Scheduler = class {
|
|
|
25388
26172
|
this.stopTask(key, existing, "Stopped");
|
|
25389
26173
|
this.tasks.delete(key);
|
|
25390
26174
|
}
|
|
25391
|
-
const schedule = this.db.select().from(schedules).where(
|
|
26175
|
+
const schedule = this.db.select().from(schedules).where(and25(eq35(schedules.projectId, projectId), eq35(schedules.kind, kind))).get();
|
|
25392
26176
|
if (schedule && schedule.enabled === 1) {
|
|
25393
26177
|
this.registerCronTask(schedule);
|
|
25394
26178
|
}
|
|
@@ -25429,14 +26213,14 @@ var Scheduler = class {
|
|
|
25429
26213
|
this.db.update(schedules).set({
|
|
25430
26214
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
25431
26215
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
25432
|
-
}).where(
|
|
26216
|
+
}).where(eq35(schedules.id, scheduleId)).run();
|
|
25433
26217
|
const label = schedule.preset ?? cronExpr;
|
|
25434
26218
|
log9.info("cron.registered", { projectId, kind, schedule: label, timezone });
|
|
25435
26219
|
}
|
|
25436
26220
|
triggerRun(scheduleId, projectId, kind) {
|
|
25437
26221
|
try {
|
|
25438
26222
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
25439
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
26223
|
+
const currentSchedule = this.db.select().from(schedules).where(eq35(schedules.id, scheduleId)).get();
|
|
25440
26224
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
25441
26225
|
log9.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
|
|
25442
26226
|
this.remove(projectId, kind);
|
|
@@ -25444,7 +26228,7 @@ var Scheduler = class {
|
|
|
25444
26228
|
}
|
|
25445
26229
|
const task = this.tasks.get(taskKey(projectId, kind));
|
|
25446
26230
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
25447
|
-
const project = this.db.select().from(projects).where(
|
|
26231
|
+
const project = this.db.select().from(projects).where(eq35(projects.id, projectId)).get();
|
|
25448
26232
|
if (!project) {
|
|
25449
26233
|
log9.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
|
|
25450
26234
|
this.remove(projectId, kind);
|
|
@@ -25464,7 +26248,7 @@ var Scheduler = class {
|
|
|
25464
26248
|
lastRunAt: now,
|
|
25465
26249
|
nextRunAt,
|
|
25466
26250
|
updatedAt: now
|
|
25467
|
-
}).where(
|
|
26251
|
+
}).where(eq35(schedules.id, currentSchedule.id)).run();
|
|
25468
26252
|
log9.info("traffic-sync.triggered", { projectName: project.name, sourceId });
|
|
25469
26253
|
this.callbacks.onTrafficSyncRequested(project.name, sourceId);
|
|
25470
26254
|
return;
|
|
@@ -25492,7 +26276,7 @@ var Scheduler = class {
|
|
|
25492
26276
|
this.db.update(schedules).set({
|
|
25493
26277
|
nextRunAt,
|
|
25494
26278
|
updatedAt: now
|
|
25495
|
-
}).where(
|
|
26279
|
+
}).where(eq35(schedules.id, currentSchedule.id)).run();
|
|
25496
26280
|
return;
|
|
25497
26281
|
}
|
|
25498
26282
|
const runId = queueResult.runId;
|
|
@@ -25500,7 +26284,7 @@ var Scheduler = class {
|
|
|
25500
26284
|
lastRunAt: now,
|
|
25501
26285
|
nextRunAt,
|
|
25502
26286
|
updatedAt: now
|
|
25503
|
-
}).where(
|
|
26287
|
+
}).where(eq35(schedules.id, currentSchedule.id)).run();
|
|
25504
26288
|
const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
|
|
25505
26289
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
25506
26290
|
log9.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
@@ -25512,7 +26296,7 @@ var Scheduler = class {
|
|
|
25512
26296
|
};
|
|
25513
26297
|
|
|
25514
26298
|
// src/notifier.ts
|
|
25515
|
-
import { eq as
|
|
26299
|
+
import { eq as eq36, desc as desc16, and as and26, inArray as inArray11, or as or5 } from "drizzle-orm";
|
|
25516
26300
|
import crypto31 from "crypto";
|
|
25517
26301
|
var log10 = createLogger("Notifier");
|
|
25518
26302
|
var Notifier = class {
|
|
@@ -25525,18 +26309,18 @@ var Notifier = class {
|
|
|
25525
26309
|
/** Called after a run completes (success, partial, or failed). */
|
|
25526
26310
|
async onRunCompleted(runId, projectId) {
|
|
25527
26311
|
log10.info("run.completed", { runId, projectId });
|
|
25528
|
-
const notifs = this.db.select().from(notifications).where(
|
|
26312
|
+
const notifs = this.db.select().from(notifications).where(eq36(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
25529
26313
|
if (notifs.length === 0) {
|
|
25530
26314
|
log10.info("notifications.none-enabled", { projectId });
|
|
25531
26315
|
return;
|
|
25532
26316
|
}
|
|
25533
26317
|
log10.info("notifications.found", { projectId, count: notifs.length });
|
|
25534
|
-
const run = this.db.select().from(runs).where(
|
|
26318
|
+
const run = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
|
|
25535
26319
|
if (!run) {
|
|
25536
26320
|
log10.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
25537
26321
|
return;
|
|
25538
26322
|
}
|
|
25539
|
-
const project = this.db.select().from(projects).where(
|
|
26323
|
+
const project = this.db.select().from(projects).where(eq36(projects.id, projectId)).get();
|
|
25540
26324
|
if (!project) {
|
|
25541
26325
|
log10.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
25542
26326
|
return;
|
|
@@ -25583,11 +26367,11 @@ var Notifier = class {
|
|
|
25583
26367
|
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
25584
26368
|
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
25585
26369
|
if (insightEvents.length === 0) return;
|
|
25586
|
-
const notifs = this.db.select().from(notifications).where(
|
|
26370
|
+
const notifs = this.db.select().from(notifications).where(eq36(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
25587
26371
|
if (notifs.length === 0) return;
|
|
25588
|
-
const run = this.db.select().from(runs).where(
|
|
26372
|
+
const run = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
|
|
25589
26373
|
if (!run) return;
|
|
25590
|
-
const project = this.db.select().from(projects).where(
|
|
26374
|
+
const project = this.db.select().from(projects).where(eq36(projects.id, projectId)).get();
|
|
25591
26375
|
if (!project) return;
|
|
25592
26376
|
for (const notif of notifs) {
|
|
25593
26377
|
const config = parseJsonColumn(notif.config, { url: "", events: [] });
|
|
@@ -25617,12 +26401,12 @@ var Notifier = class {
|
|
|
25617
26401
|
}
|
|
25618
26402
|
}
|
|
25619
26403
|
computeTransitions(runId, projectId) {
|
|
25620
|
-
const thisRun = this.db.select().from(runs).where(
|
|
26404
|
+
const thisRun = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
|
|
25621
26405
|
if (!thisRun) return [];
|
|
25622
|
-
const groupSiblings = this.db.select().from(runs).where(
|
|
25623
|
-
|
|
25624
|
-
|
|
25625
|
-
|
|
26406
|
+
const groupSiblings = this.db.select().from(runs).where(and26(
|
|
26407
|
+
eq36(runs.projectId, projectId),
|
|
26408
|
+
eq36(runs.kind, thisRun.kind),
|
|
26409
|
+
eq36(runs.createdAt, thisRun.createdAt)
|
|
25626
26410
|
)).all();
|
|
25627
26411
|
const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
|
|
25628
26412
|
if (stillPending) return [];
|
|
@@ -25638,17 +26422,17 @@ var Notifier = class {
|
|
|
25638
26422
|
return candidate.id > best.id ? candidate : best;
|
|
25639
26423
|
});
|
|
25640
26424
|
if (winner.id !== runId) return [];
|
|
25641
|
-
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
26425
|
+
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq36(projects.id, projectId)).get();
|
|
25642
26426
|
const locationCount = Math.max(
|
|
25643
26427
|
1,
|
|
25644
26428
|
parseJsonColumn(projectLocations?.locations ?? null, []).length
|
|
25645
26429
|
);
|
|
25646
26430
|
const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
|
|
25647
26431
|
const recentRuns = this.db.select().from(runs).where(
|
|
25648
|
-
|
|
25649
|
-
|
|
25650
|
-
|
|
25651
|
-
|
|
26432
|
+
and26(
|
|
26433
|
+
eq36(runs.projectId, projectId),
|
|
26434
|
+
eq36(runs.kind, thisRun.kind),
|
|
26435
|
+
or5(eq36(runs.status, "completed"), eq36(runs.status, "partial"))
|
|
25652
26436
|
)
|
|
25653
26437
|
).orderBy(desc16(runs.createdAt), desc16(runs.id)).limit(RECENT_FETCH_LIMIT).all();
|
|
25654
26438
|
const groups = groupRunsByCreatedAt(recentRuns);
|
|
@@ -25665,19 +26449,21 @@ var Notifier = class {
|
|
|
25665
26449
|
provider: querySnapshots.provider,
|
|
25666
26450
|
location: querySnapshots.location,
|
|
25667
26451
|
citationState: querySnapshots.citationState
|
|
25668
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
26452
|
+
}).from(querySnapshots).leftJoin(queries, eq36(querySnapshots.queryId, queries.id)).where(inArray11(querySnapshots.runId, currentRunIds)).all();
|
|
25669
26453
|
const previousSnapshots = this.db.select({
|
|
25670
26454
|
queryId: querySnapshots.queryId,
|
|
25671
26455
|
provider: querySnapshots.provider,
|
|
25672
26456
|
location: querySnapshots.location,
|
|
25673
26457
|
citationState: querySnapshots.citationState
|
|
25674
|
-
}).from(querySnapshots).where(
|
|
26458
|
+
}).from(querySnapshots).where(inArray11(querySnapshots.runId, previousRunIds)).all();
|
|
25675
26459
|
const prevMap = /* @__PURE__ */ new Map();
|
|
25676
26460
|
for (const s of previousSnapshots) {
|
|
26461
|
+
if (s.queryId == null) continue;
|
|
25677
26462
|
prevMap.set(`${s.queryId}:${s.provider}:${s.location ?? ""}`, s.citationState);
|
|
25678
26463
|
}
|
|
25679
26464
|
const transitions = [];
|
|
25680
26465
|
for (const s of currentSnapshots) {
|
|
26466
|
+
if (s.queryId == null) continue;
|
|
25681
26467
|
const key = `${s.queryId}:${s.provider}:${s.location ?? ""}`;
|
|
25682
26468
|
const prevState = prevMap.get(key);
|
|
25683
26469
|
if (prevState && prevState !== s.citationState) {
|
|
@@ -25743,7 +26529,7 @@ var Notifier = class {
|
|
|
25743
26529
|
};
|
|
25744
26530
|
|
|
25745
26531
|
// src/run-coordinator.ts
|
|
25746
|
-
import { eq as
|
|
26532
|
+
import { eq as eq37 } from "drizzle-orm";
|
|
25747
26533
|
var log11 = createLogger("RunCoordinator");
|
|
25748
26534
|
var RunCoordinator = class {
|
|
25749
26535
|
constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
|
|
@@ -25754,7 +26540,7 @@ var RunCoordinator = class {
|
|
|
25754
26540
|
this.onAeroEvent = onAeroEvent;
|
|
25755
26541
|
}
|
|
25756
26542
|
async onRunCompleted(runId, projectId) {
|
|
25757
|
-
const runRow = this.db.select().from(runs).where(
|
|
26543
|
+
const runRow = this.db.select().from(runs).where(eq37(runs.id, runId)).get();
|
|
25758
26544
|
const kind = runRow?.kind ?? RunKinds["answer-visibility"];
|
|
25759
26545
|
let insightCount = 0;
|
|
25760
26546
|
let criticalOrHigh = 0;
|
|
@@ -25809,7 +26595,7 @@ var RunCoordinator = class {
|
|
|
25809
26595
|
* so the Aero queue is never starved of a follow-up.
|
|
25810
26596
|
*/
|
|
25811
26597
|
buildDiscoveryAeroContext(runId, projectId, status, error) {
|
|
25812
|
-
const session = this.db.select().from(discoverySessions).where(
|
|
26598
|
+
const session = this.db.select().from(discoverySessions).where(eq37(discoverySessions.runId, runId)).get();
|
|
25813
26599
|
const competitorMap = session ? parseJsonColumn(session.competitorMap, []) : [];
|
|
25814
26600
|
return {
|
|
25815
26601
|
kind: RunKinds["aeo-discover-probe"],
|
|
@@ -25832,7 +26618,7 @@ var RunCoordinator = class {
|
|
|
25832
26618
|
|
|
25833
26619
|
// src/agent/session-registry.ts
|
|
25834
26620
|
import crypto33 from "crypto";
|
|
25835
|
-
import { eq as
|
|
26621
|
+
import { eq as eq39 } from "drizzle-orm";
|
|
25836
26622
|
|
|
25837
26623
|
// src/agent/session.ts
|
|
25838
26624
|
import fs11 from "fs";
|
|
@@ -26182,7 +26968,7 @@ function resolveSessionProviderAndModel(config, opts) {
|
|
|
26182
26968
|
|
|
26183
26969
|
// src/agent/memory-store.ts
|
|
26184
26970
|
import crypto32 from "crypto";
|
|
26185
|
-
import { and as
|
|
26971
|
+
import { and as and27, desc as desc17, eq as eq38, like as like2, sql as sql13 } from "drizzle-orm";
|
|
26186
26972
|
var COMPACTION_KEY_PREFIX = "compaction:";
|
|
26187
26973
|
var COMPACTION_NOTES_PER_SESSION = 3;
|
|
26188
26974
|
function rowToDto2(row) {
|
|
@@ -26196,7 +26982,7 @@ function rowToDto2(row) {
|
|
|
26196
26982
|
};
|
|
26197
26983
|
}
|
|
26198
26984
|
function listMemoryEntries(db, projectId, opts = {}) {
|
|
26199
|
-
const query = db.select().from(agentMemory).where(
|
|
26985
|
+
const query = db.select().from(agentMemory).where(eq38(agentMemory.projectId, projectId)).orderBy(desc17(agentMemory.updatedAt));
|
|
26200
26986
|
const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
|
|
26201
26987
|
return rows.map(rowToDto2);
|
|
26202
26988
|
}
|
|
@@ -26227,12 +27013,12 @@ function upsertMemoryEntry(db, args) {
|
|
|
26227
27013
|
updatedAt: now
|
|
26228
27014
|
}
|
|
26229
27015
|
}).run();
|
|
26230
|
-
const row = db.select().from(agentMemory).where(
|
|
27016
|
+
const row = db.select().from(agentMemory).where(and27(eq38(agentMemory.projectId, args.projectId), eq38(agentMemory.key, args.key))).get();
|
|
26231
27017
|
if (!row) throw new Error("memory upsert produced no row");
|
|
26232
27018
|
return rowToDto2(row);
|
|
26233
27019
|
}
|
|
26234
27020
|
function deleteMemoryEntry(db, projectId, key) {
|
|
26235
|
-
const result = db.delete(agentMemory).where(
|
|
27021
|
+
const result = db.delete(agentMemory).where(and27(eq38(agentMemory.projectId, projectId), eq38(agentMemory.key, key))).run();
|
|
26236
27022
|
const changes = result.changes ?? 0;
|
|
26237
27023
|
return changes > 0;
|
|
26238
27024
|
}
|
|
@@ -26261,8 +27047,8 @@ function writeCompactionNote(db, args) {
|
|
|
26261
27047
|
}).run();
|
|
26262
27048
|
const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
|
|
26263
27049
|
const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
|
|
26264
|
-
|
|
26265
|
-
|
|
27050
|
+
and27(
|
|
27051
|
+
eq38(agentMemory.projectId, args.projectId),
|
|
26266
27052
|
like2(agentMemory.key, `${sessionPrefix}%`)
|
|
26267
27053
|
)
|
|
26268
27054
|
).orderBy(desc17(agentMemory.updatedAt)).all();
|
|
@@ -26270,7 +27056,7 @@ function writeCompactionNote(db, args) {
|
|
|
26270
27056
|
if (stale.length > 0) {
|
|
26271
27057
|
tx.delete(agentMemory).where(sql13`${agentMemory.id} IN (${sql13.join(stale.map((s) => sql13`${s}`), sql13`, `)})`).run();
|
|
26272
27058
|
}
|
|
26273
|
-
const row = tx.select().from(agentMemory).where(
|
|
27059
|
+
const row = tx.select().from(agentMemory).where(and27(eq38(agentMemory.projectId, args.projectId), eq38(agentMemory.key, key))).get();
|
|
26274
27060
|
if (row) inserted = rowToDto2(row);
|
|
26275
27061
|
});
|
|
26276
27062
|
if (!inserted) throw new Error("compaction note write produced no row");
|
|
@@ -26452,7 +27238,7 @@ var SessionRegistry = class {
|
|
|
26452
27238
|
modelProvider: effectiveProvider,
|
|
26453
27239
|
modelId: effectiveModelId,
|
|
26454
27240
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
26455
|
-
}).where(
|
|
27241
|
+
}).where(eq39(agentSessions.projectId, projectId)).run();
|
|
26456
27242
|
}
|
|
26457
27243
|
const agent2 = createAeroSession({
|
|
26458
27244
|
projectName,
|
|
@@ -26666,7 +27452,7 @@ ${lines.join("\n")}
|
|
|
26666
27452
|
modelProvider: nextProvider,
|
|
26667
27453
|
modelId: nextModelId,
|
|
26668
27454
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
26669
|
-
}).where(
|
|
27455
|
+
}).where(eq39(agentSessions.projectId, projectId)).run();
|
|
26670
27456
|
}
|
|
26671
27457
|
/** Persist a session's transcript back to the DB. Call after any run settles. */
|
|
26672
27458
|
save(projectName) {
|
|
@@ -26828,11 +27614,11 @@ ${lines.join("\n")}
|
|
|
26828
27614
|
return id;
|
|
26829
27615
|
}
|
|
26830
27616
|
tryResolveProjectId(projectName) {
|
|
26831
|
-
const row = this.opts.db.select({ id: projects.id }).from(projects).where(
|
|
27617
|
+
const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq39(projects.name, projectName)).get();
|
|
26832
27618
|
return row?.id;
|
|
26833
27619
|
}
|
|
26834
27620
|
loadRow(projectId) {
|
|
26835
|
-
const row = this.opts.db.select().from(agentSessions).where(
|
|
27621
|
+
const row = this.opts.db.select().from(agentSessions).where(eq39(agentSessions.projectId, projectId)).get();
|
|
26836
27622
|
return row ?? null;
|
|
26837
27623
|
}
|
|
26838
27624
|
insertRow(params) {
|
|
@@ -26851,14 +27637,14 @@ ${lines.join("\n")}
|
|
|
26851
27637
|
}
|
|
26852
27638
|
updateRow(projectId, patch) {
|
|
26853
27639
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
26854
|
-
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(
|
|
27640
|
+
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq39(agentSessions.projectId, projectId)).run();
|
|
26855
27641
|
}
|
|
26856
27642
|
};
|
|
26857
27643
|
|
|
26858
27644
|
// src/agent/agent-routes.ts
|
|
26859
|
-
import { eq as
|
|
27645
|
+
import { eq as eq40 } from "drizzle-orm";
|
|
26860
27646
|
function resolveProject2(db, name) {
|
|
26861
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
27647
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq40(projects.name, name)).get();
|
|
26862
27648
|
if (!row) throw notFound("project", name);
|
|
26863
27649
|
return row;
|
|
26864
27650
|
}
|
|
@@ -26867,7 +27653,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
26867
27653
|
"/projects/:name/agent/transcript",
|
|
26868
27654
|
async (request) => {
|
|
26869
27655
|
const project = resolveProject2(opts.db, request.params.name);
|
|
26870
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
27656
|
+
const row = opts.db.select().from(agentSessions).where(eq40(agentSessions.projectId, project.id)).get();
|
|
26871
27657
|
if (!row) {
|
|
26872
27658
|
return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
26873
27659
|
}
|
|
@@ -26891,7 +27677,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
26891
27677
|
async (request) => {
|
|
26892
27678
|
const project = resolveProject2(opts.db, request.params.name);
|
|
26893
27679
|
opts.sessionRegistry.reset(project.name);
|
|
26894
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
27680
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq40(agentSessions.projectId, project.id)).run();
|
|
26895
27681
|
return { status: "reset" };
|
|
26896
27682
|
}
|
|
26897
27683
|
);
|
|
@@ -27009,7 +27795,7 @@ import { runAeoAudit } from "@ainyc/aeo-audit";
|
|
|
27009
27795
|
|
|
27010
27796
|
// src/site-fetch.ts
|
|
27011
27797
|
import https2 from "https";
|
|
27012
|
-
var
|
|
27798
|
+
var FETCH_TIMEOUT_MS2 = 1e4;
|
|
27013
27799
|
var MAX_TEXT_LENGTH = 4e3;
|
|
27014
27800
|
var MAX_BODY_BYTES = 512e3;
|
|
27015
27801
|
var USER_AGENT = "Canonry/1.0 (site-analysis)";
|
|
@@ -27034,7 +27820,7 @@ function fetchWithPinnedAddress(target) {
|
|
|
27034
27820
|
port,
|
|
27035
27821
|
path: path15,
|
|
27036
27822
|
method: "GET",
|
|
27037
|
-
timeout:
|
|
27823
|
+
timeout: FETCH_TIMEOUT_MS2,
|
|
27038
27824
|
servername: target.url.hostname,
|
|
27039
27825
|
// SNI for TLS
|
|
27040
27826
|
headers: {
|
|
@@ -27355,7 +28141,7 @@ var SnapshotService = class {
|
|
|
27355
28141
|
provider: provider.adapter.name,
|
|
27356
28142
|
displayName: provider.adapter.displayName,
|
|
27357
28143
|
model: raw.model,
|
|
27358
|
-
mentioned: determineAnswerMentioned(normalized.answerText, ctx.companyName, answerVisibilityDomains),
|
|
28144
|
+
mentioned: determineAnswerMentioned(normalized.answerText, [ctx.companyName], answerVisibilityDomains),
|
|
27359
28145
|
cited: citesTargetDomain(normalized.citedDomains, normalized.groundingSources, ctx.domain),
|
|
27360
28146
|
describedAccurately: "unknown",
|
|
27361
28147
|
accuracyNotes: null,
|
|
@@ -27722,8 +28508,8 @@ function clipText(value, length) {
|
|
|
27722
28508
|
}
|
|
27723
28509
|
|
|
27724
28510
|
// src/server.ts
|
|
27725
|
-
var
|
|
27726
|
-
var { version:
|
|
28511
|
+
var _require3 = createRequire4(import.meta.url);
|
|
28512
|
+
var { version: PKG_VERSION2 } = _require3("../package.json");
|
|
27727
28513
|
var log14 = createLogger("Server");
|
|
27728
28514
|
var DEFAULT_QUOTA = {
|
|
27729
28515
|
maxConcurrency: 2,
|
|
@@ -27914,7 +28700,7 @@ async function createServer(opts) {
|
|
|
27914
28700
|
intelligenceService,
|
|
27915
28701
|
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
27916
28702
|
async (ctx) => {
|
|
27917
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
28703
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq41(projects.id, ctx.projectId)).get();
|
|
27918
28704
|
if (!project) return;
|
|
27919
28705
|
let content;
|
|
27920
28706
|
if (ctx.kind === RunKinds["aeo-discover-probe"]) {
|
|
@@ -28115,7 +28901,7 @@ async function createServer(opts) {
|
|
|
28115
28901
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
28116
28902
|
if (opts.config.apiKey) {
|
|
28117
28903
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
28118
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
28904
|
+
const existing = opts.db.select().from(apiKeys).where(eq41(apiKeys.keyHash, keyHash)).get();
|
|
28119
28905
|
if (!existing) {
|
|
28120
28906
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
28121
28907
|
opts.db.insert(apiKeys).values({
|
|
@@ -28167,7 +28953,7 @@ async function createServer(opts) {
|
|
|
28167
28953
|
};
|
|
28168
28954
|
const getDefaultApiKey = () => {
|
|
28169
28955
|
if (!opts.config.apiKey) return void 0;
|
|
28170
|
-
return opts.db.select().from(apiKeys).where(
|
|
28956
|
+
return opts.db.select().from(apiKeys).where(eq41(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
28171
28957
|
};
|
|
28172
28958
|
const createPasswordSession = (reply) => {
|
|
28173
28959
|
const key = getDefaultApiKey();
|
|
@@ -28224,12 +29010,12 @@ async function createServer(opts) {
|
|
|
28224
29010
|
return reply.send({ authenticated: true });
|
|
28225
29011
|
}
|
|
28226
29012
|
if (apiKey) {
|
|
28227
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
29013
|
+
const key = opts.db.select().from(apiKeys).where(eq41(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
28228
29014
|
if (!key || key.revokedAt) {
|
|
28229
29015
|
const err2 = authInvalid();
|
|
28230
29016
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
28231
29017
|
}
|
|
28232
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
29018
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq41(apiKeys.id, key.id)).run();
|
|
28233
29019
|
const sessionId = createSession(key.id);
|
|
28234
29020
|
reply.header("set-cookie", serializeSessionCookie({
|
|
28235
29021
|
name: SESSION_COOKIE_NAME,
|
|
@@ -28283,6 +29069,12 @@ async function createServer(opts) {
|
|
|
28283
29069
|
skipAuth: false,
|
|
28284
29070
|
sessionCookieName: SESSION_COOKIE_NAME,
|
|
28285
29071
|
resolveSessionApiKeyId,
|
|
29072
|
+
// Local canonry serve runs on the operator's machine, where pointing a
|
|
29073
|
+
// webhook at localhost (Discord test container, Pipedream-mock dev server,
|
|
29074
|
+
// etc.) is a legitimate workflow. Default to allowing it for the local
|
|
29075
|
+
// installer; cloud deployments inherit the secure default of `false` by
|
|
29076
|
+
// not passing this option. Override with CANONRY_ALLOW_LOOPBACK_WEBHOOKS=0.
|
|
29077
|
+
allowLoopbackWebhooks: process.env.CANONRY_ALLOW_LOOPBACK_WEBHOOKS !== "0",
|
|
28286
29078
|
// Local-only Aero agent routes. Registered here so they inherit api-routes'
|
|
28287
29079
|
// auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
|
|
28288
29080
|
registerAuthenticatedRoutes: async (scope) => {
|
|
@@ -28403,7 +29195,7 @@ async function createServer(opts) {
|
|
|
28403
29195
|
discoverLatestRelease,
|
|
28404
29196
|
openApiInfo: {
|
|
28405
29197
|
title: "Canonry API",
|
|
28406
|
-
version:
|
|
29198
|
+
version: PKG_VERSION2,
|
|
28407
29199
|
includeCanonryLocal: true
|
|
28408
29200
|
},
|
|
28409
29201
|
providerSummary,
|
|
@@ -28550,6 +29342,19 @@ async function createServer(opts) {
|
|
|
28550
29342
|
onProjectDeleted: (projectId) => {
|
|
28551
29343
|
scheduler.removeAllForProject(projectId);
|
|
28552
29344
|
},
|
|
29345
|
+
onAliasesChanged: (projectId, projectName) => {
|
|
29346
|
+
setImmediate(() => {
|
|
29347
|
+
try {
|
|
29348
|
+
const result = backfillProjectAnswerMentions(opts.db, projectId);
|
|
29349
|
+
app.log.info(
|
|
29350
|
+
{ projectId, projectName, ...result },
|
|
29351
|
+
"aliases changed \u2014 recomputed mention fields on historical snapshots"
|
|
29352
|
+
);
|
|
29353
|
+
} catch (err) {
|
|
29354
|
+
app.log.error({ err, projectId, projectName }, "alias-triggered backfill failed");
|
|
29355
|
+
}
|
|
29356
|
+
});
|
|
29357
|
+
},
|
|
28553
29358
|
getTelemetryStatus: () => {
|
|
28554
29359
|
const enabled = isTelemetryEnabled();
|
|
28555
29360
|
return {
|
|
@@ -28683,16 +29488,21 @@ async function createServer(opts) {
|
|
|
28683
29488
|
return reply.status(404).send({ error: "Not found" });
|
|
28684
29489
|
});
|
|
28685
29490
|
}
|
|
28686
|
-
const healthHandler =
|
|
28687
|
-
|
|
28688
|
-
|
|
28689
|
-
|
|
28690
|
-
|
|
28691
|
-
|
|
29491
|
+
const healthHandler = () => {
|
|
29492
|
+
const update = checkLatestVersionForServer();
|
|
29493
|
+
return {
|
|
29494
|
+
status: "ok",
|
|
29495
|
+
service: "canonry",
|
|
29496
|
+
version: PKG_VERSION2,
|
|
29497
|
+
...basePath ? { basePath: basePath.replace(/\/$/, "") } : {},
|
|
29498
|
+
...update ? { updateAvailable: update } : {}
|
|
29499
|
+
};
|
|
29500
|
+
};
|
|
28692
29501
|
app.get("/health", healthHandler);
|
|
28693
29502
|
if (basePath) {
|
|
28694
29503
|
app.get(`${basePath}health`, healthHandler);
|
|
28695
29504
|
}
|
|
29505
|
+
checkLatestVersionForServer();
|
|
28696
29506
|
scheduler.start();
|
|
28697
29507
|
app.addHook("onClose", async () => {
|
|
28698
29508
|
scheduler.stop();
|
|
@@ -28753,16 +29563,17 @@ export {
|
|
|
28753
29563
|
showFirstRunNotice,
|
|
28754
29564
|
detectAndTrackUpgrade,
|
|
28755
29565
|
trackEvent,
|
|
28756
|
-
|
|
28757
|
-
|
|
28758
|
-
|
|
28759
|
-
|
|
28760
|
-
|
|
28761
|
-
|
|
28762
|
-
|
|
29566
|
+
backfillAnswerVisibilityCommand,
|
|
29567
|
+
backfillNormalizedPaths,
|
|
29568
|
+
backfillNormalizedPathsCommand,
|
|
29569
|
+
backfillAiReferralPaths,
|
|
29570
|
+
backfillAiReferralPathsCommand,
|
|
29571
|
+
backfillAnswerMentionsCommand,
|
|
29572
|
+
backfillInsightsCommand,
|
|
28763
29573
|
renderReportHtml,
|
|
28764
29574
|
setGoogleAuthConfig,
|
|
28765
29575
|
formatAuditFactorScore,
|
|
29576
|
+
checkLatestVersionForCli,
|
|
28766
29577
|
listAgentProviders,
|
|
28767
29578
|
coerceAgentProvider,
|
|
28768
29579
|
createServer
|