@canonry/canonry 4.141.0 → 4.142.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 +12 -5
- package/assets/assets/{AuditHistoryPanel-DlyLhzqq.js → AuditHistoryPanel-CfaF5B_e.js} +1 -1
- package/assets/assets/{BacklinksPage-DNxtwQBF.js → BacklinksPage-BIytWBpL.js} +1 -1
- package/assets/assets/{ChartPrimitives-BStLR_NC.js → ChartPrimitives-0tQT6GE7.js} +1 -1
- package/assets/assets/{HistoryPage-CIcCgoWK.js → HistoryPage-Bn6IH8WV.js} +1 -1
- package/assets/assets/{ProjectPage-BSI3Qum9.js → ProjectPage-DsQic9wX.js} +1 -1
- package/assets/assets/{RunRow-C0mFtLlc.js → RunRow-RHd8Bd52.js} +1 -1
- package/assets/assets/{RunsPage-C-yo0yoj.js → RunsPage-BsDqqQFX.js} +1 -1
- package/assets/assets/{SettingsPage-jg4MXOuo.js → SettingsPage-CX2LkFDB.js} +1 -1
- package/assets/assets/{TrafficPage-CnYHIg8t.js → TrafficPage-I19b7i9m.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-CEdyQT2P.js → TrafficSourceDetailPage-DOLEAPsc.js} +1 -1
- package/assets/assets/{arrow-left-DUv0imIl.js → arrow-left-BG9o86__.js} +1 -1
- package/assets/assets/{extract-error-message-CCOGDwlm.js → extract-error-message-D0cgM2XG.js} +1 -1
- package/assets/assets/{index-D6EP4dQs.js → index-CYGBt2f0.js} +73 -73
- package/assets/assets/{trash-2-1Yg9W-vs.js → trash-2-C_THWxO_.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-ZCFQGF3V.js → chunk-D7NJUGNQ.js} +10 -7
- package/dist/{chunk-OJOA425V.js → chunk-LS6SEZVF.js} +78 -3
- package/dist/{chunk-2LUSZFB6.js → chunk-NR6OGEV3.js} +207 -49
- package/dist/{chunk-HXXO2AF5.js → chunk-UFPP5CSN.js} +4 -4
- package/dist/cli.js +62 -50
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-4HQIUC7U.js → intelligence-service-EOUP37XN.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +7 -7
|
@@ -282,6 +282,7 @@ import {
|
|
|
282
282
|
orderLocationsDefaultFirst,
|
|
283
283
|
organicEvidenceDtoSchema,
|
|
284
284
|
organicEvidencePeriodSchema,
|
|
285
|
+
parseBoundedRate,
|
|
285
286
|
parseInclusiveEndMs,
|
|
286
287
|
parseReportPeriodDays,
|
|
287
288
|
parseRunError,
|
|
@@ -311,6 +312,7 @@ import {
|
|
|
311
312
|
researchRunDetailSchema,
|
|
312
313
|
researchRunListSchema,
|
|
313
314
|
resolveConfigSpecQueries,
|
|
315
|
+
resolveDateRange,
|
|
314
316
|
resolveLocations,
|
|
315
317
|
resolveSnapshotRequestQueries,
|
|
316
318
|
resultsExportDtoSchema,
|
|
@@ -380,7 +382,7 @@ import {
|
|
|
380
382
|
wordpressSchemaDeployResultDtoSchema,
|
|
381
383
|
wordpressSchemaStatusResultDtoSchema,
|
|
382
384
|
wordpressStatusDtoSchema
|
|
383
|
-
} from "./chunk-
|
|
385
|
+
} from "./chunk-LS6SEZVF.js";
|
|
384
386
|
|
|
385
387
|
// src/intelligence-service.ts
|
|
386
388
|
import { eq as eq45, desc as desc22, asc as asc9, and as and36, ne as ne6, or as or10, inArray as inArray15, gte as gte12, lte as lte8 } from "drizzle-orm";
|
|
@@ -951,6 +953,23 @@ var gaDailyTotals = sqliteTable("ga_daily_totals", {
|
|
|
951
953
|
date: text("date").notNull(),
|
|
952
954
|
sessions: integer("sessions").notNull().default(0),
|
|
953
955
|
users: integer("users").notNull().default(0),
|
|
956
|
+
/**
|
|
957
|
+
* GA4's `engagementRate` for the day (0-1), requested directly from the Data
|
|
958
|
+
* API. Nullable, with no default: every row written before the metric was
|
|
959
|
+
* added has no reading, and defaulting to 0 would report a real "nobody
|
|
960
|
+
* engaged" day for the whole pre-migration period.
|
|
961
|
+
*/
|
|
962
|
+
engagementRate: real("engagement_rate"),
|
|
963
|
+
/**
|
|
964
|
+
* GA4's `newUsers` for the day. Nullable: rows written before this column
|
|
965
|
+
* existed have no reading, and a 0 would read as a real "no new users" day.
|
|
966
|
+
*
|
|
967
|
+
* Stored on its own merit. It is NOT an input to a returning-users figure:
|
|
968
|
+
* a visitor can be first-seen and return inside the same range, so
|
|
969
|
+
* `users - newUsers` does not reconstruct one. That needs the
|
|
970
|
+
* `newVsReturning` dimension.
|
|
971
|
+
*/
|
|
972
|
+
newUsers: integer("new_users"),
|
|
954
973
|
syncedAt: text("synced_at").notNull(),
|
|
955
974
|
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" }),
|
|
956
975
|
createdAt: text("created_at").notNull()
|
|
@@ -4443,6 +4462,25 @@ var MIGRATION_VERSIONS = [
|
|
|
4443
4462
|
ON query_basket_versions(project_id, checksum)`,
|
|
4444
4463
|
`ALTER TABLE runs ADD COLUMN query_basket_revision INTEGER`
|
|
4445
4464
|
]
|
|
4465
|
+
},
|
|
4466
|
+
{
|
|
4467
|
+
// GA4 engagement metrics on the property-level daily series.
|
|
4468
|
+
//
|
|
4469
|
+
// `engagement_rate` and `new_users` are both real GA4 metrics, requested
|
|
4470
|
+
// directly. No returning-users column: GA4 exposes no such metric, and
|
|
4471
|
+
// `users - new_users` does not reconstruct one because a visitor can be
|
|
4472
|
+
// first-seen AND return inside the same range. That needs the
|
|
4473
|
+
// `newVsReturning` dimension, which changes the row shape of the sync.
|
|
4474
|
+
//
|
|
4475
|
+
// Both columns are NULLABLE with no default. Every row written before this
|
|
4476
|
+
// migration has no reading, and NOT NULL DEFAULT 0 would turn that absence
|
|
4477
|
+
// into a real "0% engaged" day.
|
|
4478
|
+
version: 116,
|
|
4479
|
+
name: "ga-daily-totals-engagement",
|
|
4480
|
+
statements: [
|
|
4481
|
+
`ALTER TABLE ga_daily_totals ADD COLUMN engagement_rate REAL`,
|
|
4482
|
+
`ALTER TABLE ga_daily_totals ADD COLUMN new_users INTEGER`
|
|
4483
|
+
]
|
|
4446
4484
|
}
|
|
4447
4485
|
];
|
|
4448
4486
|
function rebuildBacklinkTableWithSource(tx, table) {
|
|
@@ -15180,6 +15218,31 @@ function clickPeriods(periods, clicks, impressions) {
|
|
|
15180
15218
|
impressions: impressions[index2] ?? 0
|
|
15181
15219
|
}));
|
|
15182
15220
|
}
|
|
15221
|
+
function engagementPeriods(periods, rows) {
|
|
15222
|
+
return periods.map((period2) => {
|
|
15223
|
+
const inPeriod = rows.filter((row) => row.date >= period2.startDate && row.date <= period2.endDate);
|
|
15224
|
+
const rateDays = inPeriod.filter((row) => row.engagementRate !== null);
|
|
15225
|
+
const weight = rateDays.reduce((sum, row) => sum + row.sessions, 0);
|
|
15226
|
+
const weighted = rateDays.reduce((sum, row) => sum + row.engagementRate * row.sessions, 0);
|
|
15227
|
+
const splitDays = inPeriod.filter((row) => row.newUsers !== null);
|
|
15228
|
+
const dailyTotalUsers = splitDays.reduce((sum, row) => sum + row.users, 0);
|
|
15229
|
+
const dailyNewUsers = splitDays.reduce((sum, row) => sum + row.newUsers, 0);
|
|
15230
|
+
return {
|
|
15231
|
+
...period2,
|
|
15232
|
+
sessions: inPeriod.reduce((sum, row) => sum + row.sessions, 0),
|
|
15233
|
+
// A bucket whose reading-days had no sessions has no rate to report; the
|
|
15234
|
+
// weighted mean would be 0/0.
|
|
15235
|
+
engagementRate: rateDays.length > 0 && weight > 0 ? weighted / weight : null,
|
|
15236
|
+
dailyTotalUsers: splitDays.length > 0 ? dailyTotalUsers : null,
|
|
15237
|
+
dailyNewUsers: splitDays.length > 0 ? dailyNewUsers : null,
|
|
15238
|
+
// Null on a zero denominator too: a share of no users is not 0%.
|
|
15239
|
+
metricsAvailable: rateDays.length > 0 || splitDays.length > 0,
|
|
15240
|
+
daysInPeriod: inPeriod.length,
|
|
15241
|
+
daysWithEngagementRate: rateDays.length,
|
|
15242
|
+
daysWithUserSplit: splitDays.length
|
|
15243
|
+
};
|
|
15244
|
+
});
|
|
15245
|
+
}
|
|
15183
15246
|
function rankEntries(entries) {
|
|
15184
15247
|
const score = (values) => ({
|
|
15185
15248
|
latest: values.at(-1) ?? 0,
|
|
@@ -15301,6 +15364,23 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
|
|
|
15301
15364
|
const gaStartDate = gaPeriods[0]?.startDate;
|
|
15302
15365
|
const acquisitionRows = gaStartDate ? db.select().from(gaAcquisitionDaily).where(and14(eq18(gaAcquisitionDaily.projectId, project.id), gte5(gaAcquisitionDaily.date, gaStartDate), lte2(gaAcquisitionDaily.date, gaAnchor))).all() : [];
|
|
15303
15366
|
const leadRows = gaStartDate ? db.select().from(gaLeadEventsDaily).where(and14(eq18(gaLeadEventsDaily.projectId, project.id), gte5(gaLeadEventsDaily.date, gaStartDate), lte2(gaLeadEventsDaily.date, gaAnchor))).all() : [];
|
|
15367
|
+
const engagementAnchor = db.select({ date: sql9`max(${gaDailyTotals.date})` }).from(gaDailyTotals).where(eq18(gaDailyTotals.projectId, project.id)).get()?.date ?? null;
|
|
15368
|
+
const engagementPeriodWindow = engagementAnchor ? buildPeriods(engagementAnchor, days) : [];
|
|
15369
|
+
const engagementStartDate = engagementPeriodWindow[0]?.startDate;
|
|
15370
|
+
const engagementRows = engagementStartDate ? db.select().from(gaDailyTotals).where(and14(
|
|
15371
|
+
eq18(gaDailyTotals.projectId, project.id),
|
|
15372
|
+
gte5(gaDailyTotals.date, engagementStartDate),
|
|
15373
|
+
lte2(gaDailyTotals.date, engagementAnchor)
|
|
15374
|
+
)).all() : [];
|
|
15375
|
+
const engagementAvailableFrom = db.select({
|
|
15376
|
+
date: sql9`min(${gaDailyTotals.date})`
|
|
15377
|
+
}).from(gaDailyTotals).where(and14(
|
|
15378
|
+
eq18(gaDailyTotals.projectId, project.id),
|
|
15379
|
+
or4(
|
|
15380
|
+
sql9`${gaDailyTotals.engagementRate} is not null`,
|
|
15381
|
+
sql9`${gaDailyTotals.newUsers} is not null`
|
|
15382
|
+
)
|
|
15383
|
+
)).get()?.date ?? null;
|
|
15304
15384
|
const gscAnchor = db.select({ date: sql9`max(${gscDailyTotals.date})` }).from(gscDailyTotals).where(eq18(gscDailyTotals.projectId, project.id)).get()?.date ?? null;
|
|
15305
15385
|
const gscPeriods = gscAnchor ? buildPeriods(gscAnchor, days) : [];
|
|
15306
15386
|
const gscStartDate = gscPeriods[0]?.startDate;
|
|
@@ -15460,6 +15540,12 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
|
|
|
15460
15540
|
periods: eventPeriods(gaPeriods, values)
|
|
15461
15541
|
}))
|
|
15462
15542
|
},
|
|
15543
|
+
engagement: {
|
|
15544
|
+
status: engagementAnchor === null ? "unavailable" : "ready",
|
|
15545
|
+
availableFromDate: engagementAvailableFrom,
|
|
15546
|
+
latestDate: engagementAnchor,
|
|
15547
|
+
periods: engagementPeriods(engagementPeriodWindow, engagementRows)
|
|
15548
|
+
},
|
|
15463
15549
|
searchDemand: gscAnchor === null ? {
|
|
15464
15550
|
status: "unavailable",
|
|
15465
15551
|
periods: [],
|
|
@@ -18108,9 +18194,21 @@ var reportAudienceQueryParameter = {
|
|
|
18108
18194
|
var analyticsWindowParameter = {
|
|
18109
18195
|
name: "window",
|
|
18110
18196
|
in: "query",
|
|
18111
|
-
description: "Time window for analytics queries.",
|
|
18197
|
+
description: "Time window for analytics queries. An unrecognised value is rejected with 400; it is never widened to the full history.",
|
|
18112
18198
|
schema: { type: "string", enum: ["7d", "30d", "90d", "all"] }
|
|
18113
18199
|
};
|
|
18200
|
+
var analyticsStartDateParameter = {
|
|
18201
|
+
name: "startDate",
|
|
18202
|
+
in: "query",
|
|
18203
|
+
description: 'Inclusive lower bound as a calendar date (YYYY-MM-DD). Takes precedence over "window", which is rolling from now and cannot name a calendar month.',
|
|
18204
|
+
schema: stringSchema
|
|
18205
|
+
};
|
|
18206
|
+
var analyticsEndDateParameter = {
|
|
18207
|
+
name: "endDate",
|
|
18208
|
+
in: "query",
|
|
18209
|
+
description: "Inclusive upper bound as a calendar date (YYYY-MM-DD).",
|
|
18210
|
+
schema: stringSchema
|
|
18211
|
+
};
|
|
18114
18212
|
var organicEvidencePeriodQueryParameter = {
|
|
18115
18213
|
name: "period",
|
|
18116
18214
|
in: "query",
|
|
@@ -21564,7 +21662,7 @@ var routeCatalog = [
|
|
|
21564
21662
|
path: "/api/v1/projects/{name}/ga/traffic",
|
|
21565
21663
|
summary: "Get GA4 landing page traffic, channel breakdown, and AI referral landing pages",
|
|
21566
21664
|
tags: ["ga4"],
|
|
21567
|
-
parameters: [nameParameter, limitQueryParameter, analyticsWindowParameter],
|
|
21665
|
+
parameters: [nameParameter, limitQueryParameter, analyticsWindowParameter, analyticsStartDateParameter, analyticsEndDateParameter],
|
|
21568
21666
|
responses: {
|
|
21569
21667
|
// TODO: Add `GaTrafficResponse` Zod schema in contracts.
|
|
21570
21668
|
200: rawJsonResponse("GA4 traffic data returned.", looseObjectSchema),
|
|
@@ -21578,7 +21676,7 @@ var routeCatalog = [
|
|
|
21578
21676
|
summary: "Get raw AI referral detail rows per day, landing page, and attribution dimension",
|
|
21579
21677
|
description: "Detail rows, not totals. One row per landing page per attribution dimension, so a single day of one source is many rows and each is commonly worth one session. Use /ga/ai-referral-daily for per-date or per-source session counts.",
|
|
21580
21678
|
tags: ["ga4"],
|
|
21581
|
-
parameters: [nameParameter, analyticsWindowParameter],
|
|
21679
|
+
parameters: [nameParameter, analyticsWindowParameter, analyticsStartDateParameter, analyticsEndDateParameter],
|
|
21582
21680
|
responses: {
|
|
21583
21681
|
200: jsonArrayResponse("AI referral history returned.", "GA4AiReferralHistoryEntry"),
|
|
21584
21682
|
400: errorResponse("GA4 is not connected."),
|
|
@@ -21591,7 +21689,7 @@ var routeCatalog = [
|
|
|
21591
21689
|
summary: "Get AI referral sessions per day and per source",
|
|
21592
21690
|
description: "Sums landing pages within one attribution dimension and never across dimensions, so totalSessions equals the aiSessionsDeduped reported by /ga/traffic for the same window. Sessions only: GA counts users as a distinct count at the grain requested, so an AI-referral user total cannot be summed from these rows and no un-dimensioned AI-referral fetch exists to supply one.",
|
|
21593
21691
|
tags: ["ga4"],
|
|
21594
|
-
parameters: [nameParameter, analyticsWindowParameter],
|
|
21692
|
+
parameters: [nameParameter, analyticsWindowParameter, analyticsStartDateParameter, analyticsEndDateParameter],
|
|
21595
21693
|
responses: {
|
|
21596
21694
|
200: jsonResponse("AI referral daily series returned.", "GA4AiReferralDailyDto"),
|
|
21597
21695
|
400: errorResponse("GA4 is not connected."),
|
|
@@ -21603,7 +21701,7 @@ var routeCatalog = [
|
|
|
21603
21701
|
path: "/api/v1/projects/{name}/ga/social-referral-history",
|
|
21604
21702
|
summary: "Get social media referral sessions per day grouped by source",
|
|
21605
21703
|
tags: ["ga4"],
|
|
21606
|
-
parameters: [nameParameter, analyticsWindowParameter],
|
|
21704
|
+
parameters: [nameParameter, analyticsWindowParameter, analyticsStartDateParameter, analyticsEndDateParameter],
|
|
21607
21705
|
responses: {
|
|
21608
21706
|
200: jsonArrayResponse("Social referral history returned.", "GA4SocialReferralHistoryEntry"),
|
|
21609
21707
|
400: errorResponse("GA4 is not connected."),
|
|
@@ -21641,7 +21739,7 @@ var routeCatalog = [
|
|
|
21641
21739
|
path: "/api/v1/projects/{name}/ga/session-history",
|
|
21642
21740
|
summary: "Get total sessions per day for the project",
|
|
21643
21741
|
tags: ["ga4"],
|
|
21644
|
-
parameters: [nameParameter, analyticsWindowParameter],
|
|
21742
|
+
parameters: [nameParameter, analyticsWindowParameter, analyticsStartDateParameter, analyticsEndDateParameter],
|
|
21645
21743
|
responses: {
|
|
21646
21744
|
200: jsonArrayResponse("Session history returned.", "GA4SessionHistoryEntry"),
|
|
21647
21745
|
400: errorResponse("GA4 is not connected."),
|
|
@@ -24233,7 +24331,17 @@ var GA4_DIMENSIONS = {
|
|
|
24233
24331
|
var GA4_METRICS = {
|
|
24234
24332
|
sessions: "sessions",
|
|
24235
24333
|
totalUsers: "totalUsers",
|
|
24236
|
-
eventCount: "eventCount"
|
|
24334
|
+
eventCount: "eventCount",
|
|
24335
|
+
/** Share of sessions that were engaged. A real GA4 metric — request it directly. */
|
|
24336
|
+
engagementRate: "engagementRate",
|
|
24337
|
+
/**
|
|
24338
|
+
* First-time users, a real GA4 metric requested directly.
|
|
24339
|
+
*
|
|
24340
|
+
* Not an input to a returning-users figure: a visitor can be first-seen and
|
|
24341
|
+
* return inside the same range, so subtracting this from totalUsers does not
|
|
24342
|
+
* reconstruct one.
|
|
24343
|
+
*/
|
|
24344
|
+
newUsers: "newUsers"
|
|
24237
24345
|
};
|
|
24238
24346
|
|
|
24239
24347
|
// ../integration-google-analytics/src/types.ts
|
|
@@ -24781,6 +24889,15 @@ async function fetchAggregateSummary(accessToken, propertyId, days) {
|
|
|
24781
24889
|
ga4Log("info", "fetch-aggregate.done", { propertyId, ...summary });
|
|
24782
24890
|
return summary;
|
|
24783
24891
|
}
|
|
24892
|
+
function parseOptionalMetric(value) {
|
|
24893
|
+
if (value === void 0 || value.trim() === "") return null;
|
|
24894
|
+
const parsed = Number(value);
|
|
24895
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
24896
|
+
}
|
|
24897
|
+
function parseOptionalCountMetric(value) {
|
|
24898
|
+
const parsed = parseOptionalMetric(value);
|
|
24899
|
+
return parsed === null ? null : Math.round(parsed);
|
|
24900
|
+
}
|
|
24784
24901
|
async function fetchDailyTotals(accessToken, propertyId, days) {
|
|
24785
24902
|
validateAccessToken2(accessToken);
|
|
24786
24903
|
validatePropertyId(propertyId);
|
|
@@ -24792,14 +24909,25 @@ async function fetchDailyTotals(accessToken, propertyId, days) {
|
|
|
24792
24909
|
const res = await runReport(accessToken, propertyId, {
|
|
24793
24910
|
dateRanges: [{ startDate: formatDate2(startDate), endDate: formatDate2(endDate) }],
|
|
24794
24911
|
dimensions: [{ name: GA4_DIMENSIONS.date }],
|
|
24795
|
-
metrics: [
|
|
24912
|
+
metrics: [
|
|
24913
|
+
{ name: GA4_METRICS.sessions },
|
|
24914
|
+
{ name: GA4_METRICS.totalUsers },
|
|
24915
|
+
{ name: GA4_METRICS.engagementRate },
|
|
24916
|
+
{ name: GA4_METRICS.newUsers }
|
|
24917
|
+
],
|
|
24796
24918
|
limit: syncDays + 1
|
|
24797
24919
|
});
|
|
24798
|
-
const rows = (res.rows ?? []).map((row) =>
|
|
24799
|
-
|
|
24800
|
-
|
|
24801
|
-
|
|
24802
|
-
|
|
24920
|
+
const rows = (res.rows ?? []).map((row) => {
|
|
24921
|
+
const users = parseInt(row.metricValues[1]?.value ?? "0", 10) || 0;
|
|
24922
|
+
const newUsers = parseOptionalCountMetric(row.metricValues[3]?.value);
|
|
24923
|
+
return {
|
|
24924
|
+
date: compactDateToIso(row.dimensionValues[0]?.value ?? ""),
|
|
24925
|
+
sessions: parseInt(row.metricValues[0]?.value ?? "0", 10) || 0,
|
|
24926
|
+
users,
|
|
24927
|
+
engagementRate: parseBoundedRate(parseOptionalMetric(row.metricValues[2]?.value)),
|
|
24928
|
+
newUsers
|
|
24929
|
+
};
|
|
24930
|
+
}).filter((row) => row.date.length > 0);
|
|
24803
24931
|
ga4Log("info", "fetch-daily-totals.done", { propertyId, days: syncDays, rows: rows.length });
|
|
24804
24932
|
return rows;
|
|
24805
24933
|
}
|
|
@@ -32699,6 +32827,12 @@ function formatSharePct(numerator, total) {
|
|
|
32699
32827
|
if (rounded === 0) return "<1%";
|
|
32700
32828
|
return `${rounded}%`;
|
|
32701
32829
|
}
|
|
32830
|
+
function dateRangeConditions(column, range) {
|
|
32831
|
+
const conditions = [];
|
|
32832
|
+
if (range.startDate) conditions.push(sql15`${column} >= ${range.startDate}`);
|
|
32833
|
+
if (range.endDate) conditions.push(sql15`${column} <= ${range.endDate}`);
|
|
32834
|
+
return conditions;
|
|
32835
|
+
}
|
|
32702
32836
|
var SOCIAL_CHANNEL_GROUPS2 = /* @__PURE__ */ new Set(["Organic Social", "Paid Social"]);
|
|
32703
32837
|
function buildChannelBreakdown(input) {
|
|
32704
32838
|
const aiSessions = [...input.aiSessionsByChannelGroup.values()].reduce((sum, sessions) => sum + sessions, 0);
|
|
@@ -33155,6 +33289,12 @@ async function ga4Routes(app, opts) {
|
|
|
33155
33289
|
date: row.date,
|
|
33156
33290
|
sessions: row.sessions,
|
|
33157
33291
|
users: row.users,
|
|
33292
|
+
// Stored as given, nulls included: an absent reading is not a 0%
|
|
33293
|
+
// engagement day. No returning-users figure is derived here —
|
|
33294
|
+
// it is derived from `users - newUsers` at read time so the stored
|
|
33295
|
+
// row keeps only what GA4 actually reported.
|
|
33296
|
+
engagementRate: row.engagementRate,
|
|
33297
|
+
newUsers: row.newUsers,
|
|
33158
33298
|
syncedAt: now,
|
|
33159
33299
|
syncRunId: runId,
|
|
33160
33300
|
createdAt: now
|
|
@@ -33354,16 +33494,12 @@ async function ga4Routes(app, opts) {
|
|
|
33354
33494
|
const project = resolveProject(app.db, request.params.name);
|
|
33355
33495
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
33356
33496
|
const limit = Math.max(1, Math.min(parseInt(request.query.limit ?? "50", 10) || 50, 500));
|
|
33357
|
-
const
|
|
33358
|
-
const
|
|
33359
|
-
const
|
|
33360
|
-
const
|
|
33361
|
-
|
|
33362
|
-
const
|
|
33363
|
-
if (cutoffDate) aiConditions.push(sql15`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
33364
|
-
const socialConditions = [eq32(gaSocialReferrals.projectId, project.id)];
|
|
33365
|
-
if (cutoffDate) socialConditions.push(sql15`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
33366
|
-
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
33497
|
+
const range = resolveDateRange(request.query);
|
|
33498
|
+
const dateFiltered = range.startDate !== null || range.endDate !== null;
|
|
33499
|
+
const snapshotConditions = [eq32(gaTrafficSnapshots.projectId, project.id), ...dateRangeConditions(gaTrafficSnapshots.date, range)];
|
|
33500
|
+
const aiConditions = [eq32(gaAiReferrals.projectId, project.id), ...dateRangeConditions(gaAiReferrals.date, range)];
|
|
33501
|
+
const socialConditions = [eq32(gaSocialReferrals.projectId, project.id), ...dateRangeConditions(gaSocialReferrals.date, range)];
|
|
33502
|
+
const windowSummaryRow = range.startDate && !range.explicitDates ? app.db.select({
|
|
33367
33503
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
33368
33504
|
totalOrganicSessions: gaTrafficWindowSummaries.totalOrganicSessions,
|
|
33369
33505
|
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
@@ -33371,15 +33507,26 @@ async function ga4Routes(app, opts) {
|
|
|
33371
33507
|
}).from(gaTrafficWindowSummaries).where(
|
|
33372
33508
|
and26(
|
|
33373
33509
|
eq32(gaTrafficWindowSummaries.projectId, project.id),
|
|
33374
|
-
eq32(gaTrafficWindowSummaries.windowKey, window)
|
|
33510
|
+
eq32(gaTrafficWindowSummaries.windowKey, range.window)
|
|
33375
33511
|
)
|
|
33376
33512
|
).get() : null;
|
|
33377
|
-
const snapshotTotalsRow =
|
|
33378
|
-
|
|
33379
|
-
|
|
33380
|
-
|
|
33381
|
-
|
|
33382
|
-
|
|
33513
|
+
const snapshotTotalsRow = dateFiltered && !windowSummaryRow ? (() => {
|
|
33514
|
+
const summed = app.db.select({
|
|
33515
|
+
totalSessions: sql15`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
33516
|
+
totalOrganicSessions: sql15`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
33517
|
+
totalUsers: sql15`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
33518
|
+
}).from(gaTrafficSnapshots).where(and26(...snapshotConditions)).get();
|
|
33519
|
+
return {
|
|
33520
|
+
totalSessions: summed?.totalSessions ?? 0,
|
|
33521
|
+
totalOrganicSessions: summed?.totalOrganicSessions ?? 0,
|
|
33522
|
+
// Unavailable for an EXPLICIT calendar range. The rolling-window
|
|
33523
|
+
// fallback keeps its historical summed value: it is wrong for the
|
|
33524
|
+
// same reason, but it predates this branch and correcting it is a
|
|
33525
|
+
// separate, visible behaviour change rather than one bundled here.
|
|
33526
|
+
totalUsers: range.explicitDates ? null : summed?.totalUsers ?? 0
|
|
33527
|
+
};
|
|
33528
|
+
})() : null;
|
|
33529
|
+
const summaryRow = dateFiltered ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
33383
33530
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
33384
33531
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
33385
33532
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
@@ -33464,7 +33611,11 @@ async function ga4Routes(app, opts) {
|
|
|
33464
33611
|
totalSessions: total,
|
|
33465
33612
|
totalOrganicSessions,
|
|
33466
33613
|
totalDirectSessions,
|
|
33467
|
-
|
|
33614
|
+
// `null` (not 0) when the range has no un-dimensioned aggregate behind
|
|
33615
|
+
// it. GA counts users as a COUNT DISTINCT at the grain requested, so the
|
|
33616
|
+
// landing-page dimensioned sum is inflated, and a 0 would read as
|
|
33617
|
+
// "nobody visited" rather than "not measurable for this range".
|
|
33618
|
+
totalUsers: summaryRow ? summaryRow.totalUsers : 0,
|
|
33468
33619
|
topPages: rows.map((r) => ({
|
|
33469
33620
|
landingPage: r.landingPage,
|
|
33470
33621
|
sessions: r.sessions ?? 0,
|
|
@@ -33531,21 +33682,32 @@ async function ga4Routes(app, opts) {
|
|
|
33531
33682
|
directSharePctDisplay: formatSharePct(totalDirectSessions, total),
|
|
33532
33683
|
socialSharePctDisplay: formatSharePct(socialSessions, total),
|
|
33533
33684
|
lastSyncedAt: latestSync?.syncedAt ?? null,
|
|
33685
|
+
// Report the range that was actually measured, so a caller can tell which
|
|
33686
|
+
// period the totals above belong to.
|
|
33687
|
+
// An EXPLICIT calendar range is reported back verbatim, even when it
|
|
33688
|
+
// covers nothing. Substituting the synced period there labelled an empty
|
|
33689
|
+
// future range with real past dates, so the numbers said "no data" while
|
|
33690
|
+
// the labels named a period that did have data.
|
|
33691
|
+
//
|
|
33692
|
+
// A rolling window still clamps: "last 30 days" is a relative ask, and a
|
|
33693
|
+
// cutoff computed from today can legitimately land after the last synced
|
|
33694
|
+
// date on a stale project. Reporting a start after the end would be its
|
|
33695
|
+
// own nonsense.
|
|
33534
33696
|
periodStart: (() => {
|
|
33535
|
-
|
|
33536
|
-
const
|
|
33697
|
+
if (range.explicitDates) return range.startDate ?? summaryMeta?.periodStart ?? null;
|
|
33698
|
+
const start = range.startDate ?? summaryMeta?.periodStart ?? null;
|
|
33699
|
+
const end = range.endDate ?? summaryMeta?.periodEnd ?? null;
|
|
33537
33700
|
if (start && end && start > end) return summaryMeta?.periodStart ?? null;
|
|
33538
33701
|
return start;
|
|
33539
33702
|
})(),
|
|
33540
|
-
periodEnd: summaryMeta?.periodEnd ?? null
|
|
33703
|
+
periodEnd: range.endDate ?? summaryMeta?.periodEnd ?? null
|
|
33541
33704
|
};
|
|
33542
33705
|
});
|
|
33543
33706
|
app.get("/projects/:name/ga/ai-referral-history", async (request, _reply) => {
|
|
33544
33707
|
const project = resolveProject(app.db, request.params.name);
|
|
33545
33708
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
33546
|
-
const
|
|
33547
|
-
const conditions = [eq32(gaAiReferrals.projectId, project.id)];
|
|
33548
|
-
if (cutoffDate) conditions.push(sql15`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
33709
|
+
const range = resolveDateRange(request.query);
|
|
33710
|
+
const conditions = [eq32(gaAiReferrals.projectId, project.id), ...dateRangeConditions(gaAiReferrals.date, range)];
|
|
33549
33711
|
const rows = app.db.select({
|
|
33550
33712
|
date: gaAiReferrals.date,
|
|
33551
33713
|
source: gaAiReferrals.source,
|
|
@@ -33568,9 +33730,8 @@ async function ga4Routes(app, opts) {
|
|
|
33568
33730
|
app.get("/projects/:name/ga/ai-referral-daily", async (request, _reply) => {
|
|
33569
33731
|
const project = resolveProject(app.db, request.params.name);
|
|
33570
33732
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
33571
|
-
const
|
|
33572
|
-
const conditions = [eq32(gaAiReferrals.projectId, project.id)];
|
|
33573
|
-
if (cutoffDate) conditions.push(sql15`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
33733
|
+
const range = resolveDateRange(request.query);
|
|
33734
|
+
const conditions = [eq32(gaAiReferrals.projectId, project.id), ...dateRangeConditions(gaAiReferrals.date, range)];
|
|
33574
33735
|
const rows = app.db.select({
|
|
33575
33736
|
date: gaAiReferrals.date,
|
|
33576
33737
|
source: gaAiReferrals.source,
|
|
@@ -33585,9 +33746,8 @@ async function ga4Routes(app, opts) {
|
|
|
33585
33746
|
app.get("/projects/:name/ga/social-referral-history", async (request, _reply) => {
|
|
33586
33747
|
const project = resolveProject(app.db, request.params.name);
|
|
33587
33748
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
33588
|
-
const
|
|
33589
|
-
const conditions = [eq32(gaSocialReferrals.projectId, project.id)];
|
|
33590
|
-
if (cutoffDate) conditions.push(sql15`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
33749
|
+
const range = resolveDateRange(request.query);
|
|
33750
|
+
const conditions = [eq32(gaSocialReferrals.projectId, project.id), ...dateRangeConditions(gaSocialReferrals.date, range)];
|
|
33591
33751
|
const rows = app.db.select({
|
|
33592
33752
|
date: gaSocialReferrals.date,
|
|
33593
33753
|
source: gaSocialReferrals.source,
|
|
@@ -33730,17 +33890,15 @@ async function ga4Routes(app, opts) {
|
|
|
33730
33890
|
app.get("/projects/:name/ga/session-history", async (request, _reply) => {
|
|
33731
33891
|
const project = resolveProject(app.db, request.params.name);
|
|
33732
33892
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
33733
|
-
const
|
|
33734
|
-
const conditions = [eq32(gaTrafficSnapshots.projectId, project.id)];
|
|
33735
|
-
if (cutoffDate) conditions.push(sql15`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
33893
|
+
const range = resolveDateRange(request.query);
|
|
33894
|
+
const conditions = [eq32(gaTrafficSnapshots.projectId, project.id), ...dateRangeConditions(gaTrafficSnapshots.date, range)];
|
|
33736
33895
|
const rows = app.db.select({
|
|
33737
33896
|
date: gaTrafficSnapshots.date,
|
|
33738
33897
|
sessions: sql15`SUM(${gaTrafficSnapshots.sessions})`,
|
|
33739
33898
|
organicSessions: sql15`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
33740
33899
|
users: sql15`SUM(${gaTrafficSnapshots.users})`
|
|
33741
33900
|
}).from(gaTrafficSnapshots).where(and26(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
33742
|
-
const totalConditions = [eq32(gaDailyTotals.projectId, project.id)];
|
|
33743
|
-
if (cutoffDate) totalConditions.push(sql15`${gaDailyTotals.date} >= ${cutoffDate}`);
|
|
33901
|
+
const totalConditions = [eq32(gaDailyTotals.projectId, project.id), ...dateRangeConditions(gaDailyTotals.date, range)];
|
|
33744
33902
|
const totals = app.db.select({ date: gaDailyTotals.date, users: gaDailyTotals.users }).from(gaDailyTotals).where(and26(...totalConditions)).all();
|
|
33745
33903
|
return buildSessionHistory(
|
|
33746
33904
|
rows.map((r) => ({
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
loadConfig,
|
|
11
11
|
loadConfigRaw,
|
|
12
12
|
saveConfigPatch
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-D7NJUGNQ.js";
|
|
14
14
|
import {
|
|
15
15
|
CC_CACHE_DIR,
|
|
16
16
|
DUCKDB_SPEC,
|
|
@@ -123,7 +123,7 @@ import {
|
|
|
123
123
|
siteAuditSnapshots,
|
|
124
124
|
toAlertView,
|
|
125
125
|
usageCounters
|
|
126
|
-
} from "./chunk-
|
|
126
|
+
} from "./chunk-NR6OGEV3.js";
|
|
127
127
|
import {
|
|
128
128
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
129
129
|
AGENT_PROVIDER_IDS,
|
|
@@ -210,7 +210,7 @@ import {
|
|
|
210
210
|
validationError,
|
|
211
211
|
winnabilityClassLabel,
|
|
212
212
|
withRetry
|
|
213
|
-
} from "./chunk-
|
|
213
|
+
} from "./chunk-LS6SEZVF.js";
|
|
214
214
|
|
|
215
215
|
// src/telemetry.ts
|
|
216
216
|
import crypto from "crypto";
|
|
@@ -7178,7 +7178,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
7178
7178
|
return result;
|
|
7179
7179
|
}
|
|
7180
7180
|
async function backfillInsightsCommand(project, opts) {
|
|
7181
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
7181
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-EOUP37XN.js");
|
|
7182
7182
|
const config = loadConfig();
|
|
7183
7183
|
const db = createClient(config.database);
|
|
7184
7184
|
migrate(db);
|