@ainyc/canonry 4.84.0 → 4.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/aero/references/regression-playbook.md +2 -0
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +22 -1
- package/assets/assets/{BacklinksPage-OrSg_iPA.js → BacklinksPage-BNrvc-gV.js} +1 -1
- package/assets/assets/{ChartPrimitives-DPBhAT_r.js → ChartPrimitives-BlIkdUdy.js} +1 -1
- package/assets/assets/{ProjectPage-CpMcEmtw.js → ProjectPage-CAyx_xNr.js} +2 -2
- package/assets/assets/{RunRow-2Rty0BAH.js → RunRow-CAPnKzi7.js} +1 -1
- package/assets/assets/{RunsPage-B3ahqf8s.js → RunsPage-idnuzKBn.js} +1 -1
- package/assets/assets/{SettingsPage-BIjeI85q.js → SettingsPage-Bka67uJq.js} +1 -1
- package/assets/assets/{TrafficPage-DjGoj691.js → TrafficPage-C_o-rA5o.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-BgKG-2q3.js → TrafficSourceDetailPage-D_jvoSTV.js} +1 -1
- package/assets/assets/{arrow-left-Cf7wmru1.js → arrow-left-B-JfzARi.js} +1 -1
- package/assets/assets/{extract-error-message-CANxezte.js → extract-error-message-BhPbjIX6.js} +1 -1
- package/assets/assets/{index-CGlPx_cu.js → index-uPSrDA8e.js} +77 -77
- package/assets/assets/{trash-2-6nHJZrvy.js → trash-2-BbRvn40h.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-BNF3HXBW.js → chunk-23HGQV22.js} +1460 -1149
- package/dist/{chunk-VJBO4VIK.js → chunk-DLBQU3VG.js} +712 -423
- package/dist/{chunk-Y3O3HBMN.js → chunk-LLJPZKHG.js} +94 -1
- package/dist/{chunk-M3IYKTSF.js → chunk-SELXBOAP.js} +19 -4
- package/dist/cli.js +210 -20
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-PDIAMP5I.js → intelligence-service-ZHUJKZRO.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +10 -10
|
@@ -46,8 +46,10 @@ import {
|
|
|
46
46
|
adsSummaryDtoSchema,
|
|
47
47
|
adsSyncResponseSchema,
|
|
48
48
|
agentProvidersResponseDtoSchema,
|
|
49
|
+
aggregateHarvestedQueries,
|
|
49
50
|
apiKeyDtoSchema,
|
|
50
51
|
apiKeyListDtoSchema,
|
|
52
|
+
applyHarvestSemanticNovelty,
|
|
51
53
|
auditLogEntrySchema,
|
|
52
54
|
authInvalid,
|
|
53
55
|
authRequired,
|
|
@@ -70,6 +72,7 @@ import {
|
|
|
70
72
|
brandKeyFromText,
|
|
71
73
|
brandLabelFromDomain,
|
|
72
74
|
brandMetricsDtoSchema,
|
|
75
|
+
buildHarvestAnchorTerms,
|
|
73
76
|
categorizeSource,
|
|
74
77
|
categorizeSourceWithCompetitors,
|
|
75
78
|
categoryLabel,
|
|
@@ -104,6 +107,7 @@ import {
|
|
|
104
107
|
deriveWinnabilityClass,
|
|
105
108
|
determineAnswerMentioned,
|
|
106
109
|
discoveryBucketSchema,
|
|
110
|
+
discoveryHarvestDtoSchema,
|
|
107
111
|
discoveryPromotePreviewSchema,
|
|
108
112
|
discoveryPromoteRequestSchema,
|
|
109
113
|
discoveryPromoteResultSchema,
|
|
@@ -130,6 +134,7 @@ import {
|
|
|
130
134
|
ga4SocialReferralHistoryEntrySchema,
|
|
131
135
|
ga4StatusDtoSchema,
|
|
132
136
|
ga4SyncResponseDtoSchema,
|
|
137
|
+
gateHarvestedSearchQueries,
|
|
133
138
|
gbpAccountListResponseSchema,
|
|
134
139
|
gbpDailyMetricListResponseSchema,
|
|
135
140
|
gbpDiscoverRequestSchema,
|
|
@@ -169,6 +174,7 @@ import {
|
|
|
169
174
|
notFound,
|
|
170
175
|
notImplemented,
|
|
171
176
|
notificationDtoSchema,
|
|
177
|
+
parseInclusiveEndMs,
|
|
172
178
|
parseRunError,
|
|
173
179
|
parseWindow,
|
|
174
180
|
pickClusterRepresentative,
|
|
@@ -230,6 +236,7 @@ import {
|
|
|
230
236
|
unsupportedKind,
|
|
231
237
|
validationError,
|
|
232
238
|
visibilityStateFromAnswerMentioned,
|
|
239
|
+
visibilityStatsDtoSchema,
|
|
233
240
|
windowCutoff,
|
|
234
241
|
winnabilityClassLabel,
|
|
235
242
|
winnabilityClassSchema,
|
|
@@ -246,10 +253,10 @@ import {
|
|
|
246
253
|
wordpressSchemaDeployResultDtoSchema,
|
|
247
254
|
wordpressSchemaStatusResultDtoSchema,
|
|
248
255
|
wordpressStatusDtoSchema
|
|
249
|
-
} from "./chunk-
|
|
256
|
+
} from "./chunk-23HGQV22.js";
|
|
250
257
|
|
|
251
258
|
// src/intelligence-service.ts
|
|
252
|
-
import { eq as
|
|
259
|
+
import { eq as eq37, desc as desc18, asc as asc5, and as and27, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
|
|
253
260
|
|
|
254
261
|
// ../db/src/client.ts
|
|
255
262
|
import { mkdirSync } from "fs";
|
|
@@ -4339,18 +4346,18 @@ function buildCitationScorecard(snapshots, queryLookup) {
|
|
|
4339
4346
|
answerMentioned: snap.answerMentioned ?? null,
|
|
4340
4347
|
model: snap.model
|
|
4341
4348
|
};
|
|
4342
|
-
const
|
|
4343
|
-
|
|
4344
|
-
if (snap.citationState === CitationStates.cited)
|
|
4345
|
-
providerCounts.set(snap.provider,
|
|
4349
|
+
const counts2 = providerCounts.get(snap.provider) ?? { cited: 0, total: 0 };
|
|
4350
|
+
counts2.total++;
|
|
4351
|
+
if (snap.citationState === CitationStates.cited) counts2.cited++;
|
|
4352
|
+
providerCounts.set(snap.provider, counts2);
|
|
4346
4353
|
}
|
|
4347
4354
|
const providerRates = providerList.map((provider) => {
|
|
4348
|
-
const
|
|
4349
|
-
const citationRate =
|
|
4355
|
+
const counts2 = providerCounts.get(provider) ?? { cited: 0, total: 0 };
|
|
4356
|
+
const citationRate = counts2.total > 0 ? Math.round(counts2.cited / counts2.total * 100) : 0;
|
|
4350
4357
|
return {
|
|
4351
4358
|
provider,
|
|
4352
|
-
citedCount:
|
|
4353
|
-
totalCount:
|
|
4359
|
+
citedCount: counts2.cited,
|
|
4360
|
+
totalCount: counts2.total,
|
|
4354
4361
|
citationRate
|
|
4355
4362
|
};
|
|
4356
4363
|
});
|
|
@@ -7918,13 +7925,13 @@ function buildRankedList(domains, limit) {
|
|
|
7918
7925
|
bySurfaceClass
|
|
7919
7926
|
};
|
|
7920
7927
|
}
|
|
7921
|
-
function buildCategoryCounts(
|
|
7928
|
+
function buildCategoryCounts(counts2) {
|
|
7922
7929
|
let grandTotal = 0;
|
|
7923
|
-
for (const domains of
|
|
7930
|
+
for (const domains of counts2.values()) {
|
|
7924
7931
|
for (const count2 of domains.values()) grandTotal += count2;
|
|
7925
7932
|
}
|
|
7926
7933
|
const result = [];
|
|
7927
|
-
for (const [category, domains] of
|
|
7934
|
+
for (const [category, domains] of counts2) {
|
|
7928
7935
|
let categoryTotal = 0;
|
|
7929
7936
|
const domainEntries = [];
|
|
7930
7937
|
for (const [domain, count2] of domains) {
|
|
@@ -11738,19 +11745,19 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
|
11738
11745
|
considered++;
|
|
11739
11746
|
if (snap.citationState === CitationStates.cited) citedQueryIds.add(snap.queryId);
|
|
11740
11747
|
if (snap.answerMentioned) mentionedQueryIds.add(snap.queryId);
|
|
11741
|
-
const
|
|
11742
|
-
|
|
11743
|
-
if (snap.citationState === CitationStates.cited)
|
|
11744
|
-
providerCounts.set(snap.provider,
|
|
11748
|
+
const counts2 = providerCounts.get(snap.provider) ?? { cited: 0, total: 0 };
|
|
11749
|
+
counts2.total++;
|
|
11750
|
+
if (snap.citationState === CitationStates.cited) counts2.cited++;
|
|
11751
|
+
providerCounts.set(snap.provider, counts2);
|
|
11745
11752
|
}
|
|
11746
11753
|
if (considered === 0) continue;
|
|
11747
11754
|
const citedQueryCount = citedQueryIds.size;
|
|
11748
11755
|
const mentionedQueryCount = mentionedQueryIds.size;
|
|
11749
11756
|
const citationRate = totalQueries > 0 ? Math.round(citedQueryCount / totalQueries * 100) : 0;
|
|
11750
11757
|
const mentionRate = totalQueries > 0 ? Math.round(mentionedQueryCount / totalQueries * 100) : 0;
|
|
11751
|
-
const providerRates = [...providerCounts.entries()].map(([provider,
|
|
11758
|
+
const providerRates = [...providerCounts.entries()].map(([provider, counts2]) => ({
|
|
11752
11759
|
provider,
|
|
11753
|
-
citationRate:
|
|
11760
|
+
citationRate: counts2.total > 0 ? Math.round(counts2.cited / counts2.total * 100) : 0
|
|
11754
11761
|
})).sort((a, b) => a.provider.localeCompare(b.provider));
|
|
11755
11762
|
points.push({
|
|
11756
11763
|
runId: run.id,
|
|
@@ -12720,8 +12727,165 @@ function normalizeDomain2(domain) {
|
|
|
12720
12727
|
return domain.toLowerCase().trim().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
|
|
12721
12728
|
}
|
|
12722
12729
|
|
|
12730
|
+
// ../api-routes/src/visibility-stats.ts
|
|
12731
|
+
import { and as and11, desc as desc8, eq as eq16, inArray as inArray8 } from "drizzle-orm";
|
|
12732
|
+
function round42(value) {
|
|
12733
|
+
return Math.round(value * 1e4) / 1e4;
|
|
12734
|
+
}
|
|
12735
|
+
function emptyAgg() {
|
|
12736
|
+
return { total: 0, checked: 0, mentioned: 0, cited: 0, first: null, last: null };
|
|
12737
|
+
}
|
|
12738
|
+
function addSnapshot(agg, snap) {
|
|
12739
|
+
agg.total++;
|
|
12740
|
+
if (snap.answerMentioned === true || snap.answerMentioned === false) agg.checked++;
|
|
12741
|
+
if (snap.answerMentioned === true) agg.mentioned++;
|
|
12742
|
+
if (snap.citationState === CitationStates.cited) agg.cited++;
|
|
12743
|
+
if (agg.first === null || snap.createdAt < agg.first) agg.first = snap.createdAt;
|
|
12744
|
+
if (agg.last === null || snap.createdAt > agg.last) agg.last = snap.createdAt;
|
|
12745
|
+
}
|
|
12746
|
+
function counts(agg) {
|
|
12747
|
+
return {
|
|
12748
|
+
total: agg.total,
|
|
12749
|
+
checked: agg.checked,
|
|
12750
|
+
mentioned: agg.mentioned,
|
|
12751
|
+
cited: agg.cited,
|
|
12752
|
+
// mention proportion is over the CHECKED sample; citation proportion is
|
|
12753
|
+
// over the full total (every snapshot is checked for citation).
|
|
12754
|
+
mentionRate: agg.checked > 0 ? round42(agg.mentioned / agg.checked) : null,
|
|
12755
|
+
citedRate: agg.total > 0 ? round42(agg.cited / agg.total) : null
|
|
12756
|
+
};
|
|
12757
|
+
}
|
|
12758
|
+
function providerEntries(byProvider) {
|
|
12759
|
+
return [...byProvider.entries()].map(([provider, agg]) => ({
|
|
12760
|
+
provider,
|
|
12761
|
+
...counts(agg),
|
|
12762
|
+
// first/last are non-null once at least one snapshot landed in the agg,
|
|
12763
|
+
// which is guaranteed for any provider that made it into the map.
|
|
12764
|
+
firstObserved: agg.first ?? "",
|
|
12765
|
+
lastObserved: agg.last ?? ""
|
|
12766
|
+
})).sort((a, b) => a.provider.localeCompare(b.provider));
|
|
12767
|
+
}
|
|
12768
|
+
function computeVisibilityStats(input) {
|
|
12769
|
+
const { groupBy } = input;
|
|
12770
|
+
const wantProviders = groupBy === "provider";
|
|
12771
|
+
const queryById = /* @__PURE__ */ new Map();
|
|
12772
|
+
const queryByText = /* @__PURE__ */ new Map();
|
|
12773
|
+
for (const q of input.queries) {
|
|
12774
|
+
queryById.set(q.id, q);
|
|
12775
|
+
queryByText.set(q.query, q);
|
|
12776
|
+
}
|
|
12777
|
+
const byQuery = /* @__PURE__ */ new Map();
|
|
12778
|
+
const totals = emptyAgg();
|
|
12779
|
+
const totalsByProvider = /* @__PURE__ */ new Map();
|
|
12780
|
+
for (const snap of input.snapshots) {
|
|
12781
|
+
let resolved;
|
|
12782
|
+
if (snap.queryId && queryById.has(snap.queryId)) resolved = queryById.get(snap.queryId);
|
|
12783
|
+
else if (snap.queryText && queryByText.has(snap.queryText)) resolved = queryByText.get(snap.queryText);
|
|
12784
|
+
if (!resolved) continue;
|
|
12785
|
+
let bucket = byQuery.get(resolved.id);
|
|
12786
|
+
if (!bucket) {
|
|
12787
|
+
bucket = { id: resolved.id, query: resolved.query, agg: emptyAgg(), byProvider: /* @__PURE__ */ new Map() };
|
|
12788
|
+
byQuery.set(resolved.id, bucket);
|
|
12789
|
+
}
|
|
12790
|
+
addSnapshot(bucket.agg, snap);
|
|
12791
|
+
addSnapshot(totals, snap);
|
|
12792
|
+
if (wantProviders) {
|
|
12793
|
+
const qpAgg = bucket.byProvider.get(snap.provider) ?? emptyAgg();
|
|
12794
|
+
addSnapshot(qpAgg, snap);
|
|
12795
|
+
bucket.byProvider.set(snap.provider, qpAgg);
|
|
12796
|
+
const tpAgg = totalsByProvider.get(snap.provider) ?? emptyAgg();
|
|
12797
|
+
addSnapshot(tpAgg, snap);
|
|
12798
|
+
totalsByProvider.set(snap.provider, tpAgg);
|
|
12799
|
+
}
|
|
12800
|
+
}
|
|
12801
|
+
const queryEntries = [...byQuery.values()].map((bucket) => ({
|
|
12802
|
+
queryId: bucket.id,
|
|
12803
|
+
query: bucket.query,
|
|
12804
|
+
...counts(bucket.agg),
|
|
12805
|
+
firstObserved: bucket.agg.first ?? "",
|
|
12806
|
+
lastObserved: bucket.agg.last ?? "",
|
|
12807
|
+
...wantProviders ? { providers: providerEntries(bucket.byProvider) } : {}
|
|
12808
|
+
})).sort((a, b) => a.query.localeCompare(b.query));
|
|
12809
|
+
return {
|
|
12810
|
+
totals: counts(totals),
|
|
12811
|
+
...wantProviders ? { byProvider: providerEntries(totalsByProvider) } : {},
|
|
12812
|
+
queries: queryEntries
|
|
12813
|
+
};
|
|
12814
|
+
}
|
|
12815
|
+
async function visibilityStatsRoutes(app) {
|
|
12816
|
+
app.get("/projects/:name/visibility-stats", async (request, reply) => {
|
|
12817
|
+
const project = resolveProject(app.db, request.params.name);
|
|
12818
|
+
const { since: sinceRaw, until: untilRaw, lastRuns: lastRunsRaw, groupBy: groupByRaw } = request.query;
|
|
12819
|
+
let groupBy = null;
|
|
12820
|
+
if (groupByRaw !== void 0 && groupByRaw !== "") {
|
|
12821
|
+
if (groupByRaw !== "provider") throw validationError('"groupBy" must be "provider"');
|
|
12822
|
+
groupBy = "provider";
|
|
12823
|
+
}
|
|
12824
|
+
const hasSince = sinceRaw !== void 0 && sinceRaw !== "";
|
|
12825
|
+
const hasUntil = untilRaw !== void 0 && untilRaw !== "";
|
|
12826
|
+
const hasLastRuns = lastRunsRaw !== void 0 && lastRunsRaw !== "";
|
|
12827
|
+
if (hasLastRuns && (hasSince || hasUntil)) {
|
|
12828
|
+
throw validationError('"lastRuns" cannot be combined with "since"/"until" \u2014 use one or the other');
|
|
12829
|
+
}
|
|
12830
|
+
let sinceMs = null;
|
|
12831
|
+
let untilMs = null;
|
|
12832
|
+
if (hasSince) {
|
|
12833
|
+
const ms = Date.parse(sinceRaw);
|
|
12834
|
+
if (Number.isNaN(ms)) throw validationError('"since" must be an ISO 8601 date/time');
|
|
12835
|
+
sinceMs = ms;
|
|
12836
|
+
}
|
|
12837
|
+
if (hasUntil) {
|
|
12838
|
+
const ms = parseInclusiveEndMs(untilRaw);
|
|
12839
|
+
if (ms === null) throw validationError('"until" must be an ISO 8601 date/time');
|
|
12840
|
+
untilMs = ms;
|
|
12841
|
+
}
|
|
12842
|
+
if (sinceMs !== null && untilMs !== null && untilMs < sinceMs) {
|
|
12843
|
+
throw validationError('"until" must be on or after "since"');
|
|
12844
|
+
}
|
|
12845
|
+
let lastRuns = null;
|
|
12846
|
+
if (hasLastRuns) {
|
|
12847
|
+
const n = Number(lastRunsRaw);
|
|
12848
|
+
if (!Number.isInteger(n) || n <= 0) throw validationError('"lastRuns" must be a positive integer');
|
|
12849
|
+
lastRuns = n;
|
|
12850
|
+
}
|
|
12851
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq16(queries.projectId, project.id)).all();
|
|
12852
|
+
let projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and11(eq16(runs.projectId, project.id), eq16(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc8(runs.createdAt)).all().filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial);
|
|
12853
|
+
if (sinceMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) >= sinceMs);
|
|
12854
|
+
if (untilMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) <= untilMs);
|
|
12855
|
+
if (lastRuns !== null) projectRuns = projectRuns.slice(0, lastRuns);
|
|
12856
|
+
const runCount = projectRuns.length;
|
|
12857
|
+
const runIds = projectRuns.map((r) => r.id);
|
|
12858
|
+
const snapshots = runIds.length > 0 && projectQueries.length > 0 ? app.db.select({
|
|
12859
|
+
queryId: querySnapshots.queryId,
|
|
12860
|
+
queryText: querySnapshots.queryText,
|
|
12861
|
+
provider: querySnapshots.provider,
|
|
12862
|
+
citationState: querySnapshots.citationState,
|
|
12863
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
12864
|
+
createdAt: querySnapshots.createdAt
|
|
12865
|
+
}).from(querySnapshots).where(inArray8(querySnapshots.runId, runIds)).all() : [];
|
|
12866
|
+
const stats = computeVisibilityStats({ queries: projectQueries, snapshots, groupBy });
|
|
12867
|
+
const response = {
|
|
12868
|
+
project: project.name,
|
|
12869
|
+
window: {
|
|
12870
|
+
since: hasSince ? sinceRaw : null,
|
|
12871
|
+
until: hasUntil ? untilRaw : null,
|
|
12872
|
+
lastRuns,
|
|
12873
|
+
runCount
|
|
12874
|
+
},
|
|
12875
|
+
totals: stats.totals,
|
|
12876
|
+
queries: stats.queries,
|
|
12877
|
+
// `groupBy` + `byProvider` appear together only when a breakdown was
|
|
12878
|
+
// requested; both are OMITTED otherwise (absent = no breakdown) so the
|
|
12879
|
+
// SDK types `groupBy` as `groupBy?: 'provider'` rather than a misleading
|
|
12880
|
+
// always-present literal.
|
|
12881
|
+
...groupBy === "provider" ? { groupBy, byProvider: stats.byProvider ?? [] } : {}
|
|
12882
|
+
};
|
|
12883
|
+
return reply.send(response);
|
|
12884
|
+
});
|
|
12885
|
+
}
|
|
12886
|
+
|
|
12723
12887
|
// ../api-routes/src/composites.ts
|
|
12724
|
-
import { eq as
|
|
12888
|
+
import { eq as eq17, and as and12, desc as desc9, sql as sql7, like, or as or4, inArray as inArray9 } from "drizzle-orm";
|
|
12725
12889
|
var TOP_INSIGHT_LIMIT = 5;
|
|
12726
12890
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
12727
12891
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -12739,7 +12903,7 @@ async function compositeRoutes(app) {
|
|
|
12739
12903
|
const project = resolveProject(app.db, request.params.name);
|
|
12740
12904
|
const filterLocation = (request.query.location ?? "").trim() || null;
|
|
12741
12905
|
const sinceIso = parseSinceFilter(request.query.since);
|
|
12742
|
-
const allRunsRaw = app.db.select().from(runs).where(
|
|
12906
|
+
const allRunsRaw = app.db.select().from(runs).where(and12(eq17(runs.projectId, project.id), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).all();
|
|
12743
12907
|
const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
|
|
12744
12908
|
const totalRuns = allRuns.length;
|
|
12745
12909
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
@@ -12752,9 +12916,9 @@ async function compositeRoutes(app) {
|
|
|
12752
12916
|
const previousVisibilityRun = pickGroupRepresentative(previousVisRunGroup);
|
|
12753
12917
|
const latestRunRow = allRuns[0] ?? null;
|
|
12754
12918
|
const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
|
|
12755
|
-
const healthRow = app.db.select().from(healthSnapshots).where(
|
|
12919
|
+
const healthRow = app.db.select().from(healthSnapshots).where(eq17(healthSnapshots.projectId, project.id)).orderBy(desc9(healthSnapshots.createdAt)).limit(1).get();
|
|
12756
12920
|
const health = healthRow ? mapHealthRow2(healthRow) : null;
|
|
12757
|
-
const insightRows = app.db.select().from(insights).where(
|
|
12921
|
+
const insightRows = app.db.select().from(insights).where(eq17(insights.projectId, project.id)).orderBy(desc9(insights.createdAt)).all();
|
|
12758
12922
|
const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
|
|
12759
12923
|
const sparklineRunIds = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => r.id);
|
|
12760
12924
|
const snapshotRunIds = new Set(sparklineRunIds);
|
|
@@ -12769,8 +12933,8 @@ async function compositeRoutes(app) {
|
|
|
12769
12933
|
previousSnapshots,
|
|
12770
12934
|
previousVisibilityRun?.createdAt ?? null
|
|
12771
12935
|
);
|
|
12772
|
-
const competitorRows = app.db.select().from(competitors).where(
|
|
12773
|
-
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
12936
|
+
const competitorRows = app.db.select().from(competitors).where(eq17(competitors.projectId, project.id)).all();
|
|
12937
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq17(queries.projectId, project.id)).all();
|
|
12774
12938
|
const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
|
|
12775
12939
|
const configuredApiProviders = project.providers.filter((p) => !p.startsWith("cdp:"));
|
|
12776
12940
|
const mentionShareCompetitors = competitorRows.map((c) => ({
|
|
@@ -12869,9 +13033,9 @@ async function compositeRoutes(app) {
|
|
|
12869
13033
|
citedDomains: querySnapshots.citedDomains,
|
|
12870
13034
|
rawResponse: querySnapshots.rawResponse,
|
|
12871
13035
|
createdAt: querySnapshots.createdAt
|
|
12872
|
-
}).from(querySnapshots).innerJoin(queries,
|
|
12873
|
-
|
|
12874
|
-
|
|
13036
|
+
}).from(querySnapshots).innerJoin(queries, eq17(querySnapshots.queryId, queries.id)).where(
|
|
13037
|
+
and12(
|
|
13038
|
+
eq17(queries.projectId, project.id),
|
|
12875
13039
|
or4(
|
|
12876
13040
|
sql7`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
12877
13041
|
sql7`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
@@ -12879,10 +13043,10 @@ async function compositeRoutes(app) {
|
|
|
12879
13043
|
like(queries.query, pattern)
|
|
12880
13044
|
)
|
|
12881
13045
|
)
|
|
12882
|
-
).orderBy(
|
|
13046
|
+
).orderBy(desc9(querySnapshots.createdAt)).limit(limit + 1).all());
|
|
12883
13047
|
const insightMatches = app.db.select().from(insights).where(
|
|
12884
|
-
|
|
12885
|
-
|
|
13048
|
+
and12(
|
|
13049
|
+
eq17(insights.projectId, project.id),
|
|
12886
13050
|
or4(
|
|
12887
13051
|
like(insights.title, pattern),
|
|
12888
13052
|
like(insights.query, pattern),
|
|
@@ -12890,7 +13054,7 @@ async function compositeRoutes(app) {
|
|
|
12890
13054
|
sql7`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
|
|
12891
13055
|
)
|
|
12892
13056
|
)
|
|
12893
|
-
).orderBy(
|
|
13057
|
+
).orderBy(desc9(insights.createdAt)).limit(limit + 1).all();
|
|
12894
13058
|
const hits = [];
|
|
12895
13059
|
for (const row of snapshotMatches) {
|
|
12896
13060
|
hits.push(buildSnapshotHit(row, rawQuery));
|
|
@@ -12960,7 +13124,7 @@ function loadSnapshotsByRunIds(app, runIds) {
|
|
|
12960
13124
|
answerText: querySnapshots.answerText,
|
|
12961
13125
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
12962
13126
|
citedDomains: querySnapshots.citedDomains
|
|
12963
|
-
}).from(querySnapshots).where(
|
|
13127
|
+
}).from(querySnapshots).where(inArray9(querySnapshots.runId, [...runIds])).all());
|
|
12964
13128
|
for (const row of rows) {
|
|
12965
13129
|
const list = result.get(row.runId) ?? [];
|
|
12966
13130
|
list.push({
|
|
@@ -13078,8 +13242,8 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
|
|
|
13078
13242
|
// NULLIF guards the degenerate impressions=0 case (SQLite returns NULL,
|
|
13079
13243
|
// which the JS coerces to 0 — caught by the impression floor anyway).
|
|
13080
13244
|
avgPosition: sql7`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
|
|
13081
|
-
}).from(gscSearchData).where(
|
|
13082
|
-
|
|
13245
|
+
}).from(gscSearchData).where(and12(
|
|
13246
|
+
eq17(gscSearchData.projectId, projectId),
|
|
13083
13247
|
sql7`${gscSearchData.date} >= ${cutoff}`,
|
|
13084
13248
|
sql7`${gscSearchData.impressions} > 0`
|
|
13085
13249
|
)).groupBy(gscSearchData.query).orderBy(sql7`SUM(${gscSearchData.impressions}) DESC`).limit(100).all();
|
|
@@ -13102,8 +13266,8 @@ function buildIndexCoverageScore(app, projectId) {
|
|
|
13102
13266
|
tooltip,
|
|
13103
13267
|
trend: []
|
|
13104
13268
|
};
|
|
13105
|
-
const gscRow = app.db.select().from(gscCoverageSnapshots).where(
|
|
13106
|
-
const bingRow = app.db.select().from(bingCoverageSnapshots).where(
|
|
13269
|
+
const gscRow = app.db.select().from(gscCoverageSnapshots).where(eq17(gscCoverageSnapshots.projectId, projectId)).orderBy(desc9(gscCoverageSnapshots.date)).limit(1).get();
|
|
13270
|
+
const bingRow = app.db.select().from(bingCoverageSnapshots).where(eq17(bingCoverageSnapshots.projectId, projectId)).orderBy(desc9(bingCoverageSnapshots.date)).limit(1).get();
|
|
13107
13271
|
const chosen = pickIndexCoverageRow(gscRow, bingRow);
|
|
13108
13272
|
if (!chosen) return empty;
|
|
13109
13273
|
const total = chosen.indexed + chosen.notIndexed;
|
|
@@ -13129,7 +13293,7 @@ function countGoogleDeindexedUrls(app, projectId) {
|
|
|
13129
13293
|
url: gscUrlInspections.url,
|
|
13130
13294
|
indexingState: gscUrlInspections.indexingState,
|
|
13131
13295
|
inspectedAt: gscUrlInspections.inspectedAt
|
|
13132
|
-
}).from(gscUrlInspections).where(
|
|
13296
|
+
}).from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, projectId)).orderBy(desc9(gscUrlInspections.inspectedAt)).all();
|
|
13133
13297
|
if (rows.length === 0) return 0;
|
|
13134
13298
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
13135
13299
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -13418,6 +13582,7 @@ var SCHEMA_TABLE = {
|
|
|
13418
13582
|
DomainClassificationsResponseDto: domainClassificationsResponseDtoSchema,
|
|
13419
13583
|
RecommendationBriefDto: recommendationBriefDtoSchema,
|
|
13420
13584
|
RecommendationExplanationDto: recommendationExplanationDtoSchema,
|
|
13585
|
+
DiscoveryHarvestDto: discoveryHarvestDtoSchema,
|
|
13421
13586
|
DiscoveryPromotePreview: discoveryPromotePreviewSchema,
|
|
13422
13587
|
DiscoveryPromoteResult: discoveryPromoteResultSchema,
|
|
13423
13588
|
DiscoverySessionDetailDto: discoverySessionDetailDtoSchema,
|
|
@@ -13475,6 +13640,7 @@ var SCHEMA_TABLE = {
|
|
|
13475
13640
|
TrafficSourceListResponse: trafficSourceListResponseSchema,
|
|
13476
13641
|
TrafficStatusResponse: trafficStatusResponseSchema,
|
|
13477
13642
|
TrafficSyncResponse: trafficSyncResponseSchema,
|
|
13643
|
+
VisibilityStatsDto: visibilityStatsDtoSchema,
|
|
13478
13644
|
WordpressAuditPageDto: wordpressAuditPageDtoSchema,
|
|
13479
13645
|
WordpressBulkMetaResultDto: wordpressBulkMetaResultDtoSchema,
|
|
13480
13646
|
WordpressDiffDto: wordpressDiffDtoSchema,
|
|
@@ -13700,6 +13866,30 @@ var analyticsWindowParameter = {
|
|
|
13700
13866
|
description: "Time window for analytics queries.",
|
|
13701
13867
|
schema: { type: "string", enum: ["7d", "30d", "90d", "all"] }
|
|
13702
13868
|
};
|
|
13869
|
+
var sinceQueryParameter = {
|
|
13870
|
+
name: "since",
|
|
13871
|
+
in: "query",
|
|
13872
|
+
description: 'Inclusive lower bound on run createdAt (ISO 8601). A date-only value (YYYY-MM-DD) is the start of that UTC day. Mutually exclusive with "lastRuns".',
|
|
13873
|
+
schema: stringSchema
|
|
13874
|
+
};
|
|
13875
|
+
var untilQueryParameter = {
|
|
13876
|
+
name: "until",
|
|
13877
|
+
in: "query",
|
|
13878
|
+
description: 'Inclusive upper bound on run createdAt (ISO 8601). A date-only value (YYYY-MM-DD) covers the whole UTC day (through 23:59:59.999). Mutually exclusive with "lastRuns".',
|
|
13879
|
+
schema: stringSchema
|
|
13880
|
+
};
|
|
13881
|
+
var lastRunsQueryParameter = {
|
|
13882
|
+
name: "lastRuns",
|
|
13883
|
+
in: "query",
|
|
13884
|
+
description: 'Aggregate only the most recent N answer-visibility runs. Mutually exclusive with "since"/"until".',
|
|
13885
|
+
schema: integerSchema
|
|
13886
|
+
};
|
|
13887
|
+
var groupByProviderQueryParameter = {
|
|
13888
|
+
name: "groupBy",
|
|
13889
|
+
in: "query",
|
|
13890
|
+
description: 'Set to "provider" to include a per-provider breakdown whose counts sum to the pooled counts.',
|
|
13891
|
+
schema: { type: "string", enum: ["provider"] }
|
|
13892
|
+
};
|
|
13703
13893
|
var wordpressEnvQueryParameter = {
|
|
13704
13894
|
name: "env",
|
|
13705
13895
|
in: "query",
|
|
@@ -14426,6 +14616,19 @@ var routeCatalog = [
|
|
|
14426
14616
|
404: errorResponse("Project not found.")
|
|
14427
14617
|
}
|
|
14428
14618
|
},
|
|
14619
|
+
{
|
|
14620
|
+
method: "get",
|
|
14621
|
+
path: "/api/v1/projects/{name}/visibility-stats",
|
|
14622
|
+
summary: "Get aggregated mention/citation stats per query",
|
|
14623
|
+
description: "Per-query mention (answer-text) and citation (source-list) counts with a sample size, pooled across many answer-visibility runs (probe-excluded). Tri-state aware: `checked` counts only snapshots where answerMentioned was recorded (null = not checked is excluded). Lets a consumer compute confidence-aware (e.g. Wilson) proportions without N+1 run fetches. With no since/until/lastRuns, EVERY completed/partial answer-visibility run is pooled \u2014 `window.runCount` reports how many; bound the window with lastRuns or since/until for a recent sample. Set groupBy=provider for a per-provider breakdown whose counts sum to the pooled counts.",
|
|
14624
|
+
tags: ["analytics"],
|
|
14625
|
+
parameters: [nameParameter, sinceQueryParameter, untilQueryParameter, lastRunsQueryParameter, groupByProviderQueryParameter],
|
|
14626
|
+
responses: {
|
|
14627
|
+
200: jsonResponse("Aggregated visibility stats returned.", "VisibilityStatsDto"),
|
|
14628
|
+
400: errorResponse("Invalid query parameters."),
|
|
14629
|
+
404: errorResponse("Project not found.")
|
|
14630
|
+
}
|
|
14631
|
+
},
|
|
14429
14632
|
{
|
|
14430
14633
|
method: "get",
|
|
14431
14634
|
path: "/api/v1/projects/{name}/snapshots/diff",
|
|
@@ -17235,6 +17438,23 @@ var routeCatalog = [
|
|
|
17235
17438
|
404: errorResponse("Project or session not found.")
|
|
17236
17439
|
}
|
|
17237
17440
|
},
|
|
17441
|
+
{
|
|
17442
|
+
method: "get",
|
|
17443
|
+
path: "/api/v1/projects/{name}/discover/sessions/{id}/harvest",
|
|
17444
|
+
summary: "Harvest issued search queries (grounding fan-out) from a session",
|
|
17445
|
+
description: "Reads the search queries the answer engine actually issued to answer each probe (Gemini's `groundingMetadata.webSearchQueries` fan-out) back out of the session's stored probe payloads, then runs a mandatory quality gate and returns the survivors as candidate seeds, ranked by how many distinct probes issued each one. The gate drops navigational/phone lookups, over-specific outliers, off-subject acronym collisions, exact already-tracked matches, and \u2014 via an embedding cosine pass over the project's tracked queries \u2014 semantic duplicates (paraphrases/synonyms an exact match can't see). `semanticNoveltyApplied` reports whether that embedding pass ran (it falls back to exact-match when embeddings are unavailable). These are a THIRD signal \u2014 *issued retrieval queries* \u2014 distinct from `mention` (answer text) and `cited` (source list); they carry no demand of their own. Read-only and derived: nothing is probed, tracked, or promoted. `minProbeHits` raises the recurrence floor; `anchor=false` disables the subject anchor for new-subject discovery on a well-scoped project. `stats` carries the raw count and a per-reason rejection tally. Issue #713.",
|
|
17446
|
+
tags: ["discovery"],
|
|
17447
|
+
parameters: [
|
|
17448
|
+
nameParameter,
|
|
17449
|
+
{ name: "id", in: "path", required: true, description: "Discovery session ID.", schema: stringSchema },
|
|
17450
|
+
{ name: "minProbeHits", in: "query", required: false, description: "Minimum number of distinct probes a candidate must appear in to be admitted (recurrence floor). Default 1.", schema: stringSchema },
|
|
17451
|
+
{ name: "anchor", in: "query", required: false, description: 'Set to "false" to disable the subject-anchor filter. Default applies it (when the subject corpus is rich enough).', schema: stringSchema }
|
|
17452
|
+
],
|
|
17453
|
+
responses: {
|
|
17454
|
+
200: jsonResponse("Harvested candidate seeds + gate stats returned.", "DiscoveryHarvestDto"),
|
|
17455
|
+
404: errorResponse("Project or session not found.")
|
|
17456
|
+
}
|
|
17457
|
+
},
|
|
17238
17458
|
{
|
|
17239
17459
|
method: "get",
|
|
17240
17460
|
path: "/api/v1/projects/{name}/discover/sessions/{id}/promote",
|
|
@@ -17691,7 +17911,7 @@ async function settingsRoutes(app, opts) {
|
|
|
17691
17911
|
|
|
17692
17912
|
// ../api-routes/src/keys.ts
|
|
17693
17913
|
import crypto12 from "crypto";
|
|
17694
|
-
import { desc as
|
|
17914
|
+
import { desc as desc10, eq as eq18 } from "drizzle-orm";
|
|
17695
17915
|
var KEYS_WRITE_SCOPE = "keys.write";
|
|
17696
17916
|
function toApiKeyDto(row) {
|
|
17697
17917
|
const scopes = Array.isArray(row.scopes) ? row.scopes : [];
|
|
@@ -17708,7 +17928,7 @@ function toApiKeyDto(row) {
|
|
|
17708
17928
|
}
|
|
17709
17929
|
async function keysRoutes(app) {
|
|
17710
17930
|
app.get("/keys", async () => {
|
|
17711
|
-
const rows = app.db.select().from(apiKeys).orderBy(
|
|
17931
|
+
const rows = app.db.select().from(apiKeys).orderBy(desc10(apiKeys.createdAt)).all();
|
|
17712
17932
|
return { keys: rows.map(toApiKeyDto) };
|
|
17713
17933
|
});
|
|
17714
17934
|
app.get("/keys/self", async (request) => {
|
|
@@ -17716,7 +17936,7 @@ async function keysRoutes(app) {
|
|
|
17716
17936
|
if (!id) {
|
|
17717
17937
|
throw notFound("API key", "self");
|
|
17718
17938
|
}
|
|
17719
|
-
const row = app.db.select().from(apiKeys).where(
|
|
17939
|
+
const row = app.db.select().from(apiKeys).where(eq18(apiKeys.id, id)).get();
|
|
17720
17940
|
if (!row) {
|
|
17721
17941
|
throw notFound("API key", id);
|
|
17722
17942
|
}
|
|
@@ -17768,7 +17988,7 @@ async function keysRoutes(app) {
|
|
|
17768
17988
|
app.post("/keys/:id/revoke", async (request) => {
|
|
17769
17989
|
requireScope(request, KEYS_WRITE_SCOPE);
|
|
17770
17990
|
const { id } = request.params;
|
|
17771
|
-
const row = app.db.select().from(apiKeys).where(
|
|
17991
|
+
const row = app.db.select().from(apiKeys).where(eq18(apiKeys.id, id)).get();
|
|
17772
17992
|
if (!row) {
|
|
17773
17993
|
throw notFound("API key", id);
|
|
17774
17994
|
}
|
|
@@ -17780,7 +18000,7 @@ async function keysRoutes(app) {
|
|
|
17780
18000
|
}
|
|
17781
18001
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17782
18002
|
app.db.transaction((tx) => {
|
|
17783
|
-
tx.update(apiKeys).set({ revokedAt: now }).where(
|
|
18003
|
+
tx.update(apiKeys).set({ revokedAt: now }).where(eq18(apiKeys.id, id)).run();
|
|
17784
18004
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
17785
18005
|
actor: "api",
|
|
17786
18006
|
action: "api-key.revoked",
|
|
@@ -17853,7 +18073,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
17853
18073
|
|
|
17854
18074
|
// ../api-routes/src/schedules.ts
|
|
17855
18075
|
import crypto13 from "crypto";
|
|
17856
|
-
import { and as
|
|
18076
|
+
import { and as and13, eq as eq19 } from "drizzle-orm";
|
|
17857
18077
|
function parseKindParam(raw) {
|
|
17858
18078
|
if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
|
|
17859
18079
|
const parsed = schedulableRunKindSchema.safeParse(raw);
|
|
@@ -17880,7 +18100,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
17880
18100
|
if (!sourceId) {
|
|
17881
18101
|
throw validationError('"sourceId" is required when kind is "traffic-sync"');
|
|
17882
18102
|
}
|
|
17883
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
18103
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq19(trafficSources.id, sourceId)).get();
|
|
17884
18104
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
17885
18105
|
throw notFound("Traffic source", sourceId);
|
|
17886
18106
|
}
|
|
@@ -17925,7 +18145,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
17925
18145
|
}
|
|
17926
18146
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17927
18147
|
const enabledBool = enabled !== false;
|
|
17928
|
-
const existing = app.db.select().from(schedules).where(
|
|
18148
|
+
const existing = app.db.select().from(schedules).where(and13(eq19(schedules.projectId, project.id), eq19(schedules.kind, kind))).get();
|
|
17929
18149
|
if (existing) {
|
|
17930
18150
|
app.db.update(schedules).set({
|
|
17931
18151
|
cronExpr,
|
|
@@ -17935,7 +18155,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
17935
18155
|
sourceId: sourceId ?? null,
|
|
17936
18156
|
enabled: enabledBool,
|
|
17937
18157
|
updatedAt: now
|
|
17938
|
-
}).where(
|
|
18158
|
+
}).where(eq19(schedules.id, existing.id)).run();
|
|
17939
18159
|
} else {
|
|
17940
18160
|
app.db.insert(schedules).values({
|
|
17941
18161
|
id: crypto13.randomUUID(),
|
|
@@ -17959,13 +18179,13 @@ async function scheduleRoutes(app, opts) {
|
|
|
17959
18179
|
diff: { kind, cronExpr, preset, timezone, providers, sourceId }
|
|
17960
18180
|
});
|
|
17961
18181
|
opts.onScheduleUpdated?.("upsert", project.id, kind);
|
|
17962
|
-
const schedule = app.db.select().from(schedules).where(
|
|
18182
|
+
const schedule = app.db.select().from(schedules).where(and13(eq19(schedules.projectId, project.id), eq19(schedules.kind, kind))).get();
|
|
17963
18183
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
17964
18184
|
});
|
|
17965
18185
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
17966
18186
|
const project = resolveProject(app.db, request.params.name);
|
|
17967
18187
|
const kind = parseKindParam(request.query?.kind);
|
|
17968
|
-
const schedule = app.db.select().from(schedules).where(
|
|
18188
|
+
const schedule = app.db.select().from(schedules).where(and13(eq19(schedules.projectId, project.id), eq19(schedules.kind, kind))).get();
|
|
17969
18189
|
if (!schedule) {
|
|
17970
18190
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
17971
18191
|
}
|
|
@@ -17974,11 +18194,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
17974
18194
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
17975
18195
|
const project = resolveProject(app.db, request.params.name);
|
|
17976
18196
|
const kind = parseKindParam(request.query?.kind);
|
|
17977
|
-
const schedule = app.db.select().from(schedules).where(
|
|
18197
|
+
const schedule = app.db.select().from(schedules).where(and13(eq19(schedules.projectId, project.id), eq19(schedules.kind, kind))).get();
|
|
17978
18198
|
if (!schedule) {
|
|
17979
18199
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
17980
18200
|
}
|
|
17981
|
-
app.db.delete(schedules).where(
|
|
18201
|
+
app.db.delete(schedules).where(eq19(schedules.id, schedule.id)).run();
|
|
17982
18202
|
writeAuditLog(app.db, {
|
|
17983
18203
|
projectId: project.id,
|
|
17984
18204
|
actor: "api",
|
|
@@ -18011,7 +18231,7 @@ function formatSchedule(row) {
|
|
|
18011
18231
|
|
|
18012
18232
|
// ../api-routes/src/notifications.ts
|
|
18013
18233
|
import crypto14 from "crypto";
|
|
18014
|
-
import { eq as
|
|
18234
|
+
import { eq as eq20 } from "drizzle-orm";
|
|
18015
18235
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
18016
18236
|
async function notificationRoutes(app, opts = {}) {
|
|
18017
18237
|
const allowLoopback = opts.allowLoopbackWebhooks === true;
|
|
@@ -18051,22 +18271,22 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
18051
18271
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
18052
18272
|
});
|
|
18053
18273
|
return reply.status(201).send({
|
|
18054
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
18274
|
+
...formatNotification(app.db.select().from(notifications).where(eq20(notifications.id, id)).get()),
|
|
18055
18275
|
webhookSecret
|
|
18056
18276
|
});
|
|
18057
18277
|
});
|
|
18058
18278
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
18059
18279
|
const project = resolveProject(app.db, request.params.name);
|
|
18060
|
-
const rows = app.db.select().from(notifications).where(
|
|
18280
|
+
const rows = app.db.select().from(notifications).where(eq20(notifications.projectId, project.id)).all();
|
|
18061
18281
|
return reply.send(rows.map(formatNotification));
|
|
18062
18282
|
});
|
|
18063
18283
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
18064
18284
|
const project = resolveProject(app.db, request.params.name);
|
|
18065
|
-
const notification = app.db.select().from(notifications).where(
|
|
18285
|
+
const notification = app.db.select().from(notifications).where(eq20(notifications.id, request.params.id)).get();
|
|
18066
18286
|
if (!notification || notification.projectId !== project.id) {
|
|
18067
18287
|
throw notFound("Notification", request.params.id);
|
|
18068
18288
|
}
|
|
18069
|
-
app.db.delete(notifications).where(
|
|
18289
|
+
app.db.delete(notifications).where(eq20(notifications.id, notification.id)).run();
|
|
18070
18290
|
writeAuditLog(app.db, {
|
|
18071
18291
|
projectId: project.id,
|
|
18072
18292
|
actor: "api",
|
|
@@ -18078,7 +18298,7 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
18078
18298
|
});
|
|
18079
18299
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
18080
18300
|
const project = resolveProject(app.db, request.params.name);
|
|
18081
|
-
const notification = app.db.select().from(notifications).where(
|
|
18301
|
+
const notification = app.db.select().from(notifications).where(eq20(notifications.id, request.params.id)).get();
|
|
18082
18302
|
if (!notification || notification.projectId !== project.id) {
|
|
18083
18303
|
throw notFound("Notification", request.params.id);
|
|
18084
18304
|
}
|
|
@@ -18131,7 +18351,7 @@ function formatNotification(row) {
|
|
|
18131
18351
|
|
|
18132
18352
|
// ../api-routes/src/google.ts
|
|
18133
18353
|
import crypto17 from "crypto";
|
|
18134
|
-
import { eq as
|
|
18354
|
+
import { eq as eq21, and as and14, desc as desc11, sql as sql8, inArray as inArray10 } from "drizzle-orm";
|
|
18135
18355
|
|
|
18136
18356
|
// ../api-routes/src/gbp-summary.ts
|
|
18137
18357
|
function computeMetricTotals(rows) {
|
|
@@ -19857,7 +20077,7 @@ async function googleRoutes(app, opts) {
|
|
|
19857
20077
|
if (!projectId) {
|
|
19858
20078
|
return reply.status(400).send("Stale OAuth state \u2014 restart the connect flow.");
|
|
19859
20079
|
}
|
|
19860
|
-
const project = app.db.select().from(projects).where(
|
|
20080
|
+
const project = app.db.select().from(projects).where(eq21(projects.id, projectId)).get();
|
|
19861
20081
|
if (!project) {
|
|
19862
20082
|
return reply.status(400).send("Project no longer exists. Restart the connect flow.");
|
|
19863
20083
|
}
|
|
@@ -19989,14 +20209,14 @@ async function googleRoutes(app, opts) {
|
|
|
19989
20209
|
if (opts.onGscSyncRequested) {
|
|
19990
20210
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
19991
20211
|
}
|
|
19992
|
-
const run = app.db.select().from(runs).where(
|
|
20212
|
+
const run = app.db.select().from(runs).where(eq21(runs.id, runId)).get();
|
|
19993
20213
|
return run;
|
|
19994
20214
|
});
|
|
19995
20215
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
19996
20216
|
const project = resolveProject(app.db, request.params.name);
|
|
19997
20217
|
const { startDate, endDate, query, page, limit, offset } = request.query;
|
|
19998
20218
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
19999
|
-
const conditions = [
|
|
20219
|
+
const conditions = [eq21(gscSearchData.projectId, project.id)];
|
|
20000
20220
|
if (startDate) conditions.push(sql8`${gscSearchData.date} >= ${startDate}`);
|
|
20001
20221
|
else if (cutoffDate) conditions.push(sql8`${gscSearchData.date} >= ${cutoffDate}`);
|
|
20002
20222
|
if (endDate) conditions.push(sql8`${gscSearchData.date} <= ${endDate}`);
|
|
@@ -20004,7 +20224,7 @@ async function googleRoutes(app, opts) {
|
|
|
20004
20224
|
if (page) conditions.push(sql8`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
|
|
20005
20225
|
const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
|
|
20006
20226
|
const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
|
|
20007
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
20227
|
+
const rows = app.db.select().from(gscSearchData).where(and14(...conditions)).orderBy(desc11(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
|
|
20008
20228
|
return rows.map((r) => ({
|
|
20009
20229
|
date: r.date,
|
|
20010
20230
|
query: r.query,
|
|
@@ -20021,7 +20241,7 @@ async function googleRoutes(app, opts) {
|
|
|
20021
20241
|
const project = resolveProject(app.db, request.params.name);
|
|
20022
20242
|
const { startDate, endDate } = request.query;
|
|
20023
20243
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
20024
|
-
const conditions = [
|
|
20244
|
+
const conditions = [eq21(gscSearchData.projectId, project.id)];
|
|
20025
20245
|
if (startDate) conditions.push(sql8`${gscSearchData.date} >= ${startDate}`);
|
|
20026
20246
|
else if (cutoffDate) conditions.push(sql8`${gscSearchData.date} >= ${cutoffDate}`);
|
|
20027
20247
|
if (endDate) conditions.push(sql8`${gscSearchData.date} <= ${endDate}`);
|
|
@@ -20029,7 +20249,7 @@ async function googleRoutes(app, opts) {
|
|
|
20029
20249
|
date: gscSearchData.date,
|
|
20030
20250
|
clicks: sql8`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
|
|
20031
20251
|
impressions: sql8`COALESCE(SUM(${gscSearchData.impressions}), 0)`
|
|
20032
|
-
}).from(gscSearchData).where(
|
|
20252
|
+
}).from(gscSearchData).where(and14(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
|
|
20033
20253
|
const daily = rows.map((r) => ({
|
|
20034
20254
|
date: r.date,
|
|
20035
20255
|
clicks: r.clicks,
|
|
@@ -20112,9 +20332,9 @@ async function googleRoutes(app, opts) {
|
|
|
20112
20332
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
20113
20333
|
const project = resolveProject(app.db, request.params.name);
|
|
20114
20334
|
const { url, limit } = request.query;
|
|
20115
|
-
const conditions = [
|
|
20116
|
-
if (url) conditions.push(
|
|
20117
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
20335
|
+
const conditions = [eq21(gscUrlInspections.projectId, project.id)];
|
|
20336
|
+
if (url) conditions.push(eq21(gscUrlInspections.url, url));
|
|
20337
|
+
const rows = app.db.select().from(gscUrlInspections).where(and14(...conditions)).orderBy(desc11(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
20118
20338
|
return rows.map((r) => ({
|
|
20119
20339
|
id: r.id,
|
|
20120
20340
|
url: r.url,
|
|
@@ -20133,7 +20353,7 @@ async function googleRoutes(app, opts) {
|
|
|
20133
20353
|
});
|
|
20134
20354
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
20135
20355
|
const project = resolveProject(app.db, request.params.name);
|
|
20136
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
20356
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
20137
20357
|
const byUrl = /* @__PURE__ */ new Map();
|
|
20138
20358
|
for (const row of allInspections) {
|
|
20139
20359
|
const existing = byUrl.get(row.url);
|
|
@@ -20161,7 +20381,7 @@ async function googleRoutes(app, opts) {
|
|
|
20161
20381
|
});
|
|
20162
20382
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
20163
20383
|
const project = resolveProject(app.db, request.params.name);
|
|
20164
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
20384
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
20165
20385
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
20166
20386
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
20167
20387
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -20210,7 +20430,7 @@ async function googleRoutes(app, opts) {
|
|
|
20210
20430
|
const total = latestByUrl.size;
|
|
20211
20431
|
const indexed = indexedUrls.length;
|
|
20212
20432
|
const notIndexed = notIndexedUrls.length;
|
|
20213
|
-
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(
|
|
20433
|
+
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(eq21(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.createdAt)).limit(1).get();
|
|
20214
20434
|
const lastSyncedAt = latestSnapshot?.createdAt ?? null;
|
|
20215
20435
|
const formatRow = (r) => ({
|
|
20216
20436
|
id: r.id,
|
|
@@ -20261,7 +20481,7 @@ async function googleRoutes(app, opts) {
|
|
|
20261
20481
|
const project = resolveProject(app.db, request.params.name);
|
|
20262
20482
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
20263
20483
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
20264
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
20484
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq21(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.date)).limit(limit).all();
|
|
20265
20485
|
return rows.map((r) => ({
|
|
20266
20486
|
date: r.date,
|
|
20267
20487
|
indexed: r.indexed,
|
|
@@ -20330,7 +20550,7 @@ async function googleRoutes(app, opts) {
|
|
|
20330
20550
|
if (opts.onInspectSitemapRequested) {
|
|
20331
20551
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
20332
20552
|
}
|
|
20333
|
-
const run = app.db.select().from(runs).where(
|
|
20553
|
+
const run = app.db.select().from(runs).where(eq21(runs.id, runId)).get();
|
|
20334
20554
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
20335
20555
|
});
|
|
20336
20556
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
|
|
@@ -20357,7 +20577,7 @@ async function googleRoutes(app, opts) {
|
|
|
20357
20577
|
if (opts.onInspectSitemapRequested) {
|
|
20358
20578
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
20359
20579
|
}
|
|
20360
|
-
const run = app.db.select().from(runs).where(
|
|
20580
|
+
const run = app.db.select().from(runs).where(eq21(runs.id, runId)).get();
|
|
20361
20581
|
return run;
|
|
20362
20582
|
});
|
|
20363
20583
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
|
|
@@ -20404,7 +20624,7 @@ async function googleRoutes(app, opts) {
|
|
|
20404
20624
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
20405
20625
|
let urlsToNotify = request.body?.urls ?? [];
|
|
20406
20626
|
if (request.body?.allUnindexed) {
|
|
20407
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
20627
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
20408
20628
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
20409
20629
|
for (const row of allInspections) {
|
|
20410
20630
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -20515,7 +20735,7 @@ async function googleRoutes(app, opts) {
|
|
|
20515
20735
|
};
|
|
20516
20736
|
}
|
|
20517
20737
|
function listSelectionResponse(projectId) {
|
|
20518
|
-
const rows = app.db.select().from(gbpLocations).where(
|
|
20738
|
+
const rows = app.db.select().from(gbpLocations).where(eq21(gbpLocations.projectId, projectId)).all();
|
|
20519
20739
|
const dtos = rows.map(rowToDto2);
|
|
20520
20740
|
return {
|
|
20521
20741
|
locations: dtos,
|
|
@@ -20524,15 +20744,15 @@ async function googleRoutes(app, opts) {
|
|
|
20524
20744
|
};
|
|
20525
20745
|
}
|
|
20526
20746
|
function clearGbpProjectData(tx, projectId) {
|
|
20527
|
-
tx.delete(gbpDailyMetrics).where(
|
|
20528
|
-
tx.delete(gbpKeywordImpressions).where(
|
|
20529
|
-
tx.delete(gbpKeywordMonthly).where(
|
|
20530
|
-
tx.delete(gbpPlaceActions).where(
|
|
20531
|
-
tx.delete(gbpLodgingSnapshots).where(
|
|
20532
|
-
tx.delete(gbpLocations).where(
|
|
20747
|
+
tx.delete(gbpDailyMetrics).where(eq21(gbpDailyMetrics.projectId, projectId)).run();
|
|
20748
|
+
tx.delete(gbpKeywordImpressions).where(eq21(gbpKeywordImpressions.projectId, projectId)).run();
|
|
20749
|
+
tx.delete(gbpKeywordMonthly).where(eq21(gbpKeywordMonthly.projectId, projectId)).run();
|
|
20750
|
+
tx.delete(gbpPlaceActions).where(eq21(gbpPlaceActions.projectId, projectId)).run();
|
|
20751
|
+
tx.delete(gbpLodgingSnapshots).where(eq21(gbpLodgingSnapshots.projectId, projectId)).run();
|
|
20752
|
+
tx.delete(gbpLocations).where(eq21(gbpLocations.projectId, projectId)).run();
|
|
20533
20753
|
}
|
|
20534
20754
|
function currentProjectAccount(projectId) {
|
|
20535
|
-
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(
|
|
20755
|
+
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(eq21(gbpLocations.projectId, projectId)).limit(1).get();
|
|
20536
20756
|
return row?.accountName ?? null;
|
|
20537
20757
|
}
|
|
20538
20758
|
app.post("/projects/:name/gbp/locations/discover", async (request) => {
|
|
@@ -20602,7 +20822,7 @@ async function googleRoutes(app, opts) {
|
|
|
20602
20822
|
app.db.transaction((tx) => {
|
|
20603
20823
|
if (switching) clearGbpProjectData(tx, project.id);
|
|
20604
20824
|
for (const remote of remoteLocations) {
|
|
20605
|
-
const existing = tx.select().from(gbpLocations).where(
|
|
20825
|
+
const existing = tx.select().from(gbpLocations).where(and14(eq21(gbpLocations.projectId, project.id), eq21(gbpLocations.locationName, remote.name))).get();
|
|
20606
20826
|
if (existing) {
|
|
20607
20827
|
tx.update(gbpLocations).set({
|
|
20608
20828
|
accountName,
|
|
@@ -20613,7 +20833,7 @@ async function googleRoutes(app, opts) {
|
|
|
20613
20833
|
placeId: remote.metadata?.placeId ?? null,
|
|
20614
20834
|
mapsUri: remote.metadata?.mapsUri ?? null,
|
|
20615
20835
|
updatedAt: now
|
|
20616
|
-
}).where(
|
|
20836
|
+
}).where(eq21(gbpLocations.id, existing.id)).run();
|
|
20617
20837
|
} else {
|
|
20618
20838
|
tx.insert(gbpLocations).values({
|
|
20619
20839
|
id: crypto17.randomUUID(),
|
|
@@ -20696,11 +20916,11 @@ async function googleRoutes(app, opts) {
|
|
|
20696
20916
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid selection request");
|
|
20697
20917
|
}
|
|
20698
20918
|
const { selected } = parsed.data;
|
|
20699
|
-
const existing = app.db.select().from(gbpLocations).where(
|
|
20919
|
+
const existing = app.db.select().from(gbpLocations).where(and14(eq21(gbpLocations.projectId, project.id), eq21(gbpLocations.locationName, locationName))).get();
|
|
20700
20920
|
if (!existing) throw notFound("GBP location", locationName);
|
|
20701
20921
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20702
20922
|
app.db.transaction((tx) => {
|
|
20703
|
-
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(
|
|
20923
|
+
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(eq21(gbpLocations.id, existing.id)).run();
|
|
20704
20924
|
writeAuditLog(tx, {
|
|
20705
20925
|
projectId: project.id,
|
|
20706
20926
|
actor: "api",
|
|
@@ -20709,7 +20929,7 @@ async function googleRoutes(app, opts) {
|
|
|
20709
20929
|
entityId: locationName
|
|
20710
20930
|
});
|
|
20711
20931
|
});
|
|
20712
|
-
const refreshed = app.db.select().from(gbpLocations).where(
|
|
20932
|
+
const refreshed = app.db.select().from(gbpLocations).where(eq21(gbpLocations.id, existing.id)).get();
|
|
20713
20933
|
return rowToDto2(refreshed);
|
|
20714
20934
|
});
|
|
20715
20935
|
app.delete("/projects/:name/gbp/connection", async (request, reply) => {
|
|
@@ -20753,10 +20973,10 @@ async function googleRoutes(app, opts) {
|
|
|
20753
20973
|
});
|
|
20754
20974
|
app.get("/projects/:name/gbp/metrics", async (request) => {
|
|
20755
20975
|
const project = resolveProject(app.db, request.params.name);
|
|
20756
|
-
const conditions = [
|
|
20757
|
-
if (request.query.locationName) conditions.push(
|
|
20758
|
-
if (request.query.metric) conditions.push(
|
|
20759
|
-
const rows = app.db.select().from(gbpDailyMetrics).where(
|
|
20976
|
+
const conditions = [eq21(gbpDailyMetrics.projectId, project.id)];
|
|
20977
|
+
if (request.query.locationName) conditions.push(eq21(gbpDailyMetrics.locationName, request.query.locationName));
|
|
20978
|
+
if (request.query.metric) conditions.push(eq21(gbpDailyMetrics.metric, request.query.metric));
|
|
20979
|
+
const rows = app.db.select().from(gbpDailyMetrics).where(and14(...conditions)).orderBy(desc11(gbpDailyMetrics.date)).all();
|
|
20760
20980
|
return {
|
|
20761
20981
|
metrics: rows.map((r) => ({ locationName: r.locationName, date: r.date, metric: r.metric, value: r.value })),
|
|
20762
20982
|
total: rows.length
|
|
@@ -20764,9 +20984,9 @@ async function googleRoutes(app, opts) {
|
|
|
20764
20984
|
});
|
|
20765
20985
|
app.get("/projects/:name/gbp/keywords", async (request) => {
|
|
20766
20986
|
const project = resolveProject(app.db, request.params.name);
|
|
20767
|
-
const conditions = [
|
|
20768
|
-
if (request.query.locationName) conditions.push(
|
|
20769
|
-
const rows = app.db.select().from(gbpKeywordImpressions).where(
|
|
20987
|
+
const conditions = [eq21(gbpKeywordImpressions.projectId, project.id)];
|
|
20988
|
+
if (request.query.locationName) conditions.push(eq21(gbpKeywordImpressions.locationName, request.query.locationName));
|
|
20989
|
+
const rows = app.db.select().from(gbpKeywordImpressions).where(and14(...conditions)).all();
|
|
20770
20990
|
rows.sort((a, b) => (b.valueCount ?? -1) - (a.valueCount ?? -1));
|
|
20771
20991
|
const thresholded = rows.filter((r) => r.valueThreshold !== null).length;
|
|
20772
20992
|
return {
|
|
@@ -20784,9 +21004,9 @@ async function googleRoutes(app, opts) {
|
|
|
20784
21004
|
});
|
|
20785
21005
|
app.get("/projects/:name/gbp/place-actions", async (request) => {
|
|
20786
21006
|
const project = resolveProject(app.db, request.params.name);
|
|
20787
|
-
const conditions = [
|
|
20788
|
-
if (request.query.locationName) conditions.push(
|
|
20789
|
-
const rows = app.db.select().from(gbpPlaceActions).where(
|
|
21007
|
+
const conditions = [eq21(gbpPlaceActions.projectId, project.id)];
|
|
21008
|
+
if (request.query.locationName) conditions.push(eq21(gbpPlaceActions.locationName, request.query.locationName));
|
|
21009
|
+
const rows = app.db.select().from(gbpPlaceActions).where(and14(...conditions)).all();
|
|
20790
21010
|
return {
|
|
20791
21011
|
placeActions: rows.map((r) => ({
|
|
20792
21012
|
locationName: r.locationName,
|
|
@@ -20801,9 +21021,9 @@ async function googleRoutes(app, opts) {
|
|
|
20801
21021
|
});
|
|
20802
21022
|
app.get("/projects/:name/gbp/lodging", async (request) => {
|
|
20803
21023
|
const project = resolveProject(app.db, request.params.name);
|
|
20804
|
-
const conditions = [
|
|
20805
|
-
if (request.query.locationName) conditions.push(
|
|
20806
|
-
const rows = app.db.select().from(gbpLodgingSnapshots).where(
|
|
21024
|
+
const conditions = [eq21(gbpLodgingSnapshots.projectId, project.id)];
|
|
21025
|
+
if (request.query.locationName) conditions.push(eq21(gbpLodgingSnapshots.locationName, request.query.locationName));
|
|
21026
|
+
const rows = app.db.select().from(gbpLodgingSnapshots).where(and14(...conditions)).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
20807
21027
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
20808
21028
|
for (const row of rows) {
|
|
20809
21029
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -20818,9 +21038,9 @@ async function googleRoutes(app, opts) {
|
|
|
20818
21038
|
});
|
|
20819
21039
|
app.get("/projects/:name/gbp/places", async (request) => {
|
|
20820
21040
|
const project = resolveProject(app.db, request.params.name);
|
|
20821
|
-
const conditions = [
|
|
20822
|
-
if (request.query.locationName) conditions.push(
|
|
20823
|
-
const rows = app.db.select().from(gbpPlaceDetails).where(
|
|
21041
|
+
const conditions = [eq21(gbpPlaceDetails.projectId, project.id)];
|
|
21042
|
+
if (request.query.locationName) conditions.push(eq21(gbpPlaceDetails.locationName, request.query.locationName));
|
|
21043
|
+
const rows = app.db.select().from(gbpPlaceDetails).where(and14(...conditions)).orderBy(desc11(gbpPlaceDetails.syncedAt)).all();
|
|
20824
21044
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
20825
21045
|
for (const row of rows) {
|
|
20826
21046
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -20839,7 +21059,7 @@ async function googleRoutes(app, opts) {
|
|
|
20839
21059
|
app.get("/projects/:name/gbp/summary", async (request) => {
|
|
20840
21060
|
const project = resolveProject(app.db, request.params.name);
|
|
20841
21061
|
const locationName = request.query.locationName ?? null;
|
|
20842
|
-
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(
|
|
21062
|
+
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and14(eq21(gbpLocations.projectId, project.id), eq21(gbpLocations.selected, true))).all().map((r) => r.n);
|
|
20843
21063
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
20844
21064
|
if (locationNames.length === 0) {
|
|
20845
21065
|
return buildGbpSummary({
|
|
@@ -20852,10 +21072,10 @@ async function googleRoutes(app, opts) {
|
|
|
20852
21072
|
lodging: []
|
|
20853
21073
|
});
|
|
20854
21074
|
}
|
|
20855
|
-
const metricRows = app.db.select().from(gbpDailyMetrics).where(
|
|
20856
|
-
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(
|
|
20857
|
-
const placeActionRows = app.db.select().from(gbpPlaceActions).where(
|
|
20858
|
-
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(
|
|
21075
|
+
const metricRows = app.db.select().from(gbpDailyMetrics).where(and14(eq21(gbpDailyMetrics.projectId, project.id), inArray10(gbpDailyMetrics.locationName, locationNames))).all();
|
|
21076
|
+
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and14(eq21(gbpKeywordImpressions.projectId, project.id), inArray10(gbpKeywordImpressions.locationName, locationNames))).all();
|
|
21077
|
+
const placeActionRows = app.db.select().from(gbpPlaceActions).where(and14(eq21(gbpPlaceActions.projectId, project.id), inArray10(gbpPlaceActions.locationName, locationNames))).all();
|
|
21078
|
+
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and14(eq21(gbpLodgingSnapshots.projectId, project.id), inArray10(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
20859
21079
|
const latestLodgingByLocation = /* @__PURE__ */ new Map();
|
|
20860
21080
|
for (const row of lodgingRows) {
|
|
20861
21081
|
if (!latestLodgingByLocation.has(row.locationName)) {
|
|
@@ -20876,7 +21096,7 @@ async function googleRoutes(app, opts) {
|
|
|
20876
21096
|
|
|
20877
21097
|
// ../api-routes/src/ads.ts
|
|
20878
21098
|
import crypto18 from "crypto";
|
|
20879
|
-
import { eq as
|
|
21099
|
+
import { eq as eq22, and as and15, asc as asc2, gte as gte3, lte as lte2, inArray as inArray11 } from "drizzle-orm";
|
|
20880
21100
|
function statusDto(row) {
|
|
20881
21101
|
if (!row) return { connected: false };
|
|
20882
21102
|
return {
|
|
@@ -20925,7 +21145,7 @@ async function adsRoutes(app, opts) {
|
|
|
20925
21145
|
createdAt: existingCfg?.createdAt ?? now,
|
|
20926
21146
|
updatedAt: now
|
|
20927
21147
|
});
|
|
20928
|
-
const existingRow = app.db.select().from(adsConnections).where(
|
|
21148
|
+
const existingRow = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
20929
21149
|
app.db.transaction((tx) => {
|
|
20930
21150
|
if (existingRow) {
|
|
20931
21151
|
tx.update(adsConnections).set({
|
|
@@ -20935,7 +21155,7 @@ async function adsRoutes(app, opts) {
|
|
|
20935
21155
|
timezone: account.timezone,
|
|
20936
21156
|
status: account.status,
|
|
20937
21157
|
updatedAt: now
|
|
20938
|
-
}).where(
|
|
21158
|
+
}).where(eq22(adsConnections.id, existingRow.id)).run();
|
|
20939
21159
|
} else {
|
|
20940
21160
|
tx.insert(adsConnections).values({
|
|
20941
21161
|
id: crypto18.randomUUID(),
|
|
@@ -20957,16 +21177,16 @@ async function adsRoutes(app, opts) {
|
|
|
20957
21177
|
entityId: account.id
|
|
20958
21178
|
}));
|
|
20959
21179
|
});
|
|
20960
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21180
|
+
const row = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
20961
21181
|
return statusDto(row);
|
|
20962
21182
|
}
|
|
20963
21183
|
);
|
|
20964
21184
|
app.delete("/projects/:name/ads/connection", async (request) => {
|
|
20965
21185
|
const project = resolveProject(app.db, request.params.name);
|
|
20966
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21186
|
+
const row = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
20967
21187
|
if (row) {
|
|
20968
21188
|
app.db.transaction((tx) => {
|
|
20969
|
-
tx.delete(adsConnections).where(
|
|
21189
|
+
tx.delete(adsConnections).where(eq22(adsConnections.id, row.id)).run();
|
|
20970
21190
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
20971
21191
|
projectId: project.id,
|
|
20972
21192
|
actor: "api",
|
|
@@ -20982,19 +21202,19 @@ async function adsRoutes(app, opts) {
|
|
|
20982
21202
|
});
|
|
20983
21203
|
app.get("/projects/:name/ads/status", async (request) => {
|
|
20984
21204
|
const project = resolveProject(app.db, request.params.name);
|
|
20985
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21205
|
+
const row = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
20986
21206
|
return statusDto(row);
|
|
20987
21207
|
});
|
|
20988
21208
|
app.post("/projects/:name/ads/sync", async (request) => {
|
|
20989
21209
|
const project = resolveProject(app.db, request.params.name);
|
|
20990
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21210
|
+
const row = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
20991
21211
|
if (!row) {
|
|
20992
21212
|
throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
|
|
20993
21213
|
}
|
|
20994
|
-
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
20995
|
-
|
|
20996
|
-
|
|
20997
|
-
|
|
21214
|
+
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and15(
|
|
21215
|
+
eq22(runs.projectId, project.id),
|
|
21216
|
+
eq22(runs.kind, RunKinds["ads-sync"]),
|
|
21217
|
+
inArray11(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
20998
21218
|
)).get();
|
|
20999
21219
|
if (inFlight) {
|
|
21000
21220
|
const existing = { runId: inFlight.id, status: inFlight.status };
|
|
@@ -21015,9 +21235,9 @@ async function adsRoutes(app, opts) {
|
|
|
21015
21235
|
});
|
|
21016
21236
|
app.get("/projects/:name/ads/campaigns", async (request) => {
|
|
21017
21237
|
const project = resolveProject(app.db, request.params.name);
|
|
21018
|
-
const campaignRows = app.db.select().from(adsCampaigns).where(
|
|
21019
|
-
const groupRows = app.db.select().from(adsAdGroups).where(
|
|
21020
|
-
const adRows = app.db.select().from(adsAds).where(
|
|
21238
|
+
const campaignRows = app.db.select().from(adsCampaigns).where(eq22(adsCampaigns.projectId, project.id)).all();
|
|
21239
|
+
const groupRows = app.db.select().from(adsAdGroups).where(eq22(adsAdGroups.projectId, project.id)).all();
|
|
21240
|
+
const adRows = app.db.select().from(adsAds).where(eq22(adsAds.projectId, project.id)).all();
|
|
21021
21241
|
const adsByGroup = /* @__PURE__ */ new Map();
|
|
21022
21242
|
for (const ad of adRows) {
|
|
21023
21243
|
const dto = {
|
|
@@ -21071,12 +21291,12 @@ async function adsRoutes(app, opts) {
|
|
|
21071
21291
|
}
|
|
21072
21292
|
parsedLevel = result.data;
|
|
21073
21293
|
}
|
|
21074
|
-
const conditions = [
|
|
21075
|
-
if (parsedLevel) conditions.push(
|
|
21076
|
-
if (entityId) conditions.push(
|
|
21294
|
+
const conditions = [eq22(adsInsightsDaily.projectId, project.id)];
|
|
21295
|
+
if (parsedLevel) conditions.push(eq22(adsInsightsDaily.level, parsedLevel));
|
|
21296
|
+
if (entityId) conditions.push(eq22(adsInsightsDaily.entityId, entityId));
|
|
21077
21297
|
if (from) conditions.push(gte3(adsInsightsDaily.date, from));
|
|
21078
21298
|
if (to) conditions.push(lte2(adsInsightsDaily.date, to));
|
|
21079
|
-
const rows = app.db.select().from(adsInsightsDaily).where(
|
|
21299
|
+
const rows = app.db.select().from(adsInsightsDaily).where(and15(...conditions)).orderBy(asc2(adsInsightsDaily.date)).all();
|
|
21080
21300
|
const dtoRows = rows.map((row) => ({
|
|
21081
21301
|
level: row.level,
|
|
21082
21302
|
entityId: row.entityId,
|
|
@@ -21087,19 +21307,19 @@ async function adsRoutes(app, opts) {
|
|
|
21087
21307
|
ctr: adsCtr(row.clicks, row.impressions),
|
|
21088
21308
|
cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
|
|
21089
21309
|
}));
|
|
21090
|
-
const conn = app.db.select().from(adsConnections).where(
|
|
21310
|
+
const conn = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
21091
21311
|
const response = { rows: dtoRows, currencyCode: conn?.currencyCode ?? null };
|
|
21092
21312
|
return response;
|
|
21093
21313
|
});
|
|
21094
21314
|
app.get("/projects/:name/ads/summary", async (request) => {
|
|
21095
21315
|
const project = resolveProject(app.db, request.params.name);
|
|
21096
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21097
|
-
const campaignCount = app.db.select().from(adsCampaigns).where(
|
|
21098
|
-
const adGroupCount = app.db.select().from(adsAdGroups).where(
|
|
21099
|
-
const adCount = app.db.select().from(adsAds).where(
|
|
21100
|
-
const campaignInsights = app.db.select().from(adsInsightsDaily).where(
|
|
21101
|
-
|
|
21102
|
-
|
|
21316
|
+
const row = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
21317
|
+
const campaignCount = app.db.select().from(adsCampaigns).where(eq22(adsCampaigns.projectId, project.id)).all().length;
|
|
21318
|
+
const adGroupCount = app.db.select().from(adsAdGroups).where(eq22(adsAdGroups.projectId, project.id)).all().length;
|
|
21319
|
+
const adCount = app.db.select().from(adsAds).where(eq22(adsAds.projectId, project.id)).all().length;
|
|
21320
|
+
const campaignInsights = app.db.select().from(adsInsightsDaily).where(and15(
|
|
21321
|
+
eq22(adsInsightsDaily.projectId, project.id),
|
|
21322
|
+
eq22(adsInsightsDaily.level, "campaign")
|
|
21103
21323
|
)).all();
|
|
21104
21324
|
let impressions = 0;
|
|
21105
21325
|
let clicks = 0;
|
|
@@ -21136,7 +21356,7 @@ async function adsRoutes(app, opts) {
|
|
|
21136
21356
|
|
|
21137
21357
|
// ../api-routes/src/bing.ts
|
|
21138
21358
|
import crypto19 from "crypto";
|
|
21139
|
-
import { eq as
|
|
21359
|
+
import { eq as eq23, and as and16, desc as desc12 } from "drizzle-orm";
|
|
21140
21360
|
|
|
21141
21361
|
// ../integration-bing/src/constants.ts
|
|
21142
21362
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -21510,7 +21730,7 @@ async function bingRoutes(app, opts) {
|
|
|
21510
21730
|
const store = requireConnectionStore();
|
|
21511
21731
|
const project = resolveProject(app.db, request.params.name);
|
|
21512
21732
|
requireConnection(store, project.canonicalDomain);
|
|
21513
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
21733
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq23(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
21514
21734
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
21515
21735
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
21516
21736
|
for (const row of allInspections) {
|
|
@@ -21599,7 +21819,7 @@ async function bingRoutes(app, opts) {
|
|
|
21599
21819
|
const project = resolveProject(app.db, request.params.name);
|
|
21600
21820
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
21601
21821
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
21602
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
21822
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq23(bingCoverageSnapshots.projectId, project.id)).orderBy(desc12(bingCoverageSnapshots.date)).limit(limit).all();
|
|
21603
21823
|
return rows.map((r) => ({
|
|
21604
21824
|
date: r.date,
|
|
21605
21825
|
indexed: r.indexed,
|
|
@@ -21611,8 +21831,8 @@ async function bingRoutes(app, opts) {
|
|
|
21611
21831
|
requireConnectionStore();
|
|
21612
21832
|
const project = resolveProject(app.db, request.params.name);
|
|
21613
21833
|
const { url, limit } = request.query;
|
|
21614
|
-
const whereClause = url ?
|
|
21615
|
-
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(
|
|
21834
|
+
const whereClause = url ? and16(eq23(bingUrlInspections.projectId, project.id), eq23(bingUrlInspections.url, url)) : eq23(bingUrlInspections.projectId, project.id);
|
|
21835
|
+
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc12(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
21616
21836
|
return filtered.map((r) => ({
|
|
21617
21837
|
id: r.id,
|
|
21618
21838
|
url: r.url,
|
|
@@ -21701,7 +21921,7 @@ async function bingRoutes(app, opts) {
|
|
|
21701
21921
|
anchorCount: result.AnchorCount ?? null,
|
|
21702
21922
|
discoveryDate
|
|
21703
21923
|
}).run();
|
|
21704
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
21924
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq23(runs.id, runId)).run();
|
|
21705
21925
|
return {
|
|
21706
21926
|
id,
|
|
21707
21927
|
url,
|
|
@@ -21717,7 +21937,7 @@ async function bingRoutes(app, opts) {
|
|
|
21717
21937
|
} catch (e) {
|
|
21718
21938
|
const msg = e instanceof Error ? e.message : String(e);
|
|
21719
21939
|
bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
|
|
21720
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
21940
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
|
|
21721
21941
|
throw e;
|
|
21722
21942
|
}
|
|
21723
21943
|
});
|
|
@@ -21744,7 +21964,7 @@ async function bingRoutes(app, opts) {
|
|
|
21744
21964
|
} else {
|
|
21745
21965
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
21746
21966
|
}
|
|
21747
|
-
const run = app.db.select().from(runs).where(
|
|
21967
|
+
const run = app.db.select().from(runs).where(eq23(runs.id, runId)).get();
|
|
21748
21968
|
return run;
|
|
21749
21969
|
});
|
|
21750
21970
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -21756,7 +21976,7 @@ async function bingRoutes(app, opts) {
|
|
|
21756
21976
|
}
|
|
21757
21977
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
21758
21978
|
if (request.body?.allUnindexed) {
|
|
21759
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
21979
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq23(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
21760
21980
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
21761
21981
|
for (const row of allInspections) {
|
|
21762
21982
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -21843,14 +22063,14 @@ async function bingRoutes(app, opts) {
|
|
|
21843
22063
|
import fs from "fs";
|
|
21844
22064
|
import path from "path";
|
|
21845
22065
|
import os from "os";
|
|
21846
|
-
import { eq as
|
|
22066
|
+
import { eq as eq24, and as and17 } from "drizzle-orm";
|
|
21847
22067
|
function getScreenshotDir() {
|
|
21848
22068
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
21849
22069
|
}
|
|
21850
22070
|
async function cdpRoutes(app, opts) {
|
|
21851
22071
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
21852
22072
|
const { snapshotId } = request.params;
|
|
21853
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(
|
|
22073
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq24(querySnapshots.id, snapshotId)).get();
|
|
21854
22074
|
if (!snapshot?.screenshotPath) {
|
|
21855
22075
|
const err = notFound("Screenshot", snapshotId);
|
|
21856
22076
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -21917,7 +22137,7 @@ async function cdpRoutes(app, opts) {
|
|
|
21917
22137
|
async (request, reply) => {
|
|
21918
22138
|
const project = resolveProject(app.db, request.params.name);
|
|
21919
22139
|
const { runId } = request.params;
|
|
21920
|
-
const run = app.db.select().from(runs).where(
|
|
22140
|
+
const run = app.db.select().from(runs).where(and17(eq24(runs.id, runId), eq24(runs.projectId, project.id))).get();
|
|
21921
22141
|
if (!run) {
|
|
21922
22142
|
const err = notFound("Run", runId);
|
|
21923
22143
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -21930,8 +22150,8 @@ async function cdpRoutes(app, opts) {
|
|
|
21930
22150
|
citedDomains: querySnapshots.citedDomains,
|
|
21931
22151
|
screenshotPath: querySnapshots.screenshotPath,
|
|
21932
22152
|
rawResponse: querySnapshots.rawResponse
|
|
21933
|
-
}).from(querySnapshots).where(
|
|
21934
|
-
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
22153
|
+
}).from(querySnapshots).where(eq24(querySnapshots.runId, runId)).all());
|
|
22154
|
+
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq24(queries.projectId, project.id)).all();
|
|
21935
22155
|
const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
|
|
21936
22156
|
const byQuery = /* @__PURE__ */ new Map();
|
|
21937
22157
|
for (const snap of snapshots) {
|
|
@@ -22014,7 +22234,7 @@ async function cdpRoutes(app, opts) {
|
|
|
22014
22234
|
|
|
22015
22235
|
// ../api-routes/src/ga.ts
|
|
22016
22236
|
import crypto20 from "crypto";
|
|
22017
|
-
import { eq as
|
|
22237
|
+
import { eq as eq25, desc as desc13, and as and18, sql as sql9 } from "drizzle-orm";
|
|
22018
22238
|
function gaLog(level, action, ctx) {
|
|
22019
22239
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
22020
22240
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -22221,10 +22441,10 @@ async function ga4Routes(app, opts) {
|
|
|
22221
22441
|
if (!saConn && !oauthConn) {
|
|
22222
22442
|
throw notFound("GA4 connection", project.name);
|
|
22223
22443
|
}
|
|
22224
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
22225
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
22226
|
-
app.db.delete(gaAiReferrals).where(
|
|
22227
|
-
app.db.delete(gaSocialReferrals).where(
|
|
22444
|
+
app.db.delete(gaTrafficSnapshots).where(eq25(gaTrafficSnapshots.projectId, project.id)).run();
|
|
22445
|
+
app.db.delete(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).run();
|
|
22446
|
+
app.db.delete(gaAiReferrals).where(eq25(gaAiReferrals.projectId, project.id)).run();
|
|
22447
|
+
app.db.delete(gaSocialReferrals).where(eq25(gaSocialReferrals.projectId, project.id)).run();
|
|
22228
22448
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
22229
22449
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
22230
22450
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -22245,7 +22465,7 @@ async function ga4Routes(app, opts) {
|
|
|
22245
22465
|
if (!connected) {
|
|
22246
22466
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
22247
22467
|
}
|
|
22248
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
22468
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
22249
22469
|
return {
|
|
22250
22470
|
connected: true,
|
|
22251
22471
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -22309,8 +22529,8 @@ async function ga4Routes(app, opts) {
|
|
|
22309
22529
|
app.db.transaction((tx) => {
|
|
22310
22530
|
if (syncTraffic) {
|
|
22311
22531
|
tx.delete(gaTrafficSnapshots).where(
|
|
22312
|
-
|
|
22313
|
-
|
|
22532
|
+
and18(
|
|
22533
|
+
eq25(gaTrafficSnapshots.projectId, project.id),
|
|
22314
22534
|
sql9`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
22315
22535
|
sql9`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
22316
22536
|
)
|
|
@@ -22333,8 +22553,8 @@ async function ga4Routes(app, opts) {
|
|
|
22333
22553
|
}
|
|
22334
22554
|
if (syncAi) {
|
|
22335
22555
|
tx.delete(gaAiReferrals).where(
|
|
22336
|
-
|
|
22337
|
-
|
|
22556
|
+
and18(
|
|
22557
|
+
eq25(gaAiReferrals.projectId, project.id),
|
|
22338
22558
|
sql9`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
22339
22559
|
sql9`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
22340
22560
|
)
|
|
@@ -22359,8 +22579,8 @@ async function ga4Routes(app, opts) {
|
|
|
22359
22579
|
}
|
|
22360
22580
|
if (syncSocial) {
|
|
22361
22581
|
tx.delete(gaSocialReferrals).where(
|
|
22362
|
-
|
|
22363
|
-
|
|
22582
|
+
and18(
|
|
22583
|
+
eq25(gaSocialReferrals.projectId, project.id),
|
|
22364
22584
|
sql9`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
22365
22585
|
sql9`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
22366
22586
|
)
|
|
@@ -22381,7 +22601,7 @@ async function ga4Routes(app, opts) {
|
|
|
22381
22601
|
}
|
|
22382
22602
|
}
|
|
22383
22603
|
if (syncSummary) {
|
|
22384
|
-
tx.delete(gaTrafficSummaries).where(
|
|
22604
|
+
tx.delete(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).run();
|
|
22385
22605
|
tx.insert(gaTrafficSummaries).values({
|
|
22386
22606
|
id: crypto20.randomUUID(),
|
|
22387
22607
|
projectId: project.id,
|
|
@@ -22393,7 +22613,7 @@ async function ga4Routes(app, opts) {
|
|
|
22393
22613
|
syncedAt: now,
|
|
22394
22614
|
syncRunId: runId
|
|
22395
22615
|
}).run();
|
|
22396
|
-
tx.delete(gaTrafficWindowSummaries).where(
|
|
22616
|
+
tx.delete(gaTrafficWindowSummaries).where(eq25(gaTrafficWindowSummaries.projectId, project.id)).run();
|
|
22397
22617
|
for (const ws of windowSummaries) {
|
|
22398
22618
|
tx.insert(gaTrafficWindowSummaries).values({
|
|
22399
22619
|
id: crypto20.randomUUID(),
|
|
@@ -22411,7 +22631,7 @@ async function ga4Routes(app, opts) {
|
|
|
22411
22631
|
}
|
|
22412
22632
|
}
|
|
22413
22633
|
});
|
|
22414
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
22634
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq25(runs.id, runId)).run();
|
|
22415
22635
|
const syncedComponents = only ? [
|
|
22416
22636
|
...syncTraffic ? ["traffic"] : [],
|
|
22417
22637
|
...syncSummary ? ["summary"] : [],
|
|
@@ -22440,7 +22660,7 @@ async function ga4Routes(app, opts) {
|
|
|
22440
22660
|
} catch (e) {
|
|
22441
22661
|
const msg = e instanceof Error ? e.message : String(e);
|
|
22442
22662
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
|
|
22443
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
22663
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
|
|
22444
22664
|
throw e;
|
|
22445
22665
|
}
|
|
22446
22666
|
});
|
|
@@ -22451,11 +22671,11 @@ async function ga4Routes(app, opts) {
|
|
|
22451
22671
|
const window = parseWindow(request.query.window);
|
|
22452
22672
|
const cutoff = windowCutoff(window);
|
|
22453
22673
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
22454
|
-
const snapshotConditions = [
|
|
22674
|
+
const snapshotConditions = [eq25(gaTrafficSnapshots.projectId, project.id)];
|
|
22455
22675
|
if (cutoffDate) snapshotConditions.push(sql9`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
22456
|
-
const aiConditions = [
|
|
22676
|
+
const aiConditions = [eq25(gaAiReferrals.projectId, project.id)];
|
|
22457
22677
|
if (cutoffDate) aiConditions.push(sql9`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
22458
|
-
const socialConditions = [
|
|
22678
|
+
const socialConditions = [eq25(gaSocialReferrals.projectId, project.id)];
|
|
22459
22679
|
if (cutoffDate) socialConditions.push(sql9`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
22460
22680
|
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
22461
22681
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
@@ -22463,42 +22683,42 @@ async function ga4Routes(app, opts) {
|
|
|
22463
22683
|
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
22464
22684
|
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
22465
22685
|
}).from(gaTrafficWindowSummaries).where(
|
|
22466
|
-
|
|
22467
|
-
|
|
22468
|
-
|
|
22686
|
+
and18(
|
|
22687
|
+
eq25(gaTrafficWindowSummaries.projectId, project.id),
|
|
22688
|
+
eq25(gaTrafficWindowSummaries.windowKey, window)
|
|
22469
22689
|
)
|
|
22470
22690
|
).get() : null;
|
|
22471
22691
|
const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
|
|
22472
22692
|
totalSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
22473
22693
|
totalOrganicSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
22474
22694
|
totalUsers: sql9`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
22475
|
-
}).from(gaTrafficSnapshots).where(
|
|
22695
|
+
}).from(gaTrafficSnapshots).where(and18(...snapshotConditions)).get() : null;
|
|
22476
22696
|
const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
22477
22697
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
22478
22698
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
22479
22699
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
22480
|
-
}).from(gaTrafficSummaries).where(
|
|
22700
|
+
}).from(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).get();
|
|
22481
22701
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
22482
22702
|
totalDirectSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
22483
|
-
}).from(gaTrafficSnapshots).where(
|
|
22703
|
+
}).from(gaTrafficSnapshots).where(and18(...snapshotConditions)).get();
|
|
22484
22704
|
const summaryMeta = app.db.select({
|
|
22485
22705
|
periodStart: gaTrafficSummaries.periodStart,
|
|
22486
22706
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
22487
|
-
}).from(gaTrafficSummaries).where(
|
|
22707
|
+
}).from(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).get();
|
|
22488
22708
|
const rows = app.db.select({
|
|
22489
22709
|
landingPage: sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
22490
22710
|
sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
|
|
22491
22711
|
organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
22492
22712
|
directSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
|
|
22493
22713
|
users: sql9`SUM(${gaTrafficSnapshots.users})`
|
|
22494
|
-
}).from(gaTrafficSnapshots).where(
|
|
22714
|
+
}).from(gaTrafficSnapshots).where(and18(...snapshotConditions)).groupBy(sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql9`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
22495
22715
|
const aiReferralRows = app.db.select({
|
|
22496
22716
|
source: gaAiReferrals.source,
|
|
22497
22717
|
medium: gaAiReferrals.medium,
|
|
22498
22718
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
22499
22719
|
sessions: sql9`SUM(${gaAiReferrals.sessions})`,
|
|
22500
22720
|
users: sql9`SUM(${gaAiReferrals.users})`
|
|
22501
|
-
}).from(gaAiReferrals).where(
|
|
22721
|
+
}).from(gaAiReferrals).where(and18(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
|
|
22502
22722
|
const aiReferralLandingPageRows = app.db.select({
|
|
22503
22723
|
source: gaAiReferrals.source,
|
|
22504
22724
|
medium: gaAiReferrals.medium,
|
|
@@ -22506,7 +22726,7 @@ async function ga4Routes(app, opts) {
|
|
|
22506
22726
|
landingPage: sql9`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
22507
22727
|
sessions: sql9`SUM(${gaAiReferrals.sessions})`,
|
|
22508
22728
|
users: sql9`SUM(${gaAiReferrals.users})`
|
|
22509
|
-
}).from(gaAiReferrals).where(
|
|
22729
|
+
}).from(gaAiReferrals).where(and18(...aiConditions)).groupBy(
|
|
22510
22730
|
gaAiReferrals.source,
|
|
22511
22731
|
gaAiReferrals.medium,
|
|
22512
22732
|
gaAiReferrals.sourceDimension,
|
|
@@ -22543,7 +22763,7 @@ async function ga4Routes(app, opts) {
|
|
|
22543
22763
|
channelGroup: gaAiReferrals.channelGroup,
|
|
22544
22764
|
sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
|
|
22545
22765
|
users: sql9`COALESCE(SUM(${gaAiReferrals.users}), 0)`
|
|
22546
|
-
}).from(gaAiReferrals).where(
|
|
22766
|
+
}).from(gaAiReferrals).where(and18(...aiConditions, eq25(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
|
|
22547
22767
|
const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
|
|
22548
22768
|
let aiBySessionUsers = 0;
|
|
22549
22769
|
for (const row of aiBySessionRows) {
|
|
@@ -22557,12 +22777,12 @@ async function ga4Routes(app, opts) {
|
|
|
22557
22777
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
22558
22778
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`,
|
|
22559
22779
|
users: sql9`SUM(${gaSocialReferrals.users})`
|
|
22560
|
-
}).from(gaSocialReferrals).where(
|
|
22780
|
+
}).from(gaSocialReferrals).where(and18(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql9`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
22561
22781
|
const socialTotals = app.db.select({
|
|
22562
22782
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`,
|
|
22563
22783
|
users: sql9`SUM(${gaSocialReferrals.users})`
|
|
22564
|
-
}).from(gaSocialReferrals).where(
|
|
22565
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
22784
|
+
}).from(gaSocialReferrals).where(and18(...socialConditions)).get();
|
|
22785
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
22566
22786
|
const total = summaryRow?.totalSessions ?? 0;
|
|
22567
22787
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
22568
22788
|
const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
|
|
@@ -22642,7 +22862,7 @@ async function ga4Routes(app, opts) {
|
|
|
22642
22862
|
const project = resolveProject(app.db, request.params.name);
|
|
22643
22863
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
22644
22864
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
22645
|
-
const conditions = [
|
|
22865
|
+
const conditions = [eq25(gaAiReferrals.projectId, project.id)];
|
|
22646
22866
|
if (cutoffDate) conditions.push(sql9`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
22647
22867
|
const rows = app.db.select({
|
|
22648
22868
|
date: gaAiReferrals.date,
|
|
@@ -22652,7 +22872,7 @@ async function ga4Routes(app, opts) {
|
|
|
22652
22872
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
22653
22873
|
sessions: sql9`SUM(${gaAiReferrals.sessions})`,
|
|
22654
22874
|
users: sql9`SUM(${gaAiReferrals.users})`
|
|
22655
|
-
}).from(gaAiReferrals).where(
|
|
22875
|
+
}).from(gaAiReferrals).where(and18(...conditions)).groupBy(
|
|
22656
22876
|
gaAiReferrals.date,
|
|
22657
22877
|
gaAiReferrals.source,
|
|
22658
22878
|
gaAiReferrals.medium,
|
|
@@ -22665,7 +22885,7 @@ async function ga4Routes(app, opts) {
|
|
|
22665
22885
|
const project = resolveProject(app.db, request.params.name);
|
|
22666
22886
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
22667
22887
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
22668
|
-
const conditions = [
|
|
22888
|
+
const conditions = [eq25(gaSocialReferrals.projectId, project.id)];
|
|
22669
22889
|
if (cutoffDate) conditions.push(sql9`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
22670
22890
|
const rows = app.db.select({
|
|
22671
22891
|
date: gaSocialReferrals.date,
|
|
@@ -22674,7 +22894,7 @@ async function ga4Routes(app, opts) {
|
|
|
22674
22894
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
22675
22895
|
sessions: gaSocialReferrals.sessions,
|
|
22676
22896
|
users: gaSocialReferrals.users
|
|
22677
|
-
}).from(gaSocialReferrals).where(
|
|
22897
|
+
}).from(gaSocialReferrals).where(and18(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
22678
22898
|
return rows;
|
|
22679
22899
|
});
|
|
22680
22900
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -22687,8 +22907,8 @@ async function ga4Routes(app, opts) {
|
|
|
22687
22907
|
d.setDate(d.getDate() - n);
|
|
22688
22908
|
return fmt(d);
|
|
22689
22909
|
};
|
|
22690
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
22691
|
-
|
|
22910
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and18(
|
|
22911
|
+
eq25(gaSocialReferrals.projectId, project.id),
|
|
22692
22912
|
sql9`${gaSocialReferrals.date} >= ${from}`,
|
|
22693
22913
|
sql9`${gaSocialReferrals.date} < ${to}`
|
|
22694
22914
|
)).get();
|
|
@@ -22700,16 +22920,16 @@ async function ga4Routes(app, opts) {
|
|
|
22700
22920
|
const sourceCurrent = app.db.select({
|
|
22701
22921
|
source: gaSocialReferrals.source,
|
|
22702
22922
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`
|
|
22703
|
-
}).from(gaSocialReferrals).where(
|
|
22704
|
-
|
|
22923
|
+
}).from(gaSocialReferrals).where(and18(
|
|
22924
|
+
eq25(gaSocialReferrals.projectId, project.id),
|
|
22705
22925
|
sql9`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
|
|
22706
22926
|
sql9`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
22707
22927
|
)).groupBy(gaSocialReferrals.source).all();
|
|
22708
22928
|
const sourcePrev = app.db.select({
|
|
22709
22929
|
source: gaSocialReferrals.source,
|
|
22710
22930
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`
|
|
22711
|
-
}).from(gaSocialReferrals).where(
|
|
22712
|
-
|
|
22931
|
+
}).from(gaSocialReferrals).where(and18(
|
|
22932
|
+
eq25(gaSocialReferrals.projectId, project.id),
|
|
22713
22933
|
sql9`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
|
|
22714
22934
|
sql9`${gaSocialReferrals.date} < ${daysAgo(7)}`
|
|
22715
22935
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -22750,16 +22970,16 @@ async function ga4Routes(app, opts) {
|
|
|
22750
22970
|
return fmt(d);
|
|
22751
22971
|
};
|
|
22752
22972
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
22753
|
-
const sumTotal = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
22754
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
22755
|
-
const sumDirect = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
22756
|
-
const sumAi = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
22757
|
-
|
|
22973
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and18(eq25(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
22974
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and18(eq25(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
22975
|
+
const sumDirect = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and18(eq25(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
22976
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and18(
|
|
22977
|
+
eq25(gaAiReferrals.projectId, project.id),
|
|
22758
22978
|
sql9`${gaAiReferrals.date} >= ${from}`,
|
|
22759
22979
|
sql9`${gaAiReferrals.date} < ${to}`,
|
|
22760
|
-
|
|
22980
|
+
eq25(gaAiReferrals.sourceDimension, "session")
|
|
22761
22981
|
)).get();
|
|
22762
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
22982
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and18(eq25(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${from}`, sql9`${gaSocialReferrals.date} < ${to}`)).get();
|
|
22763
22983
|
const todayStr = fmt(today);
|
|
22764
22984
|
const buildTrend = (sum) => {
|
|
22765
22985
|
const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
|
|
@@ -22768,17 +22988,17 @@ async function ga4Routes(app, opts) {
|
|
|
22768
22988
|
const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
|
|
22769
22989
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
22770
22990
|
};
|
|
22771
|
-
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
22772
|
-
|
|
22991
|
+
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and18(
|
|
22992
|
+
eq25(gaAiReferrals.projectId, project.id),
|
|
22773
22993
|
sql9`${gaAiReferrals.date} >= ${daysAgo(7)}`,
|
|
22774
22994
|
sql9`${gaAiReferrals.date} < ${todayStr}`,
|
|
22775
|
-
|
|
22995
|
+
eq25(gaAiReferrals.sourceDimension, "session")
|
|
22776
22996
|
)).groupBy(gaAiReferrals.source).all();
|
|
22777
|
-
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
22778
|
-
|
|
22997
|
+
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and18(
|
|
22998
|
+
eq25(gaAiReferrals.projectId, project.id),
|
|
22779
22999
|
sql9`${gaAiReferrals.date} >= ${daysAgo(14)}`,
|
|
22780
23000
|
sql9`${gaAiReferrals.date} < ${daysAgo(7)}`,
|
|
22781
|
-
|
|
23001
|
+
eq25(gaAiReferrals.sourceDimension, "session")
|
|
22782
23002
|
)).groupBy(gaAiReferrals.source).all();
|
|
22783
23003
|
const findBiggestMover = (current, prev) => {
|
|
22784
23004
|
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
@@ -22794,8 +23014,8 @@ async function ga4Routes(app, opts) {
|
|
|
22794
23014
|
}
|
|
22795
23015
|
return mover;
|
|
22796
23016
|
};
|
|
22797
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
22798
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
23017
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and18(eq25(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql9`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
23018
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and18(eq25(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql9`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
22799
23019
|
return {
|
|
22800
23020
|
total: buildTrend(sumTotal),
|
|
22801
23021
|
organic: buildTrend(sumOrganic),
|
|
@@ -22810,14 +23030,14 @@ async function ga4Routes(app, opts) {
|
|
|
22810
23030
|
const project = resolveProject(app.db, request.params.name);
|
|
22811
23031
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
22812
23032
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
22813
|
-
const conditions = [
|
|
23033
|
+
const conditions = [eq25(gaTrafficSnapshots.projectId, project.id)];
|
|
22814
23034
|
if (cutoffDate) conditions.push(sql9`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
22815
23035
|
const rows = app.db.select({
|
|
22816
23036
|
date: gaTrafficSnapshots.date,
|
|
22817
23037
|
sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
|
|
22818
23038
|
organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
22819
23039
|
users: sql9`SUM(${gaTrafficSnapshots.users})`
|
|
22820
|
-
}).from(gaTrafficSnapshots).where(
|
|
23040
|
+
}).from(gaTrafficSnapshots).where(and18(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
22821
23041
|
return rows.map((r) => ({
|
|
22822
23042
|
date: r.date,
|
|
22823
23043
|
sessions: r.sessions ?? 0,
|
|
@@ -22833,7 +23053,7 @@ async function ga4Routes(app, opts) {
|
|
|
22833
23053
|
sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
|
|
22834
23054
|
organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
22835
23055
|
users: sql9`SUM(${gaTrafficSnapshots.users})`
|
|
22836
|
-
}).from(gaTrafficSnapshots).where(
|
|
23056
|
+
}).from(gaTrafficSnapshots).where(eq25(gaTrafficSnapshots.projectId, project.id)).groupBy(sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql9`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
22837
23057
|
return {
|
|
22838
23058
|
pages: trafficPages.map((r) => ({
|
|
22839
23059
|
landingPage: r.landingPage,
|
|
@@ -24481,7 +24701,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
24481
24701
|
|
|
24482
24702
|
// ../api-routes/src/backlinks.ts
|
|
24483
24703
|
import crypto22 from "crypto";
|
|
24484
|
-
import { and as
|
|
24704
|
+
import { and as and20, asc as asc3, desc as desc14, eq as eq26, sql as sql10 } from "drizzle-orm";
|
|
24485
24705
|
|
|
24486
24706
|
// ../integration-commoncrawl/src/constants.ts
|
|
24487
24707
|
import os2 from "os";
|
|
@@ -24884,7 +25104,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
24884
25104
|
}
|
|
24885
25105
|
|
|
24886
25106
|
// ../api-routes/src/backlinks-filter.ts
|
|
24887
|
-
import { and as
|
|
25107
|
+
import { and as and19, ne as ne3, notLike } from "drizzle-orm";
|
|
24888
25108
|
var BACKLINK_FILTER_PATTERNS = [
|
|
24889
25109
|
"*.google.com",
|
|
24890
25110
|
"*.googleusercontent.com",
|
|
@@ -24907,7 +25127,7 @@ function backlinkCrawlerExclusionClause() {
|
|
|
24907
25127
|
conditions.push(ne3(backlinkDomains.linkingDomain, pattern));
|
|
24908
25128
|
}
|
|
24909
25129
|
}
|
|
24910
|
-
const combined =
|
|
25130
|
+
const combined = and19(...conditions);
|
|
24911
25131
|
if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
|
|
24912
25132
|
return combined;
|
|
24913
25133
|
}
|
|
@@ -24978,11 +25198,11 @@ function mapRunRow(row) {
|
|
|
24978
25198
|
}
|
|
24979
25199
|
function latestSummaryForProject(db, projectId, source, release) {
|
|
24980
25200
|
const conditions = [
|
|
24981
|
-
|
|
24982
|
-
|
|
25201
|
+
eq26(backlinkSummaries.projectId, projectId),
|
|
25202
|
+
eq26(backlinkSummaries.source, source)
|
|
24983
25203
|
];
|
|
24984
|
-
if (release) conditions.push(
|
|
24985
|
-
return db.select().from(backlinkSummaries).where(
|
|
25204
|
+
if (release) conditions.push(eq26(backlinkSummaries.release, release));
|
|
25205
|
+
return db.select().from(backlinkSummaries).where(and20(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
24986
25206
|
}
|
|
24987
25207
|
function parseExcludeCrawlers(value) {
|
|
24988
25208
|
if (!value) return false;
|
|
@@ -24990,12 +25210,12 @@ function parseExcludeCrawlers(value) {
|
|
|
24990
25210
|
return lower === "1" || lower === "true" || lower === "yes";
|
|
24991
25211
|
}
|
|
24992
25212
|
function computeFilteredSummary(db, base) {
|
|
24993
|
-
const baseDomainCondition =
|
|
24994
|
-
|
|
24995
|
-
|
|
24996
|
-
|
|
25213
|
+
const baseDomainCondition = and20(
|
|
25214
|
+
eq26(backlinkDomains.projectId, base.projectId),
|
|
25215
|
+
eq26(backlinkDomains.source, base.source),
|
|
25216
|
+
eq26(backlinkDomains.release, base.release)
|
|
24997
25217
|
);
|
|
24998
|
-
const filteredCondition =
|
|
25218
|
+
const filteredCondition = and20(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
24999
25219
|
const unfilteredAgg = db.select({
|
|
25000
25220
|
count: sql10`count(*)`,
|
|
25001
25221
|
total: sql10`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
@@ -25004,7 +25224,7 @@ function computeFilteredSummary(db, base) {
|
|
|
25004
25224
|
count: sql10`count(*)`,
|
|
25005
25225
|
total: sql10`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
25006
25226
|
}).from(backlinkDomains).where(filteredCondition).get();
|
|
25007
|
-
const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(
|
|
25227
|
+
const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc14(backlinkDomains.numHosts)).limit(10).all();
|
|
25008
25228
|
const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
|
|
25009
25229
|
const totalHosts = Number(filteredAgg?.total ?? 0);
|
|
25010
25230
|
const unfilteredLinkingDomains = Number(unfilteredAgg?.count ?? 0);
|
|
@@ -25025,13 +25245,13 @@ function computeFilteredSummary(db, base) {
|
|
|
25025
25245
|
};
|
|
25026
25246
|
}
|
|
25027
25247
|
function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
|
|
25028
|
-
const summary = db.select().from(backlinkSummaries).where(
|
|
25248
|
+
const summary = db.select().from(backlinkSummaries).where(and20(eq26(backlinkSummaries.projectId, projectId), eq26(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
25029
25249
|
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
25030
25250
|
if (summary && excludeCrawlers) {
|
|
25031
|
-
const filtered = db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(
|
|
25032
|
-
|
|
25033
|
-
|
|
25034
|
-
|
|
25251
|
+
const filtered = db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(and20(
|
|
25252
|
+
eq26(backlinkDomains.projectId, projectId),
|
|
25253
|
+
eq26(backlinkDomains.source, source),
|
|
25254
|
+
eq26(backlinkDomains.release, summary.release),
|
|
25035
25255
|
backlinkCrawlerExclusionClause()
|
|
25036
25256
|
)).get();
|
|
25037
25257
|
totalLinkingDomains = Number(filtered?.count ?? 0);
|
|
@@ -25046,7 +25266,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
|
|
|
25046
25266
|
};
|
|
25047
25267
|
}
|
|
25048
25268
|
function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
|
|
25049
|
-
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
25269
|
+
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
25050
25270
|
const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
|
|
25051
25271
|
const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
|
|
25052
25272
|
const sources = [
|
|
@@ -25102,7 +25322,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25102
25322
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
25103
25323
|
);
|
|
25104
25324
|
}
|
|
25105
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
25325
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.release, release)).get();
|
|
25106
25326
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
25107
25327
|
if (existing) {
|
|
25108
25328
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -25113,9 +25333,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
25113
25333
|
phaseDetail: null,
|
|
25114
25334
|
error: null,
|
|
25115
25335
|
updatedAt: now
|
|
25116
|
-
}).where(
|
|
25336
|
+
}).where(eq26(ccReleaseSyncs.id, existing.id)).run();
|
|
25117
25337
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
25118
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
25338
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.id, existing.id)).get();
|
|
25119
25339
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
25120
25340
|
}
|
|
25121
25341
|
const id = crypto22.randomUUID();
|
|
@@ -25127,15 +25347,15 @@ async function backlinksRoutes(app, opts) {
|
|
|
25127
25347
|
updatedAt: now
|
|
25128
25348
|
}).run();
|
|
25129
25349
|
opts.onReleaseSyncRequested(id, release);
|
|
25130
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
25350
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.id, id)).get();
|
|
25131
25351
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
25132
25352
|
});
|
|
25133
25353
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
25134
|
-
const row = app.db.select().from(ccReleaseSyncs).orderBy(
|
|
25354
|
+
const row = app.db.select().from(ccReleaseSyncs).orderBy(desc14(ccReleaseSyncs.updatedAt)).limit(1).get();
|
|
25135
25355
|
return reply.send(row ? mapSyncRow(row) : null);
|
|
25136
25356
|
});
|
|
25137
25357
|
app.get("/backlinks/syncs", async (_request, reply) => {
|
|
25138
|
-
const rows = app.db.select().from(ccReleaseSyncs).orderBy(
|
|
25358
|
+
const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc14(ccReleaseSyncs.updatedAt)).all();
|
|
25139
25359
|
return reply.send(rows.map(mapSyncRow));
|
|
25140
25360
|
});
|
|
25141
25361
|
app.get("/backlinks/releases", async (_request, reply) => {
|
|
@@ -25185,7 +25405,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25185
25405
|
createdAt: now
|
|
25186
25406
|
}).run();
|
|
25187
25407
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
25188
|
-
const run = app.db.select().from(runs).where(
|
|
25408
|
+
const run = app.db.select().from(runs).where(eq26(runs.id, runId)).get();
|
|
25189
25409
|
return reply.status(201).send(mapRunRow(run));
|
|
25190
25410
|
});
|
|
25191
25411
|
app.get(
|
|
@@ -25211,18 +25431,18 @@ async function backlinksRoutes(app, opts) {
|
|
|
25211
25431
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
25212
25432
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
25213
25433
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
25214
|
-
const baseDomainCondition =
|
|
25215
|
-
|
|
25216
|
-
|
|
25217
|
-
|
|
25434
|
+
const baseDomainCondition = and20(
|
|
25435
|
+
eq26(backlinkDomains.projectId, project.id),
|
|
25436
|
+
eq26(backlinkDomains.source, source),
|
|
25437
|
+
eq26(backlinkDomains.release, targetRelease)
|
|
25218
25438
|
);
|
|
25219
|
-
const domainCondition = excludeCrawlers ?
|
|
25439
|
+
const domainCondition = excludeCrawlers ? and20(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
25220
25440
|
const totalRow = app.db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
25221
25441
|
const rows = app.db.select({
|
|
25222
25442
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
25223
25443
|
numHosts: backlinkDomains.numHosts,
|
|
25224
25444
|
source: backlinkDomains.source
|
|
25225
|
-
}).from(backlinkDomains).where(domainCondition).orderBy(
|
|
25445
|
+
}).from(backlinkDomains).where(domainCondition).orderBy(desc14(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
|
|
25226
25446
|
let summary = null;
|
|
25227
25447
|
if (summaryRow) {
|
|
25228
25448
|
summary = excludeCrawlers ? computeFilteredSummary(app.db, summaryRow) : mapSummaryRow(summaryRow);
|
|
@@ -25240,7 +25460,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25240
25460
|
async (request, reply) => {
|
|
25241
25461
|
const project = resolveProject(app.db, request.params.name);
|
|
25242
25462
|
const source = parseSourceParam(request.query.source);
|
|
25243
|
-
const rows = app.db.select().from(backlinkSummaries).where(
|
|
25463
|
+
const rows = app.db.select().from(backlinkSummaries).where(and20(eq26(backlinkSummaries.projectId, project.id), eq26(backlinkSummaries.source, source))).orderBy(asc3(backlinkSummaries.queriedAt)).all();
|
|
25244
25464
|
const response = rows.map((r) => ({
|
|
25245
25465
|
release: r.release,
|
|
25246
25466
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -25287,7 +25507,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25287
25507
|
createdAt: now
|
|
25288
25508
|
}).run();
|
|
25289
25509
|
opts.onBingBacklinkSyncRequested(runId, project.id);
|
|
25290
|
-
const run = app.db.select().from(runs).where(
|
|
25510
|
+
const run = app.db.select().from(runs).where(eq26(runs.id, runId)).get();
|
|
25291
25511
|
return reply.status(201).send(mapRunRow(run));
|
|
25292
25512
|
}
|
|
25293
25513
|
);
|
|
@@ -25296,7 +25516,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25296
25516
|
// ../api-routes/src/traffic.ts
|
|
25297
25517
|
import crypto24 from "crypto";
|
|
25298
25518
|
import { Agent as UndiciAgent } from "undici";
|
|
25299
|
-
import { and as
|
|
25519
|
+
import { and as and21, desc as desc15, eq as eq27, gte as gte4, lte as lte3, sql as sql11 } from "drizzle-orm";
|
|
25300
25520
|
|
|
25301
25521
|
// ../integration-cloud-run/src/auth.ts
|
|
25302
25522
|
import crypto23 from "crypto";
|
|
@@ -29113,8 +29333,8 @@ async function runBackfillTask(options) {
|
|
|
29113
29333
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
29114
29334
|
try {
|
|
29115
29335
|
app.db.transaction((tx) => {
|
|
29116
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
29117
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
29336
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq27(runs.id, runId)).run();
|
|
29337
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq27(trafficSources.id, sourceRow.id)).run();
|
|
29118
29338
|
});
|
|
29119
29339
|
} catch {
|
|
29120
29340
|
}
|
|
@@ -29129,7 +29349,7 @@ async function runBackfillTask(options) {
|
|
|
29129
29349
|
if (allEvents.length === 0) {
|
|
29130
29350
|
const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
29131
29351
|
try {
|
|
29132
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(
|
|
29352
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq27(runs.id, runId)).run();
|
|
29133
29353
|
} catch {
|
|
29134
29354
|
}
|
|
29135
29355
|
return;
|
|
@@ -29151,29 +29371,29 @@ async function runBackfillTask(options) {
|
|
|
29151
29371
|
try {
|
|
29152
29372
|
app.db.transaction((tx) => {
|
|
29153
29373
|
tx.delete(crawlerEventsHourly).where(
|
|
29154
|
-
|
|
29155
|
-
|
|
29374
|
+
and21(
|
|
29375
|
+
eq27(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
29156
29376
|
gte4(crawlerEventsHourly.tsHour, windowStartIso),
|
|
29157
29377
|
lte3(crawlerEventsHourly.tsHour, windowEndIso)
|
|
29158
29378
|
)
|
|
29159
29379
|
).run();
|
|
29160
29380
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
29161
|
-
|
|
29162
|
-
|
|
29381
|
+
and21(
|
|
29382
|
+
eq27(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
29163
29383
|
gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
29164
29384
|
lte3(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
29165
29385
|
)
|
|
29166
29386
|
).run();
|
|
29167
29387
|
tx.delete(aiReferralEventsHourly).where(
|
|
29168
|
-
|
|
29169
|
-
|
|
29388
|
+
and21(
|
|
29389
|
+
eq27(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
29170
29390
|
gte4(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
29171
29391
|
lte3(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
29172
29392
|
)
|
|
29173
29393
|
).run();
|
|
29174
29394
|
tx.delete(rawEventSamples).where(
|
|
29175
|
-
|
|
29176
|
-
|
|
29395
|
+
and21(
|
|
29396
|
+
eq27(rawEventSamples.sourceId, sourceRow.id),
|
|
29177
29397
|
gte4(rawEventSamples.ts, windowStartIso),
|
|
29178
29398
|
lte3(rawEventSamples.ts, windowEndIso)
|
|
29179
29399
|
)
|
|
@@ -29262,8 +29482,8 @@ async function runBackfillTask(options) {
|
|
|
29262
29482
|
lastError: null,
|
|
29263
29483
|
lastEventIds: newRingBuffer,
|
|
29264
29484
|
updatedAt: finishedAt
|
|
29265
|
-
}).where(
|
|
29266
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
29485
|
+
}).where(eq27(trafficSources.id, sourceRow.id)).run();
|
|
29486
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq27(runs.id, runId)).run();
|
|
29267
29487
|
});
|
|
29268
29488
|
} catch (e) {
|
|
29269
29489
|
markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -29345,7 +29565,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29345
29565
|
createdAt: existing?.createdAt ?? now,
|
|
29346
29566
|
updatedAt: now
|
|
29347
29567
|
});
|
|
29348
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
29568
|
+
const activeSource = app.db.select().from(trafficSources).where(eq27(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
|
|
29349
29569
|
const config = {
|
|
29350
29570
|
gcpProjectId,
|
|
29351
29571
|
serviceName: serviceName ?? null,
|
|
@@ -29361,8 +29581,8 @@ async function trafficRoutes(app, opts) {
|
|
|
29361
29581
|
lastError: null,
|
|
29362
29582
|
configJson: config,
|
|
29363
29583
|
updatedAt: now
|
|
29364
|
-
}).where(
|
|
29365
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29584
|
+
}).where(eq27(trafficSources.id, activeSource.id)).run();
|
|
29585
|
+
sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, activeSource.id)).get();
|
|
29366
29586
|
} else {
|
|
29367
29587
|
const newId = crypto24.randomUUID();
|
|
29368
29588
|
app.db.insert(trafficSources).values({
|
|
@@ -29379,7 +29599,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29379
29599
|
createdAt: now,
|
|
29380
29600
|
updatedAt: now
|
|
29381
29601
|
}).run();
|
|
29382
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29602
|
+
sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, newId)).get();
|
|
29383
29603
|
}
|
|
29384
29604
|
writeAuditLog(app.db, {
|
|
29385
29605
|
projectId: project.id,
|
|
@@ -29431,7 +29651,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29431
29651
|
createdAt: existing?.createdAt ?? now,
|
|
29432
29652
|
updatedAt: now
|
|
29433
29653
|
});
|
|
29434
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
29654
|
+
const activeSource = app.db.select().from(trafficSources).where(eq27(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
|
|
29435
29655
|
const config = { baseUrl, username };
|
|
29436
29656
|
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
29437
29657
|
let sourceRow;
|
|
@@ -29442,8 +29662,8 @@ async function trafficRoutes(app, opts) {
|
|
|
29442
29662
|
lastError: null,
|
|
29443
29663
|
configJson: config,
|
|
29444
29664
|
updatedAt: now
|
|
29445
|
-
}).where(
|
|
29446
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29665
|
+
}).where(eq27(trafficSources.id, activeSource.id)).run();
|
|
29666
|
+
sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, activeSource.id)).get();
|
|
29447
29667
|
} else {
|
|
29448
29668
|
const newId = crypto24.randomUUID();
|
|
29449
29669
|
app.db.insert(trafficSources).values({
|
|
@@ -29460,7 +29680,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29460
29680
|
createdAt: now,
|
|
29461
29681
|
updatedAt: now
|
|
29462
29682
|
}).run();
|
|
29463
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29683
|
+
sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, newId)).get();
|
|
29464
29684
|
}
|
|
29465
29685
|
writeAuditLog(app.db, {
|
|
29466
29686
|
projectId: project.id,
|
|
@@ -29514,7 +29734,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29514
29734
|
createdAt: existing?.createdAt ?? now,
|
|
29515
29735
|
updatedAt: now
|
|
29516
29736
|
});
|
|
29517
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
29737
|
+
const activeSource = app.db.select().from(trafficSources).where(eq27(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
|
|
29518
29738
|
const config = { projectId, teamId, environment };
|
|
29519
29739
|
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
29520
29740
|
const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
|
|
@@ -29526,8 +29746,8 @@ async function trafficRoutes(app, opts) {
|
|
|
29526
29746
|
lastError: null,
|
|
29527
29747
|
configJson: config,
|
|
29528
29748
|
updatedAt: now
|
|
29529
|
-
}).where(
|
|
29530
|
-
row = tx.select().from(trafficSources).where(
|
|
29749
|
+
}).where(eq27(trafficSources.id, activeSource.id)).run();
|
|
29750
|
+
row = tx.select().from(trafficSources).where(eq27(trafficSources.id, activeSource.id)).get();
|
|
29531
29751
|
} else {
|
|
29532
29752
|
const newId = crypto24.randomUUID();
|
|
29533
29753
|
tx.insert(trafficSources).values({
|
|
@@ -29552,12 +29772,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29552
29772
|
createdAt: now,
|
|
29553
29773
|
updatedAt: now
|
|
29554
29774
|
}).run();
|
|
29555
|
-
row = tx.select().from(trafficSources).where(
|
|
29775
|
+
row = tx.select().from(trafficSources).where(eq27(trafficSources.id, newId)).get();
|
|
29556
29776
|
}
|
|
29557
29777
|
const existingSchedule = tx.select().from(schedules).where(
|
|
29558
|
-
|
|
29559
|
-
|
|
29560
|
-
|
|
29778
|
+
and21(
|
|
29779
|
+
eq27(schedules.projectId, project.id),
|
|
29780
|
+
eq27(schedules.kind, SchedulableRunKinds["traffic-sync"])
|
|
29561
29781
|
)
|
|
29562
29782
|
).get();
|
|
29563
29783
|
let created = false;
|
|
@@ -29606,7 +29826,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29606
29826
|
});
|
|
29607
29827
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
29608
29828
|
const project = resolveProject(app.db, request.params.name);
|
|
29609
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
29829
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, request.params.id)).get();
|
|
29610
29830
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
29611
29831
|
throw notFound("Traffic source", request.params.id);
|
|
29612
29832
|
}
|
|
@@ -29632,8 +29852,8 @@ async function trafficRoutes(app, opts) {
|
|
|
29632
29852
|
const markFailed = (msg, errorCode) => {
|
|
29633
29853
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
29634
29854
|
app.db.transaction((tx) => {
|
|
29635
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
29636
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
29855
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq27(runs.id, runId)).run();
|
|
29856
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq27(trafficSources.id, sourceRow.id)).run();
|
|
29637
29857
|
});
|
|
29638
29858
|
try {
|
|
29639
29859
|
opts.onTrafficSynced?.({
|
|
@@ -29714,7 +29934,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29714
29934
|
}
|
|
29715
29935
|
const credential = credentialStore.getConnection(project.name);
|
|
29716
29936
|
if (!credential) {
|
|
29717
|
-
app.db.delete(runs).where(
|
|
29937
|
+
app.db.delete(runs).where(eq27(runs.id, runId)).run();
|
|
29718
29938
|
throw validationError(
|
|
29719
29939
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
29720
29940
|
);
|
|
@@ -29763,12 +29983,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29763
29983
|
auditAction = "traffic.vercel.synced";
|
|
29764
29984
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
29765
29985
|
if (!credentialStore) {
|
|
29766
|
-
app.db.delete(runs).where(
|
|
29986
|
+
app.db.delete(runs).where(eq27(runs.id, runId)).run();
|
|
29767
29987
|
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
29768
29988
|
}
|
|
29769
29989
|
const credential = credentialStore.getConnection(project.name);
|
|
29770
29990
|
if (!credential) {
|
|
29771
|
-
app.db.delete(runs).where(
|
|
29991
|
+
app.db.delete(runs).where(eq27(runs.id, runId)).run();
|
|
29772
29992
|
throw validationError(
|
|
29773
29993
|
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
29774
29994
|
);
|
|
@@ -29868,7 +30088,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29868
30088
|
let aiReferralHitsCount = 0;
|
|
29869
30089
|
let unknownHitsCount = 0;
|
|
29870
30090
|
app.db.transaction((tx) => {
|
|
29871
|
-
const latestRow = tx.select().from(trafficSources).where(
|
|
30091
|
+
const latestRow = tx.select().from(trafficSources).where(eq27(trafficSources.id, sourceRow.id)).get();
|
|
29872
30092
|
const previousIds = latestRow.lastEventIds ?? [];
|
|
29873
30093
|
const seenEventIds = new Set(previousIds);
|
|
29874
30094
|
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
@@ -30036,8 +30256,8 @@ async function trafficRoutes(app, opts) {
|
|
|
30036
30256
|
if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
30037
30257
|
sourceUpdate.lastCursor = nextCursor ?? null;
|
|
30038
30258
|
}
|
|
30039
|
-
tx.update(trafficSources).set(sourceUpdate).where(
|
|
30040
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
30259
|
+
tx.update(trafficSources).set(sourceUpdate).where(eq27(trafficSources.id, sourceRow.id)).run();
|
|
30260
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq27(runs.id, runId)).run();
|
|
30041
30261
|
});
|
|
30042
30262
|
writeAuditLog(app.db, {
|
|
30043
30263
|
projectId: project.id,
|
|
@@ -30089,7 +30309,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30089
30309
|
});
|
|
30090
30310
|
app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
|
|
30091
30311
|
const project = resolveProject(app.db, request.params.name);
|
|
30092
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
30312
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, request.params.id)).get();
|
|
30093
30313
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
30094
30314
|
throw notFound("Traffic source", request.params.id);
|
|
30095
30315
|
}
|
|
@@ -30274,36 +30494,36 @@ async function trafficRoutes(app, opts) {
|
|
|
30274
30494
|
});
|
|
30275
30495
|
function buildSourceDetail(projectId, row, since) {
|
|
30276
30496
|
const crawlerTotals = app.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
30277
|
-
|
|
30278
|
-
|
|
30497
|
+
and21(
|
|
30498
|
+
eq27(crawlerEventsHourly.sourceId, row.id),
|
|
30279
30499
|
gte4(crawlerEventsHourly.tsHour, since)
|
|
30280
30500
|
)
|
|
30281
30501
|
).get();
|
|
30282
30502
|
const aiUserFetchTotals = app.db.select({ total: sql11`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
30283
|
-
|
|
30284
|
-
|
|
30503
|
+
and21(
|
|
30504
|
+
eq27(aiUserFetchEventsHourly.sourceId, row.id),
|
|
30285
30505
|
gte4(aiUserFetchEventsHourly.tsHour, since)
|
|
30286
30506
|
)
|
|
30287
30507
|
).get();
|
|
30288
30508
|
const aiTotals = app.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
30289
|
-
|
|
30290
|
-
|
|
30509
|
+
and21(
|
|
30510
|
+
eq27(aiReferralEventsHourly.sourceId, row.id),
|
|
30291
30511
|
gte4(aiReferralEventsHourly.tsHour, since)
|
|
30292
30512
|
)
|
|
30293
30513
|
).get();
|
|
30294
30514
|
const sampleTotals = app.db.select({ total: sql11`COUNT(*)` }).from(rawEventSamples).where(
|
|
30295
|
-
|
|
30296
|
-
|
|
30515
|
+
and21(
|
|
30516
|
+
eq27(rawEventSamples.sourceId, row.id),
|
|
30297
30517
|
gte4(rawEventSamples.ts, since)
|
|
30298
30518
|
)
|
|
30299
30519
|
).get();
|
|
30300
30520
|
const latestRun = app.db.select().from(runs).where(
|
|
30301
|
-
|
|
30302
|
-
|
|
30303
|
-
|
|
30304
|
-
|
|
30521
|
+
and21(
|
|
30522
|
+
eq27(runs.projectId, projectId),
|
|
30523
|
+
eq27(runs.kind, RunKinds["traffic-sync"]),
|
|
30524
|
+
eq27(runs.sourceId, row.id)
|
|
30305
30525
|
)
|
|
30306
|
-
).orderBy(
|
|
30526
|
+
).orderBy(desc15(runs.startedAt)).limit(1).get();
|
|
30307
30527
|
return {
|
|
30308
30528
|
...rowToDto(row),
|
|
30309
30529
|
totals24h: {
|
|
@@ -30329,7 +30549,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30329
30549
|
"`advanceToNow` must be `true`. There is no implicit reset."
|
|
30330
30550
|
);
|
|
30331
30551
|
}
|
|
30332
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
30552
|
+
const sourceRow = app.db.select().from(trafficSources).where(and21(eq27(trafficSources.projectId, project.id), eq27(trafficSources.id, request.params.id))).get();
|
|
30333
30553
|
if (!sourceRow) {
|
|
30334
30554
|
throw notFound("traffic source", request.params.id);
|
|
30335
30555
|
}
|
|
@@ -30346,7 +30566,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30346
30566
|
status: TrafficSourceStatuses.connected,
|
|
30347
30567
|
lastError: null,
|
|
30348
30568
|
updatedAt: now
|
|
30349
|
-
}).where(
|
|
30569
|
+
}).where(eq27(trafficSources.id, sourceRow.id)).run();
|
|
30350
30570
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
30351
30571
|
projectId: project.id,
|
|
30352
30572
|
actor: "api",
|
|
@@ -30354,20 +30574,20 @@ async function trafficRoutes(app, opts) {
|
|
|
30354
30574
|
entityType: "traffic_source",
|
|
30355
30575
|
entityId: sourceRow.id
|
|
30356
30576
|
}));
|
|
30357
|
-
updatedRow = tx.select().from(trafficSources).where(
|
|
30577
|
+
updatedRow = tx.select().from(trafficSources).where(eq27(trafficSources.id, sourceRow.id)).get();
|
|
30358
30578
|
});
|
|
30359
30579
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
30360
30580
|
});
|
|
30361
30581
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
30362
30582
|
const project = resolveProject(app.db, request.params.name);
|
|
30363
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
30583
|
+
const rows = app.db.select().from(trafficSources).where(eq27(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
30364
30584
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
30365
30585
|
const response = { sources };
|
|
30366
30586
|
return response;
|
|
30367
30587
|
});
|
|
30368
30588
|
app.get("/projects/:name/traffic/status", async (request) => {
|
|
30369
30589
|
const project = resolveProject(app.db, request.params.name);
|
|
30370
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
30590
|
+
const rows = app.db.select().from(trafficSources).where(eq27(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
30371
30591
|
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
30372
30592
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
30373
30593
|
const response = { sources };
|
|
@@ -30377,7 +30597,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30377
30597
|
"/projects/:name/traffic/sources/:id",
|
|
30378
30598
|
async (request) => {
|
|
30379
30599
|
const project = resolveProject(app.db, request.params.name);
|
|
30380
|
-
const row = app.db.select().from(trafficSources).where(
|
|
30600
|
+
const row = app.db.select().from(trafficSources).where(eq27(trafficSources.id, request.params.id)).get();
|
|
30381
30601
|
if (!row || row.projectId !== project.id) {
|
|
30382
30602
|
throw notFound("Traffic source", request.params.id);
|
|
30383
30603
|
}
|
|
@@ -30428,15 +30648,15 @@ async function trafficRoutes(app, opts) {
|
|
|
30428
30648
|
let aiReferralTotal = 0;
|
|
30429
30649
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
30430
30650
|
const crawlerFilters = [
|
|
30431
|
-
|
|
30651
|
+
eq27(crawlerEventsHourly.projectId, project.id),
|
|
30432
30652
|
gte4(crawlerEventsHourly.tsHour, sinceIso),
|
|
30433
30653
|
lte3(crawlerEventsHourly.tsHour, untilIso)
|
|
30434
30654
|
];
|
|
30435
|
-
if (sourceIdParam) crawlerFilters.push(
|
|
30436
|
-
const crawlerWhere =
|
|
30655
|
+
if (sourceIdParam) crawlerFilters.push(eq27(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
30656
|
+
const crawlerWhere = and21(...crawlerFilters);
|
|
30437
30657
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
|
|
30438
30658
|
crawlerTotal = Number(total?.total ?? 0);
|
|
30439
|
-
const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(
|
|
30659
|
+
const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc15(crawlerEventsHourly.tsHour)).limit(limit).all();
|
|
30440
30660
|
for (const r of rows) {
|
|
30441
30661
|
events.push({
|
|
30442
30662
|
kind: TrafficEventKinds.crawler,
|
|
@@ -30453,15 +30673,15 @@ async function trafficRoutes(app, opts) {
|
|
|
30453
30673
|
}
|
|
30454
30674
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
30455
30675
|
const userFetchFilters = [
|
|
30456
|
-
|
|
30676
|
+
eq27(aiUserFetchEventsHourly.projectId, project.id),
|
|
30457
30677
|
gte4(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
30458
30678
|
lte3(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
30459
30679
|
];
|
|
30460
|
-
if (sourceIdParam) userFetchFilters.push(
|
|
30461
|
-
const userFetchWhere =
|
|
30680
|
+
if (sourceIdParam) userFetchFilters.push(eq27(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
30681
|
+
const userFetchWhere = and21(...userFetchFilters);
|
|
30462
30682
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
30463
30683
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
30464
|
-
const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(
|
|
30684
|
+
const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(desc15(aiUserFetchEventsHourly.tsHour)).limit(limit).all();
|
|
30465
30685
|
for (const r of rows) {
|
|
30466
30686
|
events.push({
|
|
30467
30687
|
kind: TrafficEventKinds["ai-user-fetch"],
|
|
@@ -30478,15 +30698,15 @@ async function trafficRoutes(app, opts) {
|
|
|
30478
30698
|
}
|
|
30479
30699
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
30480
30700
|
const aiFilters = [
|
|
30481
|
-
|
|
30701
|
+
eq27(aiReferralEventsHourly.projectId, project.id),
|
|
30482
30702
|
gte4(aiReferralEventsHourly.tsHour, sinceIso),
|
|
30483
30703
|
lte3(aiReferralEventsHourly.tsHour, untilIso)
|
|
30484
30704
|
];
|
|
30485
|
-
if (sourceIdParam) aiFilters.push(
|
|
30486
|
-
const aiWhere =
|
|
30705
|
+
if (sourceIdParam) aiFilters.push(eq27(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
30706
|
+
const aiWhere = and21(...aiFilters);
|
|
30487
30707
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
30488
30708
|
aiReferralTotal = Number(total?.total ?? 0);
|
|
30489
|
-
const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(
|
|
30709
|
+
const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc15(aiReferralEventsHourly.tsHour)).limit(limit).all();
|
|
30490
30710
|
for (const r of rows) {
|
|
30491
30711
|
events.push({
|
|
30492
30712
|
kind: TrafficEventKinds["ai-referral"],
|
|
@@ -30687,7 +30907,7 @@ function readInstalledManifest(skillDir) {
|
|
|
30687
30907
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
30688
30908
|
|
|
30689
30909
|
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
30690
|
-
import { and as
|
|
30910
|
+
import { and as and22, eq as eq28 } from "drizzle-orm";
|
|
30691
30911
|
function skippedNoProject() {
|
|
30692
30912
|
return {
|
|
30693
30913
|
status: CheckStatuses.skipped,
|
|
@@ -30703,8 +30923,8 @@ var BACKLINKS_CHECKS = [
|
|
|
30703
30923
|
title: "Backlinks source connected",
|
|
30704
30924
|
run: (ctx) => {
|
|
30705
30925
|
if (!ctx.project) return skippedNoProject();
|
|
30706
|
-
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(
|
|
30707
|
-
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
30926
|
+
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq28(projects.id, ctx.project.id)).get();
|
|
30927
|
+
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
30708
30928
|
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
30709
30929
|
const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
|
|
30710
30930
|
const connected = [];
|
|
@@ -30719,9 +30939,9 @@ var BACKLINKS_CHECKS = [
|
|
|
30719
30939
|
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
|
|
30720
30940
|
};
|
|
30721
30941
|
}
|
|
30722
|
-
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(
|
|
30723
|
-
|
|
30724
|
-
|
|
30942
|
+
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and22(
|
|
30943
|
+
eq28(backlinkSummaries.projectId, ctx.project.id),
|
|
30944
|
+
eq28(backlinkSummaries.source, BacklinkSources.commoncrawl)
|
|
30725
30945
|
)).limit(1).get() : false;
|
|
30726
30946
|
return {
|
|
30727
30947
|
status: CheckStatuses.ok,
|
|
@@ -30881,7 +31101,7 @@ var BING_AUTH_CHECKS = [
|
|
|
30881
31101
|
];
|
|
30882
31102
|
|
|
30883
31103
|
// ../api-routes/src/doctor/checks/content.ts
|
|
30884
|
-
import { eq as
|
|
31104
|
+
import { eq as eq29 } from "drizzle-orm";
|
|
30885
31105
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
30886
31106
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
30887
31107
|
function skippedNoProject2() {
|
|
@@ -30894,7 +31114,7 @@ function skippedNoProject2() {
|
|
|
30894
31114
|
}
|
|
30895
31115
|
function loadProject(ctx) {
|
|
30896
31116
|
if (!ctx.project) return null;
|
|
30897
|
-
return ctx.db.select().from(projects).where(
|
|
31117
|
+
return ctx.db.select().from(projects).where(eq29(projects.id, ctx.project.id)).get() ?? null;
|
|
30898
31118
|
}
|
|
30899
31119
|
function percent(value) {
|
|
30900
31120
|
return Math.round(value * 100);
|
|
@@ -30986,7 +31206,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
30986
31206
|
);
|
|
30987
31207
|
|
|
30988
31208
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
30989
|
-
import { eq as
|
|
31209
|
+
import { eq as eq30 } from "drizzle-orm";
|
|
30990
31210
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
30991
31211
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
30992
31212
|
var adsConnectionCheck = {
|
|
@@ -31003,7 +31223,7 @@ var adsConnectionCheck = {
|
|
|
31003
31223
|
remediation: null
|
|
31004
31224
|
};
|
|
31005
31225
|
}
|
|
31006
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
31226
|
+
const row = ctx.db.select().from(adsConnections).where(eq30(adsConnections.projectId, ctx.project.id)).get();
|
|
31007
31227
|
if (!row) {
|
|
31008
31228
|
return {
|
|
31009
31229
|
status: CheckStatuses.skipped,
|
|
@@ -31053,7 +31273,7 @@ var adsRecentSyncCheck = {
|
|
|
31053
31273
|
remediation: null
|
|
31054
31274
|
};
|
|
31055
31275
|
}
|
|
31056
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
31276
|
+
const row = ctx.db.select().from(adsConnections).where(eq30(adsConnections.projectId, ctx.project.id)).get();
|
|
31057
31277
|
if (!row) {
|
|
31058
31278
|
return {
|
|
31059
31279
|
status: CheckStatuses.skipped,
|
|
@@ -31244,7 +31464,7 @@ var ga4ConnectionCheck = {
|
|
|
31244
31464
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
31245
31465
|
|
|
31246
31466
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
31247
|
-
import { and as
|
|
31467
|
+
import { and as and23, eq as eq31 } from "drizzle-orm";
|
|
31248
31468
|
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
31249
31469
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
31250
31470
|
function skippedNoProject3() {
|
|
@@ -31477,7 +31697,7 @@ var recentSyncCheck = {
|
|
|
31477
31697
|
title: "GBP recent sync",
|
|
31478
31698
|
run: (ctx) => {
|
|
31479
31699
|
if (!ctx.project) return skippedNoProject3();
|
|
31480
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(
|
|
31700
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and23(eq31(gbpLocations.projectId, ctx.project.id), eq31(gbpLocations.selected, true))).all();
|
|
31481
31701
|
if (selected.length === 0) {
|
|
31482
31702
|
return {
|
|
31483
31703
|
status: CheckStatuses.skipped,
|
|
@@ -31537,7 +31757,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
31537
31757
|
);
|
|
31538
31758
|
|
|
31539
31759
|
// ../api-routes/src/doctor/checks/places.ts
|
|
31540
|
-
import { eq as
|
|
31760
|
+
import { eq as eq32 } from "drizzle-orm";
|
|
31541
31761
|
var apiKeyCheck = {
|
|
31542
31762
|
id: "gbp.places.api-key",
|
|
31543
31763
|
category: CheckCategories.auth,
|
|
@@ -31582,7 +31802,7 @@ var apiKeyCheck = {
|
|
|
31582
31802
|
details: { tier: cfg.tier }
|
|
31583
31803
|
};
|
|
31584
31804
|
}
|
|
31585
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
31805
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq32(gbpLocations.projectId, ctx.project.id)).all();
|
|
31586
31806
|
const selected = rows.filter((r) => r.selected);
|
|
31587
31807
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
31588
31808
|
const details = {
|
|
@@ -32033,7 +32253,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
32033
32253
|
];
|
|
32034
32254
|
|
|
32035
32255
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
32036
|
-
import { and as
|
|
32256
|
+
import { and as and24, eq as eq33, gte as gte5, ne as ne4, sql as sql12 } from "drizzle-orm";
|
|
32037
32257
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
32038
32258
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
32039
32259
|
function skippedNoProject5() {
|
|
@@ -32047,8 +32267,8 @@ function skippedNoProject5() {
|
|
|
32047
32267
|
function loadProbes(ctx) {
|
|
32048
32268
|
if (!ctx.project) return [];
|
|
32049
32269
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
32050
|
-
|
|
32051
|
-
|
|
32270
|
+
and24(
|
|
32271
|
+
eq33(trafficSources.projectId, ctx.project.id),
|
|
32052
32272
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
32053
32273
|
)
|
|
32054
32274
|
).all();
|
|
@@ -32128,16 +32348,16 @@ var recentDataCheck = {
|
|
|
32128
32348
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
32129
32349
|
const recentCrawlers = Number(
|
|
32130
32350
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
32131
|
-
|
|
32132
|
-
|
|
32351
|
+
and24(
|
|
32352
|
+
eq33(crawlerEventsHourly.projectId, ctx.project.id),
|
|
32133
32353
|
gte5(crawlerEventsHourly.tsHour, warnCutoff)
|
|
32134
32354
|
)
|
|
32135
32355
|
).get()?.total ?? 0
|
|
32136
32356
|
);
|
|
32137
32357
|
const recentReferrals = Number(
|
|
32138
32358
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
32139
|
-
|
|
32140
|
-
|
|
32359
|
+
and24(
|
|
32360
|
+
eq33(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
32141
32361
|
gte5(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
32142
32362
|
)
|
|
32143
32363
|
).get()?.total ?? 0
|
|
@@ -32152,16 +32372,16 @@ var recentDataCheck = {
|
|
|
32152
32372
|
}
|
|
32153
32373
|
const olderCrawlers = Number(
|
|
32154
32374
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
32155
|
-
|
|
32156
|
-
|
|
32375
|
+
and24(
|
|
32376
|
+
eq33(crawlerEventsHourly.projectId, ctx.project.id),
|
|
32157
32377
|
gte5(crawlerEventsHourly.tsHour, failCutoff)
|
|
32158
32378
|
)
|
|
32159
32379
|
).get()?.total ?? 0
|
|
32160
32380
|
);
|
|
32161
32381
|
const olderReferrals = Number(
|
|
32162
32382
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
32163
|
-
|
|
32164
|
-
|
|
32383
|
+
and24(
|
|
32384
|
+
eq33(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
32165
32385
|
gte5(aiReferralEventsHourly.tsHour, failCutoff)
|
|
32166
32386
|
)
|
|
32167
32387
|
).get()?.total ?? 0
|
|
@@ -32575,7 +32795,7 @@ async function doctorRoutes(app, opts) {
|
|
|
32575
32795
|
|
|
32576
32796
|
// ../api-routes/src/discovery/routes.ts
|
|
32577
32797
|
import crypto26 from "crypto";
|
|
32578
|
-
import { and as
|
|
32798
|
+
import { and as and25, desc as desc16, eq as eq34, gte as gte6, inArray as inArray12 } from "drizzle-orm";
|
|
32579
32799
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
32580
32800
|
async function discoveryRoutes(app, opts) {
|
|
32581
32801
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -32607,16 +32827,16 @@ async function discoveryRoutes(app, opts) {
|
|
|
32607
32827
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32608
32828
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
32609
32829
|
const decision = app.db.transaction((tx) => {
|
|
32610
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
32611
|
-
|
|
32612
|
-
|
|
32613
|
-
|
|
32830
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and25(
|
|
32831
|
+
eq34(discoverySessions.projectId, project.id),
|
|
32832
|
+
eq34(discoverySessions.icpDescription, icpDescription),
|
|
32833
|
+
inArray12(discoverySessions.status, [
|
|
32614
32834
|
DiscoverySessionStatuses.queued,
|
|
32615
32835
|
DiscoverySessionStatuses.seeding,
|
|
32616
32836
|
DiscoverySessionStatuses.probing
|
|
32617
32837
|
]),
|
|
32618
32838
|
gte6(discoverySessions.createdAt, ageFloorIso)
|
|
32619
|
-
)).orderBy(
|
|
32839
|
+
)).orderBy(desc16(discoverySessions.createdAt)).get();
|
|
32620
32840
|
if (existing && existing.runId) {
|
|
32621
32841
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
32622
32842
|
}
|
|
@@ -32679,7 +32899,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32679
32899
|
const project = resolveProject(app.db, request.params.name);
|
|
32680
32900
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
32681
32901
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
32682
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
32902
|
+
const rows = app.db.select().from(discoverySessions).where(eq34(discoverySessions.projectId, project.id)).orderBy(desc16(discoverySessions.createdAt)).limit(limit).all();
|
|
32683
32903
|
return reply.send(rows.map(serializeSession));
|
|
32684
32904
|
}
|
|
32685
32905
|
);
|
|
@@ -32687,11 +32907,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
32687
32907
|
"/projects/:name/discover/sessions/:id",
|
|
32688
32908
|
async (request, reply) => {
|
|
32689
32909
|
const project = resolveProject(app.db, request.params.name);
|
|
32690
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32910
|
+
const session = app.db.select().from(discoverySessions).where(eq34(discoverySessions.id, request.params.id)).get();
|
|
32691
32911
|
if (!session || session.projectId !== project.id) {
|
|
32692
32912
|
throw notFound("Discovery session", request.params.id);
|
|
32693
32913
|
}
|
|
32694
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32914
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq34(discoveryProbes.sessionId, session.id)).all();
|
|
32695
32915
|
const detail = {
|
|
32696
32916
|
...serializeSession(session),
|
|
32697
32917
|
probes: probeRows.map(serializeProbe)
|
|
@@ -32699,16 +32919,82 @@ async function discoveryRoutes(app, opts) {
|
|
|
32699
32919
|
return reply.send(detail);
|
|
32700
32920
|
}
|
|
32701
32921
|
);
|
|
32922
|
+
app.get(
|
|
32923
|
+
"/projects/:name/discover/sessions/:id/harvest",
|
|
32924
|
+
async (request, reply) => {
|
|
32925
|
+
const project = resolveProject(app.db, request.params.name);
|
|
32926
|
+
const session = app.db.select().from(discoverySessions).where(eq34(discoverySessions.id, request.params.id)).get();
|
|
32927
|
+
if (!session || session.projectId !== project.id) {
|
|
32928
|
+
throw notFound("Discovery session", request.params.id);
|
|
32929
|
+
}
|
|
32930
|
+
const parsedFloor = parseInt(request.query.minProbeHits ?? "", 10);
|
|
32931
|
+
const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
|
|
32932
|
+
const applyAnchor = request.query.anchor !== "false";
|
|
32933
|
+
const provider = session.seedProvider ?? "gemini";
|
|
32934
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq34(discoveryProbes.sessionId, session.id)).all();
|
|
32935
|
+
const extract = opts.harvestSearchQueries;
|
|
32936
|
+
const probesWithQueries = probeRows.map((row) => {
|
|
32937
|
+
if (!extract || !row.rawResponse) return { searchQueries: [] };
|
|
32938
|
+
try {
|
|
32939
|
+
const raw = JSON.parse(row.rawResponse);
|
|
32940
|
+
return { searchQueries: extract({ provider, rawResponse: raw }) };
|
|
32941
|
+
} catch {
|
|
32942
|
+
return { searchQueries: [] };
|
|
32943
|
+
}
|
|
32944
|
+
});
|
|
32945
|
+
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq34(queries.projectId, project.id)).all().map((r) => r.query);
|
|
32946
|
+
const anchorTerms = buildHarvestAnchorTerms(
|
|
32947
|
+
[session.icpDescription ?? "", ...trackedQueries],
|
|
32948
|
+
effectiveDomains(project)
|
|
32949
|
+
);
|
|
32950
|
+
const aggregated = aggregateHarvestedQueries(probesWithQueries);
|
|
32951
|
+
let result = gateHarvestedSearchQueries({
|
|
32952
|
+
candidates: aggregated,
|
|
32953
|
+
trackedQueries,
|
|
32954
|
+
anchorTerms,
|
|
32955
|
+
minProbeHits,
|
|
32956
|
+
applyAnchor
|
|
32957
|
+
});
|
|
32958
|
+
let semanticNoveltyApplied = false;
|
|
32959
|
+
if (opts.embedQueries && result.admitted.length > 0 && trackedQueries.length > 0) {
|
|
32960
|
+
try {
|
|
32961
|
+
const candidateTexts = result.admitted.map((c) => c.query);
|
|
32962
|
+
const vectors = await opts.embedQueries([...candidateTexts, ...trackedQueries]);
|
|
32963
|
+
if (vectors.length === candidateTexts.length + trackedQueries.length) {
|
|
32964
|
+
result = applyHarvestSemanticNovelty({
|
|
32965
|
+
result,
|
|
32966
|
+
candidateVectors: vectors.slice(0, candidateTexts.length),
|
|
32967
|
+
trackedVectors: vectors.slice(candidateTexts.length)
|
|
32968
|
+
});
|
|
32969
|
+
semanticNoveltyApplied = true;
|
|
32970
|
+
}
|
|
32971
|
+
} catch {
|
|
32972
|
+
}
|
|
32973
|
+
}
|
|
32974
|
+
const harvest = {
|
|
32975
|
+
sessionId: session.id,
|
|
32976
|
+
projectId: project.id,
|
|
32977
|
+
provider,
|
|
32978
|
+
status: session.status,
|
|
32979
|
+
minProbeHits,
|
|
32980
|
+
anchorApplied: result.anchorApplied,
|
|
32981
|
+
semanticNoveltyApplied,
|
|
32982
|
+
candidates: result.admitted,
|
|
32983
|
+
stats: result.stats
|
|
32984
|
+
};
|
|
32985
|
+
return reply.send(harvest);
|
|
32986
|
+
}
|
|
32987
|
+
);
|
|
32702
32988
|
app.get(
|
|
32703
32989
|
"/projects/:name/discover/sessions/:id/promote",
|
|
32704
32990
|
async (request, reply) => {
|
|
32705
32991
|
const project = resolveProject(app.db, request.params.name);
|
|
32706
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32992
|
+
const session = app.db.select().from(discoverySessions).where(eq34(discoverySessions.id, request.params.id)).get();
|
|
32707
32993
|
if (!session || session.projectId !== project.id) {
|
|
32708
32994
|
throw notFound("Discovery session", request.params.id);
|
|
32709
32995
|
}
|
|
32710
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32711
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32996
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq34(discoveryProbes.sessionId, session.id)).all();
|
|
32997
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
32712
32998
|
const seenCompetitors = new Set(existingCompetitors);
|
|
32713
32999
|
const cited = /* @__PURE__ */ new Set();
|
|
32714
33000
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -32737,7 +33023,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32737
33023
|
);
|
|
32738
33024
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
32739
33025
|
const project = resolveProject(app.db, request.params.name);
|
|
32740
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
33026
|
+
const session = app.db.select().from(discoverySessions).where(eq34(discoverySessions.id, request.params.id)).get();
|
|
32741
33027
|
if (!session || session.projectId !== project.id) {
|
|
32742
33028
|
throw notFound("Discovery session", request.params.id);
|
|
32743
33029
|
}
|
|
@@ -32760,7 +33046,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32760
33046
|
const bucketSet = new Set(buckets);
|
|
32761
33047
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
32762
33048
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
32763
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
33049
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq34(discoveryProbes.sessionId, session.id)).all();
|
|
32764
33050
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
32765
33051
|
for (const probe of probeRows) {
|
|
32766
33052
|
if (!probe.bucket) continue;
|
|
@@ -32768,7 +33054,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32768
33054
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
32769
33055
|
}
|
|
32770
33056
|
const existingQueries = new Set(
|
|
32771
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
33057
|
+
app.db.select({ query: queries.query }).from(queries).where(eq34(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
32772
33058
|
);
|
|
32773
33059
|
const promotedQueries = [];
|
|
32774
33060
|
const skippedQueries = [];
|
|
@@ -32784,7 +33070,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32784
33070
|
const skippedCompetitors = [];
|
|
32785
33071
|
if (includeCompetitors) {
|
|
32786
33072
|
const existingCompetitors = new Set(
|
|
32787
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
33073
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
32788
33074
|
);
|
|
32789
33075
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
32790
33076
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -32891,7 +33177,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
32891
33177
|
|
|
32892
33178
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
32893
33179
|
import crypto27 from "crypto";
|
|
32894
|
-
import { eq as
|
|
33180
|
+
import { eq as eq35 } from "drizzle-orm";
|
|
32895
33181
|
var DEFAULT_MAX_PROBES = 100;
|
|
32896
33182
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
32897
33183
|
function classifyProbeBucket(input) {
|
|
@@ -32904,7 +33190,7 @@ function classifyProbeBucket(input) {
|
|
|
32904
33190
|
}
|
|
32905
33191
|
function buildCompetitorMap(probes, project, classification = {}) {
|
|
32906
33192
|
const canonical = new Set(project.canonicalDomains.map((d) => d.toLowerCase()));
|
|
32907
|
-
const
|
|
33193
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
32908
33194
|
for (const probe of probes) {
|
|
32909
33195
|
const seenInProbe = /* @__PURE__ */ new Set();
|
|
32910
33196
|
for (const raw of probe.citedDomains) {
|
|
@@ -32912,10 +33198,10 @@ function buildCompetitorMap(probes, project, classification = {}) {
|
|
|
32912
33198
|
if (canonical.has(domain)) continue;
|
|
32913
33199
|
if (seenInProbe.has(domain)) continue;
|
|
32914
33200
|
seenInProbe.add(domain);
|
|
32915
|
-
|
|
33201
|
+
counts2.set(domain, (counts2.get(domain) ?? 0) + 1);
|
|
32916
33202
|
}
|
|
32917
33203
|
}
|
|
32918
|
-
return Array.from(
|
|
33204
|
+
return Array.from(counts2.entries()).map(([domain, hits]) => ({
|
|
32919
33205
|
domain,
|
|
32920
33206
|
hits,
|
|
32921
33207
|
competitorType: classification[domain] ?? DiscoveryCompetitorTypes.unknown
|
|
@@ -32945,7 +33231,7 @@ async function executeDiscovery(opts) {
|
|
|
32945
33231
|
status: DiscoverySessionStatuses.seeding,
|
|
32946
33232
|
dedupThreshold,
|
|
32947
33233
|
startedAt
|
|
32948
|
-
}).where(
|
|
33234
|
+
}).where(eq35(discoverySessions.id, opts.sessionId)).run();
|
|
32949
33235
|
const seedResult = await opts.deps.seed({
|
|
32950
33236
|
project: opts.project,
|
|
32951
33237
|
icpDescription: opts.icpDescription,
|
|
@@ -32971,7 +33257,7 @@ async function executeDiscovery(opts) {
|
|
|
32971
33257
|
seedCountRaw,
|
|
32972
33258
|
seedCount,
|
|
32973
33259
|
warning
|
|
32974
|
-
}).where(
|
|
33260
|
+
}).where(eq35(discoverySessions.id, opts.sessionId)).run();
|
|
32975
33261
|
const probeRows = [];
|
|
32976
33262
|
const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
|
|
32977
33263
|
for (const query of probedCanonicals) {
|
|
@@ -33012,7 +33298,7 @@ async function executeDiscovery(opts) {
|
|
|
33012
33298
|
wastedCount: buckets["wasted-surface"],
|
|
33013
33299
|
competitorMap,
|
|
33014
33300
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33015
|
-
}).where(
|
|
33301
|
+
}).where(eq35(discoverySessions.id, opts.sessionId)).run();
|
|
33016
33302
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
33017
33303
|
return {
|
|
33018
33304
|
buckets,
|
|
@@ -33052,7 +33338,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
33052
33338
|
status: DiscoverySessionStatuses.failed,
|
|
33053
33339
|
error,
|
|
33054
33340
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33055
|
-
}).where(
|
|
33341
|
+
}).where(eq35(discoverySessions.id, sessionId)).run();
|
|
33056
33342
|
}
|
|
33057
33343
|
function dedupeStrings(input) {
|
|
33058
33344
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -33070,7 +33356,7 @@ function dedupeStrings(input) {
|
|
|
33070
33356
|
|
|
33071
33357
|
// ../api-routes/src/technical-aeo.ts
|
|
33072
33358
|
import crypto28 from "crypto";
|
|
33073
|
-
import { and as
|
|
33359
|
+
import { and as and26, asc as asc4, count, desc as desc17, eq as eq36, inArray as inArray13 } from "drizzle-orm";
|
|
33074
33360
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
33075
33361
|
function emptyScore(projectName) {
|
|
33076
33362
|
return {
|
|
@@ -33102,12 +33388,12 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
33102
33388
|
async function technicalAeoRoutes(app, opts) {
|
|
33103
33389
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
33104
33390
|
const project = resolveProject(app.db, request.params.name);
|
|
33105
|
-
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs,
|
|
33106
|
-
|
|
33107
|
-
|
|
33108
|
-
|
|
33391
|
+
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq36(siteAuditSnapshots.runId, runs.id)).where(and26(
|
|
33392
|
+
eq36(siteAuditSnapshots.projectId, project.id),
|
|
33393
|
+
eq36(runs.kind, RunKinds["site-audit"]),
|
|
33394
|
+
inArray13(runs.status, SURFACEABLE_STATUSES),
|
|
33109
33395
|
notProbeRun()
|
|
33110
|
-
)).orderBy(
|
|
33396
|
+
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
33111
33397
|
const latest = rows[0];
|
|
33112
33398
|
if (!latest) return emptyScore(project.name);
|
|
33113
33399
|
const snap = latest.snap;
|
|
@@ -33137,24 +33423,24 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
33137
33423
|
});
|
|
33138
33424
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
33139
33425
|
const project = resolveProject(app.db, request.params.name);
|
|
33140
|
-
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs,
|
|
33141
|
-
|
|
33142
|
-
|
|
33143
|
-
|
|
33426
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq36(siteAuditSnapshots.runId, runs.id)).where(and26(
|
|
33427
|
+
eq36(siteAuditSnapshots.projectId, project.id),
|
|
33428
|
+
eq36(runs.kind, RunKinds["site-audit"]),
|
|
33429
|
+
inArray13(runs.status, SURFACEABLE_STATUSES),
|
|
33144
33430
|
notProbeRun()
|
|
33145
|
-
)).orderBy(
|
|
33431
|
+
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
33146
33432
|
if (!latest) {
|
|
33147
33433
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
33148
33434
|
}
|
|
33149
33435
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
33150
|
-
const conds = [
|
|
33151
|
-
if (statusFilter) conds.push(
|
|
33152
|
-
const where =
|
|
33436
|
+
const conds = [eq36(siteAuditPages.runId, latest.runId)];
|
|
33437
|
+
if (statusFilter) conds.push(eq36(siteAuditPages.status, statusFilter));
|
|
33438
|
+
const where = and26(...conds);
|
|
33153
33439
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
33154
33440
|
const total = totalRow?.value ?? 0;
|
|
33155
33441
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
33156
33442
|
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
33157
|
-
const orderBy = request.query.sort === "score-desc" ?
|
|
33443
|
+
const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ? asc4(siteAuditPages.url) : asc4(siteAuditPages.overallScore);
|
|
33158
33444
|
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
33159
33445
|
const pages = rows.map((row) => ({
|
|
33160
33446
|
url: row.url,
|
|
@@ -33173,12 +33459,12 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
33173
33459
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
33174
33460
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
33175
33461
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
33176
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
33177
|
-
|
|
33178
|
-
|
|
33179
|
-
|
|
33462
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq36(siteAuditSnapshots.runId, runs.id)).where(and26(
|
|
33463
|
+
eq36(siteAuditSnapshots.projectId, project.id),
|
|
33464
|
+
eq36(runs.kind, RunKinds["site-audit"]),
|
|
33465
|
+
inArray13(runs.status, SURFACEABLE_STATUSES),
|
|
33180
33466
|
notProbeRun()
|
|
33181
|
-
)).orderBy(
|
|
33467
|
+
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
33182
33468
|
return { project: project.name, points: rows.reverse() };
|
|
33183
33469
|
});
|
|
33184
33470
|
app.post("/projects/:name/technical-aeo/runs", async (request) => {
|
|
@@ -33187,10 +33473,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
33187
33473
|
if (!parsed.success) {
|
|
33188
33474
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
33189
33475
|
}
|
|
33190
|
-
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
33191
|
-
|
|
33192
|
-
|
|
33193
|
-
|
|
33476
|
+
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and26(
|
|
33477
|
+
eq36(runs.projectId, project.id),
|
|
33478
|
+
eq36(runs.kind, RunKinds["site-audit"]),
|
|
33479
|
+
inArray13(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
33194
33480
|
)).get();
|
|
33195
33481
|
if (existing) {
|
|
33196
33482
|
return { runId: existing.id, status: existing.status };
|
|
@@ -33293,6 +33579,7 @@ async function apiRoutes(app, opts) {
|
|
|
33293
33579
|
await api.register(intelligenceRoutes);
|
|
33294
33580
|
await api.register(reportRoutes);
|
|
33295
33581
|
await api.register(citationRoutes);
|
|
33582
|
+
await api.register(visibilityStatsRoutes);
|
|
33296
33583
|
await api.register(compositeRoutes);
|
|
33297
33584
|
await api.register(contentRoutes, {
|
|
33298
33585
|
explainContentRecommendation: opts.explainContentRecommendation,
|
|
@@ -33383,7 +33670,9 @@ async function apiRoutes(app, opts) {
|
|
|
33383
33670
|
discoverLatestRelease: opts.discoverLatestRelease
|
|
33384
33671
|
});
|
|
33385
33672
|
await api.register(discoveryRoutes, {
|
|
33386
|
-
onDiscoveryRunRequested: opts.onDiscoveryRunRequested
|
|
33673
|
+
onDiscoveryRunRequested: opts.onDiscoveryRunRequested,
|
|
33674
|
+
harvestSearchQueries: opts.harvestSearchQueries,
|
|
33675
|
+
embedQueries: opts.embedQueries
|
|
33387
33676
|
});
|
|
33388
33677
|
await api.register(technicalAeoRoutes, {
|
|
33389
33678
|
onSiteAuditRequested: opts.onSiteAuditRequested
|
|
@@ -33776,16 +34065,16 @@ var IntelligenceService = class {
|
|
|
33776
34065
|
*/
|
|
33777
34066
|
analyzeAndPersist(runId, projectId) {
|
|
33778
34067
|
const recentRuns = this.db.select().from(runs).where(
|
|
33779
|
-
|
|
33780
|
-
|
|
33781
|
-
or5(
|
|
34068
|
+
and27(
|
|
34069
|
+
eq37(runs.projectId, projectId),
|
|
34070
|
+
or5(eq37(runs.status, "completed"), eq37(runs.status, "partial")),
|
|
33782
34071
|
// Defensive: RunCoordinator already skips probes before this is
|
|
33783
34072
|
// called, but if a future call site invokes analyzeAndPersist
|
|
33784
34073
|
// directly for a probe, probes still must not pollute the
|
|
33785
34074
|
// intelligence window.
|
|
33786
34075
|
ne5(runs.trigger, RunTriggers.probe)
|
|
33787
34076
|
)
|
|
33788
|
-
).orderBy(
|
|
34077
|
+
).orderBy(desc18(runs.finishedAt), desc18(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
|
|
33789
34078
|
if (recentRuns.length === 0) {
|
|
33790
34079
|
log.info("intelligence.skip", { runId, reason: "no completed runs" });
|
|
33791
34080
|
return null;
|
|
@@ -33860,7 +34149,7 @@ var IntelligenceService = class {
|
|
|
33860
34149
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
33861
34150
|
*/
|
|
33862
34151
|
analyzeAndPersistGbp(runId, projectId) {
|
|
33863
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
34152
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq37(runs.id, runId)).get();
|
|
33864
34153
|
if (!runRow) {
|
|
33865
34154
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
33866
34155
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -33868,9 +34157,9 @@ var IntelligenceService = class {
|
|
|
33868
34157
|
}
|
|
33869
34158
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
33870
34159
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
33871
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
33872
|
-
|
|
33873
|
-
|
|
34160
|
+
const selected = this.db.select().from(gbpLocations).where(and27(
|
|
34161
|
+
eq37(gbpLocations.projectId, projectId),
|
|
34162
|
+
eq37(gbpLocations.selected, true),
|
|
33874
34163
|
gte7(gbpLocations.syncedAt, windowStart),
|
|
33875
34164
|
lte4(gbpLocations.syncedAt, windowEnd)
|
|
33876
34165
|
)).all();
|
|
@@ -33905,10 +34194,10 @@ var IntelligenceService = class {
|
|
|
33905
34194
|
}
|
|
33906
34195
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
33907
34196
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
33908
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
33909
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
33910
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
33911
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(
|
|
34197
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and27(eq37(gbpDailyMetrics.projectId, projectId), eq37(gbpDailyMetrics.locationName, locationName))).all();
|
|
34198
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and27(eq37(gbpPlaceActions.projectId, projectId), eq37(gbpPlaceActions.locationName, locationName))).all();
|
|
34199
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and27(eq37(gbpLodgingSnapshots.projectId, projectId), eq37(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
34200
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and27(eq37(gbpPlaceDetails.projectId, projectId), eq37(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
33912
34201
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
33913
34202
|
const summary = buildGbpSummary({
|
|
33914
34203
|
locationName,
|
|
@@ -33940,7 +34229,7 @@ var IntelligenceService = class {
|
|
|
33940
34229
|
/** Build the month-over-month keyword series for a location from the
|
|
33941
34230
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
33942
34231
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
33943
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
34232
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and27(eq37(gbpKeywordMonthly.projectId, projectId), eq37(gbpKeywordMonthly.locationName, locationName))).all();
|
|
33944
34233
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
33945
34234
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
33946
34235
|
const recentMonth = months[0] ?? null;
|
|
@@ -33971,7 +34260,7 @@ var IntelligenceService = class {
|
|
|
33971
34260
|
*/
|
|
33972
34261
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
33973
34262
|
const covered = new Set(coveredLocationNames);
|
|
33974
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
34263
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and27(eq37(insights.projectId, projectId), eq37(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
33975
34264
|
const staleIds = [];
|
|
33976
34265
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
33977
34266
|
for (const row of existing) {
|
|
@@ -33982,7 +34271,7 @@ var IntelligenceService = class {
|
|
|
33982
34271
|
}
|
|
33983
34272
|
this.db.transaction((tx) => {
|
|
33984
34273
|
for (const id of staleIds) {
|
|
33985
|
-
tx.delete(insights).where(
|
|
34274
|
+
tx.delete(insights).where(eq37(insights.id, id)).run();
|
|
33986
34275
|
}
|
|
33987
34276
|
for (const insight of gbpInsights) {
|
|
33988
34277
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -34060,7 +34349,7 @@ var IntelligenceService = class {
|
|
|
34060
34349
|
* create per run + aggregate). DB is left untouched.
|
|
34061
34350
|
*/
|
|
34062
34351
|
backfill(projectName, opts, onProgress) {
|
|
34063
|
-
const project = this.db.select().from(projects).where(
|
|
34352
|
+
const project = this.db.select().from(projects).where(eq37(projects.name, projectName)).get();
|
|
34064
34353
|
if (!project) {
|
|
34065
34354
|
throw new Error(`Project "${projectName}" not found`);
|
|
34066
34355
|
}
|
|
@@ -34073,9 +34362,9 @@ var IntelligenceService = class {
|
|
|
34073
34362
|
sinceTimestamp = parsed;
|
|
34074
34363
|
}
|
|
34075
34364
|
const allRuns = this.db.select().from(runs).where(
|
|
34076
|
-
|
|
34077
|
-
|
|
34078
|
-
or5(
|
|
34365
|
+
and27(
|
|
34366
|
+
eq37(runs.projectId, project.id),
|
|
34367
|
+
or5(eq37(runs.status, "completed"), eq37(runs.status, "partial")),
|
|
34079
34368
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
34080
34369
|
ne5(runs.trigger, RunTriggers.probe)
|
|
34081
34370
|
)
|
|
@@ -34108,7 +34397,7 @@ var IntelligenceService = class {
|
|
|
34108
34397
|
let wouldDeleteTotal = 0;
|
|
34109
34398
|
const existingByRunId = /* @__PURE__ */ new Map();
|
|
34110
34399
|
if (isDryRun && targetRuns.length > 0) {
|
|
34111
|
-
const rows = this.db.select({ runId: insights.runId }).from(insights).where(
|
|
34400
|
+
const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray14(insights.runId, targetRuns.map((r) => r.id))).all();
|
|
34112
34401
|
for (const r of rows) {
|
|
34113
34402
|
if (r.runId == null) continue;
|
|
34114
34403
|
existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
|
|
@@ -34154,7 +34443,7 @@ var IntelligenceService = class {
|
|
|
34154
34443
|
return { processed, skipped, totalInsights };
|
|
34155
34444
|
}
|
|
34156
34445
|
loadTrackedCompetitors(projectId) {
|
|
34157
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
34446
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq37(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
34158
34447
|
}
|
|
34159
34448
|
/**
|
|
34160
34449
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -34175,15 +34464,15 @@ var IntelligenceService = class {
|
|
|
34175
34464
|
}
|
|
34176
34465
|
persistResult(result, runId, projectId) {
|
|
34177
34466
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
34178
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
34467
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq37(insights.runId, runId)).all();
|
|
34179
34468
|
for (const row of existingInsights) {
|
|
34180
34469
|
if (row.dismissed) {
|
|
34181
34470
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
34182
34471
|
}
|
|
34183
34472
|
}
|
|
34184
34473
|
this.db.transaction((tx) => {
|
|
34185
|
-
tx.delete(insights).where(
|
|
34186
|
-
tx.delete(healthSnapshots).where(
|
|
34474
|
+
tx.delete(insights).where(eq37(insights.runId, runId)).run();
|
|
34475
|
+
tx.delete(healthSnapshots).where(eq37(healthSnapshots.runId, runId)).run();
|
|
34187
34476
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
34188
34477
|
for (const insight of result.insights) {
|
|
34189
34478
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -34236,28 +34525,28 @@ var IntelligenceService = class {
|
|
|
34236
34525
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
34237
34526
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
34238
34527
|
if (regressions.length === 0) return rawInsights;
|
|
34239
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
34528
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq37(gscSearchData.projectId, projectId)).all();
|
|
34240
34529
|
const gscConnected = gscRows.length > 0;
|
|
34241
34530
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
34242
34531
|
for (const row of gscRows) {
|
|
34243
34532
|
const key = row.query.toLowerCase();
|
|
34244
34533
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
34245
34534
|
}
|
|
34246
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
34535
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq37(projects.id, projectId)).get();
|
|
34247
34536
|
const locationCount = Math.max(
|
|
34248
34537
|
1,
|
|
34249
34538
|
(projectRow?.locations ?? []).length
|
|
34250
34539
|
);
|
|
34251
34540
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
34252
34541
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
34253
|
-
|
|
34254
|
-
|
|
34255
|
-
|
|
34256
|
-
or5(
|
|
34542
|
+
and27(
|
|
34543
|
+
eq37(runs.projectId, projectId),
|
|
34544
|
+
eq37(runs.kind, RunKinds["answer-visibility"]),
|
|
34545
|
+
or5(eq37(runs.status, "completed"), eq37(runs.status, "partial")),
|
|
34257
34546
|
// Defensive — see top of file.
|
|
34258
34547
|
ne5(runs.trigger, RunTriggers.probe)
|
|
34259
34548
|
)
|
|
34260
|
-
).orderBy(
|
|
34549
|
+
).orderBy(desc18(runs.createdAt), desc18(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
|
|
34261
34550
|
const recentGroups = groupRunsByCreatedAt(recentRunRows);
|
|
34262
34551
|
const recentRunIds = [];
|
|
34263
34552
|
const recentRunIdToCreatedAt = /* @__PURE__ */ new Map();
|
|
@@ -34273,7 +34562,7 @@ var IntelligenceService = class {
|
|
|
34273
34562
|
const haveHistory = recentRunIds.length > 0;
|
|
34274
34563
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
34275
34564
|
if (haveHistory) {
|
|
34276
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
34565
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and27(eq37(insights.type, "regression"), inArray14(insights.runId, recentRunIds))).all();
|
|
34277
34566
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
34278
34567
|
for (const row of priorRows) {
|
|
34279
34568
|
if (!row.runId) continue;
|
|
@@ -34302,7 +34591,7 @@ var IntelligenceService = class {
|
|
|
34302
34591
|
});
|
|
34303
34592
|
}
|
|
34304
34593
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
34305
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
34594
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq37(projects.id, projectId)).get();
|
|
34306
34595
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
34307
34596
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
34308
34597
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -34319,7 +34608,7 @@ var IntelligenceService = class {
|
|
|
34319
34608
|
citedDomains: querySnapshots.citedDomains,
|
|
34320
34609
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
34321
34610
|
snapshotLocation: querySnapshots.location
|
|
34322
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
34611
|
+
}).from(querySnapshots).leftJoin(queries, eq37(querySnapshots.queryId, queries.id)).where(eq37(querySnapshots.runId, runId)).all();
|
|
34323
34612
|
const snapshots = [];
|
|
34324
34613
|
let orphanCount = 0;
|
|
34325
34614
|
for (const r of rows) {
|