@canonry/canonry 4.131.0 → 4.132.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +7 -0
- package/assets/assets/{AuditHistoryPanel-yJXZMO5t.js → AuditHistoryPanel-Carj_FCP.js} +1 -1
- package/assets/assets/{BacklinksPage-6VTLbAYx.js → BacklinksPage-CqOTDVIn.js} +1 -1
- package/assets/assets/{ChartPrimitives-D5YGiDLi.js → ChartPrimitives-EuQ82l0S.js} +1 -1
- package/assets/assets/{HistoryPage-BxdTM6-R.js → HistoryPage-D6XQfiLX.js} +1 -1
- package/assets/assets/{ProjectPage-BJKxZPRO.js → ProjectPage-Dswa9Ah6.js} +1 -1
- package/assets/assets/{RunRow-DTrAO7tE.js → RunRow-ThryEZFR.js} +1 -1
- package/assets/assets/{RunsPage-CHU0DQGl.js → RunsPage-RFs2WkTC.js} +1 -1
- package/assets/assets/{SettingsPage-bOIwju6t.js → SettingsPage-DoUccn9s.js} +1 -1
- package/assets/assets/{TrafficPage-BKvuR482.js → TrafficPage-CVvSDZPY.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-3-iUTgQg.js → TrafficSourceDetailPage-BTUQUBeS.js} +1 -1
- package/assets/assets/{arrow-left-BXSfGhr6.js → arrow-left-BoCNqusn.js} +1 -1
- package/assets/assets/{extract-error-message-DEAY62LS.js → extract-error-message-BsfOVrKi.js} +1 -1
- package/assets/assets/{index-CfjIYitV.js → index-jJurRky0.js} +85 -85
- package/assets/assets/{trash-2-O1wo9IX1.js → trash-2-BPb4368K.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-73HPY6XQ.js → chunk-5FWAPJFX.js} +45 -1
- package/dist/{chunk-3WBAJKRR.js → chunk-7BD2ZOPV.js} +86 -0
- package/dist/{chunk-ZXVWL5HX.js → chunk-QIJKS5KN.js} +743 -301
- package/dist/{chunk-5GQ6FF5R.js → chunk-RWTOEEG3.js} +4 -4
- package/dist/cli.js +67 -5
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-YJKQQYJQ.js → intelligence-service-RU2ACYPY.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +8 -8
|
@@ -200,6 +200,9 @@ import {
|
|
|
200
200
|
ga4SocialReferralHistoryEntrySchema,
|
|
201
201
|
ga4StatusDtoSchema,
|
|
202
202
|
ga4SyncResponseDtoSchema,
|
|
203
|
+
gaMeasurementAnalysisDtoSchema,
|
|
204
|
+
gaMeasurementAnalysisWindowSchema,
|
|
205
|
+
gaMeasurementHostScopeSchema,
|
|
203
206
|
gateHarvestedSearchQueries,
|
|
204
207
|
gbpAccountListResponseSchema,
|
|
205
208
|
gbpAttributesListResponseSchema,
|
|
@@ -344,10 +347,10 @@ import {
|
|
|
344
347
|
wordpressSchemaDeployResultDtoSchema,
|
|
345
348
|
wordpressSchemaStatusResultDtoSchema,
|
|
346
349
|
wordpressStatusDtoSchema
|
|
347
|
-
} from "./chunk-
|
|
350
|
+
} from "./chunk-7BD2ZOPV.js";
|
|
348
351
|
|
|
349
352
|
// src/intelligence-service.ts
|
|
350
|
-
import { eq as
|
|
353
|
+
import { eq as eq43, desc as desc19, asc as asc9, and as and34, ne as ne6, or as or9, inArray as inArray15, gte as gte11, lte as lte7 } from "drizzle-orm";
|
|
351
354
|
|
|
352
355
|
// ../db/src/client.ts
|
|
353
356
|
import { mkdirSync } from "fs";
|
|
@@ -12620,31 +12623,31 @@ function renderAiReferrals(report) {
|
|
|
12620
12623
|
</div>`
|
|
12621
12624
|
);
|
|
12622
12625
|
}
|
|
12623
|
-
function serverActivityHeading(audience, hasData,
|
|
12626
|
+
function serverActivityHeading(audience, hasData, windowDays2) {
|
|
12624
12627
|
const isClient = audience === "client";
|
|
12625
12628
|
return {
|
|
12626
12629
|
id: "server-activity",
|
|
12627
12630
|
eyebrow: isClient ? "AI engine attention" : "Section 10",
|
|
12628
12631
|
title: "AI Visibility \u2014 Server-Side",
|
|
12629
|
-
intro: isClient ? hasData ? `What AI engines actually do in your server logs over the last ${
|
|
12632
|
+
intro: isClient ? hasData ? `What AI engines actually do in your server logs over the last ${windowDays2} days \u2014 the other half of citations.` : "Live telemetry from your server logs." : "What AI engines actually do in your server logs \u2014 direct evidence, complementary to citations (which measure what they say)."
|
|
12630
12633
|
};
|
|
12631
12634
|
}
|
|
12632
12635
|
function renderServerActivity(report, audience) {
|
|
12633
12636
|
const sa = report.serverActivity;
|
|
12634
12637
|
const isClient = audience === "client";
|
|
12635
|
-
const
|
|
12636
|
-
const windowLabel = `${
|
|
12637
|
-
const priorWindowLabel = `vs prior ${
|
|
12638
|
+
const windowDays2 = report.meta.periodDays;
|
|
12639
|
+
const windowLabel = `${windowDays2}d`;
|
|
12640
|
+
const priorWindowLabel = `vs prior ${windowDays2} days`;
|
|
12638
12641
|
if (!sa) {
|
|
12639
12642
|
if (isClient) return "";
|
|
12640
12643
|
return section(
|
|
12641
|
-
serverActivityHeading("agency", false,
|
|
12644
|
+
serverActivityHeading("agency", false, windowDays2),
|
|
12642
12645
|
renderEmpty("Connect a server-side traffic source to surface what AI engines do directly in your server logs \u2014 distinct from GA4 click-throughs.")
|
|
12643
12646
|
);
|
|
12644
12647
|
}
|
|
12645
12648
|
if (!sa.hasData) {
|
|
12646
12649
|
return section(
|
|
12647
|
-
serverActivityHeading(audience, false,
|
|
12650
|
+
serverActivityHeading(audience, false, windowDays2),
|
|
12648
12651
|
renderEmpty(isClient ? "Your server-side traffic source is connected. Numbers will appear after the next sync." : "Source connected \u2014 collecting your first data. Numbers will appear after the next sync.")
|
|
12649
12652
|
);
|
|
12650
12653
|
}
|
|
@@ -12677,7 +12680,7 @@ function renderServerActivity(report, audience) {
|
|
|
12677
12680
|
<td class="numeric">${formatNumber(o.referralArrivals)}</td>
|
|
12678
12681
|
</tr>`).join("");
|
|
12679
12682
|
return section(
|
|
12680
|
-
serverActivityHeading("client", true,
|
|
12683
|
+
serverActivityHeading("client", true, windowDays2),
|
|
12681
12684
|
`<div class="metric-grid">
|
|
12682
12685
|
<div class="metric">
|
|
12683
12686
|
<div class="label">AI bot requests observed</div>
|
|
@@ -12738,10 +12741,10 @@ function renderServerActivity(report, audience) {
|
|
|
12738
12741
|
const trendChart = sa.dailyTrend.length > 0 ? renderLineChart(
|
|
12739
12742
|
sa.dailyTrend.map((d) => ({ x: d.date, y: d.verifiedCrawlerHits, label: d.date.slice(5) })),
|
|
12740
12743
|
COLORS.series[1],
|
|
12741
|
-
`Verified crawler hits over time (last ${
|
|
12744
|
+
`Verified crawler hits over time (last ${windowDays2} days)`
|
|
12742
12745
|
) : "";
|
|
12743
12746
|
return section(
|
|
12744
|
-
serverActivityHeading("agency", true,
|
|
12747
|
+
serverActivityHeading("agency", true, windowDays2),
|
|
12745
12748
|
`<div class="metric-grid">
|
|
12746
12749
|
<div class="metric">
|
|
12747
12750
|
<div class="label">Verified crawler hits (${windowLabel})</div>
|
|
@@ -13271,11 +13274,11 @@ var TOP_CAMPAIGN_LIMIT = 10;
|
|
|
13271
13274
|
var INSIGHT_LOOKBACK_RUNS = 5;
|
|
13272
13275
|
var SERVER_ACTIVITY_TOP_PATHS_LIMIT = 10;
|
|
13273
13276
|
var GA_WINDOW_SUMMARY_KEYS = { 7: "7d", 30: "30d", 90: "90d" };
|
|
13274
|
-
function windowStartDate(endDate,
|
|
13277
|
+
function windowStartDate(endDate, windowDays2) {
|
|
13275
13278
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(endDate);
|
|
13276
13279
|
if (!m) return endDate;
|
|
13277
13280
|
const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
|
|
13278
|
-
d.setUTCDate(d.getUTCDate() - (
|
|
13281
|
+
d.setUTCDate(d.getUTCDate() - (windowDays2 - 1));
|
|
13279
13282
|
return d.toISOString().slice(0, 10);
|
|
13280
13283
|
}
|
|
13281
13284
|
function safeNum(value) {
|
|
@@ -13319,14 +13322,14 @@ function loadQueryLookup(db, projectId) {
|
|
|
13319
13322
|
for (const row of rows) byId.set(row.id, row.query);
|
|
13320
13323
|
return { byId };
|
|
13321
13324
|
}
|
|
13322
|
-
function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries,
|
|
13325
|
+
function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries, windowDays2) {
|
|
13323
13326
|
const allRows = db.select().from(gscSearchData).where(eq16(gscSearchData.projectId, projectId)).all();
|
|
13324
13327
|
const allDailyTotals = readGscDailyTotals(db, projectId, "", "9999-12-31");
|
|
13325
13328
|
let maxDate = "";
|
|
13326
13329
|
for (const r of allRows) if (r.date > maxDate) maxDate = r.date;
|
|
13327
13330
|
for (const r of allDailyTotals) if (r.date > maxDate) maxDate = r.date;
|
|
13328
13331
|
if (!maxDate) return null;
|
|
13329
|
-
const startDate = windowStartDate(maxDate,
|
|
13332
|
+
const startDate = windowStartDate(maxDate, windowDays2);
|
|
13330
13333
|
const rows = allRows.filter((r) => r.date >= startDate && r.date <= maxDate);
|
|
13331
13334
|
const dailyTotals = allDailyTotals.filter((r) => r.date >= startDate && r.date <= maxDate);
|
|
13332
13335
|
if (rows.length === 0 && dailyTotals.length === 0) return null;
|
|
@@ -13419,8 +13422,8 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
|
|
|
13419
13422
|
gscButNotTracked
|
|
13420
13423
|
};
|
|
13421
13424
|
}
|
|
13422
|
-
function buildGaSection(db, projectId,
|
|
13423
|
-
const gaWindowKey = GA_WINDOW_SUMMARY_KEYS[
|
|
13425
|
+
function buildGaSection(db, projectId, windowDays2) {
|
|
13426
|
+
const gaWindowKey = GA_WINDOW_SUMMARY_KEYS[windowDays2];
|
|
13424
13427
|
const windowSummary = gaWindowKey ? db.select().from(gaTrafficWindowSummaries).where(
|
|
13425
13428
|
and12(
|
|
13426
13429
|
eq16(gaTrafficWindowSummaries.projectId, projectId),
|
|
@@ -13432,7 +13435,7 @@ function buildGaSection(db, projectId, windowDays) {
|
|
|
13432
13435
|
if (!windowSummary && !fallbackSummary && allSnapshotRows.length === 0) return null;
|
|
13433
13436
|
let snapshotMaxDate = "";
|
|
13434
13437
|
for (const r of allSnapshotRows) if (r.date > snapshotMaxDate) snapshotMaxDate = r.date;
|
|
13435
|
-
const snapshotStartDate = snapshotMaxDate ? windowStartDate(snapshotMaxDate,
|
|
13438
|
+
const snapshotStartDate = snapshotMaxDate ? windowStartDate(snapshotMaxDate, windowDays2) : "";
|
|
13436
13439
|
const snapshotRows = snapshotStartDate ? allSnapshotRows.filter((r) => r.date >= snapshotStartDate && r.date <= snapshotMaxDate) : allSnapshotRows;
|
|
13437
13440
|
const totalSessions = windowSummary?.totalSessions ?? fallbackSummary?.totalSessions ?? snapshotRows.reduce((s, r) => s + r.sessions, 0);
|
|
13438
13441
|
const totalUsers = windowSummary?.totalUsers ?? fallbackSummary?.totalUsers ?? snapshotRows.reduce((s, r) => s + r.users, 0);
|
|
@@ -13544,8 +13547,8 @@ function buildSocialReferrals(db, projectId) {
|
|
|
13544
13547
|
topCampaigns
|
|
13545
13548
|
};
|
|
13546
13549
|
}
|
|
13547
|
-
function resolveAiReferralWindow(db, projectId,
|
|
13548
|
-
const windowKey = GA_WINDOW_SUMMARY_KEYS[
|
|
13550
|
+
function resolveAiReferralWindow(db, projectId, windowDays2) {
|
|
13551
|
+
const windowKey = GA_WINDOW_SUMMARY_KEYS[windowDays2];
|
|
13549
13552
|
const windowSummary = windowKey ? db.select({ periodStart: gaTrafficWindowSummaries.periodStart, periodEnd: gaTrafficWindowSummaries.periodEnd }).from(gaTrafficWindowSummaries).where(and12(
|
|
13550
13553
|
eq16(gaTrafficWindowSummaries.projectId, projectId),
|
|
13551
13554
|
eq16(gaTrafficWindowSummaries.windowKey, windowKey)
|
|
@@ -13553,10 +13556,10 @@ function resolveAiReferralWindow(db, projectId, windowDays) {
|
|
|
13553
13556
|
if (windowSummary) return { start: windowSummary.periodStart, end: windowSummary.periodEnd };
|
|
13554
13557
|
const latestSummary = db.select({ periodEnd: gaTrafficSummaries.periodEnd }).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, projectId)).orderBy(desc7(gaTrafficSummaries.syncedAt)).get();
|
|
13555
13558
|
const end = latestSummary?.periodEnd ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
13556
|
-
return { start: windowStartDate(end,
|
|
13559
|
+
return { start: windowStartDate(end, windowDays2), end };
|
|
13557
13560
|
}
|
|
13558
|
-
function buildAiReferrals(db, projectId,
|
|
13559
|
-
const window = resolveAiReferralWindow(db, projectId,
|
|
13561
|
+
function buildAiReferrals(db, projectId, windowDays2) {
|
|
13562
|
+
const window = resolveAiReferralWindow(db, projectId, windowDays2);
|
|
13560
13563
|
const rows = db.select().from(gaAiReferrals).where(and12(
|
|
13561
13564
|
eq16(gaAiReferrals.projectId, projectId),
|
|
13562
13565
|
sql8`${gaAiReferrals.date} >= ${window.start}`,
|
|
@@ -13658,7 +13661,7 @@ function nonSubresourceReferralPathCondition() {
|
|
|
13658
13661
|
AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.woff2'
|
|
13659
13662
|
`;
|
|
13660
13663
|
}
|
|
13661
|
-
function buildServerActivity(db, projectId,
|
|
13664
|
+
function buildServerActivity(db, projectId, windowDays2) {
|
|
13662
13665
|
const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
|
|
13663
13666
|
and12(
|
|
13664
13667
|
eq16(trafficSources.projectId, projectId),
|
|
@@ -13668,8 +13671,8 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
13668
13671
|
if (sourceRows.length === 0) return null;
|
|
13669
13672
|
const now = /* @__PURE__ */ new Date();
|
|
13670
13673
|
const headlineEnd = now.toISOString();
|
|
13671
|
-
const headlineStartMs = now.getTime() -
|
|
13672
|
-
const priorStartMs = headlineStartMs -
|
|
13674
|
+
const headlineStartMs = now.getTime() - windowDays2 * 24 * 60 * 6e4;
|
|
13675
|
+
const priorStartMs = headlineStartMs - windowDays2 * 24 * 60 * 6e4;
|
|
13673
13676
|
const trendStartMs = headlineStartMs;
|
|
13674
13677
|
const headlineStart = new Date(headlineStartMs).toISOString();
|
|
13675
13678
|
const priorStart = new Date(priorStartMs).toISOString();
|
|
@@ -16511,6 +16514,7 @@ var SCHEMA_TABLE = {
|
|
|
16511
16514
|
ResearchRunListDto: researchRunListSchema,
|
|
16512
16515
|
DoctorReportDto: doctorReportSchema,
|
|
16513
16516
|
GA4AiReferralHistoryEntry: ga4AiReferralHistoryEntrySchema,
|
|
16517
|
+
GA4MeasurementAnalysisDto: gaMeasurementAnalysisDtoSchema,
|
|
16514
16518
|
GA4SessionHistoryEntry: ga4SessionHistoryEntrySchema,
|
|
16515
16519
|
GA4SocialReferralHistoryEntry: ga4SocialReferralHistoryEntrySchema,
|
|
16516
16520
|
GA4StatusDto: ga4StatusDtoSchema,
|
|
@@ -20122,6 +20126,47 @@ var routeCatalog = [
|
|
|
20122
20126
|
404: errorResponse("Project not found.")
|
|
20123
20127
|
}
|
|
20124
20128
|
},
|
|
20129
|
+
{
|
|
20130
|
+
method: "get",
|
|
20131
|
+
path: "/api/v1/projects/{name}/ga/measurement-analysis",
|
|
20132
|
+
summary: "Get GA4 measurement analysis",
|
|
20133
|
+
tags: ["ga4"],
|
|
20134
|
+
parameters: [
|
|
20135
|
+
nameParameter,
|
|
20136
|
+
{
|
|
20137
|
+
name: "window",
|
|
20138
|
+
in: "query",
|
|
20139
|
+
description: "30-day cohorts to return.",
|
|
20140
|
+
schema: { type: "string", enum: ["30d", "60d", "90d"], default: "90d" }
|
|
20141
|
+
},
|
|
20142
|
+
{
|
|
20143
|
+
name: "hostScope",
|
|
20144
|
+
in: "query",
|
|
20145
|
+
description: "Configured marketing hosts or every observed host.",
|
|
20146
|
+
schema: { type: "string", enum: ["marketing", "all"], default: "marketing" }
|
|
20147
|
+
},
|
|
20148
|
+
{
|
|
20149
|
+
name: "pathPrefix",
|
|
20150
|
+
in: "query",
|
|
20151
|
+
description: "Optional boundary-safe normalized landing-page prefix.",
|
|
20152
|
+
schema: stringSchema
|
|
20153
|
+
},
|
|
20154
|
+
{
|
|
20155
|
+
name: "limit",
|
|
20156
|
+
in: "query",
|
|
20157
|
+
description: "Maximum page and query detail rows. Native channels are never truncated.",
|
|
20158
|
+
schema: { type: "integer", minimum: 1, maximum: 100, default: 100 }
|
|
20159
|
+
}
|
|
20160
|
+
],
|
|
20161
|
+
responses: {
|
|
20162
|
+
200: jsonResponse(
|
|
20163
|
+
"GA4 measurement analysis returned.",
|
|
20164
|
+
"GA4MeasurementAnalysisDto"
|
|
20165
|
+
),
|
|
20166
|
+
400: errorResponse("Invalid measurement analysis filters."),
|
|
20167
|
+
404: errorResponse("Project not found.")
|
|
20168
|
+
}
|
|
20169
|
+
},
|
|
20125
20170
|
{
|
|
20126
20171
|
method: "post",
|
|
20127
20172
|
path: "/api/v1/projects/{name}/ga/sync",
|
|
@@ -22059,9 +22104,9 @@ function computeFreshness(rows, asOfDate) {
|
|
|
22059
22104
|
const pendingDays = dataThroughDate ? Math.max(0, daysBetween(dataThroughDate, asOfDate)) : 0;
|
|
22060
22105
|
return { dataThroughDate, latestStoredDate: latestStoredDate || null, pendingDays };
|
|
22061
22106
|
}
|
|
22062
|
-
function buildTimeseries(rows, freshness,
|
|
22107
|
+
function buildTimeseries(rows, freshness, windowDays2 = 30) {
|
|
22063
22108
|
if (rows.length === 0 || freshness.latestStoredDate === null) return [];
|
|
22064
|
-
const windowLow = shiftDate(freshness.latestStoredDate, -(
|
|
22109
|
+
const windowLow = shiftDate(freshness.latestStoredDate, -(windowDays2 - 1));
|
|
22065
22110
|
const metricKeys = /* @__PURE__ */ new Set();
|
|
22066
22111
|
const byDate = /* @__PURE__ */ new Map();
|
|
22067
22112
|
for (const row of rows) {
|
|
@@ -31230,6 +31275,402 @@ async function ga4Routes(app, opts) {
|
|
|
31230
31275
|
});
|
|
31231
31276
|
}
|
|
31232
31277
|
|
|
31278
|
+
// ../api-routes/src/ga-measurement-analysis.ts
|
|
31279
|
+
import { and as and24, eq as eq30, gte as gte7, lte as lte5, or as or7, sql as sql14 } from "drizzle-orm";
|
|
31280
|
+
function addDays(date, offset) {
|
|
31281
|
+
const value = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
|
|
31282
|
+
value.setUTCDate(value.getUTCDate() + offset);
|
|
31283
|
+
return value.toISOString().slice(0, 10);
|
|
31284
|
+
}
|
|
31285
|
+
function windowDays(window) {
|
|
31286
|
+
if (window === "30d") return 30;
|
|
31287
|
+
if (window === "60d") return 60;
|
|
31288
|
+
return 90;
|
|
31289
|
+
}
|
|
31290
|
+
function buildPeriods(anchor, days) {
|
|
31291
|
+
const labels = days === 30 ? ["latest"] : days === 60 ? ["previous", "latest"] : ["earliest", "middle", "latest"];
|
|
31292
|
+
return labels.map((label, index2) => ({
|
|
31293
|
+
label,
|
|
31294
|
+
startDate: addDays(anchor, -days + index2 * 30 + 1),
|
|
31295
|
+
endDate: addDays(anchor, -days + (index2 + 1) * 30)
|
|
31296
|
+
}));
|
|
31297
|
+
}
|
|
31298
|
+
function aggregateByKey(rows, periods, keyOf, valueOf) {
|
|
31299
|
+
const result = /* @__PURE__ */ new Map();
|
|
31300
|
+
for (const row of rows) {
|
|
31301
|
+
const periodIndex = periods.findIndex(
|
|
31302
|
+
(period2) => row.date >= period2.startDate && row.date <= period2.endDate
|
|
31303
|
+
);
|
|
31304
|
+
if (periodIndex < 0) continue;
|
|
31305
|
+
const values = result.get(keyOf(row)) ?? Array(periods.length).fill(0);
|
|
31306
|
+
values[periodIndex] = (values[periodIndex] ?? 0) + valueOf(row);
|
|
31307
|
+
result.set(keyOf(row), values);
|
|
31308
|
+
}
|
|
31309
|
+
return result;
|
|
31310
|
+
}
|
|
31311
|
+
function sessionPeriods(periods, values) {
|
|
31312
|
+
return periods.map((period2, index2) => ({
|
|
31313
|
+
...period2,
|
|
31314
|
+
sessions: values[index2] ?? 0
|
|
31315
|
+
}));
|
|
31316
|
+
}
|
|
31317
|
+
function eventPeriods(periods, values) {
|
|
31318
|
+
return periods.map((period2, index2) => ({
|
|
31319
|
+
...period2,
|
|
31320
|
+
eventCount: values[index2] ?? 0
|
|
31321
|
+
}));
|
|
31322
|
+
}
|
|
31323
|
+
function clickPeriods(periods, clicks, impressions) {
|
|
31324
|
+
return periods.map((period2, index2) => ({
|
|
31325
|
+
...period2,
|
|
31326
|
+
clicks: clicks[index2] ?? 0,
|
|
31327
|
+
impressions: impressions[index2] ?? 0
|
|
31328
|
+
}));
|
|
31329
|
+
}
|
|
31330
|
+
function rankEntries(entries) {
|
|
31331
|
+
const score = (values) => ({
|
|
31332
|
+
latest: values.at(-1) ?? 0,
|
|
31333
|
+
total: values.reduce((sum, value) => sum + value, 0)
|
|
31334
|
+
});
|
|
31335
|
+
return [...entries].sort(([leftKey, leftValues], [rightKey, rightValues]) => {
|
|
31336
|
+
const left = score(leftValues);
|
|
31337
|
+
const right = score(rightValues);
|
|
31338
|
+
return right.latest - left.latest || right.total - left.total || leftKey.localeCompare(rightKey);
|
|
31339
|
+
});
|
|
31340
|
+
}
|
|
31341
|
+
function normalizeHost(value) {
|
|
31342
|
+
return hostOf(value) ?? value.trim().toLowerCase().replace(/^www\./, "");
|
|
31343
|
+
}
|
|
31344
|
+
function stableUnique(values, normalize) {
|
|
31345
|
+
const result = [];
|
|
31346
|
+
const seen = /* @__PURE__ */ new Set();
|
|
31347
|
+
for (const value of values) {
|
|
31348
|
+
const normalized = normalize(value);
|
|
31349
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
31350
|
+
seen.add(normalized);
|
|
31351
|
+
result.push(normalized);
|
|
31352
|
+
}
|
|
31353
|
+
return result;
|
|
31354
|
+
}
|
|
31355
|
+
function matchesHost(value, marketingHosts) {
|
|
31356
|
+
const normalized = normalizeHost(value);
|
|
31357
|
+
return marketingHosts.some(
|
|
31358
|
+
(candidate) => normalized === candidate || normalized.endsWith(`.${candidate}`)
|
|
31359
|
+
);
|
|
31360
|
+
}
|
|
31361
|
+
function normalizeLandingPage(value) {
|
|
31362
|
+
return normalizeUrlPath(value) ?? "/";
|
|
31363
|
+
}
|
|
31364
|
+
function normalizePathPrefix(value) {
|
|
31365
|
+
if (!value) return null;
|
|
31366
|
+
const normalized = normalizeLandingPage(value);
|
|
31367
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
31368
|
+
}
|
|
31369
|
+
function matchesPathPrefix(value, prefix) {
|
|
31370
|
+
if (!prefix) return true;
|
|
31371
|
+
const pathname = value.split("?")[0] ?? value;
|
|
31372
|
+
if (prefix === "/") return true;
|
|
31373
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
31374
|
+
}
|
|
31375
|
+
function splitKey(key) {
|
|
31376
|
+
const separator = key.indexOf("\0");
|
|
31377
|
+
if (separator < 0) return [key, "/"];
|
|
31378
|
+
return [key.slice(0, separator), key.slice(separator + 1)];
|
|
31379
|
+
}
|
|
31380
|
+
function parseGscPage(row) {
|
|
31381
|
+
try {
|
|
31382
|
+
const url = new URL(row.page);
|
|
31383
|
+
return {
|
|
31384
|
+
...row,
|
|
31385
|
+
hostName: url.hostname,
|
|
31386
|
+
landingPage: normalizeLandingPage(url.pathname)
|
|
31387
|
+
};
|
|
31388
|
+
} catch {
|
|
31389
|
+
return null;
|
|
31390
|
+
}
|
|
31391
|
+
}
|
|
31392
|
+
function buildGaMeasurementAnalysis(db, projectName, options = {}) {
|
|
31393
|
+
const parsedWindow = gaMeasurementAnalysisWindowSchema.safeParse(options.window ?? "90d");
|
|
31394
|
+
if (!parsedWindow.success) {
|
|
31395
|
+
throw validationError('"window" must be one of: 30d, 60d, 90d');
|
|
31396
|
+
}
|
|
31397
|
+
const parsedHostScope = gaMeasurementHostScopeSchema.safeParse(
|
|
31398
|
+
options.hostScope ?? "marketing"
|
|
31399
|
+
);
|
|
31400
|
+
if (!parsedHostScope.success) {
|
|
31401
|
+
throw validationError('"hostScope" must be one of: marketing, all');
|
|
31402
|
+
}
|
|
31403
|
+
const limit = Number(options.limit ?? 100);
|
|
31404
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
31405
|
+
throw validationError('"limit" must be an integer between 1 and 100');
|
|
31406
|
+
}
|
|
31407
|
+
const project = resolveProject(db, projectName);
|
|
31408
|
+
const days = windowDays(parsedWindow.data);
|
|
31409
|
+
const pathPrefix = normalizePathPrefix(options.pathPrefix);
|
|
31410
|
+
const marketingHosts = stableUnique(
|
|
31411
|
+
[
|
|
31412
|
+
project.canonicalDomain,
|
|
31413
|
+
...project.ownedDomains,
|
|
31414
|
+
...project.measurement.marketingHosts
|
|
31415
|
+
],
|
|
31416
|
+
normalizeHost
|
|
31417
|
+
);
|
|
31418
|
+
const brandTerms = stableUnique(
|
|
31419
|
+
[project.displayName, ...project.aliases, ...project.measurement.brandTerms],
|
|
31420
|
+
(value) => value.trim()
|
|
31421
|
+
);
|
|
31422
|
+
const hostIsIncluded = (hostName) => parsedHostScope.data === "all" || matchesHost(hostName, marketingHosts);
|
|
31423
|
+
const pageIsIncluded = (landingPage) => matchesPathPrefix(landingPage, pathPrefix);
|
|
31424
|
+
const scopedConditions = (hostColumn, normalizedPathColumn, rawPathColumn) => {
|
|
31425
|
+
const conditions = [];
|
|
31426
|
+
if (parsedHostScope.data === "marketing") {
|
|
31427
|
+
const lowerHost = sql14`lower(${hostColumn})`;
|
|
31428
|
+
const normalizedHost = sql14`case when ${lowerHost} like 'www.%' then substr(${lowerHost}, 5) else ${lowerHost} end`;
|
|
31429
|
+
conditions.push(or7(...marketingHosts.flatMap((host) => [
|
|
31430
|
+
sql14`${normalizedHost} = ${host}`,
|
|
31431
|
+
sql14`${normalizedHost} like ${`%.${host}`}`
|
|
31432
|
+
])));
|
|
31433
|
+
}
|
|
31434
|
+
if (pathPrefix && pathPrefix !== "/") {
|
|
31435
|
+
const rawPath = sql14`case when instr(${rawPathColumn}, '?') = 0 then ${rawPathColumn} else substr(${rawPathColumn}, 1, instr(${rawPathColumn}, '?') - 1) end`;
|
|
31436
|
+
const landingPath = sql14`case when trim(coalesce(${normalizedPathColumn}, '')) = '' then ${rawPath} else ${normalizedPathColumn} end`;
|
|
31437
|
+
conditions.push(or7(sql14`${landingPath} = ${pathPrefix}`, sql14`${landingPath} like ${`${pathPrefix}/%`}`));
|
|
31438
|
+
}
|
|
31439
|
+
return conditions;
|
|
31440
|
+
};
|
|
31441
|
+
const acquisitionScope = scopedConditions(gaAcquisitionDaily.hostName, gaAcquisitionDaily.landingPageNormalized, gaAcquisitionDaily.landingPage);
|
|
31442
|
+
const leadLandingScope = scopedConditions(gaLeadEventsDaily.hostName, gaLeadEventsDaily.landingPageNormalized, gaLeadEventsDaily.landingPage);
|
|
31443
|
+
const acquisitionAnchor = db.select({ date: sql14`max(${gaAcquisitionDaily.date})` }).from(gaAcquisitionDaily).where(and24(eq30(gaAcquisitionDaily.projectId, project.id), ...acquisitionScope)).get()?.date ?? null;
|
|
31444
|
+
const leadAnchor = db.select({ date: sql14`max(${gaLeadEventsDaily.date})` }).from(gaLeadEventsDaily).where(and24(eq30(gaLeadEventsDaily.projectId, project.id), or7(
|
|
31445
|
+
eq30(gaLeadEventsDaily.attributionScope, "channel"),
|
|
31446
|
+
and24(eq30(gaLeadEventsDaily.attributionScope, "landing-page"), ...leadLandingScope)
|
|
31447
|
+
))).get()?.date ?? null;
|
|
31448
|
+
const gaAnchor = [acquisitionAnchor, leadAnchor].filter((date) => date !== null).sort().at(-1) ?? null;
|
|
31449
|
+
const gaPeriods = gaAnchor ? buildPeriods(gaAnchor, days) : [];
|
|
31450
|
+
const gaStartDate = gaPeriods[0]?.startDate;
|
|
31451
|
+
const acquisitionRows = gaStartDate ? db.select().from(gaAcquisitionDaily).where(and24(eq30(gaAcquisitionDaily.projectId, project.id), gte7(gaAcquisitionDaily.date, gaStartDate), lte5(gaAcquisitionDaily.date, gaAnchor))).all() : [];
|
|
31452
|
+
const leadRows = gaStartDate ? db.select().from(gaLeadEventsDaily).where(and24(eq30(gaLeadEventsDaily.projectId, project.id), gte7(gaLeadEventsDaily.date, gaStartDate), lte5(gaLeadEventsDaily.date, gaAnchor))).all() : [];
|
|
31453
|
+
const gscAnchor = db.select({ date: sql14`max(${gscDailyTotals.date})` }).from(gscDailyTotals).where(eq30(gscDailyTotals.projectId, project.id)).get()?.date ?? null;
|
|
31454
|
+
const gscPeriods = gscAnchor ? buildPeriods(gscAnchor, days) : [];
|
|
31455
|
+
const gscStartDate = gscPeriods[0]?.startDate;
|
|
31456
|
+
const propertyRows = gscStartDate ? db.select().from(gscDailyTotals).where(and24(eq30(gscDailyTotals.projectId, project.id), gte7(gscDailyTotals.date, gscStartDate), lte5(gscDailyTotals.date, gscAnchor))).all() : [];
|
|
31457
|
+
const queryRows = gscStartDate ? db.select().from(gscQueryDailyTotals).where(and24(eq30(gscQueryDailyTotals.projectId, project.id), gte7(gscQueryDailyTotals.date, gscStartDate), lte5(gscQueryDailyTotals.date, gscAnchor))).all() : [];
|
|
31458
|
+
const rawPageRows = gscStartDate ? db.select().from(gscSearchData).where(and24(eq30(gscSearchData.projectId, project.id), gte7(gscSearchData.date, gscStartDate), lte5(gscSearchData.date, gscAnchor))).all() : [];
|
|
31459
|
+
const state = db.select().from(gaMeasurementSyncStates).where(eq30(gaMeasurementSyncStates.projectId, project.id)).get();
|
|
31460
|
+
const acquisition = acquisitionRows.filter((row) => {
|
|
31461
|
+
const landingPage = row.landingPageNormalized ?? normalizeLandingPage(row.landingPage);
|
|
31462
|
+
return hostIsIncluded(row.hostName) && pageIsIncluded(landingPage);
|
|
31463
|
+
});
|
|
31464
|
+
const acquisitionTotals = aggregateByKey(
|
|
31465
|
+
acquisition,
|
|
31466
|
+
gaPeriods,
|
|
31467
|
+
() => "all",
|
|
31468
|
+
(row) => row.sessions
|
|
31469
|
+
).get("all") ?? [];
|
|
31470
|
+
const acquisitionChannels = aggregateByKey(
|
|
31471
|
+
acquisition,
|
|
31472
|
+
gaPeriods,
|
|
31473
|
+
(row) => row.channelGroup,
|
|
31474
|
+
(row) => row.sessions
|
|
31475
|
+
);
|
|
31476
|
+
const acquisitionPages = aggregateByKey(
|
|
31477
|
+
acquisition,
|
|
31478
|
+
gaPeriods,
|
|
31479
|
+
(row) => `${row.hostName}\0${row.landingPageNormalized ?? normalizeLandingPage(row.landingPage)}`,
|
|
31480
|
+
(row) => row.sessions
|
|
31481
|
+
);
|
|
31482
|
+
const selectedLeadRows = leadRows.filter((row) => gaPeriods.some((period2) => row.date >= period2.startDate && row.date <= period2.endDate));
|
|
31483
|
+
const hasLeadTimeline = leadRows.length > 0 || state?.leadSyncedAt != null;
|
|
31484
|
+
const hasChannelOnlyLeads = selectedLeadRows.some((row) => row.attributionScope === "channel");
|
|
31485
|
+
const stateLeadScope = state === void 0 ? null : state.leadAttributionScope;
|
|
31486
|
+
const firstObservedLeadScope = selectedLeadRows.length > 0 ? selectedLeadRows[0].attributionScope : null;
|
|
31487
|
+
const attributionScope = hasChannelOnlyLeads ? "channel" : stateLeadScope ?? firstObservedLeadScope;
|
|
31488
|
+
const hostAndPathFiltersApplied = attributionScope === "landing-page";
|
|
31489
|
+
const leads = selectedLeadRows.filter((row) => {
|
|
31490
|
+
if (row.attributionScope === "channel") return true;
|
|
31491
|
+
const landingPage = row.landingPageNormalized ?? normalizeLandingPage(row.landingPage);
|
|
31492
|
+
return hostIsIncluded(row.hostName) && pageIsIncluded(landingPage);
|
|
31493
|
+
});
|
|
31494
|
+
const leadTotals = aggregateByKey(
|
|
31495
|
+
leads,
|
|
31496
|
+
gaPeriods,
|
|
31497
|
+
() => "all",
|
|
31498
|
+
(row) => row.eventCount
|
|
31499
|
+
).get("all") ?? [];
|
|
31500
|
+
const leadChannels = aggregateByKey(
|
|
31501
|
+
leads,
|
|
31502
|
+
gaPeriods,
|
|
31503
|
+
(row) => row.channelGroup,
|
|
31504
|
+
(row) => row.eventCount
|
|
31505
|
+
);
|
|
31506
|
+
const propertyClicks = aggregateByKey(
|
|
31507
|
+
propertyRows,
|
|
31508
|
+
gscPeriods,
|
|
31509
|
+
() => "all",
|
|
31510
|
+
(row) => row.clicks
|
|
31511
|
+
).get("all") ?? [];
|
|
31512
|
+
const propertyImpressions = aggregateByKey(
|
|
31513
|
+
propertyRows,
|
|
31514
|
+
gscPeriods,
|
|
31515
|
+
() => "all",
|
|
31516
|
+
(row) => row.impressions
|
|
31517
|
+
).get("all") ?? [];
|
|
31518
|
+
const queryClicks = aggregateByKey(
|
|
31519
|
+
queryRows,
|
|
31520
|
+
gscPeriods,
|
|
31521
|
+
() => "all",
|
|
31522
|
+
(row) => row.clicks
|
|
31523
|
+
).get("all") ?? [];
|
|
31524
|
+
const queryImpressions = aggregateByKey(
|
|
31525
|
+
queryRows,
|
|
31526
|
+
gscPeriods,
|
|
31527
|
+
() => "all",
|
|
31528
|
+
(row) => row.impressions
|
|
31529
|
+
).get("all") ?? [];
|
|
31530
|
+
const brandedQueries = new Set(filterBrandedSeedCandidates({
|
|
31531
|
+
candidates: queryRows.map((row) => row.query),
|
|
31532
|
+
brandNames: brandTerms,
|
|
31533
|
+
canonicalDomains: [project.canonicalDomain, ...project.ownedDomains]
|
|
31534
|
+
}).droppedBranded);
|
|
31535
|
+
const brandedClicks = aggregateByKey(
|
|
31536
|
+
queryRows.filter((row) => brandedQueries.has(row.query)),
|
|
31537
|
+
gscPeriods,
|
|
31538
|
+
() => "all",
|
|
31539
|
+
(row) => row.clicks
|
|
31540
|
+
).get("all") ?? [];
|
|
31541
|
+
const brandedImpressions = aggregateByKey(
|
|
31542
|
+
queryRows.filter((row) => brandedQueries.has(row.query)),
|
|
31543
|
+
gscPeriods,
|
|
31544
|
+
() => "all",
|
|
31545
|
+
(row) => row.impressions
|
|
31546
|
+
).get("all") ?? [];
|
|
31547
|
+
const perQueryClicks = aggregateByKey(
|
|
31548
|
+
queryRows,
|
|
31549
|
+
gscPeriods,
|
|
31550
|
+
(row) => row.query,
|
|
31551
|
+
(row) => row.clicks
|
|
31552
|
+
);
|
|
31553
|
+
const perQueryImpressions = aggregateByKey(
|
|
31554
|
+
queryRows,
|
|
31555
|
+
gscPeriods,
|
|
31556
|
+
(row) => row.query,
|
|
31557
|
+
(row) => row.impressions
|
|
31558
|
+
);
|
|
31559
|
+
const gscPageRows = rawPageRows.map(parseGscPage).filter((row) => row !== null).filter((row) => hostIsIncluded(row.hostName) && pageIsIncluded(row.landingPage));
|
|
31560
|
+
const perPageClicks = aggregateByKey(
|
|
31561
|
+
gscPageRows,
|
|
31562
|
+
gscPeriods,
|
|
31563
|
+
(row) => `${row.hostName}\0${row.landingPage}`,
|
|
31564
|
+
(row) => row.clicks
|
|
31565
|
+
);
|
|
31566
|
+
const perPageImpressions = aggregateByKey(
|
|
31567
|
+
gscPageRows,
|
|
31568
|
+
gscPeriods,
|
|
31569
|
+
(row) => `${row.hostName}\0${row.landingPage}`,
|
|
31570
|
+
(row) => row.impressions
|
|
31571
|
+
);
|
|
31572
|
+
return gaMeasurementAnalysisDtoSchema.parse({
|
|
31573
|
+
window: parsedWindow.data,
|
|
31574
|
+
bucketDays: 30,
|
|
31575
|
+
filters: {
|
|
31576
|
+
hostScope: parsedHostScope.data,
|
|
31577
|
+
marketingHosts,
|
|
31578
|
+
pathPrefix,
|
|
31579
|
+
brandTerms,
|
|
31580
|
+
queryMixScope: "property"
|
|
31581
|
+
},
|
|
31582
|
+
acquisition: {
|
|
31583
|
+
status: state?.acquisitionStatus ?? "never-synced",
|
|
31584
|
+
error: state?.acquisitionError ?? null,
|
|
31585
|
+
syncedAt: state?.acquisitionSyncedAt ?? null,
|
|
31586
|
+
periods: gaAnchor ? sessionPeriods(gaPeriods, acquisitionTotals) : [],
|
|
31587
|
+
channels: rankEntries(acquisitionChannels).map(([channelGroup, values]) => ({
|
|
31588
|
+
channelGroup,
|
|
31589
|
+
periods: sessionPeriods(gaPeriods, values)
|
|
31590
|
+
})),
|
|
31591
|
+
pages: rankEntries(acquisitionPages).slice(0, limit).map(([key, values]) => {
|
|
31592
|
+
const [hostName, landingPage] = splitKey(key);
|
|
31593
|
+
return {
|
|
31594
|
+
hostName,
|
|
31595
|
+
landingPage,
|
|
31596
|
+
periods: sessionPeriods(gaPeriods, values)
|
|
31597
|
+
};
|
|
31598
|
+
})
|
|
31599
|
+
},
|
|
31600
|
+
leads: {
|
|
31601
|
+
status: state?.leadStatus ?? "never-synced",
|
|
31602
|
+
error: state?.leadError ?? null,
|
|
31603
|
+
syncedAt: state?.leadSyncedAt ?? null,
|
|
31604
|
+
attributionScope,
|
|
31605
|
+
hostAndPathFiltersApplied,
|
|
31606
|
+
periods: gaAnchor && hasLeadTimeline ? eventPeriods(gaPeriods, leadTotals) : [],
|
|
31607
|
+
channels: rankEntries(leadChannels).map(([channelGroup, values]) => ({
|
|
31608
|
+
channelGroup,
|
|
31609
|
+
periods: eventPeriods(gaPeriods, values)
|
|
31610
|
+
}))
|
|
31611
|
+
},
|
|
31612
|
+
searchDemand: gscAnchor === null ? {
|
|
31613
|
+
status: "unavailable",
|
|
31614
|
+
periods: [],
|
|
31615
|
+
queries: [],
|
|
31616
|
+
pages: [],
|
|
31617
|
+
latestDate: null
|
|
31618
|
+
} : {
|
|
31619
|
+
status: "ready",
|
|
31620
|
+
latestDate: gscAnchor,
|
|
31621
|
+
periods: gscPeriods.map((period2, index2) => ({
|
|
31622
|
+
...period2,
|
|
31623
|
+
propertyClicks: propertyClicks[index2] ?? 0,
|
|
31624
|
+
propertyImpressions: propertyImpressions[index2] ?? 0,
|
|
31625
|
+
reportedQueryClicks: queryClicks[index2] ?? 0,
|
|
31626
|
+
reportedQueryImpressions: queryImpressions[index2] ?? 0,
|
|
31627
|
+
brandedClicks: brandedClicks[index2] ?? 0,
|
|
31628
|
+
brandedImpressions: brandedImpressions[index2] ?? 0,
|
|
31629
|
+
nonBrandedClicks: Math.max(
|
|
31630
|
+
0,
|
|
31631
|
+
(queryClicks[index2] ?? 0) - (brandedClicks[index2] ?? 0)
|
|
31632
|
+
),
|
|
31633
|
+
nonBrandedImpressions: Math.max(
|
|
31634
|
+
0,
|
|
31635
|
+
(queryImpressions[index2] ?? 0) - (brandedImpressions[index2] ?? 0)
|
|
31636
|
+
),
|
|
31637
|
+
unreportedClicks: Math.max(
|
|
31638
|
+
0,
|
|
31639
|
+
(propertyClicks[index2] ?? 0) - (queryClicks[index2] ?? 0)
|
|
31640
|
+
),
|
|
31641
|
+
unreportedImpressions: Math.max(
|
|
31642
|
+
0,
|
|
31643
|
+
(propertyImpressions[index2] ?? 0) - (queryImpressions[index2] ?? 0)
|
|
31644
|
+
)
|
|
31645
|
+
})),
|
|
31646
|
+
queries: rankEntries(perQueryClicks).slice(0, limit).map(([query, values]) => ({
|
|
31647
|
+
query,
|
|
31648
|
+
classification: brandedQueries.has(query) ? "branded" : "non-branded",
|
|
31649
|
+
periods: clickPeriods(
|
|
31650
|
+
gscPeriods,
|
|
31651
|
+
values,
|
|
31652
|
+
perQueryImpressions.get(query) ?? []
|
|
31653
|
+
)
|
|
31654
|
+
})),
|
|
31655
|
+
pages: rankEntries(perPageImpressions).slice(0, limit).map(([key, impressions]) => {
|
|
31656
|
+
const [hostName, landingPage] = splitKey(key);
|
|
31657
|
+
return {
|
|
31658
|
+
hostName,
|
|
31659
|
+
landingPage,
|
|
31660
|
+
periods: clickPeriods(
|
|
31661
|
+
gscPeriods,
|
|
31662
|
+
perPageClicks.get(key) ?? [],
|
|
31663
|
+
impressions
|
|
31664
|
+
)
|
|
31665
|
+
};
|
|
31666
|
+
})
|
|
31667
|
+
}
|
|
31668
|
+
});
|
|
31669
|
+
}
|
|
31670
|
+
async function gaMeasurementAnalysisRoutes(app) {
|
|
31671
|
+
app.get("/projects/:name/ga/measurement-analysis", (request) => buildGaMeasurementAnalysis(app.db, request.params.name, request.query));
|
|
31672
|
+
}
|
|
31673
|
+
|
|
31233
31674
|
// ../integration-wordpress/src/types.ts
|
|
31234
31675
|
var WordpressApiError = class extends Error {
|
|
31235
31676
|
statusCode;
|
|
@@ -32866,7 +33307,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
32866
33307
|
|
|
32867
33308
|
// ../api-routes/src/backlinks.ts
|
|
32868
33309
|
import crypto26 from "crypto";
|
|
32869
|
-
import { and as
|
|
33310
|
+
import { and as and26, asc as asc7, desc as desc14, eq as eq31, sql as sql15 } from "drizzle-orm";
|
|
32870
33311
|
|
|
32871
33312
|
// ../integration-commoncrawl/src/constants.ts
|
|
32872
33313
|
import os2 from "os";
|
|
@@ -33156,7 +33597,7 @@ async function queryBacklinks(opts) {
|
|
|
33156
33597
|
const reversed = opts.targets.map(reverseDomain);
|
|
33157
33598
|
const targetList = reversed.map(quote).join(", ");
|
|
33158
33599
|
const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
|
|
33159
|
-
const
|
|
33600
|
+
const sql18 = `
|
|
33160
33601
|
WITH vertices AS (
|
|
33161
33602
|
SELECT * FROM read_csv(
|
|
33162
33603
|
${quote(opts.vertexPath)},
|
|
@@ -33192,7 +33633,7 @@ async function queryBacklinks(opts) {
|
|
|
33192
33633
|
const conn = await instance.connect();
|
|
33193
33634
|
let rows;
|
|
33194
33635
|
try {
|
|
33195
|
-
const reader = await conn.runAndReadAll(
|
|
33636
|
+
const reader = await conn.runAndReadAll(sql18);
|
|
33196
33637
|
rows = reader.getRowObjects();
|
|
33197
33638
|
} finally {
|
|
33198
33639
|
conn.disconnectSync?.();
|
|
@@ -33269,7 +33710,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
33269
33710
|
}
|
|
33270
33711
|
|
|
33271
33712
|
// ../api-routes/src/backlinks-filter.ts
|
|
33272
|
-
import { and as
|
|
33713
|
+
import { and as and25, ne as ne4, notLike } from "drizzle-orm";
|
|
33273
33714
|
var BACKLINK_FILTER_PATTERNS = [
|
|
33274
33715
|
"*.google.com",
|
|
33275
33716
|
"*.googleusercontent.com",
|
|
@@ -33292,7 +33733,7 @@ function backlinkCrawlerExclusionClause() {
|
|
|
33292
33733
|
conditions.push(ne4(backlinkDomains.linkingDomain, pattern));
|
|
33293
33734
|
}
|
|
33294
33735
|
}
|
|
33295
|
-
const combined =
|
|
33736
|
+
const combined = and25(...conditions);
|
|
33296
33737
|
if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
|
|
33297
33738
|
return combined;
|
|
33298
33739
|
}
|
|
@@ -33363,11 +33804,11 @@ function mapRunRow(row) {
|
|
|
33363
33804
|
}
|
|
33364
33805
|
function latestSummaryForProject(db, projectId, source, release) {
|
|
33365
33806
|
const conditions = [
|
|
33366
|
-
|
|
33367
|
-
|
|
33807
|
+
eq31(backlinkSummaries.projectId, projectId),
|
|
33808
|
+
eq31(backlinkSummaries.source, source)
|
|
33368
33809
|
];
|
|
33369
|
-
if (release) conditions.push(
|
|
33370
|
-
return db.select().from(backlinkSummaries).where(
|
|
33810
|
+
if (release) conditions.push(eq31(backlinkSummaries.release, release));
|
|
33811
|
+
return db.select().from(backlinkSummaries).where(and26(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
33371
33812
|
}
|
|
33372
33813
|
function parseExcludeCrawlers(value) {
|
|
33373
33814
|
if (!value) return false;
|
|
@@ -33375,19 +33816,19 @@ function parseExcludeCrawlers(value) {
|
|
|
33375
33816
|
return lower === "1" || lower === "true" || lower === "yes";
|
|
33376
33817
|
}
|
|
33377
33818
|
function computeFilteredSummary(db, base) {
|
|
33378
|
-
const baseDomainCondition =
|
|
33379
|
-
|
|
33380
|
-
|
|
33381
|
-
|
|
33819
|
+
const baseDomainCondition = and26(
|
|
33820
|
+
eq31(backlinkDomains.projectId, base.projectId),
|
|
33821
|
+
eq31(backlinkDomains.source, base.source),
|
|
33822
|
+
eq31(backlinkDomains.release, base.release)
|
|
33382
33823
|
);
|
|
33383
|
-
const filteredCondition =
|
|
33824
|
+
const filteredCondition = and26(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
33384
33825
|
const unfilteredAgg = db.select({
|
|
33385
|
-
count:
|
|
33386
|
-
total:
|
|
33826
|
+
count: sql15`count(*)`,
|
|
33827
|
+
total: sql15`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
33387
33828
|
}).from(backlinkDomains).where(baseDomainCondition).get();
|
|
33388
33829
|
const filteredAgg = db.select({
|
|
33389
|
-
count:
|
|
33390
|
-
total:
|
|
33830
|
+
count: sql15`count(*)`,
|
|
33831
|
+
total: sql15`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
33391
33832
|
}).from(backlinkDomains).where(filteredCondition).get();
|
|
33392
33833
|
const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc14(backlinkDomains.numHosts)).limit(10).all();
|
|
33393
33834
|
const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
|
|
@@ -33410,13 +33851,13 @@ function computeFilteredSummary(db, base) {
|
|
|
33410
33851
|
};
|
|
33411
33852
|
}
|
|
33412
33853
|
function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
|
|
33413
|
-
const summary = db.select().from(backlinkSummaries).where(
|
|
33854
|
+
const summary = db.select().from(backlinkSummaries).where(and26(eq31(backlinkSummaries.projectId, projectId), eq31(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
33414
33855
|
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
33415
33856
|
if (summary && excludeCrawlers) {
|
|
33416
|
-
const filtered = db.select({ count:
|
|
33417
|
-
|
|
33418
|
-
|
|
33419
|
-
|
|
33857
|
+
const filtered = db.select({ count: sql15`count(*)` }).from(backlinkDomains).where(and26(
|
|
33858
|
+
eq31(backlinkDomains.projectId, projectId),
|
|
33859
|
+
eq31(backlinkDomains.source, source),
|
|
33860
|
+
eq31(backlinkDomains.release, summary.release),
|
|
33420
33861
|
backlinkCrawlerExclusionClause()
|
|
33421
33862
|
)).get();
|
|
33422
33863
|
totalLinkingDomains = Number(filtered?.count ?? 0);
|
|
@@ -33431,7 +33872,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
|
|
|
33431
33872
|
};
|
|
33432
33873
|
}
|
|
33433
33874
|
function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
|
|
33434
|
-
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
33875
|
+
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
33435
33876
|
const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
|
|
33436
33877
|
const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
|
|
33437
33878
|
const sources = [
|
|
@@ -33487,7 +33928,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33487
33928
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
33488
33929
|
);
|
|
33489
33930
|
}
|
|
33490
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
33931
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.release, release)).get();
|
|
33491
33932
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33492
33933
|
if (existing) {
|
|
33493
33934
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -33498,9 +33939,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
33498
33939
|
phaseDetail: null,
|
|
33499
33940
|
error: null,
|
|
33500
33941
|
updatedAt: now
|
|
33501
|
-
}).where(
|
|
33942
|
+
}).where(eq31(ccReleaseSyncs.id, existing.id)).run();
|
|
33502
33943
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
33503
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
33944
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.id, existing.id)).get();
|
|
33504
33945
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
33505
33946
|
}
|
|
33506
33947
|
const id = crypto26.randomUUID();
|
|
@@ -33512,7 +33953,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33512
33953
|
updatedAt: now
|
|
33513
33954
|
}).run();
|
|
33514
33955
|
opts.onReleaseSyncRequested(id, release);
|
|
33515
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
33956
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.id, id)).get();
|
|
33516
33957
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
33517
33958
|
});
|
|
33518
33959
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
@@ -33570,7 +34011,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33570
34011
|
createdAt: now
|
|
33571
34012
|
}).run();
|
|
33572
34013
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
33573
|
-
const run = app.db.select().from(runs).where(
|
|
34014
|
+
const run = app.db.select().from(runs).where(eq31(runs.id, runId)).get();
|
|
33574
34015
|
return reply.status(201).send(mapRunRow(run));
|
|
33575
34016
|
});
|
|
33576
34017
|
app.get(
|
|
@@ -33596,13 +34037,13 @@ async function backlinksRoutes(app, opts) {
|
|
|
33596
34037
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
33597
34038
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
33598
34039
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
33599
|
-
const baseDomainCondition =
|
|
33600
|
-
|
|
33601
|
-
|
|
33602
|
-
|
|
34040
|
+
const baseDomainCondition = and26(
|
|
34041
|
+
eq31(backlinkDomains.projectId, project.id),
|
|
34042
|
+
eq31(backlinkDomains.source, source),
|
|
34043
|
+
eq31(backlinkDomains.release, targetRelease)
|
|
33603
34044
|
);
|
|
33604
|
-
const domainCondition = excludeCrawlers ?
|
|
33605
|
-
const totalRow = app.db.select({ count:
|
|
34045
|
+
const domainCondition = excludeCrawlers ? and26(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
34046
|
+
const totalRow = app.db.select({ count: sql15`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
33606
34047
|
const rows = app.db.select({
|
|
33607
34048
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
33608
34049
|
numHosts: backlinkDomains.numHosts,
|
|
@@ -33625,7 +34066,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33625
34066
|
async (request, reply) => {
|
|
33626
34067
|
const project = resolveProject(app.db, request.params.name);
|
|
33627
34068
|
const source = parseSourceParam(request.query.source);
|
|
33628
|
-
const rows = app.db.select().from(backlinkSummaries).where(
|
|
34069
|
+
const rows = app.db.select().from(backlinkSummaries).where(and26(eq31(backlinkSummaries.projectId, project.id), eq31(backlinkSummaries.source, source))).orderBy(asc7(backlinkSummaries.queriedAt)).all();
|
|
33629
34070
|
const response = rows.map((r) => ({
|
|
33630
34071
|
release: r.release,
|
|
33631
34072
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -33672,7 +34113,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33672
34113
|
createdAt: now
|
|
33673
34114
|
}).run();
|
|
33674
34115
|
opts.onBingBacklinkSyncRequested(runId, project.id);
|
|
33675
|
-
const run = app.db.select().from(runs).where(
|
|
34116
|
+
const run = app.db.select().from(runs).where(eq31(runs.id, runId)).get();
|
|
33676
34117
|
return reply.status(201).send(mapRunRow(run));
|
|
33677
34118
|
}
|
|
33678
34119
|
);
|
|
@@ -33681,7 +34122,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33681
34122
|
// ../api-routes/src/traffic.ts
|
|
33682
34123
|
import crypto28 from "crypto";
|
|
33683
34124
|
import { Agent as UndiciAgent } from "undici";
|
|
33684
|
-
import { and as
|
|
34125
|
+
import { and as and27, desc as desc15, eq as eq32, gte as gte8, lte as lte6, sql as sql16 } from "drizzle-orm";
|
|
33685
34126
|
|
|
33686
34127
|
// ../integration-cloud-run/src/auth.ts
|
|
33687
34128
|
import crypto27 from "crypto";
|
|
@@ -36464,24 +36905,24 @@ var DEFAULT_AI_REFERRER_RULES = [
|
|
|
36464
36905
|
|
|
36465
36906
|
// ../integration-traffic/src/classifier.ts
|
|
36466
36907
|
var USER_FETCH_PURPOSE = "user-agent";
|
|
36467
|
-
function
|
|
36908
|
+
function normalizeHost2(host) {
|
|
36468
36909
|
return host.trim().toLowerCase().replace(/^www\./, "");
|
|
36469
36910
|
}
|
|
36470
36911
|
function hostMatches(host, domain) {
|
|
36471
|
-
const normalizedHost =
|
|
36472
|
-
const normalizedDomain =
|
|
36912
|
+
const normalizedHost = normalizeHost2(host);
|
|
36913
|
+
const normalizedDomain = normalizeHost2(domain);
|
|
36473
36914
|
return normalizedHost === normalizedDomain || normalizedHost.endsWith(`.${normalizedDomain}`);
|
|
36474
36915
|
}
|
|
36475
36916
|
function utmTokenMatchesDomain(utmSource, domain) {
|
|
36476
36917
|
if (hostMatches(utmSource, domain)) return true;
|
|
36477
|
-
const normalizedUtm =
|
|
36478
|
-
const firstLabel =
|
|
36918
|
+
const normalizedUtm = normalizeHost2(utmSource);
|
|
36919
|
+
const firstLabel = normalizeHost2(domain).split(".")[0];
|
|
36479
36920
|
return Boolean(firstLabel) && normalizedUtm === firstLabel;
|
|
36480
36921
|
}
|
|
36481
36922
|
function hostFromUrl(value) {
|
|
36482
36923
|
if (!value) return null;
|
|
36483
36924
|
try {
|
|
36484
|
-
return
|
|
36925
|
+
return normalizeHost2(new URL(value).hostname);
|
|
36485
36926
|
} catch {
|
|
36486
36927
|
return null;
|
|
36487
36928
|
}
|
|
@@ -36490,7 +36931,7 @@ function utmSourceFromQuery(queryString) {
|
|
|
36490
36931
|
if (!queryString) return null;
|
|
36491
36932
|
const params = new URLSearchParams(queryString);
|
|
36492
36933
|
const source = params.get("utm_source");
|
|
36493
|
-
return source ?
|
|
36934
|
+
return source ? normalizeHost2(source) : null;
|
|
36494
36935
|
}
|
|
36495
36936
|
function utmSourceFromUrl(value) {
|
|
36496
36937
|
if (!value) return null;
|
|
@@ -36654,13 +37095,13 @@ function sessionWindowBucket(value, windowMs) {
|
|
|
36654
37095
|
if (Number.isNaN(date.getTime())) return value;
|
|
36655
37096
|
return new Date(Math.floor(date.getTime() / windowMs) * windowMs).toISOString();
|
|
36656
37097
|
}
|
|
36657
|
-
function
|
|
37098
|
+
function normalizeHost3(host) {
|
|
36658
37099
|
if (!host) return null;
|
|
36659
37100
|
return host.trim().toLowerCase().replace(/^www\./, "") || null;
|
|
36660
37101
|
}
|
|
36661
37102
|
function sameHost(a, b) {
|
|
36662
|
-
const normalizedA =
|
|
36663
|
-
const normalizedB =
|
|
37103
|
+
const normalizedA = normalizeHost3(a);
|
|
37104
|
+
const normalizedB = normalizeHost3(b);
|
|
36664
37105
|
return !!normalizedA && !!normalizedB && normalizedA === normalizedB;
|
|
36665
37106
|
}
|
|
36666
37107
|
function pathFromSameOriginReferer(event) {
|
|
@@ -37577,8 +38018,8 @@ async function runBackfillTask(options) {
|
|
|
37577
38018
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
37578
38019
|
try {
|
|
37579
38020
|
app.db.transaction((tx) => {
|
|
37580
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
37581
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
38021
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq32(runs.id, runId)).run();
|
|
38022
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq32(trafficSources.id, sourceRow.id)).run();
|
|
37582
38023
|
});
|
|
37583
38024
|
} catch {
|
|
37584
38025
|
}
|
|
@@ -37593,7 +38034,7 @@ async function runBackfillTask(options) {
|
|
|
37593
38034
|
if (allEvents.length === 0) {
|
|
37594
38035
|
const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
37595
38036
|
try {
|
|
37596
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(
|
|
38037
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq32(runs.id, runId)).run();
|
|
37597
38038
|
} catch {
|
|
37598
38039
|
}
|
|
37599
38040
|
return;
|
|
@@ -37615,31 +38056,31 @@ async function runBackfillTask(options) {
|
|
|
37615
38056
|
try {
|
|
37616
38057
|
app.db.transaction((tx) => {
|
|
37617
38058
|
tx.delete(crawlerEventsHourly).where(
|
|
37618
|
-
|
|
37619
|
-
|
|
37620
|
-
|
|
37621
|
-
|
|
38059
|
+
and27(
|
|
38060
|
+
eq32(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
38061
|
+
gte8(crawlerEventsHourly.tsHour, windowStartIso),
|
|
38062
|
+
lte6(crawlerEventsHourly.tsHour, windowEndIso)
|
|
37622
38063
|
)
|
|
37623
38064
|
).run();
|
|
37624
38065
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
37625
|
-
|
|
37626
|
-
|
|
37627
|
-
|
|
37628
|
-
|
|
38066
|
+
and27(
|
|
38067
|
+
eq32(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
38068
|
+
gte8(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
38069
|
+
lte6(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
37629
38070
|
)
|
|
37630
38071
|
).run();
|
|
37631
38072
|
tx.delete(aiReferralEventsHourly).where(
|
|
37632
|
-
|
|
37633
|
-
|
|
37634
|
-
|
|
37635
|
-
|
|
38073
|
+
and27(
|
|
38074
|
+
eq32(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
38075
|
+
gte8(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
38076
|
+
lte6(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
37636
38077
|
)
|
|
37637
38078
|
).run();
|
|
37638
38079
|
tx.delete(rawEventSamples).where(
|
|
37639
|
-
|
|
37640
|
-
|
|
37641
|
-
|
|
37642
|
-
|
|
38080
|
+
and27(
|
|
38081
|
+
eq32(rawEventSamples.sourceId, sourceRow.id),
|
|
38082
|
+
gte8(rawEventSamples.ts, windowStartIso),
|
|
38083
|
+
lte6(rawEventSamples.ts, windowEndIso)
|
|
37643
38084
|
)
|
|
37644
38085
|
).run();
|
|
37645
38086
|
for (const bucket of report.crawlerEventsHourly) {
|
|
@@ -37728,8 +38169,8 @@ async function runBackfillTask(options) {
|
|
|
37728
38169
|
lastError: null,
|
|
37729
38170
|
lastEventIds: newRingBuffer,
|
|
37730
38171
|
updatedAt: finishedAt
|
|
37731
|
-
}).where(
|
|
37732
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
38172
|
+
}).where(eq32(trafficSources.id, sourceRow.id)).run();
|
|
38173
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq32(runs.id, runId)).run();
|
|
37733
38174
|
});
|
|
37734
38175
|
} catch (e) {
|
|
37735
38176
|
markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -37811,7 +38252,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37811
38252
|
createdAt: existing?.createdAt ?? now,
|
|
37812
38253
|
updatedAt: now
|
|
37813
38254
|
});
|
|
37814
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
38255
|
+
const activeSource = app.db.select().from(trafficSources).where(eq32(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
|
|
37815
38256
|
const config = {
|
|
37816
38257
|
gcpProjectId,
|
|
37817
38258
|
serviceName: serviceName ?? null,
|
|
@@ -37827,8 +38268,8 @@ async function trafficRoutes(app, opts) {
|
|
|
37827
38268
|
lastError: null,
|
|
37828
38269
|
configJson: config,
|
|
37829
38270
|
updatedAt: now
|
|
37830
|
-
}).where(
|
|
37831
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
38271
|
+
}).where(eq32(trafficSources.id, activeSource.id)).run();
|
|
38272
|
+
sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, activeSource.id)).get();
|
|
37832
38273
|
} else {
|
|
37833
38274
|
const newId = crypto28.randomUUID();
|
|
37834
38275
|
app.db.insert(trafficSources).values({
|
|
@@ -37845,7 +38286,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37845
38286
|
createdAt: now,
|
|
37846
38287
|
updatedAt: now
|
|
37847
38288
|
}).run();
|
|
37848
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
38289
|
+
sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, newId)).get();
|
|
37849
38290
|
}
|
|
37850
38291
|
writeAuditLog(app.db, {
|
|
37851
38292
|
projectId: project.id,
|
|
@@ -37897,7 +38338,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37897
38338
|
createdAt: existing?.createdAt ?? now,
|
|
37898
38339
|
updatedAt: now
|
|
37899
38340
|
});
|
|
37900
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
38341
|
+
const activeSource = app.db.select().from(trafficSources).where(eq32(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
|
|
37901
38342
|
const config = { baseUrl, username };
|
|
37902
38343
|
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
37903
38344
|
let sourceRow;
|
|
@@ -37908,8 +38349,8 @@ async function trafficRoutes(app, opts) {
|
|
|
37908
38349
|
lastError: null,
|
|
37909
38350
|
configJson: config,
|
|
37910
38351
|
updatedAt: now
|
|
37911
|
-
}).where(
|
|
37912
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
38352
|
+
}).where(eq32(trafficSources.id, activeSource.id)).run();
|
|
38353
|
+
sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, activeSource.id)).get();
|
|
37913
38354
|
} else {
|
|
37914
38355
|
const newId = crypto28.randomUUID();
|
|
37915
38356
|
app.db.insert(trafficSources).values({
|
|
@@ -37926,7 +38367,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37926
38367
|
createdAt: now,
|
|
37927
38368
|
updatedAt: now
|
|
37928
38369
|
}).run();
|
|
37929
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
38370
|
+
sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, newId)).get();
|
|
37930
38371
|
}
|
|
37931
38372
|
writeAuditLog(app.db, {
|
|
37932
38373
|
projectId: project.id,
|
|
@@ -37980,7 +38421,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37980
38421
|
createdAt: existing?.createdAt ?? now,
|
|
37981
38422
|
updatedAt: now
|
|
37982
38423
|
});
|
|
37983
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
38424
|
+
const activeSource = app.db.select().from(trafficSources).where(eq32(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
|
|
37984
38425
|
const config = { projectId, teamId, environment };
|
|
37985
38426
|
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
37986
38427
|
const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
|
|
@@ -37992,8 +38433,8 @@ async function trafficRoutes(app, opts) {
|
|
|
37992
38433
|
lastError: null,
|
|
37993
38434
|
configJson: config,
|
|
37994
38435
|
updatedAt: now
|
|
37995
|
-
}).where(
|
|
37996
|
-
row = tx.select().from(trafficSources).where(
|
|
38436
|
+
}).where(eq32(trafficSources.id, activeSource.id)).run();
|
|
38437
|
+
row = tx.select().from(trafficSources).where(eq32(trafficSources.id, activeSource.id)).get();
|
|
37997
38438
|
} else {
|
|
37998
38439
|
const newId = crypto28.randomUUID();
|
|
37999
38440
|
tx.insert(trafficSources).values({
|
|
@@ -38018,12 +38459,12 @@ async function trafficRoutes(app, opts) {
|
|
|
38018
38459
|
createdAt: now,
|
|
38019
38460
|
updatedAt: now
|
|
38020
38461
|
}).run();
|
|
38021
|
-
row = tx.select().from(trafficSources).where(
|
|
38462
|
+
row = tx.select().from(trafficSources).where(eq32(trafficSources.id, newId)).get();
|
|
38022
38463
|
}
|
|
38023
38464
|
const existingSchedule = tx.select().from(schedules).where(
|
|
38024
|
-
|
|
38025
|
-
|
|
38026
|
-
|
|
38465
|
+
and27(
|
|
38466
|
+
eq32(schedules.projectId, project.id),
|
|
38467
|
+
eq32(schedules.kind, SchedulableRunKinds["traffic-sync"])
|
|
38027
38468
|
)
|
|
38028
38469
|
).get();
|
|
38029
38470
|
let created = false;
|
|
@@ -38072,7 +38513,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38072
38513
|
});
|
|
38073
38514
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
38074
38515
|
const project = resolveProject(app.db, request.params.name);
|
|
38075
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
38516
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, request.params.id)).get();
|
|
38076
38517
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
38077
38518
|
throw notFound("Traffic source", request.params.id);
|
|
38078
38519
|
}
|
|
@@ -38098,8 +38539,8 @@ async function trafficRoutes(app, opts) {
|
|
|
38098
38539
|
const markFailed = (msg, errorCode) => {
|
|
38099
38540
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38100
38541
|
app.db.transaction((tx) => {
|
|
38101
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
38102
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
38542
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq32(runs.id, runId)).run();
|
|
38543
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq32(trafficSources.id, sourceRow.id)).run();
|
|
38103
38544
|
});
|
|
38104
38545
|
try {
|
|
38105
38546
|
opts.onTrafficSynced?.({
|
|
@@ -38180,7 +38621,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38180
38621
|
}
|
|
38181
38622
|
const credential = credentialStore.getConnection(project.name);
|
|
38182
38623
|
if (!credential) {
|
|
38183
|
-
app.db.delete(runs).where(
|
|
38624
|
+
app.db.delete(runs).where(eq32(runs.id, runId)).run();
|
|
38184
38625
|
throw validationError(
|
|
38185
38626
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
38186
38627
|
);
|
|
@@ -38229,12 +38670,12 @@ async function trafficRoutes(app, opts) {
|
|
|
38229
38670
|
auditAction = "traffic.vercel.synced";
|
|
38230
38671
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
38231
38672
|
if (!credentialStore) {
|
|
38232
|
-
app.db.delete(runs).where(
|
|
38673
|
+
app.db.delete(runs).where(eq32(runs.id, runId)).run();
|
|
38233
38674
|
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
38234
38675
|
}
|
|
38235
38676
|
const credential = credentialStore.getConnection(project.name);
|
|
38236
38677
|
if (!credential) {
|
|
38237
|
-
app.db.delete(runs).where(
|
|
38678
|
+
app.db.delete(runs).where(eq32(runs.id, runId)).run();
|
|
38238
38679
|
throw validationError(
|
|
38239
38680
|
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
38240
38681
|
);
|
|
@@ -38334,7 +38775,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38334
38775
|
let aiReferralHitsCount = 0;
|
|
38335
38776
|
let unknownHitsCount = 0;
|
|
38336
38777
|
app.db.transaction((tx) => {
|
|
38337
|
-
const latestRow = tx.select().from(trafficSources).where(
|
|
38778
|
+
const latestRow = tx.select().from(trafficSources).where(eq32(trafficSources.id, sourceRow.id)).get();
|
|
38338
38779
|
const previousIds = latestRow.lastEventIds ?? [];
|
|
38339
38780
|
const seenEventIds = new Set(previousIds);
|
|
38340
38781
|
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
@@ -38382,7 +38823,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38382
38823
|
crawlerEventsHourly.status
|
|
38383
38824
|
],
|
|
38384
38825
|
set: {
|
|
38385
|
-
hits:
|
|
38826
|
+
hits: sql16`${crawlerEventsHourly.hits} + ${bucket.hits}`,
|
|
38386
38827
|
sampledUserAgent: bucket.sampledUserAgent,
|
|
38387
38828
|
updatedAt: finishedAt
|
|
38388
38829
|
}
|
|
@@ -38415,7 +38856,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38415
38856
|
aiUserFetchEventsHourly.status
|
|
38416
38857
|
],
|
|
38417
38858
|
set: {
|
|
38418
|
-
hits:
|
|
38859
|
+
hits: sql16`${aiUserFetchEventsHourly.hits} + ${bucket.hits}`,
|
|
38419
38860
|
sampledUserAgent: bucket.sampledUserAgent,
|
|
38420
38861
|
updatedAt: finishedAt
|
|
38421
38862
|
}
|
|
@@ -38455,9 +38896,9 @@ async function trafficRoutes(app, opts) {
|
|
|
38455
38896
|
// colliding bucket adds its paid and organic arrivals to the ones
|
|
38456
38897
|
// already there rather than overwriting a single label.
|
|
38457
38898
|
set: {
|
|
38458
|
-
sessionsOrHits:
|
|
38459
|
-
paidSessionsOrHits:
|
|
38460
|
-
organicSessionsOrHits:
|
|
38899
|
+
sessionsOrHits: sql16`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
|
|
38900
|
+
paidSessionsOrHits: sql16`${aiReferralEventsHourly.paidSessionsOrHits} + ${bucket.paidHits}`,
|
|
38901
|
+
organicSessionsOrHits: sql16`${aiReferralEventsHourly.organicSessionsOrHits} + ${bucket.organicHits}`,
|
|
38461
38902
|
updatedAt: finishedAt
|
|
38462
38903
|
}
|
|
38463
38904
|
}).run();
|
|
@@ -38509,8 +38950,8 @@ async function trafficRoutes(app, opts) {
|
|
|
38509
38950
|
if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
38510
38951
|
sourceUpdate.lastCursor = nextCursor ?? null;
|
|
38511
38952
|
}
|
|
38512
|
-
tx.update(trafficSources).set(sourceUpdate).where(
|
|
38513
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
38953
|
+
tx.update(trafficSources).set(sourceUpdate).where(eq32(trafficSources.id, sourceRow.id)).run();
|
|
38954
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq32(runs.id, runId)).run();
|
|
38514
38955
|
});
|
|
38515
38956
|
writeAuditLog(app.db, {
|
|
38516
38957
|
projectId: project.id,
|
|
@@ -38562,7 +39003,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38562
39003
|
});
|
|
38563
39004
|
app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
|
|
38564
39005
|
const project = resolveProject(app.db, request.params.name);
|
|
38565
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
39006
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, request.params.id)).get();
|
|
38566
39007
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
38567
39008
|
throw notFound("Traffic source", request.params.id);
|
|
38568
39009
|
}
|
|
@@ -38748,40 +39189,40 @@ async function trafficRoutes(app, opts) {
|
|
|
38748
39189
|
function buildSourceDetail(projectId, row, since) {
|
|
38749
39190
|
const crawlerPathRows = app.db.select({
|
|
38750
39191
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
38751
|
-
hits:
|
|
39192
|
+
hits: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
38752
39193
|
}).from(crawlerEventsHourly).where(
|
|
38753
|
-
|
|
38754
|
-
|
|
38755
|
-
|
|
39194
|
+
and27(
|
|
39195
|
+
eq32(crawlerEventsHourly.sourceId, row.id),
|
|
39196
|
+
gte8(crawlerEventsHourly.tsHour, since)
|
|
38756
39197
|
)
|
|
38757
39198
|
).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
38758
39199
|
const crawlerSegments = segmentCrawlerHits(
|
|
38759
39200
|
crawlerPathRows.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
|
|
38760
39201
|
);
|
|
38761
39202
|
const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
|
|
38762
|
-
const aiUserFetchTotals = app.db.select({ total:
|
|
38763
|
-
|
|
38764
|
-
|
|
38765
|
-
|
|
39203
|
+
const aiUserFetchTotals = app.db.select({ total: sql16`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
39204
|
+
and27(
|
|
39205
|
+
eq32(aiUserFetchEventsHourly.sourceId, row.id),
|
|
39206
|
+
gte8(aiUserFetchEventsHourly.tsHour, since)
|
|
38766
39207
|
)
|
|
38767
39208
|
).get();
|
|
38768
|
-
const aiTotals = app.db.select({ total:
|
|
38769
|
-
|
|
38770
|
-
|
|
38771
|
-
|
|
39209
|
+
const aiTotals = app.db.select({ total: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
39210
|
+
and27(
|
|
39211
|
+
eq32(aiReferralEventsHourly.sourceId, row.id),
|
|
39212
|
+
gte8(aiReferralEventsHourly.tsHour, since)
|
|
38772
39213
|
)
|
|
38773
39214
|
).get();
|
|
38774
|
-
const sampleTotals = app.db.select({ total:
|
|
38775
|
-
|
|
38776
|
-
|
|
38777
|
-
|
|
39215
|
+
const sampleTotals = app.db.select({ total: sql16`COUNT(*)` }).from(rawEventSamples).where(
|
|
39216
|
+
and27(
|
|
39217
|
+
eq32(rawEventSamples.sourceId, row.id),
|
|
39218
|
+
gte8(rawEventSamples.ts, since)
|
|
38778
39219
|
)
|
|
38779
39220
|
).get();
|
|
38780
39221
|
const latestRun = app.db.select().from(runs).where(
|
|
38781
|
-
|
|
38782
|
-
|
|
38783
|
-
|
|
38784
|
-
|
|
39222
|
+
and27(
|
|
39223
|
+
eq32(runs.projectId, projectId),
|
|
39224
|
+
eq32(runs.kind, RunKinds["traffic-sync"]),
|
|
39225
|
+
eq32(runs.sourceId, row.id)
|
|
38785
39226
|
)
|
|
38786
39227
|
).orderBy(desc15(runs.startedAt)).limit(1).get();
|
|
38787
39228
|
return {
|
|
@@ -38812,7 +39253,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38812
39253
|
"`advanceToNow` must be `true`. There is no implicit reset."
|
|
38813
39254
|
);
|
|
38814
39255
|
}
|
|
38815
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
39256
|
+
const sourceRow = app.db.select().from(trafficSources).where(and27(eq32(trafficSources.projectId, project.id), eq32(trafficSources.id, request.params.id))).get();
|
|
38816
39257
|
if (!sourceRow) {
|
|
38817
39258
|
throw notFound("traffic source", request.params.id);
|
|
38818
39259
|
}
|
|
@@ -38829,7 +39270,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38829
39270
|
status: TrafficSourceStatuses.connected,
|
|
38830
39271
|
lastError: null,
|
|
38831
39272
|
updatedAt: now
|
|
38832
|
-
}).where(
|
|
39273
|
+
}).where(eq32(trafficSources.id, sourceRow.id)).run();
|
|
38833
39274
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
38834
39275
|
projectId: project.id,
|
|
38835
39276
|
actor: "api",
|
|
@@ -38837,20 +39278,20 @@ async function trafficRoutes(app, opts) {
|
|
|
38837
39278
|
entityType: "traffic_source",
|
|
38838
39279
|
entityId: sourceRow.id
|
|
38839
39280
|
}));
|
|
38840
|
-
updatedRow = tx.select().from(trafficSources).where(
|
|
39281
|
+
updatedRow = tx.select().from(trafficSources).where(eq32(trafficSources.id, sourceRow.id)).get();
|
|
38841
39282
|
});
|
|
38842
39283
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
38843
39284
|
});
|
|
38844
39285
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
38845
39286
|
const project = resolveProject(app.db, request.params.name);
|
|
38846
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
39287
|
+
const rows = app.db.select().from(trafficSources).where(eq32(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
38847
39288
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
38848
39289
|
const response = { sources };
|
|
38849
39290
|
return response;
|
|
38850
39291
|
});
|
|
38851
39292
|
app.get("/projects/:name/traffic/status", async (request) => {
|
|
38852
39293
|
const project = resolveProject(app.db, request.params.name);
|
|
38853
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
39294
|
+
const rows = app.db.select().from(trafficSources).where(eq32(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
38854
39295
|
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
38855
39296
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
38856
39297
|
const response = { sources };
|
|
@@ -38860,7 +39301,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38860
39301
|
"/projects/:name/traffic/sources/:id",
|
|
38861
39302
|
async (request) => {
|
|
38862
39303
|
const project = resolveProject(app.db, request.params.name);
|
|
38863
|
-
const row = app.db.select().from(trafficSources).where(
|
|
39304
|
+
const row = app.db.select().from(trafficSources).where(eq32(trafficSources.id, request.params.id)).get();
|
|
38864
39305
|
if (!row || row.projectId !== project.id) {
|
|
38865
39306
|
throw notFound("Traffic source", request.params.id);
|
|
38866
39307
|
}
|
|
@@ -38925,26 +39366,26 @@ async function trafficRoutes(app, opts) {
|
|
|
38925
39366
|
const seriesByBucket = /* @__PURE__ */ new Map();
|
|
38926
39367
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
38927
39368
|
const crawlerFilters = [
|
|
38928
|
-
|
|
38929
|
-
|
|
38930
|
-
|
|
39369
|
+
eq32(crawlerEventsHourly.projectId, project.id),
|
|
39370
|
+
gte8(crawlerEventsHourly.tsHour, sinceIso),
|
|
39371
|
+
lte6(crawlerEventsHourly.tsHour, untilIso)
|
|
38931
39372
|
];
|
|
38932
|
-
if (sourceIdParam) crawlerFilters.push(
|
|
38933
|
-
const crawlerWhere =
|
|
39373
|
+
if (sourceIdParam) crawlerFilters.push(eq32(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
39374
|
+
const crawlerWhere = and27(...crawlerFilters);
|
|
38934
39375
|
const pathTotals = app.db.select({
|
|
38935
39376
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
38936
|
-
hits:
|
|
38937
|
-
rows:
|
|
39377
|
+
hits: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
|
|
39378
|
+
rows: sql16`COUNT(*)`
|
|
38938
39379
|
}).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
38939
39380
|
crawlerSegments = segmentCrawlerHits(
|
|
38940
39381
|
pathTotals.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
|
|
38941
39382
|
);
|
|
38942
39383
|
crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
|
|
38943
39384
|
totalEventRows += pathTotals.reduce((sum, row) => sum + Number(row.rows), 0);
|
|
38944
|
-
const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ?
|
|
39385
|
+
const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql16`substr(${crawlerEventsHourly.tsHour}, 1, 10)` : crawlerEventsHourly.tsHour;
|
|
38945
39386
|
const crawlerSeries = app.db.select({
|
|
38946
39387
|
bucket: crawlerSeriesBucket,
|
|
38947
|
-
hits:
|
|
39388
|
+
hits: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
38948
39389
|
}).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerSeriesBucket).all();
|
|
38949
39390
|
for (const row of crawlerSeries) {
|
|
38950
39391
|
trafficSeriesPoint(seriesByBucket, row.bucket).crawlerHits = Number(row.hits);
|
|
@@ -38967,22 +39408,22 @@ async function trafficRoutes(app, opts) {
|
|
|
38967
39408
|
}
|
|
38968
39409
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
38969
39410
|
const userFetchFilters = [
|
|
38970
|
-
|
|
38971
|
-
|
|
38972
|
-
|
|
39411
|
+
eq32(aiUserFetchEventsHourly.projectId, project.id),
|
|
39412
|
+
gte8(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
39413
|
+
lte6(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
38973
39414
|
];
|
|
38974
|
-
if (sourceIdParam) userFetchFilters.push(
|
|
38975
|
-
const userFetchWhere =
|
|
39415
|
+
if (sourceIdParam) userFetchFilters.push(eq32(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
39416
|
+
const userFetchWhere = and27(...userFetchFilters);
|
|
38976
39417
|
const total = app.db.select({
|
|
38977
|
-
total:
|
|
38978
|
-
rows:
|
|
39418
|
+
total: sql16`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`,
|
|
39419
|
+
rows: sql16`COUNT(*)`
|
|
38979
39420
|
}).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
38980
39421
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
38981
39422
|
totalEventRows += Number(total?.rows ?? 0);
|
|
38982
|
-
const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ?
|
|
39423
|
+
const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql16`substr(${aiUserFetchEventsHourly.tsHour}, 1, 10)` : aiUserFetchEventsHourly.tsHour;
|
|
38983
39424
|
const userFetchSeries = app.db.select({
|
|
38984
39425
|
bucket: userFetchSeriesBucket,
|
|
38985
|
-
hits:
|
|
39426
|
+
hits: sql16`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
|
|
38986
39427
|
}).from(aiUserFetchEventsHourly).where(userFetchWhere).groupBy(userFetchSeriesBucket).all();
|
|
38987
39428
|
for (const row of userFetchSeries) {
|
|
38988
39429
|
trafficSeriesPoint(seriesByBucket, row.bucket).aiUserFetchHits = Number(row.hits);
|
|
@@ -39004,17 +39445,17 @@ async function trafficRoutes(app, opts) {
|
|
|
39004
39445
|
}
|
|
39005
39446
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
39006
39447
|
const aiFilters = [
|
|
39007
|
-
|
|
39008
|
-
|
|
39009
|
-
|
|
39448
|
+
eq32(aiReferralEventsHourly.projectId, project.id),
|
|
39449
|
+
gte8(aiReferralEventsHourly.tsHour, sinceIso),
|
|
39450
|
+
lte6(aiReferralEventsHourly.tsHour, untilIso)
|
|
39010
39451
|
];
|
|
39011
|
-
if (sourceIdParam) aiFilters.push(
|
|
39012
|
-
const aiWhere =
|
|
39452
|
+
if (sourceIdParam) aiFilters.push(eq32(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
39453
|
+
const aiWhere = and27(...aiFilters);
|
|
39013
39454
|
const total = app.db.select({
|
|
39014
|
-
total:
|
|
39015
|
-
paid:
|
|
39016
|
-
organic:
|
|
39017
|
-
rows:
|
|
39455
|
+
total: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
39456
|
+
paid: sql16`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
|
|
39457
|
+
organic: sql16`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`,
|
|
39458
|
+
rows: sql16`COUNT(*)`
|
|
39018
39459
|
}).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
39019
39460
|
aiReferralCounts = aiReferralClassCounts(
|
|
39020
39461
|
Number(total?.total ?? 0),
|
|
@@ -39022,10 +39463,10 @@ async function trafficRoutes(app, opts) {
|
|
|
39022
39463
|
Number(total?.organic ?? 0)
|
|
39023
39464
|
);
|
|
39024
39465
|
totalEventRows += Number(total?.rows ?? 0);
|
|
39025
|
-
const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ?
|
|
39466
|
+
const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql16`substr(${aiReferralEventsHourly.tsHour}, 1, 10)` : aiReferralEventsHourly.tsHour;
|
|
39026
39467
|
const referralSeries = app.db.select({
|
|
39027
39468
|
bucket: referralSeriesBucket,
|
|
39028
|
-
hits:
|
|
39469
|
+
hits: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
39029
39470
|
}).from(aiReferralEventsHourly).where(aiWhere).groupBy(referralSeriesBucket).all();
|
|
39030
39471
|
for (const row of referralSeries) {
|
|
39031
39472
|
trafficSeriesPoint(seriesByBucket, row.bucket).aiReferralHits = Number(row.hits);
|
|
@@ -39328,7 +39769,7 @@ function readInstalledManifest(skillDir) {
|
|
|
39328
39769
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
39329
39770
|
|
|
39330
39771
|
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
39331
|
-
import { and as
|
|
39772
|
+
import { and as and28, eq as eq33 } from "drizzle-orm";
|
|
39332
39773
|
function skippedNoProject() {
|
|
39333
39774
|
return {
|
|
39334
39775
|
status: CheckStatuses.skipped,
|
|
@@ -39344,8 +39785,8 @@ var BACKLINKS_CHECKS = [
|
|
|
39344
39785
|
title: "Backlinks source connected",
|
|
39345
39786
|
run: (ctx) => {
|
|
39346
39787
|
if (!ctx.project) return skippedNoProject();
|
|
39347
|
-
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(
|
|
39348
|
-
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
39788
|
+
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq33(projects.id, ctx.project.id)).get();
|
|
39789
|
+
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq33(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
39349
39790
|
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
39350
39791
|
const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
|
|
39351
39792
|
const connected = [];
|
|
@@ -39360,9 +39801,9 @@ var BACKLINKS_CHECKS = [
|
|
|
39360
39801
|
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
|
|
39361
39802
|
};
|
|
39362
39803
|
}
|
|
39363
|
-
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(
|
|
39364
|
-
|
|
39365
|
-
|
|
39804
|
+
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and28(
|
|
39805
|
+
eq33(backlinkSummaries.projectId, ctx.project.id),
|
|
39806
|
+
eq33(backlinkSummaries.source, BacklinkSources.commoncrawl)
|
|
39366
39807
|
)).limit(1).get() : false;
|
|
39367
39808
|
return {
|
|
39368
39809
|
status: CheckStatuses.ok,
|
|
@@ -39522,7 +39963,7 @@ var BING_AUTH_CHECKS = [
|
|
|
39522
39963
|
];
|
|
39523
39964
|
|
|
39524
39965
|
// ../api-routes/src/doctor/checks/content.ts
|
|
39525
|
-
import { eq as
|
|
39966
|
+
import { eq as eq34 } from "drizzle-orm";
|
|
39526
39967
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
39527
39968
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
39528
39969
|
function skippedNoProject2() {
|
|
@@ -39535,7 +39976,7 @@ function skippedNoProject2() {
|
|
|
39535
39976
|
}
|
|
39536
39977
|
function loadProject(ctx) {
|
|
39537
39978
|
if (!ctx.project) return null;
|
|
39538
|
-
return ctx.db.select().from(projects).where(
|
|
39979
|
+
return ctx.db.select().from(projects).where(eq34(projects.id, ctx.project.id)).get() ?? null;
|
|
39539
39980
|
}
|
|
39540
39981
|
function percent(value) {
|
|
39541
39982
|
return Math.round(value * 100);
|
|
@@ -39627,7 +40068,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
39627
40068
|
);
|
|
39628
40069
|
|
|
39629
40070
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
39630
|
-
import { eq as
|
|
40071
|
+
import { eq as eq35 } from "drizzle-orm";
|
|
39631
40072
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
39632
40073
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
39633
40074
|
var adsConnectionCheck = {
|
|
@@ -39644,7 +40085,7 @@ var adsConnectionCheck = {
|
|
|
39644
40085
|
remediation: null
|
|
39645
40086
|
};
|
|
39646
40087
|
}
|
|
39647
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
40088
|
+
const row = ctx.db.select().from(adsConnections).where(eq35(adsConnections.projectId, ctx.project.id)).get();
|
|
39648
40089
|
if (!row) {
|
|
39649
40090
|
return {
|
|
39650
40091
|
status: CheckStatuses.skipped,
|
|
@@ -39694,7 +40135,7 @@ var adsRecentSyncCheck = {
|
|
|
39694
40135
|
remediation: null
|
|
39695
40136
|
};
|
|
39696
40137
|
}
|
|
39697
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
40138
|
+
const row = ctx.db.select().from(adsConnections).where(eq35(adsConnections.projectId, ctx.project.id)).get();
|
|
39698
40139
|
if (!row) {
|
|
39699
40140
|
return {
|
|
39700
40141
|
status: CheckStatuses.skipped,
|
|
@@ -39885,7 +40326,7 @@ var ga4ConnectionCheck = {
|
|
|
39885
40326
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
39886
40327
|
|
|
39887
40328
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
39888
|
-
import { and as
|
|
40329
|
+
import { and as and29, eq as eq36 } from "drizzle-orm";
|
|
39889
40330
|
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
39890
40331
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
39891
40332
|
function skippedNoProject3() {
|
|
@@ -40118,7 +40559,7 @@ var recentSyncCheck = {
|
|
|
40118
40559
|
title: "GBP recent sync",
|
|
40119
40560
|
run: (ctx) => {
|
|
40120
40561
|
if (!ctx.project) return skippedNoProject3();
|
|
40121
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(
|
|
40562
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and29(eq36(gbpLocations.projectId, ctx.project.id), eq36(gbpLocations.selected, true))).all();
|
|
40122
40563
|
if (selected.length === 0) {
|
|
40123
40564
|
return {
|
|
40124
40565
|
status: CheckStatuses.skipped,
|
|
@@ -40178,7 +40619,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
40178
40619
|
);
|
|
40179
40620
|
|
|
40180
40621
|
// ../api-routes/src/doctor/checks/places.ts
|
|
40181
|
-
import { eq as
|
|
40622
|
+
import { eq as eq37 } from "drizzle-orm";
|
|
40182
40623
|
var apiKeyCheck = {
|
|
40183
40624
|
id: "gbp.places.api-key",
|
|
40184
40625
|
category: CheckCategories.auth,
|
|
@@ -40223,7 +40664,7 @@ var apiKeyCheck = {
|
|
|
40223
40664
|
details: { tier: cfg.tier }
|
|
40224
40665
|
};
|
|
40225
40666
|
}
|
|
40226
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
40667
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq37(gbpLocations.projectId, ctx.project.id)).all();
|
|
40227
40668
|
const selected = rows.filter((r) => r.selected);
|
|
40228
40669
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
40229
40670
|
const details = {
|
|
@@ -40720,7 +41161,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
40720
41161
|
];
|
|
40721
41162
|
|
|
40722
41163
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
40723
|
-
import { and as
|
|
41164
|
+
import { and as and30, eq as eq38, gte as gte9, ne as ne5, sql as sql17 } from "drizzle-orm";
|
|
40724
41165
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
40725
41166
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
40726
41167
|
function skippedNoProject5() {
|
|
@@ -40734,8 +41175,8 @@ function skippedNoProject5() {
|
|
|
40734
41175
|
function loadProbes(ctx) {
|
|
40735
41176
|
if (!ctx.project) return [];
|
|
40736
41177
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
40737
|
-
|
|
40738
|
-
|
|
41178
|
+
and30(
|
|
41179
|
+
eq38(trafficSources.projectId, ctx.project.id),
|
|
40739
41180
|
ne5(trafficSources.status, TrafficSourceStatuses.archived)
|
|
40740
41181
|
)
|
|
40741
41182
|
).all();
|
|
@@ -40814,18 +41255,18 @@ var recentDataCheck = {
|
|
|
40814
41255
|
const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
|
|
40815
41256
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
40816
41257
|
const recentCrawlers = Number(
|
|
40817
|
-
ctx.db.select({ total:
|
|
40818
|
-
|
|
40819
|
-
|
|
40820
|
-
|
|
41258
|
+
ctx.db.select({ total: sql17`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
41259
|
+
and30(
|
|
41260
|
+
eq38(crawlerEventsHourly.projectId, ctx.project.id),
|
|
41261
|
+
gte9(crawlerEventsHourly.tsHour, warnCutoff)
|
|
40821
41262
|
)
|
|
40822
41263
|
).get()?.total ?? 0
|
|
40823
41264
|
);
|
|
40824
41265
|
const recentReferrals = Number(
|
|
40825
|
-
ctx.db.select({ total:
|
|
40826
|
-
|
|
40827
|
-
|
|
40828
|
-
|
|
41266
|
+
ctx.db.select({ total: sql17`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
41267
|
+
and30(
|
|
41268
|
+
eq38(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
41269
|
+
gte9(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
40829
41270
|
)
|
|
40830
41271
|
).get()?.total ?? 0
|
|
40831
41272
|
);
|
|
@@ -40838,18 +41279,18 @@ var recentDataCheck = {
|
|
|
40838
41279
|
};
|
|
40839
41280
|
}
|
|
40840
41281
|
const olderCrawlers = Number(
|
|
40841
|
-
ctx.db.select({ total:
|
|
40842
|
-
|
|
40843
|
-
|
|
40844
|
-
|
|
41282
|
+
ctx.db.select({ total: sql17`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
41283
|
+
and30(
|
|
41284
|
+
eq38(crawlerEventsHourly.projectId, ctx.project.id),
|
|
41285
|
+
gte9(crawlerEventsHourly.tsHour, failCutoff)
|
|
40845
41286
|
)
|
|
40846
41287
|
).get()?.total ?? 0
|
|
40847
41288
|
);
|
|
40848
41289
|
const olderReferrals = Number(
|
|
40849
|
-
ctx.db.select({ total:
|
|
40850
|
-
|
|
40851
|
-
|
|
40852
|
-
|
|
41290
|
+
ctx.db.select({ total: sql17`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
41291
|
+
and30(
|
|
41292
|
+
eq38(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
41293
|
+
gte9(aiReferralEventsHourly.tsHour, failCutoff)
|
|
40853
41294
|
)
|
|
40854
41295
|
).get()?.total ?? 0
|
|
40855
41296
|
);
|
|
@@ -41266,7 +41707,7 @@ async function doctorRoutes(app, opts) {
|
|
|
41266
41707
|
|
|
41267
41708
|
// ../api-routes/src/discovery/routes.ts
|
|
41268
41709
|
import crypto30 from "crypto";
|
|
41269
|
-
import { and as
|
|
41710
|
+
import { and as and31, desc as desc16, eq as eq39, gte as gte10, inArray as inArray13, isNull as isNull3, or as or8 } from "drizzle-orm";
|
|
41270
41711
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
41271
41712
|
async function discoveryRoutes(app, opts) {
|
|
41272
41713
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -41300,13 +41741,13 @@ async function discoveryRoutes(app, opts) {
|
|
|
41300
41741
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41301
41742
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
41302
41743
|
const decision = app.db.transaction((tx) => {
|
|
41303
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
41304
|
-
|
|
41305
|
-
|
|
41744
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and31(
|
|
41745
|
+
eq39(discoverySessions.projectId, project.id),
|
|
41746
|
+
eq39(discoverySessions.icpDescription, icpDescription),
|
|
41306
41747
|
// Buyer is part of session identity: it changes the seed prompt's
|
|
41307
41748
|
// semantics, so a request with a different (or no) buyer must start
|
|
41308
41749
|
// its own session, never adopt another buyer's probes.
|
|
41309
|
-
parsed.data.buyerDescription == null ? isNull3(discoverySessions.buyerDescription) :
|
|
41750
|
+
parsed.data.buyerDescription == null ? isNull3(discoverySessions.buyerDescription) : eq39(discoverySessions.buyerDescription, parsed.data.buyerDescription),
|
|
41310
41751
|
// Locations are identity too: a different service-area subset seeds
|
|
41311
41752
|
// and probes a different geo, so it must never reuse another geo's
|
|
41312
41753
|
// session. resolveLocations is deterministic (project-config order),
|
|
@@ -41316,17 +41757,17 @@ async function discoveryRoutes(app, opts) {
|
|
|
41316
41757
|
// locations a NULL row's subset is unknowable, so it conservatively
|
|
41317
41758
|
// never reuses — a one-time, bounded (2h window) non-reuse after
|
|
41318
41759
|
// upgrade, never a wrong reuse.
|
|
41319
|
-
locations.length === 0 ?
|
|
41760
|
+
locations.length === 0 ? or8(isNull3(discoverySessions.locations), eq39(discoverySessions.locations, locations)) : eq39(discoverySessions.locations, locations),
|
|
41320
41761
|
// Seed provider set is identity: a different phrasing distribution
|
|
41321
41762
|
// must never reuse another set's session. Null (= Gemini-only
|
|
41322
41763
|
// default, including explicit ['gemini']) matches legacy rows.
|
|
41323
|
-
seedProviders == null ? isNull3(discoverySessions.seedProviders) :
|
|
41764
|
+
seedProviders == null ? isNull3(discoverySessions.seedProviders) : eq39(discoverySessions.seedProviders, seedProviders),
|
|
41324
41765
|
inArray13(discoverySessions.status, [
|
|
41325
41766
|
DiscoverySessionStatuses.queued,
|
|
41326
41767
|
DiscoverySessionStatuses.seeding,
|
|
41327
41768
|
DiscoverySessionStatuses.probing
|
|
41328
41769
|
]),
|
|
41329
|
-
|
|
41770
|
+
gte10(discoverySessions.createdAt, ageFloorIso)
|
|
41330
41771
|
)).orderBy(desc16(discoverySessions.createdAt)).get();
|
|
41331
41772
|
if (existing && existing.runId) {
|
|
41332
41773
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
@@ -41396,7 +41837,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41396
41837
|
const project = resolveProject(app.db, request.params.name);
|
|
41397
41838
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
41398
41839
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
41399
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
41840
|
+
const rows = app.db.select().from(discoverySessions).where(eq39(discoverySessions.projectId, project.id)).orderBy(desc16(discoverySessions.createdAt)).limit(limit).all();
|
|
41400
41841
|
return reply.send(rows.map(serializeSession));
|
|
41401
41842
|
}
|
|
41402
41843
|
);
|
|
@@ -41404,11 +41845,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
41404
41845
|
"/projects/:name/discover/sessions/:id",
|
|
41405
41846
|
async (request, reply) => {
|
|
41406
41847
|
const project = resolveProject(app.db, request.params.name);
|
|
41407
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
41848
|
+
const session = app.db.select().from(discoverySessions).where(eq39(discoverySessions.id, request.params.id)).get();
|
|
41408
41849
|
if (!session || session.projectId !== project.id) {
|
|
41409
41850
|
throw notFound("Discovery session", request.params.id);
|
|
41410
41851
|
}
|
|
41411
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
41852
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq39(discoveryProbes.sessionId, session.id)).all();
|
|
41412
41853
|
const detail = {
|
|
41413
41854
|
...serializeSession(session),
|
|
41414
41855
|
probes: probeRows.map(serializeProbe)
|
|
@@ -41420,7 +41861,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41420
41861
|
"/projects/:name/discover/sessions/:id/harvest",
|
|
41421
41862
|
async (request, reply) => {
|
|
41422
41863
|
const project = resolveProject(app.db, request.params.name);
|
|
41423
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
41864
|
+
const session = app.db.select().from(discoverySessions).where(eq39(discoverySessions.id, request.params.id)).get();
|
|
41424
41865
|
if (!session || session.projectId !== project.id) {
|
|
41425
41866
|
throw notFound("Discovery session", request.params.id);
|
|
41426
41867
|
}
|
|
@@ -41428,7 +41869,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41428
41869
|
const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
|
|
41429
41870
|
const applyAnchor = request.query.anchor !== "false";
|
|
41430
41871
|
const provider = session.seedProvider ?? "gemini";
|
|
41431
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
41872
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq39(discoveryProbes.sessionId, session.id)).all();
|
|
41432
41873
|
const extract = opts.harvestSearchQueries;
|
|
41433
41874
|
const probesWithQueries = probeRows.map((row) => {
|
|
41434
41875
|
if (!extract || !row.rawResponse) return { searchQueries: [] };
|
|
@@ -41439,7 +41880,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41439
41880
|
return { searchQueries: [] };
|
|
41440
41881
|
}
|
|
41441
41882
|
});
|
|
41442
|
-
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(
|
|
41883
|
+
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq39(queries.projectId, project.id)).all().map((r) => r.query);
|
|
41443
41884
|
const anchorTerms = buildHarvestAnchorTerms(
|
|
41444
41885
|
[session.icpDescription ?? "", ...trackedQueries],
|
|
41445
41886
|
effectiveDomains(project)
|
|
@@ -41486,12 +41927,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
41486
41927
|
"/projects/:name/discover/sessions/:id/promote",
|
|
41487
41928
|
async (request, reply) => {
|
|
41488
41929
|
const project = resolveProject(app.db, request.params.name);
|
|
41489
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
41930
|
+
const session = app.db.select().from(discoverySessions).where(eq39(discoverySessions.id, request.params.id)).get();
|
|
41490
41931
|
if (!session || session.projectId !== project.id) {
|
|
41491
41932
|
throw notFound("Discovery session", request.params.id);
|
|
41492
41933
|
}
|
|
41493
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
41494
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
41934
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq39(discoveryProbes.sessionId, session.id)).all();
|
|
41935
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq39(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
41495
41936
|
const seenCompetitors = new Set(existingCompetitors);
|
|
41496
41937
|
const cited = /* @__PURE__ */ new Set();
|
|
41497
41938
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -41520,7 +41961,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41520
41961
|
);
|
|
41521
41962
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
41522
41963
|
const project = resolveProject(app.db, request.params.name);
|
|
41523
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
41964
|
+
const session = app.db.select().from(discoverySessions).where(eq39(discoverySessions.id, request.params.id)).get();
|
|
41524
41965
|
if (!session || session.projectId !== project.id) {
|
|
41525
41966
|
throw notFound("Discovery session", request.params.id);
|
|
41526
41967
|
}
|
|
@@ -41543,7 +41984,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41543
41984
|
const bucketSet = new Set(buckets);
|
|
41544
41985
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
41545
41986
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
41546
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
41987
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq39(discoveryProbes.sessionId, session.id)).all();
|
|
41547
41988
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
41548
41989
|
for (const probe of probeRows) {
|
|
41549
41990
|
if (!probe.bucket) continue;
|
|
@@ -41551,7 +41992,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41551
41992
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
41552
41993
|
}
|
|
41553
41994
|
const existingQueries = new Set(
|
|
41554
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
41995
|
+
app.db.select({ query: queries.query }).from(queries).where(eq39(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
41555
41996
|
);
|
|
41556
41997
|
const promotedQueries = [];
|
|
41557
41998
|
const skippedQueries = [];
|
|
@@ -41567,7 +42008,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41567
42008
|
const skippedCompetitors = [];
|
|
41568
42009
|
if (includeCompetitors) {
|
|
41569
42010
|
const existingCompetitors = new Set(
|
|
41570
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
42011
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq39(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
41571
42012
|
);
|
|
41572
42013
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
41573
42014
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -41686,7 +42127,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
41686
42127
|
|
|
41687
42128
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
41688
42129
|
import crypto31 from "crypto";
|
|
41689
|
-
import { eq as
|
|
42130
|
+
import { eq as eq40 } from "drizzle-orm";
|
|
41690
42131
|
var DEFAULT_MAX_PROBES = 100;
|
|
41691
42132
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
41692
42133
|
function classifyProbeBucket(input) {
|
|
@@ -41773,7 +42214,7 @@ async function executeDiscovery(opts) {
|
|
|
41773
42214
|
status: DiscoverySessionStatuses.seeding,
|
|
41774
42215
|
dedupThreshold,
|
|
41775
42216
|
startedAt
|
|
41776
|
-
}).where(
|
|
42217
|
+
}).where(eq40(discoverySessions.id, opts.sessionId)).run();
|
|
41777
42218
|
const seedResult = await opts.deps.seed({
|
|
41778
42219
|
project: opts.project,
|
|
41779
42220
|
icpDescription: opts.icpDescription,
|
|
@@ -41834,7 +42275,7 @@ async function executeDiscovery(opts) {
|
|
|
41834
42275
|
dedupBandPairFraction: dedupStats.bandPairFraction,
|
|
41835
42276
|
dedupPairsTotal: dedupStats.pairsTotal,
|
|
41836
42277
|
warning
|
|
41837
|
-
}).where(
|
|
42278
|
+
}).where(eq40(discoverySessions.id, opts.sessionId)).run();
|
|
41838
42279
|
const probeLocation = opts.locations?.[0];
|
|
41839
42280
|
const probeResults = await mapWithConcurrency(
|
|
41840
42281
|
probedCanonicals,
|
|
@@ -41888,7 +42329,7 @@ async function executeDiscovery(opts) {
|
|
|
41888
42329
|
wastedCount: buckets["wasted-surface"],
|
|
41889
42330
|
competitorMap,
|
|
41890
42331
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
41891
|
-
}).where(
|
|
42332
|
+
}).where(eq40(discoverySessions.id, opts.sessionId)).run();
|
|
41892
42333
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
41893
42334
|
return {
|
|
41894
42335
|
buckets,
|
|
@@ -41928,7 +42369,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
41928
42369
|
status: DiscoverySessionStatuses.failed,
|
|
41929
42370
|
error,
|
|
41930
42371
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
41931
|
-
}).where(
|
|
42372
|
+
}).where(eq40(discoverySessions.id, sessionId)).run();
|
|
41932
42373
|
}
|
|
41933
42374
|
function dedupeStrings(input) {
|
|
41934
42375
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -41946,7 +42387,7 @@ function dedupeStrings(input) {
|
|
|
41946
42387
|
|
|
41947
42388
|
// ../api-routes/src/technical-aeo.ts
|
|
41948
42389
|
import crypto32 from "crypto";
|
|
41949
|
-
import { and as
|
|
42390
|
+
import { and as and32, asc as asc8, count, desc as desc17, eq as eq41, inArray as inArray14, lt as lt4 } from "drizzle-orm";
|
|
41950
42391
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
41951
42392
|
function emptyScore(projectName) {
|
|
41952
42393
|
return {
|
|
@@ -41979,19 +42420,19 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
41979
42420
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
41980
42421
|
const project = resolveProject(app.db, request.params.name);
|
|
41981
42422
|
const baseFilters = [
|
|
41982
|
-
|
|
41983
|
-
|
|
42423
|
+
eq41(siteAuditSnapshots.projectId, project.id),
|
|
42424
|
+
eq41(runs.kind, RunKinds["site-audit"]),
|
|
41984
42425
|
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
41985
42426
|
notProbeRun()
|
|
41986
42427
|
];
|
|
41987
|
-
const targetFilters = request.query.runId ? [...baseFilters,
|
|
41988
|
-
const latest = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs,
|
|
42428
|
+
const targetFilters = request.query.runId ? [...baseFilters, eq41(siteAuditSnapshots.runId, request.query.runId)] : baseFilters;
|
|
42429
|
+
const latest = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq41(siteAuditSnapshots.runId, runs.id)).where(and32(...targetFilters)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
41989
42430
|
if (!latest) {
|
|
41990
42431
|
if (request.query.runId) throw notFound("Site audit run", request.query.runId);
|
|
41991
42432
|
return emptyScore(project.name);
|
|
41992
42433
|
}
|
|
41993
42434
|
const snap = latest.snap;
|
|
41994
|
-
const previous = app.db.select({ snap: siteAuditSnapshots }).from(siteAuditSnapshots).innerJoin(runs,
|
|
42435
|
+
const previous = app.db.select({ snap: siteAuditSnapshots }).from(siteAuditSnapshots).innerJoin(runs, eq41(siteAuditSnapshots.runId, runs.id)).where(and32(...baseFilters, lt4(siteAuditSnapshots.createdAt, snap.createdAt))).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get()?.snap ?? null;
|
|
41995
42436
|
const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
|
|
41996
42437
|
const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
|
|
41997
42438
|
return {
|
|
@@ -42018,13 +42459,13 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
42018
42459
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
42019
42460
|
const project = resolveProject(app.db, request.params.name);
|
|
42020
42461
|
const targetFilters = [
|
|
42021
|
-
|
|
42022
|
-
|
|
42462
|
+
eq41(siteAuditSnapshots.projectId, project.id),
|
|
42463
|
+
eq41(runs.kind, RunKinds["site-audit"]),
|
|
42023
42464
|
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
42024
42465
|
notProbeRun()
|
|
42025
42466
|
];
|
|
42026
|
-
if (request.query.runId) targetFilters.push(
|
|
42027
|
-
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs,
|
|
42467
|
+
if (request.query.runId) targetFilters.push(eq41(siteAuditSnapshots.runId, request.query.runId));
|
|
42468
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq41(siteAuditSnapshots.runId, runs.id)).where(and32(...targetFilters)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
42028
42469
|
if (!latest && request.query.runId) {
|
|
42029
42470
|
throw notFound("Site audit run", request.query.runId);
|
|
42030
42471
|
}
|
|
@@ -42032,9 +42473,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
42032
42473
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
42033
42474
|
}
|
|
42034
42475
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
42035
|
-
const conds = [
|
|
42036
|
-
if (statusFilter) conds.push(
|
|
42037
|
-
const where =
|
|
42476
|
+
const conds = [eq41(siteAuditPages.runId, latest.runId)];
|
|
42477
|
+
if (statusFilter) conds.push(eq41(siteAuditPages.status, statusFilter));
|
|
42478
|
+
const where = and32(...conds);
|
|
42038
42479
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
42039
42480
|
const total = totalRow?.value ?? 0;
|
|
42040
42481
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
@@ -42058,9 +42499,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
42058
42499
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
42059
42500
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
42060
42501
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
42061
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
42062
|
-
|
|
42063
|
-
|
|
42502
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq41(siteAuditSnapshots.runId, runs.id)).where(and32(
|
|
42503
|
+
eq41(siteAuditSnapshots.projectId, project.id),
|
|
42504
|
+
eq41(runs.kind, RunKinds["site-audit"]),
|
|
42064
42505
|
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
42065
42506
|
notProbeRun()
|
|
42066
42507
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
@@ -42072,9 +42513,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
42072
42513
|
if (!parsed.success) {
|
|
42073
42514
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
42074
42515
|
}
|
|
42075
|
-
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
42076
|
-
|
|
42077
|
-
|
|
42516
|
+
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and32(
|
|
42517
|
+
eq41(runs.projectId, project.id),
|
|
42518
|
+
eq41(runs.kind, RunKinds["site-audit"]),
|
|
42078
42519
|
inArray14(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
42079
42520
|
)).get();
|
|
42080
42521
|
if (existing) {
|
|
@@ -42100,7 +42541,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
42100
42541
|
|
|
42101
42542
|
// ../api-routes/src/research.ts
|
|
42102
42543
|
import crypto33 from "crypto";
|
|
42103
|
-
import { and as
|
|
42544
|
+
import { and as and33, desc as desc18, eq as eq42 } from "drizzle-orm";
|
|
42104
42545
|
var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
|
|
42105
42546
|
async function researchRoutes(app, opts) {
|
|
42106
42547
|
app.post("/projects/:name/research/runs", async (request, reply) => {
|
|
@@ -42130,7 +42571,7 @@ async function researchRoutes(app, opts) {
|
|
|
42130
42571
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
42131
42572
|
const decision = app.db.transaction((tx) => {
|
|
42132
42573
|
if (input.idempotencyKey) {
|
|
42133
|
-
const existing = tx.select().from(researchRuns).where(
|
|
42574
|
+
const existing = tx.select().from(researchRuns).where(and33(eq42(researchRuns.projectId, project.id), eq42(researchRuns.idempotencyKey, input.idempotencyKey))).get();
|
|
42134
42575
|
if (existing) {
|
|
42135
42576
|
if (existing.requestHash !== requestHash2) throw alreadyExists("Research idempotency key", input.idempotencyKey);
|
|
42136
42577
|
return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
|
|
@@ -42151,7 +42592,7 @@ async function researchRoutes(app, opts) {
|
|
|
42151
42592
|
const project = resolveProject(app.db, request.params.name);
|
|
42152
42593
|
const requested = Number.parseInt(request.query.limit ?? "", 10);
|
|
42153
42594
|
const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
|
|
42154
|
-
const runs2 = app.db.select().from(researchRuns).where(
|
|
42595
|
+
const runs2 = app.db.select().from(researchRuns).where(eq42(researchRuns.projectId, project.id)).orderBy(desc18(researchRuns.createdAt)).limit(limit).all().map(serializeRun);
|
|
42155
42596
|
return { runs: runs2 };
|
|
42156
42597
|
});
|
|
42157
42598
|
app.get("/projects/:name/research/runs/:runId", async (request) => {
|
|
@@ -42160,9 +42601,9 @@ async function researchRoutes(app, opts) {
|
|
|
42160
42601
|
});
|
|
42161
42602
|
}
|
|
42162
42603
|
function getDetail(app, projectId, id) {
|
|
42163
|
-
const row = app.db.select().from(researchRuns).where(
|
|
42604
|
+
const row = app.db.select().from(researchRuns).where(and33(eq42(researchRuns.id, id), eq42(researchRuns.projectId, projectId))).get();
|
|
42164
42605
|
if (!row) throw notFound("Research run", id);
|
|
42165
|
-
const queries2 = app.db.select().from(researchRunQueries).where(
|
|
42606
|
+
const queries2 = app.db.select().from(researchRunQueries).where(eq42(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
|
|
42166
42607
|
return { ...serializeRun(row), queries: queries2 };
|
|
42167
42608
|
}
|
|
42168
42609
|
function serializeRun(row) {
|
|
@@ -42329,6 +42770,7 @@ async function apiRoutes(app, opts) {
|
|
|
42329
42770
|
googleConnectionStore: opts.googleConnectionStore,
|
|
42330
42771
|
getGoogleAuthConfig: opts.getGoogleAuthConfig
|
|
42331
42772
|
});
|
|
42773
|
+
await api.register(gaMeasurementAnalysisRoutes);
|
|
42332
42774
|
await api.register(trafficRoutes, {
|
|
42333
42775
|
cloudRunCredentialStore: opts.cloudRunCredentialStore,
|
|
42334
42776
|
pullCloudRunEvents: opts.pullCloudRunEvents,
|
|
@@ -42756,9 +43198,9 @@ var IntelligenceService = class {
|
|
|
42756
43198
|
*/
|
|
42757
43199
|
analyzeAndPersist(runId, projectId) {
|
|
42758
43200
|
const recentRuns = this.db.select().from(runs).where(
|
|
42759
|
-
|
|
42760
|
-
|
|
42761
|
-
|
|
43201
|
+
and34(
|
|
43202
|
+
eq43(runs.projectId, projectId),
|
|
43203
|
+
or9(eq43(runs.status, "completed"), eq43(runs.status, "partial")),
|
|
42762
43204
|
// Defensive: RunCoordinator already skips probes before this is
|
|
42763
43205
|
// called, but if a future call site invokes analyzeAndPersist
|
|
42764
43206
|
// directly for a probe, probes still must not pollute the
|
|
@@ -42840,7 +43282,7 @@ var IntelligenceService = class {
|
|
|
42840
43282
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
42841
43283
|
*/
|
|
42842
43284
|
analyzeAndPersistGbp(runId, projectId) {
|
|
42843
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
43285
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq43(runs.id, runId)).get();
|
|
42844
43286
|
if (!runRow) {
|
|
42845
43287
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
42846
43288
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -42848,11 +43290,11 @@ var IntelligenceService = class {
|
|
|
42848
43290
|
}
|
|
42849
43291
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
42850
43292
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
42851
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
42852
|
-
|
|
42853
|
-
|
|
42854
|
-
|
|
42855
|
-
|
|
43293
|
+
const selected = this.db.select().from(gbpLocations).where(and34(
|
|
43294
|
+
eq43(gbpLocations.projectId, projectId),
|
|
43295
|
+
eq43(gbpLocations.selected, true),
|
|
43296
|
+
gte11(gbpLocations.syncedAt, windowStart),
|
|
43297
|
+
lte7(gbpLocations.syncedAt, windowEnd)
|
|
42856
43298
|
)).all();
|
|
42857
43299
|
if (selected.length === 0) {
|
|
42858
43300
|
log.info("gbp-intelligence.skip", { runId, reason: "no locations synced during run" });
|
|
@@ -42885,12 +43327,12 @@ var IntelligenceService = class {
|
|
|
42885
43327
|
}
|
|
42886
43328
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
42887
43329
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
42888
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
42889
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
42890
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
42891
|
-
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(
|
|
43330
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and34(eq43(gbpDailyMetrics.projectId, projectId), eq43(gbpDailyMetrics.locationName, locationName))).all();
|
|
43331
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and34(eq43(gbpPlaceActions.projectId, projectId), eq43(gbpPlaceActions.locationName, locationName))).all();
|
|
43332
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and34(eq43(gbpLodgingSnapshots.projectId, projectId), eq43(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc19(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
43333
|
+
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and34(eq43(gbpLocations.projectId, projectId), eq43(gbpLocations.locationName, locationName))).get();
|
|
42892
43334
|
const descriptionMissing = !(ownerRow?.description ?? "").trim();
|
|
42893
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(
|
|
43335
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and34(eq43(gbpPlaceDetails.projectId, projectId), eq43(gbpPlaceDetails.locationName, locationName))).orderBy(desc19(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
42894
43336
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
42895
43337
|
const summary = buildGbpSummary({
|
|
42896
43338
|
locationName,
|
|
@@ -42926,7 +43368,7 @@ var IntelligenceService = class {
|
|
|
42926
43368
|
/** Build the month-over-month keyword series for a location from the
|
|
42927
43369
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
42928
43370
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
42929
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
43371
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and34(eq43(gbpKeywordMonthly.projectId, projectId), eq43(gbpKeywordMonthly.locationName, locationName))).all();
|
|
42930
43372
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
42931
43373
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
42932
43374
|
const recentMonth = months[0] ?? null;
|
|
@@ -42957,7 +43399,7 @@ var IntelligenceService = class {
|
|
|
42957
43399
|
*/
|
|
42958
43400
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
42959
43401
|
const covered = new Set(coveredLocationNames);
|
|
42960
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
43402
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and34(eq43(insights.projectId, projectId), eq43(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
42961
43403
|
const staleIds = [];
|
|
42962
43404
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
42963
43405
|
for (const row of existing) {
|
|
@@ -42968,7 +43410,7 @@ var IntelligenceService = class {
|
|
|
42968
43410
|
}
|
|
42969
43411
|
this.db.transaction((tx) => {
|
|
42970
43412
|
for (const id of staleIds) {
|
|
42971
|
-
tx.delete(insights).where(
|
|
43413
|
+
tx.delete(insights).where(eq43(insights.id, id)).run();
|
|
42972
43414
|
}
|
|
42973
43415
|
for (const insight of gbpInsights) {
|
|
42974
43416
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -43046,7 +43488,7 @@ var IntelligenceService = class {
|
|
|
43046
43488
|
* create per run + aggregate). DB is left untouched.
|
|
43047
43489
|
*/
|
|
43048
43490
|
backfill(projectName, opts, onProgress) {
|
|
43049
|
-
const project = this.db.select().from(projects).where(
|
|
43491
|
+
const project = this.db.select().from(projects).where(eq43(projects.name, projectName)).get();
|
|
43050
43492
|
if (!project) {
|
|
43051
43493
|
throw new Error(`Project "${projectName}" not found`);
|
|
43052
43494
|
}
|
|
@@ -43059,9 +43501,9 @@ var IntelligenceService = class {
|
|
|
43059
43501
|
sinceTimestamp = parsed;
|
|
43060
43502
|
}
|
|
43061
43503
|
const allRuns = this.db.select().from(runs).where(
|
|
43062
|
-
|
|
43063
|
-
|
|
43064
|
-
|
|
43504
|
+
and34(
|
|
43505
|
+
eq43(runs.projectId, project.id),
|
|
43506
|
+
or9(eq43(runs.status, "completed"), eq43(runs.status, "partial")),
|
|
43065
43507
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
43066
43508
|
ne6(runs.trigger, RunTriggers.probe)
|
|
43067
43509
|
)
|
|
@@ -43140,7 +43582,7 @@ var IntelligenceService = class {
|
|
|
43140
43582
|
return { processed, skipped, totalInsights };
|
|
43141
43583
|
}
|
|
43142
43584
|
loadTrackedCompetitors(projectId) {
|
|
43143
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
43585
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq43(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
43144
43586
|
}
|
|
43145
43587
|
/**
|
|
43146
43588
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -43161,15 +43603,15 @@ var IntelligenceService = class {
|
|
|
43161
43603
|
}
|
|
43162
43604
|
persistResult(result, runId, projectId) {
|
|
43163
43605
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
43164
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
43606
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq43(insights.runId, runId)).all();
|
|
43165
43607
|
for (const row of existingInsights) {
|
|
43166
43608
|
if (row.dismissed) {
|
|
43167
43609
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
43168
43610
|
}
|
|
43169
43611
|
}
|
|
43170
43612
|
this.db.transaction((tx) => {
|
|
43171
|
-
tx.delete(insights).where(
|
|
43172
|
-
tx.delete(healthSnapshots).where(
|
|
43613
|
+
tx.delete(insights).where(eq43(insights.runId, runId)).run();
|
|
43614
|
+
tx.delete(healthSnapshots).where(eq43(healthSnapshots.runId, runId)).run();
|
|
43173
43615
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
43174
43616
|
for (const insight of result.insights) {
|
|
43175
43617
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -43222,24 +43664,24 @@ var IntelligenceService = class {
|
|
|
43222
43664
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
43223
43665
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
43224
43666
|
if (regressions.length === 0) return rawInsights;
|
|
43225
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
43667
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq43(gscSearchData.projectId, projectId)).all();
|
|
43226
43668
|
const gscConnected = gscRows.length > 0;
|
|
43227
43669
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
43228
43670
|
for (const row of gscRows) {
|
|
43229
43671
|
const key = row.query.toLowerCase();
|
|
43230
43672
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
43231
43673
|
}
|
|
43232
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
43674
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq43(projects.id, projectId)).get();
|
|
43233
43675
|
const locationCount = Math.max(
|
|
43234
43676
|
1,
|
|
43235
43677
|
(projectRow?.locations ?? []).length
|
|
43236
43678
|
);
|
|
43237
43679
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
43238
43680
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
43239
|
-
|
|
43240
|
-
|
|
43241
|
-
|
|
43242
|
-
|
|
43681
|
+
and34(
|
|
43682
|
+
eq43(runs.projectId, projectId),
|
|
43683
|
+
eq43(runs.kind, RunKinds["answer-visibility"]),
|
|
43684
|
+
or9(eq43(runs.status, "completed"), eq43(runs.status, "partial")),
|
|
43243
43685
|
// Defensive — see top of file.
|
|
43244
43686
|
ne6(runs.trigger, RunTriggers.probe)
|
|
43245
43687
|
)
|
|
@@ -43259,7 +43701,7 @@ var IntelligenceService = class {
|
|
|
43259
43701
|
const haveHistory = recentRunIds.length > 0;
|
|
43260
43702
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
43261
43703
|
if (haveHistory) {
|
|
43262
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
43704
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and34(eq43(insights.type, "regression"), inArray15(insights.runId, recentRunIds))).all();
|
|
43263
43705
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
43264
43706
|
for (const row of priorRows) {
|
|
43265
43707
|
if (!row.runId) continue;
|
|
@@ -43288,7 +43730,7 @@ var IntelligenceService = class {
|
|
|
43288
43730
|
});
|
|
43289
43731
|
}
|
|
43290
43732
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
43291
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
43733
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq43(projects.id, projectId)).get();
|
|
43292
43734
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
43293
43735
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
43294
43736
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -43305,7 +43747,7 @@ var IntelligenceService = class {
|
|
|
43305
43747
|
citedDomains: querySnapshots.citedDomains,
|
|
43306
43748
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
43307
43749
|
snapshotLocation: querySnapshots.location
|
|
43308
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
43750
|
+
}).from(querySnapshots).leftJoin(queries, eq43(querySnapshots.queryId, queries.id)).where(eq43(querySnapshots.runId, runId)).all();
|
|
43309
43751
|
const snapshots = [];
|
|
43310
43752
|
let orphanCount = 0;
|
|
43311
43753
|
for (const r of rows) {
|