@canonry/canonry 4.130.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-Ydzb2Y_Y.js → AuditHistoryPanel-Carj_FCP.js} +1 -1
- package/assets/assets/{BacklinksPage-DYodEusK.js → BacklinksPage-CqOTDVIn.js} +1 -1
- package/assets/assets/{ChartPrimitives-D67hsnF7.js → ChartPrimitives-EuQ82l0S.js} +1 -1
- package/assets/assets/{HistoryPage-_h9ITL1w.js → HistoryPage-D6XQfiLX.js} +1 -1
- package/assets/assets/{ProjectPage-fCJKCcHE.js → ProjectPage-Dswa9Ah6.js} +1 -1
- package/assets/assets/{RunRow-DzUNyTE-.js → RunRow-ThryEZFR.js} +1 -1
- package/assets/assets/{RunsPage-FTvAjptk.js → RunsPage-RFs2WkTC.js} +1 -1
- package/assets/assets/{SettingsPage-CvgpYEAt.js → SettingsPage-DoUccn9s.js} +1 -1
- package/assets/assets/{TrafficPage-qaMW9k6o.js → TrafficPage-CVvSDZPY.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-CRCso7_A.js → TrafficSourceDetailPage-BTUQUBeS.js} +1 -1
- package/assets/assets/{arrow-left-C--lHdP2.js → arrow-left-BoCNqusn.js} +1 -1
- package/assets/assets/{extract-error-message-DVqYbKpB.js → extract-error-message-BsfOVrKi.js} +1 -1
- package/assets/assets/{index-CdXn0JEf.js → index-jJurRky0.js} +102 -102
- package/assets/assets/{trash-2-CyKRU3tR.js → trash-2-BPb4368K.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-WIALMCXM.js → chunk-5FWAPJFX.js} +45 -1
- package/dist/{chunk-NVRJZU6V.js → chunk-7BD2ZOPV.js} +90 -0
- package/dist/{chunk-5XU6WEMK.js → chunk-QIJKS5KN.js} +1050 -304
- package/dist/{chunk-QR5U2K3O.js → chunk-RWTOEEG3.js} +4 -4
- package/dist/cli.js +67 -5
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-53GUE6EI.js → intelligence-service-RU2ACYPY.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +10 -10
|
@@ -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) {
|
|
@@ -22551,11 +22596,14 @@ var GA4_DIMENSIONS = {
|
|
|
22551
22596
|
firstUserSource: "firstUserSource",
|
|
22552
22597
|
firstUserMedium: "firstUserMedium",
|
|
22553
22598
|
sessionDefaultChannelGrouping: "sessionDefaultChannelGrouping",
|
|
22554
|
-
sessionDefaultChannelGroup: "sessionDefaultChannelGroup"
|
|
22599
|
+
sessionDefaultChannelGroup: "sessionDefaultChannelGroup",
|
|
22600
|
+
hostName: "hostName",
|
|
22601
|
+
eventName: "eventName"
|
|
22555
22602
|
};
|
|
22556
22603
|
var GA4_METRICS = {
|
|
22557
22604
|
sessions: "sessions",
|
|
22558
|
-
totalUsers: "totalUsers"
|
|
22605
|
+
totalUsers: "totalUsers",
|
|
22606
|
+
eventCount: "eventCount"
|
|
22559
22607
|
};
|
|
22560
22608
|
|
|
22561
22609
|
// ../integration-google-analytics/src/types.ts
|
|
@@ -22824,6 +22872,100 @@ async function batchRunReports(accessToken, propertyId, requests) {
|
|
|
22824
22872
|
function formatDate2(d) {
|
|
22825
22873
|
return d.toISOString().slice(0, 10);
|
|
22826
22874
|
}
|
|
22875
|
+
function measurementDateRange(days) {
|
|
22876
|
+
const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
|
|
22877
|
+
const endDate = /* @__PURE__ */ new Date();
|
|
22878
|
+
const startDate = /* @__PURE__ */ new Date();
|
|
22879
|
+
startDate.setUTCDate(startDate.getUTCDate() - syncDays);
|
|
22880
|
+
return { syncDays, dateRange: { startDate: formatDate2(startDate), endDate: formatDate2(endDate) } };
|
|
22881
|
+
}
|
|
22882
|
+
async function fetchPagedReport(accessToken, propertyId, baseRequest, pageSize = 1e4) {
|
|
22883
|
+
const rows = [];
|
|
22884
|
+
let offset = 0;
|
|
22885
|
+
for (let page = 0; page < GA4_MAX_PAGES; page++) {
|
|
22886
|
+
const response = await runReport(accessToken, propertyId, { ...baseRequest, limit: pageSize, offset });
|
|
22887
|
+
const pageRows = response.rows ?? [];
|
|
22888
|
+
rows.push(...pageRows);
|
|
22889
|
+
offset += pageRows.length;
|
|
22890
|
+
if (pageRows.length < pageSize || response.rowCount !== void 0 && offset >= response.rowCount) break;
|
|
22891
|
+
if (page === GA4_MAX_PAGES - 1) {
|
|
22892
|
+
throw new Error(`GA4 report truncated: page limit (${GA4_MAX_PAGES}) reached before all rows were fetched`);
|
|
22893
|
+
}
|
|
22894
|
+
}
|
|
22895
|
+
return rows;
|
|
22896
|
+
}
|
|
22897
|
+
async function fetchAcquisitionByChannel(accessToken, propertyId, days, options) {
|
|
22898
|
+
validateAccessToken2(accessToken);
|
|
22899
|
+
validatePropertyId(propertyId);
|
|
22900
|
+
const { syncDays, dateRange } = measurementDateRange(days);
|
|
22901
|
+
const rows = await fetchPagedReport(accessToken, propertyId, {
|
|
22902
|
+
dateRanges: [dateRange],
|
|
22903
|
+
dimensions: [
|
|
22904
|
+
{ name: GA4_DIMENSIONS.date },
|
|
22905
|
+
{ name: GA4_DIMENSIONS.sessionDefaultChannelGroup },
|
|
22906
|
+
{ name: GA4_DIMENSIONS.sessionSource },
|
|
22907
|
+
{ name: GA4_DIMENSIONS.sessionMedium },
|
|
22908
|
+
{ name: GA4_DIMENSIONS.hostName },
|
|
22909
|
+
{ name: GA4_DIMENSIONS.landingPagePlusQueryString }
|
|
22910
|
+
],
|
|
22911
|
+
metrics: [{ name: GA4_METRICS.sessions }]
|
|
22912
|
+
}, options?.pageSize);
|
|
22913
|
+
ga4Log("info", "fetch-acquisition.done", { propertyId, days: syncDays, rowCount: rows.length });
|
|
22914
|
+
return { ...dateRange, rows: rows.map((row) => ({
|
|
22915
|
+
date: compactDateToIso(row.dimensionValues[0]?.value ?? ""),
|
|
22916
|
+
channelGroup: row.dimensionValues[1]?.value ?? "(not set)",
|
|
22917
|
+
source: row.dimensionValues[2]?.value ?? "(not set)",
|
|
22918
|
+
medium: row.dimensionValues[3]?.value ?? "(not set)",
|
|
22919
|
+
hostName: row.dimensionValues[4]?.value ?? "(not set)",
|
|
22920
|
+
landingPage: row.dimensionValues[5]?.value ?? "(not set)",
|
|
22921
|
+
sessions: parseInt(row.metricValues[0]?.value ?? "0", 10) || 0
|
|
22922
|
+
})) };
|
|
22923
|
+
}
|
|
22924
|
+
function isLandingPageDimensionIncompatibility(error) {
|
|
22925
|
+
return error instanceof GA4ApiError && error.status === 400 && /landingPagePlusQueryString/i.test(error.message) && /incompatib/i.test(error.message);
|
|
22926
|
+
}
|
|
22927
|
+
async function fetchLeadEvents(accessToken, propertyId, eventNames, days) {
|
|
22928
|
+
validateAccessToken2(accessToken);
|
|
22929
|
+
validatePropertyId(propertyId);
|
|
22930
|
+
const { dateRange } = measurementDateRange(days);
|
|
22931
|
+
const filter = { orGroup: { expressions: eventNames.map((value) => ({ filter: {
|
|
22932
|
+
fieldName: GA4_DIMENSIONS.eventName,
|
|
22933
|
+
stringFilter: { matchType: "EXACT", value }
|
|
22934
|
+
} })) } };
|
|
22935
|
+
const dimensions = [
|
|
22936
|
+
{ name: GA4_DIMENSIONS.date },
|
|
22937
|
+
{ name: GA4_DIMENSIONS.eventName },
|
|
22938
|
+
{ name: GA4_DIMENSIONS.sessionDefaultChannelGroup },
|
|
22939
|
+
{ name: GA4_DIMENSIONS.sessionSource },
|
|
22940
|
+
{ name: GA4_DIMENSIONS.sessionMedium }
|
|
22941
|
+
];
|
|
22942
|
+
const request = (landingPage) => ({
|
|
22943
|
+
dateRanges: [dateRange],
|
|
22944
|
+
dimensions: landingPage ? [...dimensions, { name: GA4_DIMENSIONS.hostName }, { name: GA4_DIMENSIONS.landingPagePlusQueryString }] : dimensions,
|
|
22945
|
+
metrics: [{ name: GA4_METRICS.eventCount }],
|
|
22946
|
+
dimensionFilter: filter
|
|
22947
|
+
});
|
|
22948
|
+
let attributionScope = "landing-page";
|
|
22949
|
+
let rawRows;
|
|
22950
|
+
try {
|
|
22951
|
+
rawRows = await fetchPagedReport(accessToken, propertyId, request(true));
|
|
22952
|
+
} catch (error) {
|
|
22953
|
+
if (!isLandingPageDimensionIncompatibility(error)) throw error;
|
|
22954
|
+
attributionScope = "channel";
|
|
22955
|
+
rawRows = await fetchPagedReport(accessToken, propertyId, request(false));
|
|
22956
|
+
}
|
|
22957
|
+
const rows = rawRows.map((row) => ({
|
|
22958
|
+
date: compactDateToIso(row.dimensionValues[0]?.value ?? ""),
|
|
22959
|
+
eventName: row.dimensionValues[1]?.value ?? "(not set)",
|
|
22960
|
+
channelGroup: row.dimensionValues[2]?.value ?? "(not set)",
|
|
22961
|
+
source: row.dimensionValues[3]?.value ?? "(not set)",
|
|
22962
|
+
medium: row.dimensionValues[4]?.value ?? "(not set)",
|
|
22963
|
+
hostName: attributionScope === "landing-page" ? row.dimensionValues[5]?.value ?? "(not available)" : "(not available)",
|
|
22964
|
+
landingPage: attributionScope === "landing-page" ? row.dimensionValues[6]?.value ?? "(not available)" : "(not available)",
|
|
22965
|
+
eventCount: parseInt(row.metricValues[0]?.value ?? "0", 10) || 0
|
|
22966
|
+
}));
|
|
22967
|
+
return { ...dateRange, attributionScope, rows };
|
|
22968
|
+
}
|
|
22827
22969
|
var AI_REFERRAL_SOURCE_FILTERS = [
|
|
22828
22970
|
{ matchType: "CONTAINS", value: "perplexity" },
|
|
22829
22971
|
{ matchType: "CONTAINS", value: "gemini" },
|
|
@@ -30050,6 +30192,134 @@ function normalizeAiTrafficClass(value) {
|
|
|
30050
30192
|
function emptyAiCounts() {
|
|
30051
30193
|
return { sessions: 0, users: 0 };
|
|
30052
30194
|
}
|
|
30195
|
+
function persistAcquisitionMeasurement(db, input) {
|
|
30196
|
+
const { startDate, endDate } = input.report;
|
|
30197
|
+
db.transaction((tx) => {
|
|
30198
|
+
tx.delete(gaAcquisitionDaily).where(and23(
|
|
30199
|
+
eq29(gaAcquisitionDaily.projectId, input.projectId),
|
|
30200
|
+
sql13`${gaAcquisitionDaily.date} >= ${startDate}`,
|
|
30201
|
+
sql13`${gaAcquisitionDaily.date} <= ${endDate}`
|
|
30202
|
+
)).run();
|
|
30203
|
+
for (const row of input.report.rows) {
|
|
30204
|
+
tx.insert(gaAcquisitionDaily).values({
|
|
30205
|
+
id: crypto24.randomUUID(),
|
|
30206
|
+
projectId: input.projectId,
|
|
30207
|
+
date: row.date,
|
|
30208
|
+
channelGroup: row.channelGroup,
|
|
30209
|
+
source: row.source,
|
|
30210
|
+
medium: row.medium,
|
|
30211
|
+
hostName: row.hostName,
|
|
30212
|
+
landingPage: row.landingPage,
|
|
30213
|
+
landingPageNormalized: normalizeUrlPath(row.landingPage),
|
|
30214
|
+
sessions: row.sessions,
|
|
30215
|
+
syncedAt: input.syncedAt,
|
|
30216
|
+
syncRunId: input.runId,
|
|
30217
|
+
createdAt: input.syncedAt
|
|
30218
|
+
}).run();
|
|
30219
|
+
}
|
|
30220
|
+
tx.insert(gaMeasurementSyncStates).values({
|
|
30221
|
+
projectId: input.projectId,
|
|
30222
|
+
acquisitionStatus: "ready",
|
|
30223
|
+
acquisitionError: null,
|
|
30224
|
+
acquisitionSyncedAt: input.syncedAt,
|
|
30225
|
+
updatedAt: input.syncedAt
|
|
30226
|
+
}).onConflictDoUpdate({
|
|
30227
|
+
target: gaMeasurementSyncStates.projectId,
|
|
30228
|
+
set: {
|
|
30229
|
+
acquisitionStatus: "ready",
|
|
30230
|
+
acquisitionError: null,
|
|
30231
|
+
acquisitionSyncedAt: input.syncedAt,
|
|
30232
|
+
updatedAt: input.syncedAt
|
|
30233
|
+
}
|
|
30234
|
+
}).run();
|
|
30235
|
+
});
|
|
30236
|
+
}
|
|
30237
|
+
function persistLeadMeasurement(db, input) {
|
|
30238
|
+
const { startDate, endDate } = input.report;
|
|
30239
|
+
db.transaction((tx) => {
|
|
30240
|
+
tx.delete(gaLeadEventsDaily).where(and23(
|
|
30241
|
+
eq29(gaLeadEventsDaily.projectId, input.projectId),
|
|
30242
|
+
sql13`${gaLeadEventsDaily.date} >= ${startDate}`,
|
|
30243
|
+
sql13`${gaLeadEventsDaily.date} <= ${endDate}`
|
|
30244
|
+
)).run();
|
|
30245
|
+
for (const row of input.report.rows) {
|
|
30246
|
+
tx.insert(gaLeadEventsDaily).values({
|
|
30247
|
+
id: crypto24.randomUUID(),
|
|
30248
|
+
projectId: input.projectId,
|
|
30249
|
+
date: row.date,
|
|
30250
|
+
eventName: row.eventName,
|
|
30251
|
+
channelGroup: row.channelGroup,
|
|
30252
|
+
source: row.source,
|
|
30253
|
+
medium: row.medium,
|
|
30254
|
+
hostName: row.hostName,
|
|
30255
|
+
landingPage: row.landingPage,
|
|
30256
|
+
landingPageNormalized: input.report.attributionScope === "landing-page" ? normalizeUrlPath(row.landingPage) : null,
|
|
30257
|
+
attributionScope: input.report.attributionScope,
|
|
30258
|
+
eventCount: row.eventCount,
|
|
30259
|
+
syncedAt: input.syncedAt,
|
|
30260
|
+
syncRunId: input.runId,
|
|
30261
|
+
createdAt: input.syncedAt
|
|
30262
|
+
}).run();
|
|
30263
|
+
}
|
|
30264
|
+
tx.insert(gaMeasurementSyncStates).values({
|
|
30265
|
+
projectId: input.projectId,
|
|
30266
|
+
leadStatus: "ready",
|
|
30267
|
+
leadError: null,
|
|
30268
|
+
leadSyncedAt: input.syncedAt,
|
|
30269
|
+
leadAttributionScope: input.report.attributionScope,
|
|
30270
|
+
updatedAt: input.syncedAt
|
|
30271
|
+
}).onConflictDoUpdate({
|
|
30272
|
+
target: gaMeasurementSyncStates.projectId,
|
|
30273
|
+
set: {
|
|
30274
|
+
leadStatus: "ready",
|
|
30275
|
+
leadError: null,
|
|
30276
|
+
leadSyncedAt: input.syncedAt,
|
|
30277
|
+
leadAttributionScope: input.report.attributionScope,
|
|
30278
|
+
updatedAt: input.syncedAt
|
|
30279
|
+
}
|
|
30280
|
+
}).run();
|
|
30281
|
+
});
|
|
30282
|
+
}
|
|
30283
|
+
function resetLeadMeasurement(db, projectId, updatedAt) {
|
|
30284
|
+
db.transaction((tx) => {
|
|
30285
|
+
tx.delete(gaLeadEventsDaily).where(eq29(gaLeadEventsDaily.projectId, projectId)).run();
|
|
30286
|
+
tx.insert(gaMeasurementSyncStates).values({ projectId, leadStatus: "never-synced", leadError: null, leadSyncedAt: null, leadAttributionScope: null, updatedAt }).onConflictDoUpdate({
|
|
30287
|
+
target: gaMeasurementSyncStates.projectId,
|
|
30288
|
+
set: { leadStatus: "never-synced", leadError: null, leadSyncedAt: null, leadAttributionScope: null, updatedAt }
|
|
30289
|
+
}).run();
|
|
30290
|
+
});
|
|
30291
|
+
}
|
|
30292
|
+
function persistMeasurementError(db, projectId, component, message, updatedAt) {
|
|
30293
|
+
if (component === "acquisition") {
|
|
30294
|
+
db.insert(gaMeasurementSyncStates).values({
|
|
30295
|
+
projectId,
|
|
30296
|
+
acquisitionStatus: "error",
|
|
30297
|
+
acquisitionError: message,
|
|
30298
|
+
updatedAt
|
|
30299
|
+
}).onConflictDoUpdate({
|
|
30300
|
+
target: gaMeasurementSyncStates.projectId,
|
|
30301
|
+
set: {
|
|
30302
|
+
acquisitionStatus: "error",
|
|
30303
|
+
acquisitionError: message,
|
|
30304
|
+
updatedAt
|
|
30305
|
+
}
|
|
30306
|
+
}).run();
|
|
30307
|
+
return;
|
|
30308
|
+
}
|
|
30309
|
+
db.insert(gaMeasurementSyncStates).values({
|
|
30310
|
+
projectId,
|
|
30311
|
+
leadStatus: "error",
|
|
30312
|
+
leadError: message,
|
|
30313
|
+
updatedAt
|
|
30314
|
+
}).onConflictDoUpdate({
|
|
30315
|
+
target: gaMeasurementSyncStates.projectId,
|
|
30316
|
+
set: {
|
|
30317
|
+
leadStatus: "error",
|
|
30318
|
+
leadError: message,
|
|
30319
|
+
updatedAt
|
|
30320
|
+
}
|
|
30321
|
+
}).run();
|
|
30322
|
+
}
|
|
30053
30323
|
function summarizeAiReferralCounts(rows) {
|
|
30054
30324
|
const dedupeGroups = /* @__PURE__ */ new Map();
|
|
30055
30325
|
const bySessionChannelGroup = /* @__PURE__ */ new Map();
|
|
@@ -30313,6 +30583,10 @@ async function ga4Routes(app, opts) {
|
|
|
30313
30583
|
const syncSummary = true;
|
|
30314
30584
|
const syncAi = !only || only === "ai";
|
|
30315
30585
|
const syncSocial = !only || only === "social";
|
|
30586
|
+
const measurementState = app.db.select().from(gaMeasurementSyncStates).where(eq29(gaMeasurementSyncStates.projectId, project.id)).get();
|
|
30587
|
+
const acquisitionDays = measurementState?.acquisitionSyncedAt != null ? Math.min(Math.max(1, days), 90) : 90;
|
|
30588
|
+
const leadDays = measurementState?.leadSyncedAt != null ? Math.min(Math.max(1, days), 90) : 90;
|
|
30589
|
+
const leadEventNames = project.measurement.leadEventNames;
|
|
30316
30590
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
30317
30591
|
const runId = crypto24.randomUUID();
|
|
30318
30592
|
app.db.insert(runs).values({
|
|
@@ -30338,6 +30612,10 @@ async function ga4Routes(app, opts) {
|
|
|
30338
30612
|
if (syncTraffic) fetches.push(fetchTrafficByLandingPage(accessToken, propertyId, days));
|
|
30339
30613
|
if (syncAi) fetches.push(fetchAiReferrals(accessToken, propertyId, days));
|
|
30340
30614
|
if (syncSocial) fetches.push(fetchSocialReferrals(accessToken, propertyId, days));
|
|
30615
|
+
const measurementFetch = Promise.allSettled([
|
|
30616
|
+
fetchAcquisitionByChannel(accessToken, propertyId, acquisitionDays),
|
|
30617
|
+
leadEventNames.length > 0 ? fetchLeadEvents(accessToken, propertyId, leadEventNames, leadDays) : Promise.resolve(null)
|
|
30618
|
+
]);
|
|
30341
30619
|
const results = await Promise.all(fetches);
|
|
30342
30620
|
const summary = results[0];
|
|
30343
30621
|
const windowSummaries = results.slice(1, 1 + WINDOW_KEYS.length);
|
|
@@ -30483,7 +30761,74 @@ async function ga4Routes(app, opts) {
|
|
|
30483
30761
|
}
|
|
30484
30762
|
}
|
|
30485
30763
|
});
|
|
30486
|
-
|
|
30764
|
+
const [acquisitionSettled, leadsSettled] = await measurementFetch;
|
|
30765
|
+
const measurementNow = (/* @__PURE__ */ new Date()).toISOString();
|
|
30766
|
+
const recordMeasurementFailure = (component, reason) => {
|
|
30767
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
30768
|
+
gaLog("warn", "measurement.component-failed", {
|
|
30769
|
+
projectId: project.id,
|
|
30770
|
+
runId,
|
|
30771
|
+
component,
|
|
30772
|
+
error: message
|
|
30773
|
+
});
|
|
30774
|
+
try {
|
|
30775
|
+
persistMeasurementError(app.db, project.id, component, message, measurementNow);
|
|
30776
|
+
} catch (stateError) {
|
|
30777
|
+
gaLog("error", "measurement.state-write-failed", {
|
|
30778
|
+
projectId: project.id,
|
|
30779
|
+
runId,
|
|
30780
|
+
component,
|
|
30781
|
+
error: stateError instanceof Error ? stateError.message : String(stateError)
|
|
30782
|
+
});
|
|
30783
|
+
}
|
|
30784
|
+
return { status: "error", days: component === "acquisition" ? acquisitionDays : leadDays, rowCount: 0, error: message };
|
|
30785
|
+
};
|
|
30786
|
+
const acquisitionResult = (() => {
|
|
30787
|
+
if (acquisitionSettled.status === "rejected") {
|
|
30788
|
+
return recordMeasurementFailure("acquisition", acquisitionSettled.reason);
|
|
30789
|
+
}
|
|
30790
|
+
try {
|
|
30791
|
+
persistAcquisitionMeasurement(app.db, {
|
|
30792
|
+
projectId: project.id,
|
|
30793
|
+
runId,
|
|
30794
|
+
syncedAt: measurementNow,
|
|
30795
|
+
report: acquisitionSettled.value
|
|
30796
|
+
});
|
|
30797
|
+
return {
|
|
30798
|
+
status: "ready",
|
|
30799
|
+
days: acquisitionDays,
|
|
30800
|
+
rowCount: acquisitionSettled.value.rows.length
|
|
30801
|
+
};
|
|
30802
|
+
} catch (error) {
|
|
30803
|
+
return recordMeasurementFailure("acquisition", error);
|
|
30804
|
+
}
|
|
30805
|
+
})();
|
|
30806
|
+
const leadResult = (() => {
|
|
30807
|
+
if (leadsSettled.status === "rejected") {
|
|
30808
|
+
return recordMeasurementFailure("leads", leadsSettled.reason);
|
|
30809
|
+
}
|
|
30810
|
+
if (leadsSettled.value === null) {
|
|
30811
|
+
resetLeadMeasurement(app.db, project.id, measurementNow);
|
|
30812
|
+
return { status: "not-configured", days: 0, rowCount: 0 };
|
|
30813
|
+
}
|
|
30814
|
+
try {
|
|
30815
|
+
persistLeadMeasurement(app.db, {
|
|
30816
|
+
projectId: project.id,
|
|
30817
|
+
runId,
|
|
30818
|
+
syncedAt: measurementNow,
|
|
30819
|
+
report: leadsSettled.value
|
|
30820
|
+
});
|
|
30821
|
+
return {
|
|
30822
|
+
status: "ready",
|
|
30823
|
+
days: leadDays,
|
|
30824
|
+
rowCount: leadsSettled.value.rows.length,
|
|
30825
|
+
attributionScope: leadsSettled.value.attributionScope
|
|
30826
|
+
};
|
|
30827
|
+
} catch (error) {
|
|
30828
|
+
return recordMeasurementFailure("leads", error);
|
|
30829
|
+
}
|
|
30830
|
+
})();
|
|
30831
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: measurementNow }).where(eq29(runs.id, runId)).run();
|
|
30487
30832
|
const syncedComponents = only ? [
|
|
30488
30833
|
...syncTraffic ? ["traffic"] : [],
|
|
30489
30834
|
...syncSummary ? ["summary"] : [],
|
|
@@ -30507,6 +30852,10 @@ async function ga4Routes(app, opts) {
|
|
|
30507
30852
|
socialReferralCount: socialReferrals.length,
|
|
30508
30853
|
days,
|
|
30509
30854
|
syncedAt: now,
|
|
30855
|
+
measurement: {
|
|
30856
|
+
acquisition: acquisitionResult,
|
|
30857
|
+
leads: leadResult
|
|
30858
|
+
},
|
|
30510
30859
|
...syncedComponents ? { syncedComponents } : {}
|
|
30511
30860
|
};
|
|
30512
30861
|
} catch (e) {
|
|
@@ -30926,6 +31275,402 @@ async function ga4Routes(app, opts) {
|
|
|
30926
31275
|
});
|
|
30927
31276
|
}
|
|
30928
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
|
+
|
|
30929
31674
|
// ../integration-wordpress/src/types.ts
|
|
30930
31675
|
var WordpressApiError = class extends Error {
|
|
30931
31676
|
statusCode;
|
|
@@ -32562,7 +33307,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
32562
33307
|
|
|
32563
33308
|
// ../api-routes/src/backlinks.ts
|
|
32564
33309
|
import crypto26 from "crypto";
|
|
32565
|
-
import { and as
|
|
33310
|
+
import { and as and26, asc as asc7, desc as desc14, eq as eq31, sql as sql15 } from "drizzle-orm";
|
|
32566
33311
|
|
|
32567
33312
|
// ../integration-commoncrawl/src/constants.ts
|
|
32568
33313
|
import os2 from "os";
|
|
@@ -32852,7 +33597,7 @@ async function queryBacklinks(opts) {
|
|
|
32852
33597
|
const reversed = opts.targets.map(reverseDomain);
|
|
32853
33598
|
const targetList = reversed.map(quote).join(", ");
|
|
32854
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)}` : "";
|
|
32855
|
-
const
|
|
33600
|
+
const sql18 = `
|
|
32856
33601
|
WITH vertices AS (
|
|
32857
33602
|
SELECT * FROM read_csv(
|
|
32858
33603
|
${quote(opts.vertexPath)},
|
|
@@ -32888,7 +33633,7 @@ async function queryBacklinks(opts) {
|
|
|
32888
33633
|
const conn = await instance.connect();
|
|
32889
33634
|
let rows;
|
|
32890
33635
|
try {
|
|
32891
|
-
const reader = await conn.runAndReadAll(
|
|
33636
|
+
const reader = await conn.runAndReadAll(sql18);
|
|
32892
33637
|
rows = reader.getRowObjects();
|
|
32893
33638
|
} finally {
|
|
32894
33639
|
conn.disconnectSync?.();
|
|
@@ -32965,7 +33710,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
32965
33710
|
}
|
|
32966
33711
|
|
|
32967
33712
|
// ../api-routes/src/backlinks-filter.ts
|
|
32968
|
-
import { and as
|
|
33713
|
+
import { and as and25, ne as ne4, notLike } from "drizzle-orm";
|
|
32969
33714
|
var BACKLINK_FILTER_PATTERNS = [
|
|
32970
33715
|
"*.google.com",
|
|
32971
33716
|
"*.googleusercontent.com",
|
|
@@ -32988,7 +33733,7 @@ function backlinkCrawlerExclusionClause() {
|
|
|
32988
33733
|
conditions.push(ne4(backlinkDomains.linkingDomain, pattern));
|
|
32989
33734
|
}
|
|
32990
33735
|
}
|
|
32991
|
-
const combined =
|
|
33736
|
+
const combined = and25(...conditions);
|
|
32992
33737
|
if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
|
|
32993
33738
|
return combined;
|
|
32994
33739
|
}
|
|
@@ -33059,11 +33804,11 @@ function mapRunRow(row) {
|
|
|
33059
33804
|
}
|
|
33060
33805
|
function latestSummaryForProject(db, projectId, source, release) {
|
|
33061
33806
|
const conditions = [
|
|
33062
|
-
|
|
33063
|
-
|
|
33807
|
+
eq31(backlinkSummaries.projectId, projectId),
|
|
33808
|
+
eq31(backlinkSummaries.source, source)
|
|
33064
33809
|
];
|
|
33065
|
-
if (release) conditions.push(
|
|
33066
|
-
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();
|
|
33067
33812
|
}
|
|
33068
33813
|
function parseExcludeCrawlers(value) {
|
|
33069
33814
|
if (!value) return false;
|
|
@@ -33071,19 +33816,19 @@ function parseExcludeCrawlers(value) {
|
|
|
33071
33816
|
return lower === "1" || lower === "true" || lower === "yes";
|
|
33072
33817
|
}
|
|
33073
33818
|
function computeFilteredSummary(db, base) {
|
|
33074
|
-
const baseDomainCondition =
|
|
33075
|
-
|
|
33076
|
-
|
|
33077
|
-
|
|
33819
|
+
const baseDomainCondition = and26(
|
|
33820
|
+
eq31(backlinkDomains.projectId, base.projectId),
|
|
33821
|
+
eq31(backlinkDomains.source, base.source),
|
|
33822
|
+
eq31(backlinkDomains.release, base.release)
|
|
33078
33823
|
);
|
|
33079
|
-
const filteredCondition =
|
|
33824
|
+
const filteredCondition = and26(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
33080
33825
|
const unfilteredAgg = db.select({
|
|
33081
|
-
count:
|
|
33082
|
-
total:
|
|
33826
|
+
count: sql15`count(*)`,
|
|
33827
|
+
total: sql15`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
33083
33828
|
}).from(backlinkDomains).where(baseDomainCondition).get();
|
|
33084
33829
|
const filteredAgg = db.select({
|
|
33085
|
-
count:
|
|
33086
|
-
total:
|
|
33830
|
+
count: sql15`count(*)`,
|
|
33831
|
+
total: sql15`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
33087
33832
|
}).from(backlinkDomains).where(filteredCondition).get();
|
|
33088
33833
|
const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc14(backlinkDomains.numHosts)).limit(10).all();
|
|
33089
33834
|
const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
|
|
@@ -33106,13 +33851,13 @@ function computeFilteredSummary(db, base) {
|
|
|
33106
33851
|
};
|
|
33107
33852
|
}
|
|
33108
33853
|
function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
|
|
33109
|
-
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();
|
|
33110
33855
|
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
33111
33856
|
if (summary && excludeCrawlers) {
|
|
33112
|
-
const filtered = db.select({ count:
|
|
33113
|
-
|
|
33114
|
-
|
|
33115
|
-
|
|
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),
|
|
33116
33861
|
backlinkCrawlerExclusionClause()
|
|
33117
33862
|
)).get();
|
|
33118
33863
|
totalLinkingDomains = Number(filtered?.count ?? 0);
|
|
@@ -33127,7 +33872,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
|
|
|
33127
33872
|
};
|
|
33128
33873
|
}
|
|
33129
33874
|
function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
|
|
33130
|
-
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();
|
|
33131
33876
|
const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
|
|
33132
33877
|
const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
|
|
33133
33878
|
const sources = [
|
|
@@ -33183,7 +33928,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33183
33928
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
33184
33929
|
);
|
|
33185
33930
|
}
|
|
33186
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
33931
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.release, release)).get();
|
|
33187
33932
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33188
33933
|
if (existing) {
|
|
33189
33934
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -33194,9 +33939,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
33194
33939
|
phaseDetail: null,
|
|
33195
33940
|
error: null,
|
|
33196
33941
|
updatedAt: now
|
|
33197
|
-
}).where(
|
|
33942
|
+
}).where(eq31(ccReleaseSyncs.id, existing.id)).run();
|
|
33198
33943
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
33199
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
33944
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.id, existing.id)).get();
|
|
33200
33945
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
33201
33946
|
}
|
|
33202
33947
|
const id = crypto26.randomUUID();
|
|
@@ -33208,7 +33953,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33208
33953
|
updatedAt: now
|
|
33209
33954
|
}).run();
|
|
33210
33955
|
opts.onReleaseSyncRequested(id, release);
|
|
33211
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
33956
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.id, id)).get();
|
|
33212
33957
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
33213
33958
|
});
|
|
33214
33959
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
@@ -33266,7 +34011,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33266
34011
|
createdAt: now
|
|
33267
34012
|
}).run();
|
|
33268
34013
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
33269
|
-
const run = app.db.select().from(runs).where(
|
|
34014
|
+
const run = app.db.select().from(runs).where(eq31(runs.id, runId)).get();
|
|
33270
34015
|
return reply.status(201).send(mapRunRow(run));
|
|
33271
34016
|
});
|
|
33272
34017
|
app.get(
|
|
@@ -33292,13 +34037,13 @@ async function backlinksRoutes(app, opts) {
|
|
|
33292
34037
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
33293
34038
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
33294
34039
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
33295
|
-
const baseDomainCondition =
|
|
33296
|
-
|
|
33297
|
-
|
|
33298
|
-
|
|
34040
|
+
const baseDomainCondition = and26(
|
|
34041
|
+
eq31(backlinkDomains.projectId, project.id),
|
|
34042
|
+
eq31(backlinkDomains.source, source),
|
|
34043
|
+
eq31(backlinkDomains.release, targetRelease)
|
|
33299
34044
|
);
|
|
33300
|
-
const domainCondition = excludeCrawlers ?
|
|
33301
|
-
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();
|
|
33302
34047
|
const rows = app.db.select({
|
|
33303
34048
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
33304
34049
|
numHosts: backlinkDomains.numHosts,
|
|
@@ -33321,7 +34066,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33321
34066
|
async (request, reply) => {
|
|
33322
34067
|
const project = resolveProject(app.db, request.params.name);
|
|
33323
34068
|
const source = parseSourceParam(request.query.source);
|
|
33324
|
-
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();
|
|
33325
34070
|
const response = rows.map((r) => ({
|
|
33326
34071
|
release: r.release,
|
|
33327
34072
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -33368,7 +34113,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33368
34113
|
createdAt: now
|
|
33369
34114
|
}).run();
|
|
33370
34115
|
opts.onBingBacklinkSyncRequested(runId, project.id);
|
|
33371
|
-
const run = app.db.select().from(runs).where(
|
|
34116
|
+
const run = app.db.select().from(runs).where(eq31(runs.id, runId)).get();
|
|
33372
34117
|
return reply.status(201).send(mapRunRow(run));
|
|
33373
34118
|
}
|
|
33374
34119
|
);
|
|
@@ -33377,7 +34122,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
33377
34122
|
// ../api-routes/src/traffic.ts
|
|
33378
34123
|
import crypto28 from "crypto";
|
|
33379
34124
|
import { Agent as UndiciAgent } from "undici";
|
|
33380
|
-
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";
|
|
33381
34126
|
|
|
33382
34127
|
// ../integration-cloud-run/src/auth.ts
|
|
33383
34128
|
import crypto27 from "crypto";
|
|
@@ -36160,24 +36905,24 @@ var DEFAULT_AI_REFERRER_RULES = [
|
|
|
36160
36905
|
|
|
36161
36906
|
// ../integration-traffic/src/classifier.ts
|
|
36162
36907
|
var USER_FETCH_PURPOSE = "user-agent";
|
|
36163
|
-
function
|
|
36908
|
+
function normalizeHost2(host) {
|
|
36164
36909
|
return host.trim().toLowerCase().replace(/^www\./, "");
|
|
36165
36910
|
}
|
|
36166
36911
|
function hostMatches(host, domain) {
|
|
36167
|
-
const normalizedHost =
|
|
36168
|
-
const normalizedDomain =
|
|
36912
|
+
const normalizedHost = normalizeHost2(host);
|
|
36913
|
+
const normalizedDomain = normalizeHost2(domain);
|
|
36169
36914
|
return normalizedHost === normalizedDomain || normalizedHost.endsWith(`.${normalizedDomain}`);
|
|
36170
36915
|
}
|
|
36171
36916
|
function utmTokenMatchesDomain(utmSource, domain) {
|
|
36172
36917
|
if (hostMatches(utmSource, domain)) return true;
|
|
36173
|
-
const normalizedUtm =
|
|
36174
|
-
const firstLabel =
|
|
36918
|
+
const normalizedUtm = normalizeHost2(utmSource);
|
|
36919
|
+
const firstLabel = normalizeHost2(domain).split(".")[0];
|
|
36175
36920
|
return Boolean(firstLabel) && normalizedUtm === firstLabel;
|
|
36176
36921
|
}
|
|
36177
36922
|
function hostFromUrl(value) {
|
|
36178
36923
|
if (!value) return null;
|
|
36179
36924
|
try {
|
|
36180
|
-
return
|
|
36925
|
+
return normalizeHost2(new URL(value).hostname);
|
|
36181
36926
|
} catch {
|
|
36182
36927
|
return null;
|
|
36183
36928
|
}
|
|
@@ -36186,7 +36931,7 @@ function utmSourceFromQuery(queryString) {
|
|
|
36186
36931
|
if (!queryString) return null;
|
|
36187
36932
|
const params = new URLSearchParams(queryString);
|
|
36188
36933
|
const source = params.get("utm_source");
|
|
36189
|
-
return source ?
|
|
36934
|
+
return source ? normalizeHost2(source) : null;
|
|
36190
36935
|
}
|
|
36191
36936
|
function utmSourceFromUrl(value) {
|
|
36192
36937
|
if (!value) return null;
|
|
@@ -36350,13 +37095,13 @@ function sessionWindowBucket(value, windowMs) {
|
|
|
36350
37095
|
if (Number.isNaN(date.getTime())) return value;
|
|
36351
37096
|
return new Date(Math.floor(date.getTime() / windowMs) * windowMs).toISOString();
|
|
36352
37097
|
}
|
|
36353
|
-
function
|
|
37098
|
+
function normalizeHost3(host) {
|
|
36354
37099
|
if (!host) return null;
|
|
36355
37100
|
return host.trim().toLowerCase().replace(/^www\./, "") || null;
|
|
36356
37101
|
}
|
|
36357
37102
|
function sameHost(a, b) {
|
|
36358
|
-
const normalizedA =
|
|
36359
|
-
const normalizedB =
|
|
37103
|
+
const normalizedA = normalizeHost3(a);
|
|
37104
|
+
const normalizedB = normalizeHost3(b);
|
|
36360
37105
|
return !!normalizedA && !!normalizedB && normalizedA === normalizedB;
|
|
36361
37106
|
}
|
|
36362
37107
|
function pathFromSameOriginReferer(event) {
|
|
@@ -37273,8 +38018,8 @@ async function runBackfillTask(options) {
|
|
|
37273
38018
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
37274
38019
|
try {
|
|
37275
38020
|
app.db.transaction((tx) => {
|
|
37276
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
37277
|
-
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();
|
|
37278
38023
|
});
|
|
37279
38024
|
} catch {
|
|
37280
38025
|
}
|
|
@@ -37289,7 +38034,7 @@ async function runBackfillTask(options) {
|
|
|
37289
38034
|
if (allEvents.length === 0) {
|
|
37290
38035
|
const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
37291
38036
|
try {
|
|
37292
|
-
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();
|
|
37293
38038
|
} catch {
|
|
37294
38039
|
}
|
|
37295
38040
|
return;
|
|
@@ -37311,31 +38056,31 @@ async function runBackfillTask(options) {
|
|
|
37311
38056
|
try {
|
|
37312
38057
|
app.db.transaction((tx) => {
|
|
37313
38058
|
tx.delete(crawlerEventsHourly).where(
|
|
37314
|
-
|
|
37315
|
-
|
|
37316
|
-
|
|
37317
|
-
|
|
38059
|
+
and27(
|
|
38060
|
+
eq32(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
38061
|
+
gte8(crawlerEventsHourly.tsHour, windowStartIso),
|
|
38062
|
+
lte6(crawlerEventsHourly.tsHour, windowEndIso)
|
|
37318
38063
|
)
|
|
37319
38064
|
).run();
|
|
37320
38065
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
37321
|
-
|
|
37322
|
-
|
|
37323
|
-
|
|
37324
|
-
|
|
38066
|
+
and27(
|
|
38067
|
+
eq32(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
38068
|
+
gte8(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
38069
|
+
lte6(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
37325
38070
|
)
|
|
37326
38071
|
).run();
|
|
37327
38072
|
tx.delete(aiReferralEventsHourly).where(
|
|
37328
|
-
|
|
37329
|
-
|
|
37330
|
-
|
|
37331
|
-
|
|
38073
|
+
and27(
|
|
38074
|
+
eq32(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
38075
|
+
gte8(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
38076
|
+
lte6(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
37332
38077
|
)
|
|
37333
38078
|
).run();
|
|
37334
38079
|
tx.delete(rawEventSamples).where(
|
|
37335
|
-
|
|
37336
|
-
|
|
37337
|
-
|
|
37338
|
-
|
|
38080
|
+
and27(
|
|
38081
|
+
eq32(rawEventSamples.sourceId, sourceRow.id),
|
|
38082
|
+
gte8(rawEventSamples.ts, windowStartIso),
|
|
38083
|
+
lte6(rawEventSamples.ts, windowEndIso)
|
|
37339
38084
|
)
|
|
37340
38085
|
).run();
|
|
37341
38086
|
for (const bucket of report.crawlerEventsHourly) {
|
|
@@ -37424,8 +38169,8 @@ async function runBackfillTask(options) {
|
|
|
37424
38169
|
lastError: null,
|
|
37425
38170
|
lastEventIds: newRingBuffer,
|
|
37426
38171
|
updatedAt: finishedAt
|
|
37427
|
-
}).where(
|
|
37428
|
-
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();
|
|
37429
38174
|
});
|
|
37430
38175
|
} catch (e) {
|
|
37431
38176
|
markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -37507,7 +38252,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37507
38252
|
createdAt: existing?.createdAt ?? now,
|
|
37508
38253
|
updatedAt: now
|
|
37509
38254
|
});
|
|
37510
|
-
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);
|
|
37511
38256
|
const config = {
|
|
37512
38257
|
gcpProjectId,
|
|
37513
38258
|
serviceName: serviceName ?? null,
|
|
@@ -37523,8 +38268,8 @@ async function trafficRoutes(app, opts) {
|
|
|
37523
38268
|
lastError: null,
|
|
37524
38269
|
configJson: config,
|
|
37525
38270
|
updatedAt: now
|
|
37526
|
-
}).where(
|
|
37527
|
-
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();
|
|
37528
38273
|
} else {
|
|
37529
38274
|
const newId = crypto28.randomUUID();
|
|
37530
38275
|
app.db.insert(trafficSources).values({
|
|
@@ -37541,7 +38286,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37541
38286
|
createdAt: now,
|
|
37542
38287
|
updatedAt: now
|
|
37543
38288
|
}).run();
|
|
37544
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
38289
|
+
sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, newId)).get();
|
|
37545
38290
|
}
|
|
37546
38291
|
writeAuditLog(app.db, {
|
|
37547
38292
|
projectId: project.id,
|
|
@@ -37593,7 +38338,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37593
38338
|
createdAt: existing?.createdAt ?? now,
|
|
37594
38339
|
updatedAt: now
|
|
37595
38340
|
});
|
|
37596
|
-
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);
|
|
37597
38342
|
const config = { baseUrl, username };
|
|
37598
38343
|
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
37599
38344
|
let sourceRow;
|
|
@@ -37604,8 +38349,8 @@ async function trafficRoutes(app, opts) {
|
|
|
37604
38349
|
lastError: null,
|
|
37605
38350
|
configJson: config,
|
|
37606
38351
|
updatedAt: now
|
|
37607
|
-
}).where(
|
|
37608
|
-
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();
|
|
37609
38354
|
} else {
|
|
37610
38355
|
const newId = crypto28.randomUUID();
|
|
37611
38356
|
app.db.insert(trafficSources).values({
|
|
@@ -37622,7 +38367,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37622
38367
|
createdAt: now,
|
|
37623
38368
|
updatedAt: now
|
|
37624
38369
|
}).run();
|
|
37625
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
38370
|
+
sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, newId)).get();
|
|
37626
38371
|
}
|
|
37627
38372
|
writeAuditLog(app.db, {
|
|
37628
38373
|
projectId: project.id,
|
|
@@ -37676,7 +38421,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37676
38421
|
createdAt: existing?.createdAt ?? now,
|
|
37677
38422
|
updatedAt: now
|
|
37678
38423
|
});
|
|
37679
|
-
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);
|
|
37680
38425
|
const config = { projectId, teamId, environment };
|
|
37681
38426
|
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
37682
38427
|
const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
|
|
@@ -37688,8 +38433,8 @@ async function trafficRoutes(app, opts) {
|
|
|
37688
38433
|
lastError: null,
|
|
37689
38434
|
configJson: config,
|
|
37690
38435
|
updatedAt: now
|
|
37691
|
-
}).where(
|
|
37692
|
-
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();
|
|
37693
38438
|
} else {
|
|
37694
38439
|
const newId = crypto28.randomUUID();
|
|
37695
38440
|
tx.insert(trafficSources).values({
|
|
@@ -37714,12 +38459,12 @@ async function trafficRoutes(app, opts) {
|
|
|
37714
38459
|
createdAt: now,
|
|
37715
38460
|
updatedAt: now
|
|
37716
38461
|
}).run();
|
|
37717
|
-
row = tx.select().from(trafficSources).where(
|
|
38462
|
+
row = tx.select().from(trafficSources).where(eq32(trafficSources.id, newId)).get();
|
|
37718
38463
|
}
|
|
37719
38464
|
const existingSchedule = tx.select().from(schedules).where(
|
|
37720
|
-
|
|
37721
|
-
|
|
37722
|
-
|
|
38465
|
+
and27(
|
|
38466
|
+
eq32(schedules.projectId, project.id),
|
|
38467
|
+
eq32(schedules.kind, SchedulableRunKinds["traffic-sync"])
|
|
37723
38468
|
)
|
|
37724
38469
|
).get();
|
|
37725
38470
|
let created = false;
|
|
@@ -37768,7 +38513,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37768
38513
|
});
|
|
37769
38514
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
37770
38515
|
const project = resolveProject(app.db, request.params.name);
|
|
37771
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
38516
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, request.params.id)).get();
|
|
37772
38517
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
37773
38518
|
throw notFound("Traffic source", request.params.id);
|
|
37774
38519
|
}
|
|
@@ -37794,8 +38539,8 @@ async function trafficRoutes(app, opts) {
|
|
|
37794
38539
|
const markFailed = (msg, errorCode) => {
|
|
37795
38540
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
37796
38541
|
app.db.transaction((tx) => {
|
|
37797
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
37798
|
-
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();
|
|
37799
38544
|
});
|
|
37800
38545
|
try {
|
|
37801
38546
|
opts.onTrafficSynced?.({
|
|
@@ -37876,7 +38621,7 @@ async function trafficRoutes(app, opts) {
|
|
|
37876
38621
|
}
|
|
37877
38622
|
const credential = credentialStore.getConnection(project.name);
|
|
37878
38623
|
if (!credential) {
|
|
37879
|
-
app.db.delete(runs).where(
|
|
38624
|
+
app.db.delete(runs).where(eq32(runs.id, runId)).run();
|
|
37880
38625
|
throw validationError(
|
|
37881
38626
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
37882
38627
|
);
|
|
@@ -37925,12 +38670,12 @@ async function trafficRoutes(app, opts) {
|
|
|
37925
38670
|
auditAction = "traffic.vercel.synced";
|
|
37926
38671
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
37927
38672
|
if (!credentialStore) {
|
|
37928
|
-
app.db.delete(runs).where(
|
|
38673
|
+
app.db.delete(runs).where(eq32(runs.id, runId)).run();
|
|
37929
38674
|
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
37930
38675
|
}
|
|
37931
38676
|
const credential = credentialStore.getConnection(project.name);
|
|
37932
38677
|
if (!credential) {
|
|
37933
|
-
app.db.delete(runs).where(
|
|
38678
|
+
app.db.delete(runs).where(eq32(runs.id, runId)).run();
|
|
37934
38679
|
throw validationError(
|
|
37935
38680
|
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
37936
38681
|
);
|
|
@@ -38030,7 +38775,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38030
38775
|
let aiReferralHitsCount = 0;
|
|
38031
38776
|
let unknownHitsCount = 0;
|
|
38032
38777
|
app.db.transaction((tx) => {
|
|
38033
|
-
const latestRow = tx.select().from(trafficSources).where(
|
|
38778
|
+
const latestRow = tx.select().from(trafficSources).where(eq32(trafficSources.id, sourceRow.id)).get();
|
|
38034
38779
|
const previousIds = latestRow.lastEventIds ?? [];
|
|
38035
38780
|
const seenEventIds = new Set(previousIds);
|
|
38036
38781
|
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
@@ -38078,7 +38823,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38078
38823
|
crawlerEventsHourly.status
|
|
38079
38824
|
],
|
|
38080
38825
|
set: {
|
|
38081
|
-
hits:
|
|
38826
|
+
hits: sql16`${crawlerEventsHourly.hits} + ${bucket.hits}`,
|
|
38082
38827
|
sampledUserAgent: bucket.sampledUserAgent,
|
|
38083
38828
|
updatedAt: finishedAt
|
|
38084
38829
|
}
|
|
@@ -38111,7 +38856,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38111
38856
|
aiUserFetchEventsHourly.status
|
|
38112
38857
|
],
|
|
38113
38858
|
set: {
|
|
38114
|
-
hits:
|
|
38859
|
+
hits: sql16`${aiUserFetchEventsHourly.hits} + ${bucket.hits}`,
|
|
38115
38860
|
sampledUserAgent: bucket.sampledUserAgent,
|
|
38116
38861
|
updatedAt: finishedAt
|
|
38117
38862
|
}
|
|
@@ -38151,9 +38896,9 @@ async function trafficRoutes(app, opts) {
|
|
|
38151
38896
|
// colliding bucket adds its paid and organic arrivals to the ones
|
|
38152
38897
|
// already there rather than overwriting a single label.
|
|
38153
38898
|
set: {
|
|
38154
|
-
sessionsOrHits:
|
|
38155
|
-
paidSessionsOrHits:
|
|
38156
|
-
organicSessionsOrHits:
|
|
38899
|
+
sessionsOrHits: sql16`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
|
|
38900
|
+
paidSessionsOrHits: sql16`${aiReferralEventsHourly.paidSessionsOrHits} + ${bucket.paidHits}`,
|
|
38901
|
+
organicSessionsOrHits: sql16`${aiReferralEventsHourly.organicSessionsOrHits} + ${bucket.organicHits}`,
|
|
38157
38902
|
updatedAt: finishedAt
|
|
38158
38903
|
}
|
|
38159
38904
|
}).run();
|
|
@@ -38205,8 +38950,8 @@ async function trafficRoutes(app, opts) {
|
|
|
38205
38950
|
if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
38206
38951
|
sourceUpdate.lastCursor = nextCursor ?? null;
|
|
38207
38952
|
}
|
|
38208
|
-
tx.update(trafficSources).set(sourceUpdate).where(
|
|
38209
|
-
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();
|
|
38210
38955
|
});
|
|
38211
38956
|
writeAuditLog(app.db, {
|
|
38212
38957
|
projectId: project.id,
|
|
@@ -38258,7 +39003,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38258
39003
|
});
|
|
38259
39004
|
app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
|
|
38260
39005
|
const project = resolveProject(app.db, request.params.name);
|
|
38261
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
39006
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq32(trafficSources.id, request.params.id)).get();
|
|
38262
39007
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
38263
39008
|
throw notFound("Traffic source", request.params.id);
|
|
38264
39009
|
}
|
|
@@ -38444,40 +39189,40 @@ async function trafficRoutes(app, opts) {
|
|
|
38444
39189
|
function buildSourceDetail(projectId, row, since) {
|
|
38445
39190
|
const crawlerPathRows = app.db.select({
|
|
38446
39191
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
38447
|
-
hits:
|
|
39192
|
+
hits: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
38448
39193
|
}).from(crawlerEventsHourly).where(
|
|
38449
|
-
|
|
38450
|
-
|
|
38451
|
-
|
|
39194
|
+
and27(
|
|
39195
|
+
eq32(crawlerEventsHourly.sourceId, row.id),
|
|
39196
|
+
gte8(crawlerEventsHourly.tsHour, since)
|
|
38452
39197
|
)
|
|
38453
39198
|
).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
38454
39199
|
const crawlerSegments = segmentCrawlerHits(
|
|
38455
39200
|
crawlerPathRows.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
|
|
38456
39201
|
);
|
|
38457
39202
|
const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
|
|
38458
|
-
const aiUserFetchTotals = app.db.select({ total:
|
|
38459
|
-
|
|
38460
|
-
|
|
38461
|
-
|
|
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)
|
|
38462
39207
|
)
|
|
38463
39208
|
).get();
|
|
38464
|
-
const aiTotals = app.db.select({ total:
|
|
38465
|
-
|
|
38466
|
-
|
|
38467
|
-
|
|
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)
|
|
38468
39213
|
)
|
|
38469
39214
|
).get();
|
|
38470
|
-
const sampleTotals = app.db.select({ total:
|
|
38471
|
-
|
|
38472
|
-
|
|
38473
|
-
|
|
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)
|
|
38474
39219
|
)
|
|
38475
39220
|
).get();
|
|
38476
39221
|
const latestRun = app.db.select().from(runs).where(
|
|
38477
|
-
|
|
38478
|
-
|
|
38479
|
-
|
|
38480
|
-
|
|
39222
|
+
and27(
|
|
39223
|
+
eq32(runs.projectId, projectId),
|
|
39224
|
+
eq32(runs.kind, RunKinds["traffic-sync"]),
|
|
39225
|
+
eq32(runs.sourceId, row.id)
|
|
38481
39226
|
)
|
|
38482
39227
|
).orderBy(desc15(runs.startedAt)).limit(1).get();
|
|
38483
39228
|
return {
|
|
@@ -38508,7 +39253,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38508
39253
|
"`advanceToNow` must be `true`. There is no implicit reset."
|
|
38509
39254
|
);
|
|
38510
39255
|
}
|
|
38511
|
-
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();
|
|
38512
39257
|
if (!sourceRow) {
|
|
38513
39258
|
throw notFound("traffic source", request.params.id);
|
|
38514
39259
|
}
|
|
@@ -38525,7 +39270,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38525
39270
|
status: TrafficSourceStatuses.connected,
|
|
38526
39271
|
lastError: null,
|
|
38527
39272
|
updatedAt: now
|
|
38528
|
-
}).where(
|
|
39273
|
+
}).where(eq32(trafficSources.id, sourceRow.id)).run();
|
|
38529
39274
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
38530
39275
|
projectId: project.id,
|
|
38531
39276
|
actor: "api",
|
|
@@ -38533,20 +39278,20 @@ async function trafficRoutes(app, opts) {
|
|
|
38533
39278
|
entityType: "traffic_source",
|
|
38534
39279
|
entityId: sourceRow.id
|
|
38535
39280
|
}));
|
|
38536
|
-
updatedRow = tx.select().from(trafficSources).where(
|
|
39281
|
+
updatedRow = tx.select().from(trafficSources).where(eq32(trafficSources.id, sourceRow.id)).get();
|
|
38537
39282
|
});
|
|
38538
39283
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
38539
39284
|
});
|
|
38540
39285
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
38541
39286
|
const project = resolveProject(app.db, request.params.name);
|
|
38542
|
-
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();
|
|
38543
39288
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
38544
39289
|
const response = { sources };
|
|
38545
39290
|
return response;
|
|
38546
39291
|
});
|
|
38547
39292
|
app.get("/projects/:name/traffic/status", async (request) => {
|
|
38548
39293
|
const project = resolveProject(app.db, request.params.name);
|
|
38549
|
-
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();
|
|
38550
39295
|
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
38551
39296
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
38552
39297
|
const response = { sources };
|
|
@@ -38556,7 +39301,7 @@ async function trafficRoutes(app, opts) {
|
|
|
38556
39301
|
"/projects/:name/traffic/sources/:id",
|
|
38557
39302
|
async (request) => {
|
|
38558
39303
|
const project = resolveProject(app.db, request.params.name);
|
|
38559
|
-
const row = app.db.select().from(trafficSources).where(
|
|
39304
|
+
const row = app.db.select().from(trafficSources).where(eq32(trafficSources.id, request.params.id)).get();
|
|
38560
39305
|
if (!row || row.projectId !== project.id) {
|
|
38561
39306
|
throw notFound("Traffic source", request.params.id);
|
|
38562
39307
|
}
|
|
@@ -38621,26 +39366,26 @@ async function trafficRoutes(app, opts) {
|
|
|
38621
39366
|
const seriesByBucket = /* @__PURE__ */ new Map();
|
|
38622
39367
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
38623
39368
|
const crawlerFilters = [
|
|
38624
|
-
|
|
38625
|
-
|
|
38626
|
-
|
|
39369
|
+
eq32(crawlerEventsHourly.projectId, project.id),
|
|
39370
|
+
gte8(crawlerEventsHourly.tsHour, sinceIso),
|
|
39371
|
+
lte6(crawlerEventsHourly.tsHour, untilIso)
|
|
38627
39372
|
];
|
|
38628
|
-
if (sourceIdParam) crawlerFilters.push(
|
|
38629
|
-
const crawlerWhere =
|
|
39373
|
+
if (sourceIdParam) crawlerFilters.push(eq32(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
39374
|
+
const crawlerWhere = and27(...crawlerFilters);
|
|
38630
39375
|
const pathTotals = app.db.select({
|
|
38631
39376
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
38632
|
-
hits:
|
|
38633
|
-
rows:
|
|
39377
|
+
hits: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
|
|
39378
|
+
rows: sql16`COUNT(*)`
|
|
38634
39379
|
}).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
38635
39380
|
crawlerSegments = segmentCrawlerHits(
|
|
38636
39381
|
pathTotals.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
|
|
38637
39382
|
);
|
|
38638
39383
|
crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
|
|
38639
39384
|
totalEventRows += pathTotals.reduce((sum, row) => sum + Number(row.rows), 0);
|
|
38640
|
-
const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ?
|
|
39385
|
+
const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql16`substr(${crawlerEventsHourly.tsHour}, 1, 10)` : crawlerEventsHourly.tsHour;
|
|
38641
39386
|
const crawlerSeries = app.db.select({
|
|
38642
39387
|
bucket: crawlerSeriesBucket,
|
|
38643
|
-
hits:
|
|
39388
|
+
hits: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
38644
39389
|
}).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerSeriesBucket).all();
|
|
38645
39390
|
for (const row of crawlerSeries) {
|
|
38646
39391
|
trafficSeriesPoint(seriesByBucket, row.bucket).crawlerHits = Number(row.hits);
|
|
@@ -38663,22 +39408,22 @@ async function trafficRoutes(app, opts) {
|
|
|
38663
39408
|
}
|
|
38664
39409
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
38665
39410
|
const userFetchFilters = [
|
|
38666
|
-
|
|
38667
|
-
|
|
38668
|
-
|
|
39411
|
+
eq32(aiUserFetchEventsHourly.projectId, project.id),
|
|
39412
|
+
gte8(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
39413
|
+
lte6(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
38669
39414
|
];
|
|
38670
|
-
if (sourceIdParam) userFetchFilters.push(
|
|
38671
|
-
const userFetchWhere =
|
|
39415
|
+
if (sourceIdParam) userFetchFilters.push(eq32(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
39416
|
+
const userFetchWhere = and27(...userFetchFilters);
|
|
38672
39417
|
const total = app.db.select({
|
|
38673
|
-
total:
|
|
38674
|
-
rows:
|
|
39418
|
+
total: sql16`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`,
|
|
39419
|
+
rows: sql16`COUNT(*)`
|
|
38675
39420
|
}).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
38676
39421
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
38677
39422
|
totalEventRows += Number(total?.rows ?? 0);
|
|
38678
|
-
const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ?
|
|
39423
|
+
const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql16`substr(${aiUserFetchEventsHourly.tsHour}, 1, 10)` : aiUserFetchEventsHourly.tsHour;
|
|
38679
39424
|
const userFetchSeries = app.db.select({
|
|
38680
39425
|
bucket: userFetchSeriesBucket,
|
|
38681
|
-
hits:
|
|
39426
|
+
hits: sql16`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
|
|
38682
39427
|
}).from(aiUserFetchEventsHourly).where(userFetchWhere).groupBy(userFetchSeriesBucket).all();
|
|
38683
39428
|
for (const row of userFetchSeries) {
|
|
38684
39429
|
trafficSeriesPoint(seriesByBucket, row.bucket).aiUserFetchHits = Number(row.hits);
|
|
@@ -38700,17 +39445,17 @@ async function trafficRoutes(app, opts) {
|
|
|
38700
39445
|
}
|
|
38701
39446
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
38702
39447
|
const aiFilters = [
|
|
38703
|
-
|
|
38704
|
-
|
|
38705
|
-
|
|
39448
|
+
eq32(aiReferralEventsHourly.projectId, project.id),
|
|
39449
|
+
gte8(aiReferralEventsHourly.tsHour, sinceIso),
|
|
39450
|
+
lte6(aiReferralEventsHourly.tsHour, untilIso)
|
|
38706
39451
|
];
|
|
38707
|
-
if (sourceIdParam) aiFilters.push(
|
|
38708
|
-
const aiWhere =
|
|
39452
|
+
if (sourceIdParam) aiFilters.push(eq32(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
39453
|
+
const aiWhere = and27(...aiFilters);
|
|
38709
39454
|
const total = app.db.select({
|
|
38710
|
-
total:
|
|
38711
|
-
paid:
|
|
38712
|
-
organic:
|
|
38713
|
-
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(*)`
|
|
38714
39459
|
}).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
38715
39460
|
aiReferralCounts = aiReferralClassCounts(
|
|
38716
39461
|
Number(total?.total ?? 0),
|
|
@@ -38718,10 +39463,10 @@ async function trafficRoutes(app, opts) {
|
|
|
38718
39463
|
Number(total?.organic ?? 0)
|
|
38719
39464
|
);
|
|
38720
39465
|
totalEventRows += Number(total?.rows ?? 0);
|
|
38721
|
-
const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ?
|
|
39466
|
+
const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql16`substr(${aiReferralEventsHourly.tsHour}, 1, 10)` : aiReferralEventsHourly.tsHour;
|
|
38722
39467
|
const referralSeries = app.db.select({
|
|
38723
39468
|
bucket: referralSeriesBucket,
|
|
38724
|
-
hits:
|
|
39469
|
+
hits: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
38725
39470
|
}).from(aiReferralEventsHourly).where(aiWhere).groupBy(referralSeriesBucket).all();
|
|
38726
39471
|
for (const row of referralSeries) {
|
|
38727
39472
|
trafficSeriesPoint(seriesByBucket, row.bucket).aiReferralHits = Number(row.hits);
|
|
@@ -39024,7 +39769,7 @@ function readInstalledManifest(skillDir) {
|
|
|
39024
39769
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
39025
39770
|
|
|
39026
39771
|
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
39027
|
-
import { and as
|
|
39772
|
+
import { and as and28, eq as eq33 } from "drizzle-orm";
|
|
39028
39773
|
function skippedNoProject() {
|
|
39029
39774
|
return {
|
|
39030
39775
|
status: CheckStatuses.skipped,
|
|
@@ -39040,8 +39785,8 @@ var BACKLINKS_CHECKS = [
|
|
|
39040
39785
|
title: "Backlinks source connected",
|
|
39041
39786
|
run: (ctx) => {
|
|
39042
39787
|
if (!ctx.project) return skippedNoProject();
|
|
39043
|
-
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(
|
|
39044
|
-
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();
|
|
39045
39790
|
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
39046
39791
|
const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
|
|
39047
39792
|
const connected = [];
|
|
@@ -39056,9 +39801,9 @@ var BACKLINKS_CHECKS = [
|
|
|
39056
39801
|
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
|
|
39057
39802
|
};
|
|
39058
39803
|
}
|
|
39059
|
-
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(
|
|
39060
|
-
|
|
39061
|
-
|
|
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)
|
|
39062
39807
|
)).limit(1).get() : false;
|
|
39063
39808
|
return {
|
|
39064
39809
|
status: CheckStatuses.ok,
|
|
@@ -39218,7 +39963,7 @@ var BING_AUTH_CHECKS = [
|
|
|
39218
39963
|
];
|
|
39219
39964
|
|
|
39220
39965
|
// ../api-routes/src/doctor/checks/content.ts
|
|
39221
|
-
import { eq as
|
|
39966
|
+
import { eq as eq34 } from "drizzle-orm";
|
|
39222
39967
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
39223
39968
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
39224
39969
|
function skippedNoProject2() {
|
|
@@ -39231,7 +39976,7 @@ function skippedNoProject2() {
|
|
|
39231
39976
|
}
|
|
39232
39977
|
function loadProject(ctx) {
|
|
39233
39978
|
if (!ctx.project) return null;
|
|
39234
|
-
return ctx.db.select().from(projects).where(
|
|
39979
|
+
return ctx.db.select().from(projects).where(eq34(projects.id, ctx.project.id)).get() ?? null;
|
|
39235
39980
|
}
|
|
39236
39981
|
function percent(value) {
|
|
39237
39982
|
return Math.round(value * 100);
|
|
@@ -39323,7 +40068,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
39323
40068
|
);
|
|
39324
40069
|
|
|
39325
40070
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
39326
|
-
import { eq as
|
|
40071
|
+
import { eq as eq35 } from "drizzle-orm";
|
|
39327
40072
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
39328
40073
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
39329
40074
|
var adsConnectionCheck = {
|
|
@@ -39340,7 +40085,7 @@ var adsConnectionCheck = {
|
|
|
39340
40085
|
remediation: null
|
|
39341
40086
|
};
|
|
39342
40087
|
}
|
|
39343
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
40088
|
+
const row = ctx.db.select().from(adsConnections).where(eq35(adsConnections.projectId, ctx.project.id)).get();
|
|
39344
40089
|
if (!row) {
|
|
39345
40090
|
return {
|
|
39346
40091
|
status: CheckStatuses.skipped,
|
|
@@ -39390,7 +40135,7 @@ var adsRecentSyncCheck = {
|
|
|
39390
40135
|
remediation: null
|
|
39391
40136
|
};
|
|
39392
40137
|
}
|
|
39393
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
40138
|
+
const row = ctx.db.select().from(adsConnections).where(eq35(adsConnections.projectId, ctx.project.id)).get();
|
|
39394
40139
|
if (!row) {
|
|
39395
40140
|
return {
|
|
39396
40141
|
status: CheckStatuses.skipped,
|
|
@@ -39581,7 +40326,7 @@ var ga4ConnectionCheck = {
|
|
|
39581
40326
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
39582
40327
|
|
|
39583
40328
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
39584
|
-
import { and as
|
|
40329
|
+
import { and as and29, eq as eq36 } from "drizzle-orm";
|
|
39585
40330
|
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
39586
40331
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
39587
40332
|
function skippedNoProject3() {
|
|
@@ -39814,7 +40559,7 @@ var recentSyncCheck = {
|
|
|
39814
40559
|
title: "GBP recent sync",
|
|
39815
40560
|
run: (ctx) => {
|
|
39816
40561
|
if (!ctx.project) return skippedNoProject3();
|
|
39817
|
-
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();
|
|
39818
40563
|
if (selected.length === 0) {
|
|
39819
40564
|
return {
|
|
39820
40565
|
status: CheckStatuses.skipped,
|
|
@@ -39874,7 +40619,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
39874
40619
|
);
|
|
39875
40620
|
|
|
39876
40621
|
// ../api-routes/src/doctor/checks/places.ts
|
|
39877
|
-
import { eq as
|
|
40622
|
+
import { eq as eq37 } from "drizzle-orm";
|
|
39878
40623
|
var apiKeyCheck = {
|
|
39879
40624
|
id: "gbp.places.api-key",
|
|
39880
40625
|
category: CheckCategories.auth,
|
|
@@ -39919,7 +40664,7 @@ var apiKeyCheck = {
|
|
|
39919
40664
|
details: { tier: cfg.tier }
|
|
39920
40665
|
};
|
|
39921
40666
|
}
|
|
39922
|
-
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();
|
|
39923
40668
|
const selected = rows.filter((r) => r.selected);
|
|
39924
40669
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
39925
40670
|
const details = {
|
|
@@ -40416,7 +41161,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
40416
41161
|
];
|
|
40417
41162
|
|
|
40418
41163
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
40419
|
-
import { and as
|
|
41164
|
+
import { and as and30, eq as eq38, gte as gte9, ne as ne5, sql as sql17 } from "drizzle-orm";
|
|
40420
41165
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
40421
41166
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
40422
41167
|
function skippedNoProject5() {
|
|
@@ -40430,8 +41175,8 @@ function skippedNoProject5() {
|
|
|
40430
41175
|
function loadProbes(ctx) {
|
|
40431
41176
|
if (!ctx.project) return [];
|
|
40432
41177
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
40433
|
-
|
|
40434
|
-
|
|
41178
|
+
and30(
|
|
41179
|
+
eq38(trafficSources.projectId, ctx.project.id),
|
|
40435
41180
|
ne5(trafficSources.status, TrafficSourceStatuses.archived)
|
|
40436
41181
|
)
|
|
40437
41182
|
).all();
|
|
@@ -40510,18 +41255,18 @@ var recentDataCheck = {
|
|
|
40510
41255
|
const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
|
|
40511
41256
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
40512
41257
|
const recentCrawlers = Number(
|
|
40513
|
-
ctx.db.select({ total:
|
|
40514
|
-
|
|
40515
|
-
|
|
40516
|
-
|
|
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)
|
|
40517
41262
|
)
|
|
40518
41263
|
).get()?.total ?? 0
|
|
40519
41264
|
);
|
|
40520
41265
|
const recentReferrals = Number(
|
|
40521
|
-
ctx.db.select({ total:
|
|
40522
|
-
|
|
40523
|
-
|
|
40524
|
-
|
|
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)
|
|
40525
41270
|
)
|
|
40526
41271
|
).get()?.total ?? 0
|
|
40527
41272
|
);
|
|
@@ -40534,18 +41279,18 @@ var recentDataCheck = {
|
|
|
40534
41279
|
};
|
|
40535
41280
|
}
|
|
40536
41281
|
const olderCrawlers = Number(
|
|
40537
|
-
ctx.db.select({ total:
|
|
40538
|
-
|
|
40539
|
-
|
|
40540
|
-
|
|
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)
|
|
40541
41286
|
)
|
|
40542
41287
|
).get()?.total ?? 0
|
|
40543
41288
|
);
|
|
40544
41289
|
const olderReferrals = Number(
|
|
40545
|
-
ctx.db.select({ total:
|
|
40546
|
-
|
|
40547
|
-
|
|
40548
|
-
|
|
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)
|
|
40549
41294
|
)
|
|
40550
41295
|
).get()?.total ?? 0
|
|
40551
41296
|
);
|
|
@@ -40962,7 +41707,7 @@ async function doctorRoutes(app, opts) {
|
|
|
40962
41707
|
|
|
40963
41708
|
// ../api-routes/src/discovery/routes.ts
|
|
40964
41709
|
import crypto30 from "crypto";
|
|
40965
|
-
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";
|
|
40966
41711
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
40967
41712
|
async function discoveryRoutes(app, opts) {
|
|
40968
41713
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -40996,13 +41741,13 @@ async function discoveryRoutes(app, opts) {
|
|
|
40996
41741
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
40997
41742
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
40998
41743
|
const decision = app.db.transaction((tx) => {
|
|
40999
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
41000
|
-
|
|
41001
|
-
|
|
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),
|
|
41002
41747
|
// Buyer is part of session identity: it changes the seed prompt's
|
|
41003
41748
|
// semantics, so a request with a different (or no) buyer must start
|
|
41004
41749
|
// its own session, never adopt another buyer's probes.
|
|
41005
|
-
parsed.data.buyerDescription == null ? isNull3(discoverySessions.buyerDescription) :
|
|
41750
|
+
parsed.data.buyerDescription == null ? isNull3(discoverySessions.buyerDescription) : eq39(discoverySessions.buyerDescription, parsed.data.buyerDescription),
|
|
41006
41751
|
// Locations are identity too: a different service-area subset seeds
|
|
41007
41752
|
// and probes a different geo, so it must never reuse another geo's
|
|
41008
41753
|
// session. resolveLocations is deterministic (project-config order),
|
|
@@ -41012,17 +41757,17 @@ async function discoveryRoutes(app, opts) {
|
|
|
41012
41757
|
// locations a NULL row's subset is unknowable, so it conservatively
|
|
41013
41758
|
// never reuses — a one-time, bounded (2h window) non-reuse after
|
|
41014
41759
|
// upgrade, never a wrong reuse.
|
|
41015
|
-
locations.length === 0 ?
|
|
41760
|
+
locations.length === 0 ? or8(isNull3(discoverySessions.locations), eq39(discoverySessions.locations, locations)) : eq39(discoverySessions.locations, locations),
|
|
41016
41761
|
// Seed provider set is identity: a different phrasing distribution
|
|
41017
41762
|
// must never reuse another set's session. Null (= Gemini-only
|
|
41018
41763
|
// default, including explicit ['gemini']) matches legacy rows.
|
|
41019
|
-
seedProviders == null ? isNull3(discoverySessions.seedProviders) :
|
|
41764
|
+
seedProviders == null ? isNull3(discoverySessions.seedProviders) : eq39(discoverySessions.seedProviders, seedProviders),
|
|
41020
41765
|
inArray13(discoverySessions.status, [
|
|
41021
41766
|
DiscoverySessionStatuses.queued,
|
|
41022
41767
|
DiscoverySessionStatuses.seeding,
|
|
41023
41768
|
DiscoverySessionStatuses.probing
|
|
41024
41769
|
]),
|
|
41025
|
-
|
|
41770
|
+
gte10(discoverySessions.createdAt, ageFloorIso)
|
|
41026
41771
|
)).orderBy(desc16(discoverySessions.createdAt)).get();
|
|
41027
41772
|
if (existing && existing.runId) {
|
|
41028
41773
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
@@ -41092,7 +41837,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41092
41837
|
const project = resolveProject(app.db, request.params.name);
|
|
41093
41838
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
41094
41839
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
41095
|
-
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();
|
|
41096
41841
|
return reply.send(rows.map(serializeSession));
|
|
41097
41842
|
}
|
|
41098
41843
|
);
|
|
@@ -41100,11 +41845,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
41100
41845
|
"/projects/:name/discover/sessions/:id",
|
|
41101
41846
|
async (request, reply) => {
|
|
41102
41847
|
const project = resolveProject(app.db, request.params.name);
|
|
41103
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
41848
|
+
const session = app.db.select().from(discoverySessions).where(eq39(discoverySessions.id, request.params.id)).get();
|
|
41104
41849
|
if (!session || session.projectId !== project.id) {
|
|
41105
41850
|
throw notFound("Discovery session", request.params.id);
|
|
41106
41851
|
}
|
|
41107
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
41852
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq39(discoveryProbes.sessionId, session.id)).all();
|
|
41108
41853
|
const detail = {
|
|
41109
41854
|
...serializeSession(session),
|
|
41110
41855
|
probes: probeRows.map(serializeProbe)
|
|
@@ -41116,7 +41861,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41116
41861
|
"/projects/:name/discover/sessions/:id/harvest",
|
|
41117
41862
|
async (request, reply) => {
|
|
41118
41863
|
const project = resolveProject(app.db, request.params.name);
|
|
41119
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
41864
|
+
const session = app.db.select().from(discoverySessions).where(eq39(discoverySessions.id, request.params.id)).get();
|
|
41120
41865
|
if (!session || session.projectId !== project.id) {
|
|
41121
41866
|
throw notFound("Discovery session", request.params.id);
|
|
41122
41867
|
}
|
|
@@ -41124,7 +41869,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41124
41869
|
const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
|
|
41125
41870
|
const applyAnchor = request.query.anchor !== "false";
|
|
41126
41871
|
const provider = session.seedProvider ?? "gemini";
|
|
41127
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
41872
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq39(discoveryProbes.sessionId, session.id)).all();
|
|
41128
41873
|
const extract = opts.harvestSearchQueries;
|
|
41129
41874
|
const probesWithQueries = probeRows.map((row) => {
|
|
41130
41875
|
if (!extract || !row.rawResponse) return { searchQueries: [] };
|
|
@@ -41135,7 +41880,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41135
41880
|
return { searchQueries: [] };
|
|
41136
41881
|
}
|
|
41137
41882
|
});
|
|
41138
|
-
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);
|
|
41139
41884
|
const anchorTerms = buildHarvestAnchorTerms(
|
|
41140
41885
|
[session.icpDescription ?? "", ...trackedQueries],
|
|
41141
41886
|
effectiveDomains(project)
|
|
@@ -41182,12 +41927,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
41182
41927
|
"/projects/:name/discover/sessions/:id/promote",
|
|
41183
41928
|
async (request, reply) => {
|
|
41184
41929
|
const project = resolveProject(app.db, request.params.name);
|
|
41185
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
41930
|
+
const session = app.db.select().from(discoverySessions).where(eq39(discoverySessions.id, request.params.id)).get();
|
|
41186
41931
|
if (!session || session.projectId !== project.id) {
|
|
41187
41932
|
throw notFound("Discovery session", request.params.id);
|
|
41188
41933
|
}
|
|
41189
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
41190
|
-
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());
|
|
41191
41936
|
const seenCompetitors = new Set(existingCompetitors);
|
|
41192
41937
|
const cited = /* @__PURE__ */ new Set();
|
|
41193
41938
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -41216,7 +41961,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41216
41961
|
);
|
|
41217
41962
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
41218
41963
|
const project = resolveProject(app.db, request.params.name);
|
|
41219
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
41964
|
+
const session = app.db.select().from(discoverySessions).where(eq39(discoverySessions.id, request.params.id)).get();
|
|
41220
41965
|
if (!session || session.projectId !== project.id) {
|
|
41221
41966
|
throw notFound("Discovery session", request.params.id);
|
|
41222
41967
|
}
|
|
@@ -41239,7 +41984,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41239
41984
|
const bucketSet = new Set(buckets);
|
|
41240
41985
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
41241
41986
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
41242
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
41987
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq39(discoveryProbes.sessionId, session.id)).all();
|
|
41243
41988
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
41244
41989
|
for (const probe of probeRows) {
|
|
41245
41990
|
if (!probe.bucket) continue;
|
|
@@ -41247,7 +41992,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41247
41992
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
41248
41993
|
}
|
|
41249
41994
|
const existingQueries = new Set(
|
|
41250
|
-
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())
|
|
41251
41996
|
);
|
|
41252
41997
|
const promotedQueries = [];
|
|
41253
41998
|
const skippedQueries = [];
|
|
@@ -41263,7 +42008,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
41263
42008
|
const skippedCompetitors = [];
|
|
41264
42009
|
if (includeCompetitors) {
|
|
41265
42010
|
const existingCompetitors = new Set(
|
|
41266
|
-
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())
|
|
41267
42012
|
);
|
|
41268
42013
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
41269
42014
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -41382,7 +42127,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
41382
42127
|
|
|
41383
42128
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
41384
42129
|
import crypto31 from "crypto";
|
|
41385
|
-
import { eq as
|
|
42130
|
+
import { eq as eq40 } from "drizzle-orm";
|
|
41386
42131
|
var DEFAULT_MAX_PROBES = 100;
|
|
41387
42132
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
41388
42133
|
function classifyProbeBucket(input) {
|
|
@@ -41469,7 +42214,7 @@ async function executeDiscovery(opts) {
|
|
|
41469
42214
|
status: DiscoverySessionStatuses.seeding,
|
|
41470
42215
|
dedupThreshold,
|
|
41471
42216
|
startedAt
|
|
41472
|
-
}).where(
|
|
42217
|
+
}).where(eq40(discoverySessions.id, opts.sessionId)).run();
|
|
41473
42218
|
const seedResult = await opts.deps.seed({
|
|
41474
42219
|
project: opts.project,
|
|
41475
42220
|
icpDescription: opts.icpDescription,
|
|
@@ -41530,7 +42275,7 @@ async function executeDiscovery(opts) {
|
|
|
41530
42275
|
dedupBandPairFraction: dedupStats.bandPairFraction,
|
|
41531
42276
|
dedupPairsTotal: dedupStats.pairsTotal,
|
|
41532
42277
|
warning
|
|
41533
|
-
}).where(
|
|
42278
|
+
}).where(eq40(discoverySessions.id, opts.sessionId)).run();
|
|
41534
42279
|
const probeLocation = opts.locations?.[0];
|
|
41535
42280
|
const probeResults = await mapWithConcurrency(
|
|
41536
42281
|
probedCanonicals,
|
|
@@ -41584,7 +42329,7 @@ async function executeDiscovery(opts) {
|
|
|
41584
42329
|
wastedCount: buckets["wasted-surface"],
|
|
41585
42330
|
competitorMap,
|
|
41586
42331
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
41587
|
-
}).where(
|
|
42332
|
+
}).where(eq40(discoverySessions.id, opts.sessionId)).run();
|
|
41588
42333
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
41589
42334
|
return {
|
|
41590
42335
|
buckets,
|
|
@@ -41624,7 +42369,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
41624
42369
|
status: DiscoverySessionStatuses.failed,
|
|
41625
42370
|
error,
|
|
41626
42371
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
41627
|
-
}).where(
|
|
42372
|
+
}).where(eq40(discoverySessions.id, sessionId)).run();
|
|
41628
42373
|
}
|
|
41629
42374
|
function dedupeStrings(input) {
|
|
41630
42375
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -41642,7 +42387,7 @@ function dedupeStrings(input) {
|
|
|
41642
42387
|
|
|
41643
42388
|
// ../api-routes/src/technical-aeo.ts
|
|
41644
42389
|
import crypto32 from "crypto";
|
|
41645
|
-
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";
|
|
41646
42391
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
41647
42392
|
function emptyScore(projectName) {
|
|
41648
42393
|
return {
|
|
@@ -41675,19 +42420,19 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
41675
42420
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
41676
42421
|
const project = resolveProject(app.db, request.params.name);
|
|
41677
42422
|
const baseFilters = [
|
|
41678
|
-
|
|
41679
|
-
|
|
42423
|
+
eq41(siteAuditSnapshots.projectId, project.id),
|
|
42424
|
+
eq41(runs.kind, RunKinds["site-audit"]),
|
|
41680
42425
|
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
41681
42426
|
notProbeRun()
|
|
41682
42427
|
];
|
|
41683
|
-
const targetFilters = request.query.runId ? [...baseFilters,
|
|
41684
|
-
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();
|
|
41685
42430
|
if (!latest) {
|
|
41686
42431
|
if (request.query.runId) throw notFound("Site audit run", request.query.runId);
|
|
41687
42432
|
return emptyScore(project.name);
|
|
41688
42433
|
}
|
|
41689
42434
|
const snap = latest.snap;
|
|
41690
|
-
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;
|
|
41691
42436
|
const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
|
|
41692
42437
|
const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
|
|
41693
42438
|
return {
|
|
@@ -41714,13 +42459,13 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
41714
42459
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
41715
42460
|
const project = resolveProject(app.db, request.params.name);
|
|
41716
42461
|
const targetFilters = [
|
|
41717
|
-
|
|
41718
|
-
|
|
42462
|
+
eq41(siteAuditSnapshots.projectId, project.id),
|
|
42463
|
+
eq41(runs.kind, RunKinds["site-audit"]),
|
|
41719
42464
|
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
41720
42465
|
notProbeRun()
|
|
41721
42466
|
];
|
|
41722
|
-
if (request.query.runId) targetFilters.push(
|
|
41723
|
-
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();
|
|
41724
42469
|
if (!latest && request.query.runId) {
|
|
41725
42470
|
throw notFound("Site audit run", request.query.runId);
|
|
41726
42471
|
}
|
|
@@ -41728,9 +42473,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
41728
42473
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
41729
42474
|
}
|
|
41730
42475
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
41731
|
-
const conds = [
|
|
41732
|
-
if (statusFilter) conds.push(
|
|
41733
|
-
const where =
|
|
42476
|
+
const conds = [eq41(siteAuditPages.runId, latest.runId)];
|
|
42477
|
+
if (statusFilter) conds.push(eq41(siteAuditPages.status, statusFilter));
|
|
42478
|
+
const where = and32(...conds);
|
|
41734
42479
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
41735
42480
|
const total = totalRow?.value ?? 0;
|
|
41736
42481
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
@@ -41754,9 +42499,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
41754
42499
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
41755
42500
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
41756
42501
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
41757
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
41758
|
-
|
|
41759
|
-
|
|
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"]),
|
|
41760
42505
|
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
41761
42506
|
notProbeRun()
|
|
41762
42507
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
@@ -41768,9 +42513,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
41768
42513
|
if (!parsed.success) {
|
|
41769
42514
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
41770
42515
|
}
|
|
41771
|
-
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
41772
|
-
|
|
41773
|
-
|
|
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"]),
|
|
41774
42519
|
inArray14(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
41775
42520
|
)).get();
|
|
41776
42521
|
if (existing) {
|
|
@@ -41796,7 +42541,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
41796
42541
|
|
|
41797
42542
|
// ../api-routes/src/research.ts
|
|
41798
42543
|
import crypto33 from "crypto";
|
|
41799
|
-
import { and as
|
|
42544
|
+
import { and as and33, desc as desc18, eq as eq42 } from "drizzle-orm";
|
|
41800
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;
|
|
41801
42546
|
async function researchRoutes(app, opts) {
|
|
41802
42547
|
app.post("/projects/:name/research/runs", async (request, reply) => {
|
|
@@ -41826,7 +42571,7 @@ async function researchRoutes(app, opts) {
|
|
|
41826
42571
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41827
42572
|
const decision = app.db.transaction((tx) => {
|
|
41828
42573
|
if (input.idempotencyKey) {
|
|
41829
|
-
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();
|
|
41830
42575
|
if (existing) {
|
|
41831
42576
|
if (existing.requestHash !== requestHash2) throw alreadyExists("Research idempotency key", input.idempotencyKey);
|
|
41832
42577
|
return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
|
|
@@ -41847,7 +42592,7 @@ async function researchRoutes(app, opts) {
|
|
|
41847
42592
|
const project = resolveProject(app.db, request.params.name);
|
|
41848
42593
|
const requested = Number.parseInt(request.query.limit ?? "", 10);
|
|
41849
42594
|
const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
|
|
41850
|
-
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);
|
|
41851
42596
|
return { runs: runs2 };
|
|
41852
42597
|
});
|
|
41853
42598
|
app.get("/projects/:name/research/runs/:runId", async (request) => {
|
|
@@ -41856,9 +42601,9 @@ async function researchRoutes(app, opts) {
|
|
|
41856
42601
|
});
|
|
41857
42602
|
}
|
|
41858
42603
|
function getDetail(app, projectId, id) {
|
|
41859
|
-
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();
|
|
41860
42605
|
if (!row) throw notFound("Research run", id);
|
|
41861
|
-
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);
|
|
41862
42607
|
return { ...serializeRun(row), queries: queries2 };
|
|
41863
42608
|
}
|
|
41864
42609
|
function serializeRun(row) {
|
|
@@ -42025,6 +42770,7 @@ async function apiRoutes(app, opts) {
|
|
|
42025
42770
|
googleConnectionStore: opts.googleConnectionStore,
|
|
42026
42771
|
getGoogleAuthConfig: opts.getGoogleAuthConfig
|
|
42027
42772
|
});
|
|
42773
|
+
await api.register(gaMeasurementAnalysisRoutes);
|
|
42028
42774
|
await api.register(trafficRoutes, {
|
|
42029
42775
|
cloudRunCredentialStore: opts.cloudRunCredentialStore,
|
|
42030
42776
|
pullCloudRunEvents: opts.pullCloudRunEvents,
|
|
@@ -42452,9 +43198,9 @@ var IntelligenceService = class {
|
|
|
42452
43198
|
*/
|
|
42453
43199
|
analyzeAndPersist(runId, projectId) {
|
|
42454
43200
|
const recentRuns = this.db.select().from(runs).where(
|
|
42455
|
-
|
|
42456
|
-
|
|
42457
|
-
|
|
43201
|
+
and34(
|
|
43202
|
+
eq43(runs.projectId, projectId),
|
|
43203
|
+
or9(eq43(runs.status, "completed"), eq43(runs.status, "partial")),
|
|
42458
43204
|
// Defensive: RunCoordinator already skips probes before this is
|
|
42459
43205
|
// called, but if a future call site invokes analyzeAndPersist
|
|
42460
43206
|
// directly for a probe, probes still must not pollute the
|
|
@@ -42536,7 +43282,7 @@ var IntelligenceService = class {
|
|
|
42536
43282
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
42537
43283
|
*/
|
|
42538
43284
|
analyzeAndPersistGbp(runId, projectId) {
|
|
42539
|
-
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();
|
|
42540
43286
|
if (!runRow) {
|
|
42541
43287
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
42542
43288
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -42544,11 +43290,11 @@ var IntelligenceService = class {
|
|
|
42544
43290
|
}
|
|
42545
43291
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
42546
43292
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
42547
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
42548
|
-
|
|
42549
|
-
|
|
42550
|
-
|
|
42551
|
-
|
|
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)
|
|
42552
43298
|
)).all();
|
|
42553
43299
|
if (selected.length === 0) {
|
|
42554
43300
|
log.info("gbp-intelligence.skip", { runId, reason: "no locations synced during run" });
|
|
@@ -42581,12 +43327,12 @@ var IntelligenceService = class {
|
|
|
42581
43327
|
}
|
|
42582
43328
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
42583
43329
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
42584
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
42585
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
42586
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
42587
|
-
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();
|
|
42588
43334
|
const descriptionMissing = !(ownerRow?.description ?? "").trim();
|
|
42589
|
-
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();
|
|
42590
43336
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
42591
43337
|
const summary = buildGbpSummary({
|
|
42592
43338
|
locationName,
|
|
@@ -42622,7 +43368,7 @@ var IntelligenceService = class {
|
|
|
42622
43368
|
/** Build the month-over-month keyword series for a location from the
|
|
42623
43369
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
42624
43370
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
42625
|
-
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();
|
|
42626
43372
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
42627
43373
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
42628
43374
|
const recentMonth = months[0] ?? null;
|
|
@@ -42653,7 +43399,7 @@ var IntelligenceService = class {
|
|
|
42653
43399
|
*/
|
|
42654
43400
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
42655
43401
|
const covered = new Set(coveredLocationNames);
|
|
42656
|
-
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();
|
|
42657
43403
|
const staleIds = [];
|
|
42658
43404
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
42659
43405
|
for (const row of existing) {
|
|
@@ -42664,7 +43410,7 @@ var IntelligenceService = class {
|
|
|
42664
43410
|
}
|
|
42665
43411
|
this.db.transaction((tx) => {
|
|
42666
43412
|
for (const id of staleIds) {
|
|
42667
|
-
tx.delete(insights).where(
|
|
43413
|
+
tx.delete(insights).where(eq43(insights.id, id)).run();
|
|
42668
43414
|
}
|
|
42669
43415
|
for (const insight of gbpInsights) {
|
|
42670
43416
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -42742,7 +43488,7 @@ var IntelligenceService = class {
|
|
|
42742
43488
|
* create per run + aggregate). DB is left untouched.
|
|
42743
43489
|
*/
|
|
42744
43490
|
backfill(projectName, opts, onProgress) {
|
|
42745
|
-
const project = this.db.select().from(projects).where(
|
|
43491
|
+
const project = this.db.select().from(projects).where(eq43(projects.name, projectName)).get();
|
|
42746
43492
|
if (!project) {
|
|
42747
43493
|
throw new Error(`Project "${projectName}" not found`);
|
|
42748
43494
|
}
|
|
@@ -42755,9 +43501,9 @@ var IntelligenceService = class {
|
|
|
42755
43501
|
sinceTimestamp = parsed;
|
|
42756
43502
|
}
|
|
42757
43503
|
const allRuns = this.db.select().from(runs).where(
|
|
42758
|
-
|
|
42759
|
-
|
|
42760
|
-
|
|
43504
|
+
and34(
|
|
43505
|
+
eq43(runs.projectId, project.id),
|
|
43506
|
+
or9(eq43(runs.status, "completed"), eq43(runs.status, "partial")),
|
|
42761
43507
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
42762
43508
|
ne6(runs.trigger, RunTriggers.probe)
|
|
42763
43509
|
)
|
|
@@ -42836,7 +43582,7 @@ var IntelligenceService = class {
|
|
|
42836
43582
|
return { processed, skipped, totalInsights };
|
|
42837
43583
|
}
|
|
42838
43584
|
loadTrackedCompetitors(projectId) {
|
|
42839
|
-
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);
|
|
42840
43586
|
}
|
|
42841
43587
|
/**
|
|
42842
43588
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -42857,15 +43603,15 @@ var IntelligenceService = class {
|
|
|
42857
43603
|
}
|
|
42858
43604
|
persistResult(result, runId, projectId) {
|
|
42859
43605
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
42860
|
-
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();
|
|
42861
43607
|
for (const row of existingInsights) {
|
|
42862
43608
|
if (row.dismissed) {
|
|
42863
43609
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
42864
43610
|
}
|
|
42865
43611
|
}
|
|
42866
43612
|
this.db.transaction((tx) => {
|
|
42867
|
-
tx.delete(insights).where(
|
|
42868
|
-
tx.delete(healthSnapshots).where(
|
|
43613
|
+
tx.delete(insights).where(eq43(insights.runId, runId)).run();
|
|
43614
|
+
tx.delete(healthSnapshots).where(eq43(healthSnapshots.runId, runId)).run();
|
|
42869
43615
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
42870
43616
|
for (const insight of result.insights) {
|
|
42871
43617
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -42918,24 +43664,24 @@ var IntelligenceService = class {
|
|
|
42918
43664
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
42919
43665
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
42920
43666
|
if (regressions.length === 0) return rawInsights;
|
|
42921
|
-
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();
|
|
42922
43668
|
const gscConnected = gscRows.length > 0;
|
|
42923
43669
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
42924
43670
|
for (const row of gscRows) {
|
|
42925
43671
|
const key = row.query.toLowerCase();
|
|
42926
43672
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
42927
43673
|
}
|
|
42928
|
-
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();
|
|
42929
43675
|
const locationCount = Math.max(
|
|
42930
43676
|
1,
|
|
42931
43677
|
(projectRow?.locations ?? []).length
|
|
42932
43678
|
);
|
|
42933
43679
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
42934
43680
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
42935
|
-
|
|
42936
|
-
|
|
42937
|
-
|
|
42938
|
-
|
|
43681
|
+
and34(
|
|
43682
|
+
eq43(runs.projectId, projectId),
|
|
43683
|
+
eq43(runs.kind, RunKinds["answer-visibility"]),
|
|
43684
|
+
or9(eq43(runs.status, "completed"), eq43(runs.status, "partial")),
|
|
42939
43685
|
// Defensive — see top of file.
|
|
42940
43686
|
ne6(runs.trigger, RunTriggers.probe)
|
|
42941
43687
|
)
|
|
@@ -42955,7 +43701,7 @@ var IntelligenceService = class {
|
|
|
42955
43701
|
const haveHistory = recentRunIds.length > 0;
|
|
42956
43702
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
42957
43703
|
if (haveHistory) {
|
|
42958
|
-
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();
|
|
42959
43705
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
42960
43706
|
for (const row of priorRows) {
|
|
42961
43707
|
if (!row.runId) continue;
|
|
@@ -42984,7 +43730,7 @@ var IntelligenceService = class {
|
|
|
42984
43730
|
});
|
|
42985
43731
|
}
|
|
42986
43732
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
42987
|
-
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();
|
|
42988
43734
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
42989
43735
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
42990
43736
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -43001,7 +43747,7 @@ var IntelligenceService = class {
|
|
|
43001
43747
|
citedDomains: querySnapshots.citedDomains,
|
|
43002
43748
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
43003
43749
|
snapshotLocation: querySnapshots.location
|
|
43004
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
43750
|
+
}).from(querySnapshots).leftJoin(queries, eq43(querySnapshots.queryId, queries.id)).where(eq43(querySnapshots.runId, runId)).all();
|
|
43005
43751
|
const snapshots = [];
|
|
43006
43752
|
let orphanCount = 0;
|
|
43007
43753
|
for (const r of rows) {
|