@canonry/canonry 4.139.1 → 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 +33 -5
- package/assets/agent-workspace/skills/canonry/references/indexing.md +2 -1
- package/assets/assets/{AuditHistoryPanel-FiMbN4_a.js → AuditHistoryPanel-CfaF5B_e.js} +1 -1
- package/assets/assets/{BacklinksPage-CLRIsKHN.js → BacklinksPage-BIytWBpL.js} +1 -1
- package/assets/assets/{ChartPrimitives-BuTqG5NR.js → ChartPrimitives-0tQT6GE7.js} +1 -1
- package/assets/assets/{HistoryPage-BWRbsFAT.js → HistoryPage-Bn6IH8WV.js} +1 -1
- package/assets/assets/{ProjectPage-DQ2kIj-L.js → ProjectPage-DsQic9wX.js} +8 -8
- package/assets/assets/{RunRow-BZobaeDq.js → RunRow-RHd8Bd52.js} +1 -1
- package/assets/assets/{RunsPage-DhrY4Ouc.js → RunsPage-BsDqqQFX.js} +1 -1
- package/assets/assets/{SettingsPage-C6VcJEgB.js → SettingsPage-CX2LkFDB.js} +1 -1
- package/assets/assets/{TrafficPage-KqYl5HUH.js → TrafficPage-I19b7i9m.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-mMJi-QZr.js → TrafficSourceDetailPage-DOLEAPsc.js} +1 -1
- package/assets/assets/{arrow-left-CpLow2Kp.js → arrow-left-BG9o86__.js} +1 -1
- package/assets/assets/{extract-error-message-dHq11Y4U.js → extract-error-message-D0cgM2XG.js} +1 -1
- package/assets/assets/index-BDo9nl4O.css +1 -0
- package/assets/assets/index-CYGBt2f0.js +213 -0
- package/assets/assets/{trash-2-CnEPNBqu.js → trash-2-C_THWxO_.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-IYTHIAAE.js → chunk-D7NJUGNQ.js} +55 -10
- package/dist/{chunk-OA2EGCUS.js → chunk-LS6SEZVF.js} +120 -4
- package/dist/{chunk-DJHSOPMU.js → chunk-NR6OGEV3.js} +326 -67
- package/dist/{chunk-YLHZQ7HQ.js → chunk-UFPP5CSN.js} +16 -5
- package/dist/cli.js +203 -63
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-BGH2MGHB.js → intelligence-service-EOUP37XN.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +6 -6
- package/assets/assets/index-CntuM6Th.css +0 -1
- package/assets/assets/index-iRW7X23W.js +0 -213
|
@@ -240,11 +240,13 @@ import {
|
|
|
240
240
|
gscDeindexedRowSchema,
|
|
241
241
|
gscDiscoverSitemapsResponseDtoSchema,
|
|
242
242
|
gscPerformanceDailyDtoSchema,
|
|
243
|
-
|
|
243
|
+
gscPerformanceOrderBySchema,
|
|
244
|
+
gscPerformanceResponseDtoSchema,
|
|
244
245
|
gscSiteListResponseDtoSchema,
|
|
245
246
|
gscSitemapListResponseDtoSchema,
|
|
246
247
|
gscSubmitSitemapsRequestDtoSchema,
|
|
247
248
|
gscSubmitSitemapsResponseDtoSchema,
|
|
249
|
+
gscTopPagesDtoSchema,
|
|
248
250
|
gscUrlInspectionDtoSchema,
|
|
249
251
|
hasLocationLabel,
|
|
250
252
|
healthSnapshotDtoSchema,
|
|
@@ -280,6 +282,7 @@ import {
|
|
|
280
282
|
orderLocationsDefaultFirst,
|
|
281
283
|
organicEvidenceDtoSchema,
|
|
282
284
|
organicEvidencePeriodSchema,
|
|
285
|
+
parseBoundedRate,
|
|
283
286
|
parseInclusiveEndMs,
|
|
284
287
|
parseReportPeriodDays,
|
|
285
288
|
parseRunError,
|
|
@@ -309,6 +312,7 @@ import {
|
|
|
309
312
|
researchRunDetailSchema,
|
|
310
313
|
researchRunListSchema,
|
|
311
314
|
resolveConfigSpecQueries,
|
|
315
|
+
resolveDateRange,
|
|
312
316
|
resolveLocations,
|
|
313
317
|
resolveSnapshotRequestQueries,
|
|
314
318
|
resultsExportDtoSchema,
|
|
@@ -378,7 +382,7 @@ import {
|
|
|
378
382
|
wordpressSchemaDeployResultDtoSchema,
|
|
379
383
|
wordpressSchemaStatusResultDtoSchema,
|
|
380
384
|
wordpressStatusDtoSchema
|
|
381
|
-
} from "./chunk-
|
|
385
|
+
} from "./chunk-LS6SEZVF.js";
|
|
382
386
|
|
|
383
387
|
// src/intelligence-service.ts
|
|
384
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";
|
|
@@ -949,6 +953,23 @@ var gaDailyTotals = sqliteTable("ga_daily_totals", {
|
|
|
949
953
|
date: text("date").notNull(),
|
|
950
954
|
sessions: integer("sessions").notNull().default(0),
|
|
951
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"),
|
|
952
973
|
syncedAt: text("synced_at").notNull(),
|
|
953
974
|
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" }),
|
|
954
975
|
createdAt: text("created_at").notNull()
|
|
@@ -4441,6 +4462,25 @@ var MIGRATION_VERSIONS = [
|
|
|
4441
4462
|
ON query_basket_versions(project_id, checksum)`,
|
|
4442
4463
|
`ALTER TABLE runs ADD COLUMN query_basket_revision INTEGER`
|
|
4443
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
|
+
]
|
|
4444
4484
|
}
|
|
4445
4485
|
];
|
|
4446
4486
|
function rebuildBacklinkTableWithSource(tx, table) {
|
|
@@ -15178,6 +15218,31 @@ function clickPeriods(periods, clicks, impressions) {
|
|
|
15178
15218
|
impressions: impressions[index2] ?? 0
|
|
15179
15219
|
}));
|
|
15180
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
|
+
}
|
|
15181
15246
|
function rankEntries(entries) {
|
|
15182
15247
|
const score = (values) => ({
|
|
15183
15248
|
latest: values.at(-1) ?? 0,
|
|
@@ -15299,6 +15364,23 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
|
|
|
15299
15364
|
const gaStartDate = gaPeriods[0]?.startDate;
|
|
15300
15365
|
const acquisitionRows = gaStartDate ? db.select().from(gaAcquisitionDaily).where(and14(eq18(gaAcquisitionDaily.projectId, project.id), gte5(gaAcquisitionDaily.date, gaStartDate), lte2(gaAcquisitionDaily.date, gaAnchor))).all() : [];
|
|
15301
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;
|
|
15302
15384
|
const gscAnchor = db.select({ date: sql9`max(${gscDailyTotals.date})` }).from(gscDailyTotals).where(eq18(gscDailyTotals.projectId, project.id)).get()?.date ?? null;
|
|
15303
15385
|
const gscPeriods = gscAnchor ? buildPeriods(gscAnchor, days) : [];
|
|
15304
15386
|
const gscStartDate = gscPeriods[0]?.startDate;
|
|
@@ -15458,6 +15540,12 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
|
|
|
15458
15540
|
periods: eventPeriods(gaPeriods, values)
|
|
15459
15541
|
}))
|
|
15460
15542
|
},
|
|
15543
|
+
engagement: {
|
|
15544
|
+
status: engagementAnchor === null ? "unavailable" : "ready",
|
|
15545
|
+
availableFromDate: engagementAvailableFrom,
|
|
15546
|
+
latestDate: engagementAnchor,
|
|
15547
|
+
periods: engagementPeriods(engagementPeriodWindow, engagementRows)
|
|
15548
|
+
},
|
|
15461
15549
|
searchDemand: gscAnchor === null ? {
|
|
15462
15550
|
status: "unavailable",
|
|
15463
15551
|
periods: [],
|
|
@@ -17766,7 +17854,8 @@ var SCHEMA_TABLE = {
|
|
|
17766
17854
|
GscCoverageSummaryDto: gscCoverageSummaryDtoSchema,
|
|
17767
17855
|
GscDeindexedRowDto: gscDeindexedRowSchema,
|
|
17768
17856
|
GscPerformanceDailyDto: gscPerformanceDailyDtoSchema,
|
|
17769
|
-
|
|
17857
|
+
GscPerformanceResponseDto: gscPerformanceResponseDtoSchema,
|
|
17858
|
+
GscTopPagesDto: gscTopPagesDtoSchema,
|
|
17770
17859
|
GscDiscoverSitemapsResponseDto: gscDiscoverSitemapsResponseDtoSchema,
|
|
17771
17860
|
GscSiteListResponseDto: gscSiteListResponseDtoSchema,
|
|
17772
17861
|
GscSubmitSitemapsResponseDto: gscSubmitSitemapsResponseDtoSchema,
|
|
@@ -18105,9 +18194,21 @@ var reportAudienceQueryParameter = {
|
|
|
18105
18194
|
var analyticsWindowParameter = {
|
|
18106
18195
|
name: "window",
|
|
18107
18196
|
in: "query",
|
|
18108
|
-
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.",
|
|
18109
18198
|
schema: { type: "string", enum: ["7d", "30d", "90d", "all"] }
|
|
18110
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
|
+
};
|
|
18111
18212
|
var organicEvidencePeriodQueryParameter = {
|
|
18112
18213
|
name: "period",
|
|
18113
18214
|
in: "query",
|
|
@@ -19717,15 +19818,22 @@ var routeCatalog = [
|
|
|
19717
19818
|
{ name: "endDate", in: "query", description: "Filter by end date.", schema: stringSchema },
|
|
19718
19819
|
{ name: "query", in: "query", description: "Filter by search query.", schema: stringSchema },
|
|
19719
19820
|
{ name: "page", in: "query", description: "Filter by page URL.", schema: stringSchema },
|
|
19821
|
+
{
|
|
19822
|
+
name: "orderBy",
|
|
19823
|
+
in: "query",
|
|
19824
|
+
description: "Row ordering, always descending. Defaults to clicks. Use date for time-series reads.",
|
|
19825
|
+
schema: { type: "string", enum: ["clicks", "impressions", "date"] }
|
|
19826
|
+
},
|
|
19720
19827
|
limitQueryParameter,
|
|
19721
19828
|
offsetQueryParameter,
|
|
19722
19829
|
analyticsWindowParameter
|
|
19723
19830
|
],
|
|
19724
19831
|
responses: {
|
|
19725
|
-
//
|
|
19726
|
-
//
|
|
19727
|
-
//
|
|
19728
|
-
200:
|
|
19832
|
+
// Envelope, not a bare array: `totalMatching` / `truncated` are how a
|
|
19833
|
+
// caller tells a page from a complete answer, and `latestAvailableDate`
|
|
19834
|
+
// is how it tells "no data" from "asked past the GSC reporting lag".
|
|
19835
|
+
200: jsonResponse("GSC performance page plus match count and data freshness.", "GscPerformanceResponseDto"),
|
|
19836
|
+
400: errorResponse("Invalid orderBy value."),
|
|
19729
19837
|
404: errorResponse("Project not found.")
|
|
19730
19838
|
}
|
|
19731
19839
|
},
|
|
@@ -19745,6 +19853,24 @@ var routeCatalog = [
|
|
|
19745
19853
|
404: errorResponse("Project not found.")
|
|
19746
19854
|
}
|
|
19747
19855
|
},
|
|
19856
|
+
{
|
|
19857
|
+
method: "get",
|
|
19858
|
+
path: "/api/v1/projects/{name}/google/gsc/top-pages",
|
|
19859
|
+
summary: "Get top GSC pages ranked by clicks",
|
|
19860
|
+
description: "One row per page, aggregated in SQL and ranked by summed clicks descending. The rows are a RANKING built from the dimensioned search-data table; `totals` is NOT their sum. Google withholds rare queries (summed clicks under-count) and fans one impression across every query/page/country/device combination (summed impressions over-count), so `totals` is read from the un-dimensioned property-level daily table and labelled `totalsSource: property-daily`. It is null when that table has no rows in the window.",
|
|
19861
|
+
tags: ["google"],
|
|
19862
|
+
parameters: [
|
|
19863
|
+
nameParameter,
|
|
19864
|
+
{ name: "startDate", in: "query", description: "Filter by start date.", schema: stringSchema },
|
|
19865
|
+
{ name: "endDate", in: "query", description: "Filter by end date.", schema: stringSchema },
|
|
19866
|
+
limitQueryParameter,
|
|
19867
|
+
analyticsWindowParameter
|
|
19868
|
+
],
|
|
19869
|
+
responses: {
|
|
19870
|
+
200: jsonResponse("Ranked pages plus the property-level window total.", "GscTopPagesDto"),
|
|
19871
|
+
404: errorResponse("Project not found.")
|
|
19872
|
+
}
|
|
19873
|
+
},
|
|
19748
19874
|
{
|
|
19749
19875
|
method: "post",
|
|
19750
19876
|
path: "/api/v1/projects/{name}/google/gsc/inspect",
|
|
@@ -21536,7 +21662,7 @@ var routeCatalog = [
|
|
|
21536
21662
|
path: "/api/v1/projects/{name}/ga/traffic",
|
|
21537
21663
|
summary: "Get GA4 landing page traffic, channel breakdown, and AI referral landing pages",
|
|
21538
21664
|
tags: ["ga4"],
|
|
21539
|
-
parameters: [nameParameter, limitQueryParameter, analyticsWindowParameter],
|
|
21665
|
+
parameters: [nameParameter, limitQueryParameter, analyticsWindowParameter, analyticsStartDateParameter, analyticsEndDateParameter],
|
|
21540
21666
|
responses: {
|
|
21541
21667
|
// TODO: Add `GaTrafficResponse` Zod schema in contracts.
|
|
21542
21668
|
200: rawJsonResponse("GA4 traffic data returned.", looseObjectSchema),
|
|
@@ -21550,7 +21676,7 @@ var routeCatalog = [
|
|
|
21550
21676
|
summary: "Get raw AI referral detail rows per day, landing page, and attribution dimension",
|
|
21551
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.",
|
|
21552
21678
|
tags: ["ga4"],
|
|
21553
|
-
parameters: [nameParameter, analyticsWindowParameter],
|
|
21679
|
+
parameters: [nameParameter, analyticsWindowParameter, analyticsStartDateParameter, analyticsEndDateParameter],
|
|
21554
21680
|
responses: {
|
|
21555
21681
|
200: jsonArrayResponse("AI referral history returned.", "GA4AiReferralHistoryEntry"),
|
|
21556
21682
|
400: errorResponse("GA4 is not connected."),
|
|
@@ -21563,7 +21689,7 @@ var routeCatalog = [
|
|
|
21563
21689
|
summary: "Get AI referral sessions per day and per source",
|
|
21564
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.",
|
|
21565
21691
|
tags: ["ga4"],
|
|
21566
|
-
parameters: [nameParameter, analyticsWindowParameter],
|
|
21692
|
+
parameters: [nameParameter, analyticsWindowParameter, analyticsStartDateParameter, analyticsEndDateParameter],
|
|
21567
21693
|
responses: {
|
|
21568
21694
|
200: jsonResponse("AI referral daily series returned.", "GA4AiReferralDailyDto"),
|
|
21569
21695
|
400: errorResponse("GA4 is not connected."),
|
|
@@ -21575,7 +21701,7 @@ var routeCatalog = [
|
|
|
21575
21701
|
path: "/api/v1/projects/{name}/ga/social-referral-history",
|
|
21576
21702
|
summary: "Get social media referral sessions per day grouped by source",
|
|
21577
21703
|
tags: ["ga4"],
|
|
21578
|
-
parameters: [nameParameter, analyticsWindowParameter],
|
|
21704
|
+
parameters: [nameParameter, analyticsWindowParameter, analyticsStartDateParameter, analyticsEndDateParameter],
|
|
21579
21705
|
responses: {
|
|
21580
21706
|
200: jsonArrayResponse("Social referral history returned.", "GA4SocialReferralHistoryEntry"),
|
|
21581
21707
|
400: errorResponse("GA4 is not connected."),
|
|
@@ -21613,7 +21739,7 @@ var routeCatalog = [
|
|
|
21613
21739
|
path: "/api/v1/projects/{name}/ga/session-history",
|
|
21614
21740
|
summary: "Get total sessions per day for the project",
|
|
21615
21741
|
tags: ["ga4"],
|
|
21616
|
-
parameters: [nameParameter, analyticsWindowParameter],
|
|
21742
|
+
parameters: [nameParameter, analyticsWindowParameter, analyticsStartDateParameter, analyticsEndDateParameter],
|
|
21617
21743
|
responses: {
|
|
21618
21744
|
200: jsonArrayResponse("Session history returned.", "GA4SessionHistoryEntry"),
|
|
21619
21745
|
400: errorResponse("GA4 is not connected."),
|
|
@@ -24205,7 +24331,17 @@ var GA4_DIMENSIONS = {
|
|
|
24205
24331
|
var GA4_METRICS = {
|
|
24206
24332
|
sessions: "sessions",
|
|
24207
24333
|
totalUsers: "totalUsers",
|
|
24208
|
-
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"
|
|
24209
24345
|
};
|
|
24210
24346
|
|
|
24211
24347
|
// ../integration-google-analytics/src/types.ts
|
|
@@ -24753,6 +24889,15 @@ async function fetchAggregateSummary(accessToken, propertyId, days) {
|
|
|
24753
24889
|
ga4Log("info", "fetch-aggregate.done", { propertyId, ...summary });
|
|
24754
24890
|
return summary;
|
|
24755
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
|
+
}
|
|
24756
24901
|
async function fetchDailyTotals(accessToken, propertyId, days) {
|
|
24757
24902
|
validateAccessToken2(accessToken);
|
|
24758
24903
|
validatePropertyId(propertyId);
|
|
@@ -24764,14 +24909,25 @@ async function fetchDailyTotals(accessToken, propertyId, days) {
|
|
|
24764
24909
|
const res = await runReport(accessToken, propertyId, {
|
|
24765
24910
|
dateRanges: [{ startDate: formatDate2(startDate), endDate: formatDate2(endDate) }],
|
|
24766
24911
|
dimensions: [{ name: GA4_DIMENSIONS.date }],
|
|
24767
|
-
metrics: [
|
|
24912
|
+
metrics: [
|
|
24913
|
+
{ name: GA4_METRICS.sessions },
|
|
24914
|
+
{ name: GA4_METRICS.totalUsers },
|
|
24915
|
+
{ name: GA4_METRICS.engagementRate },
|
|
24916
|
+
{ name: GA4_METRICS.newUsers }
|
|
24917
|
+
],
|
|
24768
24918
|
limit: syncDays + 1
|
|
24769
24919
|
});
|
|
24770
|
-
const rows = (res.rows ?? []).map((row) =>
|
|
24771
|
-
|
|
24772
|
-
|
|
24773
|
-
|
|
24774
|
-
|
|
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);
|
|
24775
24931
|
ga4Log("info", "fetch-daily-totals.done", { propertyId, days: syncDays, rows: rows.length });
|
|
24776
24932
|
return rows;
|
|
24777
24933
|
}
|
|
@@ -25773,6 +25929,13 @@ async function googleRoutes(app, opts) {
|
|
|
25773
25929
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
25774
25930
|
const project = resolveProject(app.db, request.params.name);
|
|
25775
25931
|
const { startDate, endDate, query, page, limit, offset } = request.query;
|
|
25932
|
+
const parsedOrderBy = gscPerformanceOrderBySchema.safeParse(request.query.orderBy ?? "clicks");
|
|
25933
|
+
if (!parsedOrderBy.success) {
|
|
25934
|
+
throw validationError(
|
|
25935
|
+
`orderBy must be one of: ${gscPerformanceOrderBySchema.options.join(", ")}`
|
|
25936
|
+
);
|
|
25937
|
+
}
|
|
25938
|
+
const orderBy = parsedOrderBy.data;
|
|
25776
25939
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
25777
25940
|
const conditions = [eq27(gscSearchData.projectId, project.id)];
|
|
25778
25941
|
if (startDate) conditions.push(sql12`${gscSearchData.date} >= ${startDate}`);
|
|
@@ -25782,18 +25945,34 @@ async function googleRoutes(app, opts) {
|
|
|
25782
25945
|
if (page) conditions.push(sql12`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
|
|
25783
25946
|
const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
|
|
25784
25947
|
const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
|
|
25785
|
-
const
|
|
25786
|
-
|
|
25787
|
-
|
|
25788
|
-
|
|
25789
|
-
|
|
25790
|
-
|
|
25791
|
-
|
|
25792
|
-
|
|
25793
|
-
|
|
25794
|
-
|
|
25795
|
-
|
|
25796
|
-
|
|
25948
|
+
const orderColumns = {
|
|
25949
|
+
clicks: [desc13(gscSearchData.clicks), desc13(gscSearchData.date), gscSearchData.query],
|
|
25950
|
+
impressions: [desc13(gscSearchData.impressions), desc13(gscSearchData.date), gscSearchData.query],
|
|
25951
|
+
date: [desc13(gscSearchData.date), desc13(gscSearchData.clicks), gscSearchData.query]
|
|
25952
|
+
}[orderBy];
|
|
25953
|
+
const rows = app.db.select().from(gscSearchData).where(and21(...conditions)).orderBy(...orderColumns).limit(limitVal).offset(offsetVal).all();
|
|
25954
|
+
const totalMatching = app.db.select({ total: sql12`COUNT(*)` }).from(gscSearchData).where(and21(...conditions)).get()?.total ?? 0;
|
|
25955
|
+
const latestAvailableDate = app.db.select({ latest: sql12`MAX(${gscSearchData.date})` }).from(gscSearchData).where(eq27(gscSearchData.projectId, project.id)).get()?.latest ?? null;
|
|
25956
|
+
return {
|
|
25957
|
+
rows: rows.map((r) => ({
|
|
25958
|
+
date: r.date,
|
|
25959
|
+
query: r.query,
|
|
25960
|
+
page: r.page,
|
|
25961
|
+
country: r.country,
|
|
25962
|
+
device: r.device,
|
|
25963
|
+
clicks: r.clicks,
|
|
25964
|
+
impressions: r.impressions,
|
|
25965
|
+
ctr: parseFloat(r.ctr),
|
|
25966
|
+
position: parseFloat(r.position)
|
|
25967
|
+
})),
|
|
25968
|
+
totalMatching,
|
|
25969
|
+
// Account for the offset: `rows.length < totalMatching` alone reports
|
|
25970
|
+
// truncated=true for a page that sits past the end, where there is
|
|
25971
|
+
// nothing further to fetch. Truncation means "more rows follow this
|
|
25972
|
+
// page", so it has to measure from where this page ends.
|
|
25973
|
+
truncated: offsetVal + rows.length < totalMatching,
|
|
25974
|
+
latestAvailableDate
|
|
25975
|
+
};
|
|
25797
25976
|
});
|
|
25798
25977
|
app.get("/projects/:name/google/gsc/performance/daily", async (request) => {
|
|
25799
25978
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -25837,6 +26016,56 @@ async function googleRoutes(app, opts) {
|
|
|
25837
26016
|
daily
|
|
25838
26017
|
};
|
|
25839
26018
|
});
|
|
26019
|
+
app.get("/projects/:name/google/gsc/top-pages", async (request) => {
|
|
26020
|
+
const project = resolveProject(app.db, request.params.name);
|
|
26021
|
+
const { startDate, endDate, limit } = request.query;
|
|
26022
|
+
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
26023
|
+
const conditions = [eq27(gscSearchData.projectId, project.id)];
|
|
26024
|
+
if (startDate) conditions.push(sql12`${gscSearchData.date} >= ${startDate}`);
|
|
26025
|
+
else if (cutoffDate) conditions.push(sql12`${gscSearchData.date} >= ${cutoffDate}`);
|
|
26026
|
+
if (endDate) conditions.push(sql12`${gscSearchData.date} <= ${endDate}`);
|
|
26027
|
+
const limitVal = Math.max(parseInt(limit ?? "50", 10) || 0, 1);
|
|
26028
|
+
const rows = app.db.select({
|
|
26029
|
+
page: gscSearchData.page,
|
|
26030
|
+
clicks: sql12`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
|
|
26031
|
+
impressions: sql12`COALESCE(SUM(${gscSearchData.impressions}), 0)`
|
|
26032
|
+
}).from(gscSearchData).where(and21(...conditions)).groupBy(gscSearchData.page).orderBy(desc13(sql12`SUM(${gscSearchData.clicks})`), desc13(sql12`SUM(${gscSearchData.impressions})`)).limit(limitVal).all();
|
|
26033
|
+
const windowStart = startDate ?? cutoffDate ?? "";
|
|
26034
|
+
const windowEnd = endDate ?? "9999-12-31";
|
|
26035
|
+
const dailyTotals = readGscDailyTotals(app.db, project.id, windowStart, windowEnd);
|
|
26036
|
+
const totalClicks = dailyTotals.reduce((sum, d) => sum + d.clicks, 0);
|
|
26037
|
+
const totalImpressions = dailyTotals.reduce((sum, d) => sum + d.impressions, 0);
|
|
26038
|
+
const rankedSpan = app.db.select({
|
|
26039
|
+
first: sql12`MIN(${gscSearchData.date})`,
|
|
26040
|
+
last: sql12`MAX(${gscSearchData.date})`
|
|
26041
|
+
}).from(gscSearchData).where(and21(...conditions)).get();
|
|
26042
|
+
const coveredFrom = dailyTotals.length > 0 ? dailyTotals[0].date : null;
|
|
26043
|
+
const coveredThrough = dailyTotals.length > 0 ? dailyTotals[dailyTotals.length - 1].date : null;
|
|
26044
|
+
const totalsComplete = Boolean(
|
|
26045
|
+
coveredFrom && coveredThrough && rankedSpan?.first && rankedSpan?.last && coveredFrom <= rankedSpan.first && coveredThrough >= rankedSpan.last
|
|
26046
|
+
);
|
|
26047
|
+
return {
|
|
26048
|
+
rows: rows.map((r) => ({
|
|
26049
|
+
page: r.page,
|
|
26050
|
+
clicks: r.clicks,
|
|
26051
|
+
impressions: r.impressions,
|
|
26052
|
+
ctr: r.impressions > 0 ? r.clicks / r.impressions : 0
|
|
26053
|
+
})),
|
|
26054
|
+
totals: dailyTotals.length > 0 ? {
|
|
26055
|
+
clicks: totalClicks,
|
|
26056
|
+
impressions: totalImpressions,
|
|
26057
|
+
ctr: totalImpressions > 0 ? totalClicks / totalImpressions : 0,
|
|
26058
|
+
days: dailyTotals.length,
|
|
26059
|
+
coveredFrom,
|
|
26060
|
+
coveredThrough,
|
|
26061
|
+
// False when the property-level totals span less than the rows above.
|
|
26062
|
+
complete: totalsComplete
|
|
26063
|
+
} : null,
|
|
26064
|
+
totalsSource: "property-daily",
|
|
26065
|
+
rankedFrom: rankedSpan?.first ?? null,
|
|
26066
|
+
rankedThrough: rankedSpan?.last ?? null
|
|
26067
|
+
};
|
|
26068
|
+
});
|
|
25840
26069
|
app.post("/projects/:name/google/gsc/inspect", async (request) => {
|
|
25841
26070
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
25842
26071
|
if (!googleClientId || !googleClientSecret) {
|
|
@@ -32598,6 +32827,12 @@ function formatSharePct(numerator, total) {
|
|
|
32598
32827
|
if (rounded === 0) return "<1%";
|
|
32599
32828
|
return `${rounded}%`;
|
|
32600
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
|
+
}
|
|
32601
32836
|
var SOCIAL_CHANNEL_GROUPS2 = /* @__PURE__ */ new Set(["Organic Social", "Paid Social"]);
|
|
32602
32837
|
function buildChannelBreakdown(input) {
|
|
32603
32838
|
const aiSessions = [...input.aiSessionsByChannelGroup.values()].reduce((sum, sessions) => sum + sessions, 0);
|
|
@@ -33054,6 +33289,12 @@ async function ga4Routes(app, opts) {
|
|
|
33054
33289
|
date: row.date,
|
|
33055
33290
|
sessions: row.sessions,
|
|
33056
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,
|
|
33057
33298
|
syncedAt: now,
|
|
33058
33299
|
syncRunId: runId,
|
|
33059
33300
|
createdAt: now
|
|
@@ -33253,16 +33494,12 @@ async function ga4Routes(app, opts) {
|
|
|
33253
33494
|
const project = resolveProject(app.db, request.params.name);
|
|
33254
33495
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
33255
33496
|
const limit = Math.max(1, Math.min(parseInt(request.query.limit ?? "50", 10) || 50, 500));
|
|
33256
|
-
const
|
|
33257
|
-
const
|
|
33258
|
-
const
|
|
33259
|
-
const
|
|
33260
|
-
|
|
33261
|
-
const
|
|
33262
|
-
if (cutoffDate) aiConditions.push(sql15`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
33263
|
-
const socialConditions = [eq32(gaSocialReferrals.projectId, project.id)];
|
|
33264
|
-
if (cutoffDate) socialConditions.push(sql15`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
33265
|
-
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({
|
|
33266
33503
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
33267
33504
|
totalOrganicSessions: gaTrafficWindowSummaries.totalOrganicSessions,
|
|
33268
33505
|
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
@@ -33270,15 +33507,26 @@ async function ga4Routes(app, opts) {
|
|
|
33270
33507
|
}).from(gaTrafficWindowSummaries).where(
|
|
33271
33508
|
and26(
|
|
33272
33509
|
eq32(gaTrafficWindowSummaries.projectId, project.id),
|
|
33273
|
-
eq32(gaTrafficWindowSummaries.windowKey, window)
|
|
33510
|
+
eq32(gaTrafficWindowSummaries.windowKey, range.window)
|
|
33274
33511
|
)
|
|
33275
33512
|
).get() : null;
|
|
33276
|
-
const snapshotTotalsRow =
|
|
33277
|
-
|
|
33278
|
-
|
|
33279
|
-
|
|
33280
|
-
|
|
33281
|
-
|
|
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({
|
|
33282
33530
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
33283
33531
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
33284
33532
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
@@ -33363,7 +33611,11 @@ async function ga4Routes(app, opts) {
|
|
|
33363
33611
|
totalSessions: total,
|
|
33364
33612
|
totalOrganicSessions,
|
|
33365
33613
|
totalDirectSessions,
|
|
33366
|
-
|
|
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,
|
|
33367
33619
|
topPages: rows.map((r) => ({
|
|
33368
33620
|
landingPage: r.landingPage,
|
|
33369
33621
|
sessions: r.sessions ?? 0,
|
|
@@ -33430,21 +33682,32 @@ async function ga4Routes(app, opts) {
|
|
|
33430
33682
|
directSharePctDisplay: formatSharePct(totalDirectSessions, total),
|
|
33431
33683
|
socialSharePctDisplay: formatSharePct(socialSessions, total),
|
|
33432
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.
|
|
33433
33696
|
periodStart: (() => {
|
|
33434
|
-
|
|
33435
|
-
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;
|
|
33436
33700
|
if (start && end && start > end) return summaryMeta?.periodStart ?? null;
|
|
33437
33701
|
return start;
|
|
33438
33702
|
})(),
|
|
33439
|
-
periodEnd: summaryMeta?.periodEnd ?? null
|
|
33703
|
+
periodEnd: range.endDate ?? summaryMeta?.periodEnd ?? null
|
|
33440
33704
|
};
|
|
33441
33705
|
});
|
|
33442
33706
|
app.get("/projects/:name/ga/ai-referral-history", async (request, _reply) => {
|
|
33443
33707
|
const project = resolveProject(app.db, request.params.name);
|
|
33444
33708
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
33445
|
-
const
|
|
33446
|
-
const conditions = [eq32(gaAiReferrals.projectId, project.id)];
|
|
33447
|
-
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)];
|
|
33448
33711
|
const rows = app.db.select({
|
|
33449
33712
|
date: gaAiReferrals.date,
|
|
33450
33713
|
source: gaAiReferrals.source,
|
|
@@ -33467,9 +33730,8 @@ async function ga4Routes(app, opts) {
|
|
|
33467
33730
|
app.get("/projects/:name/ga/ai-referral-daily", async (request, _reply) => {
|
|
33468
33731
|
const project = resolveProject(app.db, request.params.name);
|
|
33469
33732
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
33470
|
-
const
|
|
33471
|
-
const conditions = [eq32(gaAiReferrals.projectId, project.id)];
|
|
33472
|
-
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)];
|
|
33473
33735
|
const rows = app.db.select({
|
|
33474
33736
|
date: gaAiReferrals.date,
|
|
33475
33737
|
source: gaAiReferrals.source,
|
|
@@ -33484,9 +33746,8 @@ async function ga4Routes(app, opts) {
|
|
|
33484
33746
|
app.get("/projects/:name/ga/social-referral-history", async (request, _reply) => {
|
|
33485
33747
|
const project = resolveProject(app.db, request.params.name);
|
|
33486
33748
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
33487
|
-
const
|
|
33488
|
-
const conditions = [eq32(gaSocialReferrals.projectId, project.id)];
|
|
33489
|
-
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)];
|
|
33490
33751
|
const rows = app.db.select({
|
|
33491
33752
|
date: gaSocialReferrals.date,
|
|
33492
33753
|
source: gaSocialReferrals.source,
|
|
@@ -33629,17 +33890,15 @@ async function ga4Routes(app, opts) {
|
|
|
33629
33890
|
app.get("/projects/:name/ga/session-history", async (request, _reply) => {
|
|
33630
33891
|
const project = resolveProject(app.db, request.params.name);
|
|
33631
33892
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
33632
|
-
const
|
|
33633
|
-
const conditions = [eq32(gaTrafficSnapshots.projectId, project.id)];
|
|
33634
|
-
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)];
|
|
33635
33895
|
const rows = app.db.select({
|
|
33636
33896
|
date: gaTrafficSnapshots.date,
|
|
33637
33897
|
sessions: sql15`SUM(${gaTrafficSnapshots.sessions})`,
|
|
33638
33898
|
organicSessions: sql15`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
33639
33899
|
users: sql15`SUM(${gaTrafficSnapshots.users})`
|
|
33640
33900
|
}).from(gaTrafficSnapshots).where(and26(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
33641
|
-
const totalConditions = [eq32(gaDailyTotals.projectId, project.id)];
|
|
33642
|
-
if (cutoffDate) totalConditions.push(sql15`${gaDailyTotals.date} >= ${cutoffDate}`);
|
|
33901
|
+
const totalConditions = [eq32(gaDailyTotals.projectId, project.id), ...dateRangeConditions(gaDailyTotals.date, range)];
|
|
33643
33902
|
const totals = app.db.select({ date: gaDailyTotals.date, users: gaDailyTotals.users }).from(gaDailyTotals).where(and26(...totalConditions)).all();
|
|
33644
33903
|
return buildSessionHistory(
|
|
33645
33904
|
rows.map((r) => ({
|